From e39619ffa2b891aecd431bfeb3519fca7a0382b2 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 00:13:41 -0700 Subject: [PATCH] fix(byo): name the server's origin on the handoff card's login line (TASK-547) The server-built handoff card ran a bare `commonly login`, which falls back to the CLI's production default. A card posted by a dev or self-hosted server would register the person's computer against commonly.me. The login line now names the server origin from PUBLIC_API_URL, then BACKEND_URL. Only a plain http(s) origin is used: path, query, fragment and credentials are dropped, and anything else keeps the bare login. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wren Agent-Model: claude/claude-haiku-5-5/max --- .../unit/services/byoHandoffService.test.ts | 49 +++++++++++++++++++ backend/services/byoHandoffService.ts | 23 ++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/unit/services/byoHandoffService.test.ts b/backend/__tests__/unit/services/byoHandoffService.test.ts index 5846f569b9..af6f7328c2 100644 --- a/backend/__tests__/unit/services/byoHandoffService.test.ts +++ b/backend/__tests__/unit/services/byoHandoffService.test.ts @@ -83,4 +83,53 @@ describe('byoHandoffService', () => { expect(defaultByoHandoffName('Sam Xu', 'claude')).toBe('sam-xu-claude'); expect(defaultByoHandoffName(undefined, 'codex')).toBe('my-codex'); }); + + describe('login line', () => { + const ENV_KEYS = ['PUBLIC_API_URL', 'BACKEND_URL']; + let saved; + + beforeEach(() => { + saved = ENV_KEYS.map((key) => [key, process.env[key]]); + ENV_KEYS.forEach((key) => { delete process.env[key]; }); + }); + + afterEach(() => { + saved.forEach(([key, value]) => { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + }); + }); + + const loginLineFor = (os) => { + const payload = buildByoHandoffPayload({ + adapter: 'codex', os, podId: 'pod-1', agentName: 'my-codex', + }); + if ('error' in payload) throw new Error(payload.error); + return payload.commands[os].split('\n')[1]; + }; + + it('stays a bare login when the server has no public origin configured', () => { + expect(loginLineFor('linux')).toBe('commonly login'); + }); + + it('names the server origin on every OS when BACKEND_URL is set', () => { + process.env.BACKEND_URL = 'http://localhost:5000'; + for (const os of ['mac', 'linux', 'windows']) { + expect(loginLineFor(os)).toBe('commonly login --instance http://localhost:5000'); + } + }); + + it('prefers PUBLIC_API_URL and reduces it to the bare origin', () => { + process.env.BACKEND_URL = 'http://backend.internal:5000'; + process.env.PUBLIC_API_URL = 'https://api.commonly.me/api?token=x#frag'; + expect(loginLineFor('mac')).toBe('commonly login --instance https://api.commonly.me'); + }); + + it('drops a configured value that is not a plain http(s) origin', () => { + for (const unsafe of ['file:///etc/passwd', 'ftp://api.commonly.me', 'http://$(id)', 'not a url']) { + process.env.PUBLIC_API_URL = unsafe; + expect(loginLineFor('mac')).toBe('commonly login'); + } + }); + }); }); diff --git a/backend/services/byoHandoffService.ts b/backend/services/byoHandoffService.ts index db5b8dc242..f2317e1c7d 100644 --- a/backend/services/byoHandoffService.ts +++ b/backend/services/byoHandoffService.ts @@ -35,6 +35,20 @@ const safeName = (value: unknown): string => String(value || '') const safePodId = (value: unknown): string => String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, ''); +// The instance this server answers on. A bare `commonly login` falls back to +// the CLI's built-in production default, so a card posted by a dev or +// self-hosted server would register the person's computer against commonly.me. +// Only a plain http(s) origin is composed into the shell line. +const PLAIN_ORIGIN = /^https?:\/\/[a-z0-9.-]+(:\d{1,5})?$/i; +const publicApiOrigin = (): string => { + try { + const { origin } = new URL(process.env.PUBLIC_API_URL || process.env.BACKEND_URL || ''); + return PLAIN_ORIGIN.test(origin) ? origin : ''; + } catch { + return ''; + } +}; + const environmentPath = (os: ByoHandoffOs): string => { if (os === 'windows') return 'C:\\Users\\you\\agents\\environment.json'; if (os === 'linux') return '/home/you/agents/environment.json'; @@ -58,6 +72,7 @@ export const buildByoHandoffCommand = ({ agentName, memoryFrom, memoryInstance = 'default', + apiOrigin = '', }: { adapter: ByoHandoffAdapter; os: ByoHandoffOs; @@ -65,13 +80,15 @@ export const buildByoHandoffCommand = ({ agentName: string; memoryFrom?: string; memoryInstance?: string; + apiOrigin?: string; }): string => { const memoryFlags = memoryFrom ? ` --memory-from ${memoryFrom} --memory-instance ${memoryInstance}` : ''; const attach = `commonly agent attach ${adapter} --pod ${podId} --name ${agentName}${memoryFlags}`; const withEnvironment = adapter === 'opencode' || adapter === 'dsh' ? `${attach} --env ${environmentPath(os)}` : attach; - return [installCommand(adapter), 'commonly login', withEnvironment, `commonly agent run ${agentName}`].join('\n'); + const login = apiOrigin ? `commonly login --instance ${apiOrigin}` : 'commonly login'; + return [installCommand(adapter), login, withEnvironment, `commonly agent run ${agentName}`].join('\n'); }; export interface ByoHandoffPayload { @@ -91,6 +108,7 @@ export const buildByoHandoffPayload = ({ agentName, memoryFrom, memoryInstance = 'default', + apiOrigin = publicApiOrigin(), }: { adapter: unknown; os: unknown; @@ -98,6 +116,7 @@ export const buildByoHandoffPayload = ({ agentName: unknown; memoryFrom?: string; memoryInstance?: string; + apiOrigin?: string; }): ByoHandoffPayload | { error: string } => { if (!isAdapter(adapter)) return { error: 'adapter must be claude, codex, opencode, or dsh' }; if (!isOs(os)) return { error: 'os must be mac, linux, or windows' }; @@ -120,7 +139,7 @@ export const buildByoHandoffPayload = ({ for (const supported of supportedOs) { commands[supported] = buildByoHandoffCommand({ adapter, os: supported, podId: normalizedPodId, agentName: normalizedName, - memoryFrom, memoryInstance, + memoryFrom, memoryInstance, apiOrigin, }); } return {