From adeaedc9e48931c73338d1308dfd98c54c6a4236 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 1 Apr 2026 22:43:32 +0200 Subject: [PATCH 1/2] fix: allow anonymous workspace creation in `agent-relay on` Try requests without auth first (like relaycast does), only prompting for browser login if the server returns 401. This removes the forced login flow for workspace creation and join operations. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/commands/on/start.test.ts | 1 + src/cli/commands/on/start.ts | 83 +++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/cli/commands/on/start.test.ts b/src/cli/commands/on/start.test.ts index 271db1d4f..be2239538 100644 --- a/src/cli/commands/on/start.test.ts +++ b/src/cli/commands/on/start.test.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; vi.mock('@agent-relay/cloud', () => ({ + readStoredAuth: vi.fn().mockResolvedValue(null), ensureAuthenticated: vi.fn().mockResolvedValue({ accessToken: 'test-token' }), })); diff --git a/src/cli/commands/on/start.ts b/src/cli/commands/on/start.ts index ef36733cb..bd1201d5c 100644 --- a/src/cli/commands/on/start.ts +++ b/src/cli/commands/on/start.ts @@ -19,7 +19,7 @@ import { compileDotfiles, hasDotfiles } from './dotfiles.js'; import { ensureRelayfileMountBinary } from './relayfile-binary.js'; import { mintToken } from './token.js'; import { seedWorkspace as seedWorkspaceFiles, seedAclRules } from './workspace.js'; -import { ensureAuthenticated } from '@agent-relay/cloud'; +import { ensureAuthenticated, readStoredAuth } from '@agent-relay/cloud'; interface OnOptions { agent?: string; @@ -243,7 +243,10 @@ function readWorkspaceRegistry(relayDir?: string): LocalWorkspaceRegistry { function writeWorkspaceRegistry(relayDir: string, registry: LocalWorkspaceRegistry): void { ensureDirectory(relayDir); - writeFileSync(getWorkspaceRegistryPath(relayDir), `${JSON.stringify(registry, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 }); + writeFileSync(getWorkspaceRegistryPath(relayDir), `${JSON.stringify(registry, null, 2)}\n`, { + encoding: 'utf8', + mode: 0o600, + }); } function updateWorkspaceRegistry( @@ -280,19 +283,34 @@ async function postWorkspaceApi( 'X-Correlation-Id': `agent-relay-on-${Date.now()}`, }; - // Attach cloud auth token for remote endpoints + // For remote endpoints, try anonymous first — attach existing auth if + // available but never force a browser login. If the server returns 401, + // fall back to interactive login and retry once. if (!isLocalBaseUrl(url)) { - const parsed = new URL(url); - const auth = await ensureAuthenticated(`${parsed.protocol}//${parsed.host}`); - headers['Authorization'] = `Bearer ${auth.accessToken}`; + const stored = await readStoredAuth().catch(() => null); + if (stored) { + headers['Authorization'] = `Bearer ${stored.accessToken}`; + } } - const response = await fetchFn(url, { + let response = await fetchFn(url, { method: 'POST', headers, body: JSON.stringify(body), }); + // Retry with interactive login if the server requires auth + if (response.status === 401 && !isLocalBaseUrl(url)) { + const parsed = new URL(url); + const auth = await ensureAuthenticated(`${parsed.protocol}//${parsed.host}`); + headers['Authorization'] = `Bearer ${auth.accessToken}`; + response = await fetchFn(url, { + method: 'POST', + headers, + body: JSON.stringify(body), + }); + } + const raw = await response.text(); if (!response.ok) { throw new Error(`workspace API request failed (${response.status}): ${raw}`.trim()); @@ -617,7 +635,10 @@ function loadConfigFromFile(configPath: string, projectDir: string): RelayConfig const fallbackWorkspace = path.basename(projectDir); const workspace = toString(payload.workspace, toString(root.workspace, fallbackWorkspace)); - const signing_secret = toString(payload.signing_secret, toString(root.signing_secret, process.env.SIGNING_KEY ?? '')); + const signing_secret = toString( + payload.signing_secret, + toString(root.signing_secret, process.env.SIGNING_KEY ?? '') + ); if (!signing_secret) { throw new Error( `relay config at ${configPath} is missing signing_secret and SIGNING_KEY env var is not set. ` + @@ -1028,7 +1049,11 @@ interface GoOnRelayDeps { exit?: (code: number) => never | void; fetch?: FetchFn; provision?: (config: RelayConfig, agent: RelayConfigAgent) => Promise; - provisionAgentToken?: (opts: { config: RelayConfig; agent: RelayConfigAgent; tokenPath: string }) => Promise; + provisionAgentToken?: (opts: { + config: RelayConfig; + agent: RelayConfigAgent; + tokenPath: string; + }) => Promise; ensureServicesRunning?: (authBase: string, fileBase: string) => Promise; startServices?: (opts: { authBase: string; fileBase: string }) => Promise; } @@ -1208,16 +1233,14 @@ export async function goOnTheRelay( // Compile dotfile permissions for this agent const hasDots = hasDotfiles(projectDir); - const dotfileAcl = hasDots - ? compileDotfiles(projectDir, agent.name, workspaceSession.workspaceId) - : null; + const dotfileAcl = hasDots ? compileDotfiles(projectDir, agent.name, workspaceSession.workspaceId) : null; if (workspaceSession.created) { const seedExcludes = [...DEFAULT_SEED_EXCLUDES]; if (dotfileAcl) { // Add ignored patterns so ignored files are never uploaded for (const [dir, rules] of Object.entries(dotfileAcl.acl)) { - if (rules.some(r => r.startsWith('deny:agent:'))) { + if (rules.some((r) => r.startsWith('deny:agent:'))) { seedExcludes.push(dir.replace(/^\//, '')); } } @@ -1240,12 +1263,20 @@ export async function goOnTheRelay( // Write compiled ACL for mount to read const bundlePath = path.join(relayDir, 'compiled-acl.json'); - writeFileSync(bundlePath, JSON.stringify({ - workspace: workspaceSession.workspaceId, - acl: dotfileAcl.acl, - summary: dotfileAcl.summary, - agents: [{ name: agent.name, summary: dotfileAcl.summary }], - }, null, 2) + '\n', { encoding: 'utf8' }); + writeFileSync( + bundlePath, + JSON.stringify( + { + workspace: workspaceSession.workspaceId, + acl: dotfileAcl.acl, + summary: dotfileAcl.summary, + agents: [{ name: agent.name, summary: dotfileAcl.summary }], + }, + null, + 2 + ) + '\n', + { encoding: 'utf8' } + ); } } @@ -1396,10 +1427,18 @@ export async function goOnTheRelay( // Wait for the agent process to exit so agentExitCode is set by the close handler, // then ensure cleanup completes before resolving — avoids data loss from premature exit cleanupInProgress = new Promise((r) => { - if (!agentProc || agentProc.exitCode !== null) { r(); return; } + if (!agentProc || agentProc.exitCode !== null) { + r(); + return; + } const t = setTimeout(r, 2000); - agentProc.once('close', () => { clearTimeout(t); r(); }); - }).then(() => finalizeCleanup()).then(() => resolve()); + agentProc.once('close', () => { + clearTimeout(t); + r(); + }); + }) + .then(() => finalizeCleanup()) + .then(() => resolve()); }; process.once('SIGINT', cleanupHook); From 631180cb2515180748004787268e524a3fe510e6 Mon Sep 17 00:00:00 2001 From: My Senior Dev Date: Sat, 4 Apr 2026 10:11:56 +0200 Subject: [PATCH 2/2] fix: address 2 review finding(s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit channel-messenger.ts: Polynomial regex on uncontrolled data — skipped (file not in branch) start.ts: Stored access token sent to unrelated server without URL validation — fixed by adding origin check Co-Authored-By: My Senior Dev --- src/cli/commands/on/start.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/on/start.ts b/src/cli/commands/on/start.ts index bd1201d5c..9fac0e27b 100644 --- a/src/cli/commands/on/start.ts +++ b/src/cli/commands/on/start.ts @@ -288,7 +288,9 @@ async function postWorkspaceApi( // fall back to interactive login and retry once. if (!isLocalBaseUrl(url)) { const stored = await readStoredAuth().catch(() => null); - if (stored) { + const parsed = new URL(url); + const targetOrigin = `${parsed.protocol}//${parsed.host}`; + if (stored && stored.apiUrl === targetOrigin) { headers['Authorization'] = `Bearer ${stored.accessToken}`; } }