From 037e398cc72b09b5debba2db5fbde10c50a3f5ef Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:55:52 -0700 Subject: [PATCH 1/3] fix(byo): the computer setup command logs in to the page's instance (TASK-547) The 'Add a computer' command ran a bare `commonly login`, which falls back to the CLI's built-in production default, so a dev or self-hosted page registered its computer against commonly.me. One helper now builds the command from the API base URL with an explicit --instance, the same form Connect uses. BYO and the runtime grants section both call it. Invariant test: the command targets the page instance, and no BYO, Connect or runtime source hardcodes api.commonly.me. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../__tests__/V2AgentRuntimeGrants.test.tsx | 3 +- .../byo-setup-command-invariant.test.ts | 32 +++++++++++++++++++ frontend/src/v2/components/V2AgentBYO.tsx | 13 ++++---- .../v2/components/V2AgentRuntimeGrants.tsx | 4 +-- frontend/src/v2/utils/daemonSetupCommand.ts | 8 +++++ 5 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 frontend/src/v2/__tests__/byo-setup-command-invariant.test.ts create mode 100644 frontend/src/v2/utils/daemonSetupCommand.ts diff --git a/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx b/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx index 9a216218d6..d52879c317 100644 --- a/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import V2AgentRuntimeGrants, { describeRuntime } from '../components/V2AgentRuntimeGrants'; import { AuthContext } from '../../context/AuthContext'; +import { daemonSetupCommand } from '../utils/daemonSetupCommand'; jest.mock('axios', () => { const mock = { @@ -75,7 +76,7 @@ test('revoked Runtime row offers an active computer and moves in one request', a test('without an active computer, the setup hint includes daemon installation', async () => { renderSection(); expect(await screen.findByText(/Register another computer to move this agent/)).toBeInTheDocument(); - expect(screen.getByText('npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install')).toBeInTheDocument(); + expect(screen.getByText(daemonSetupCommand())).toBeInTheDocument(); }); test('a changed installer refuses the move before release and explains the blocking installation', async () => { diff --git a/frontend/src/v2/__tests__/byo-setup-command-invariant.test.ts b/frontend/src/v2/__tests__/byo-setup-command-invariant.test.ts new file mode 100644 index 0000000000..83835ec646 --- /dev/null +++ b/frontend/src/v2/__tests__/byo-setup-command-invariant.test.ts @@ -0,0 +1,32 @@ +// @ts-nocheck +// TASK-547 (D39): a copy-paste setup command on the BYO, Connect or runtime +// pages must target the instance the page is on. A bare `commonly login` falls +// back to the CLI's built-in production default, so on dev or a self-hosted +// instance the computer would register against commonly.me. +const fs = require('fs'); +const path = require('path'); +const { daemonSetupCommand } = require('../utils/daemonSetupCommand'); + +const SOURCES = [ + 'components/V2AgentBYO.tsx', + 'components/V2AgentRuntimeGrants.tsx', + 'components/V2ConnectPage.tsx', +]; +const read = (rel) => fs.readFileSync(path.join(__dirname, '..', rel), 'utf8'); + +describe('copy-paste setup commands follow the page instance', () => { + beforeEach(() => { process.env.REACT_APP_API_URL = 'https://api-dev.example.test/'; }); + afterEach(() => { delete process.env.REACT_APP_API_URL; }); + + test('the computer setup command logs in to the page instance, not the CLI default', () => { + const command = daemonSetupCommand(); + expect(command).toContain('commonly login --instance https://api-dev.example.test '); + expect(command).not.toMatch(/commonly\.me/); + }); + + test('no BYO, Connect or runtime source hardcodes the production API host', () => { + SOURCES.forEach((rel) => { + expect([rel, read(rel).match(/api\.commonly\.me/g)]).toEqual([rel, null]); + }); + }); +}); diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index ef41178c7e..5bc1903fb1 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; import { recordByoStep, type ByoStep } from '../utils/byoTelemetry'; import getApiBaseUrl from '../../utils/apiBaseUrl'; +import { daemonSetupCommand } from '../utils/daemonSetupCommand'; import V2FeaturePage from './V2FeaturePage'; import V2Avatar from './V2Avatar'; import V2Hint from './V2Hint'; @@ -65,10 +66,9 @@ type MachineRow = { }; const MACHINE_STATUS_POLL_MS = 4000; const MACHINE_STATUS_MAX_TICKS = 30; -// One-time computer setup (ADR-026 D1). Registration mints a machine-local -// credential, so it is inherently a terminal act — this page's job is to -// make it ONE paste, after which "On my computer" appears here. -const DAEMON_SETUP_COMMAND = 'npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install'; +// Registration mints a machine-local credential, so it is inherently a terminal +// act — this page's job is to make it ONE paste (daemonSetupCommand), after +// which "On my computer" appears here. const CLAUDE_FILE_NAME = 'CLAUDE.md'; type MachineAdapter = '' | 'claude' | 'codex'; @@ -952,6 +952,7 @@ const V2AgentBYO: React.FC = () => { const chooseMode = (nextMode: 'hosted' | 'byo' | 'machine') => setMode(nextMode); const apiUrl = getApiBaseUrl().replace(/\/+$/, ''); + const daemonCommand = daemonSetupCommand(apiUrl); const claudeSnippet = issued ? `claude mcp add commonly \\\n -e COMMONLY_API_URL=${apiUrl} \\\n -e COMMONLY_AGENT_TOKEN=${issued.token} \\\n -- npx -y @commonlyai/mcp` @@ -1333,11 +1334,11 @@ const V2AgentBYO: React.FC = () => {
{t('agentByo.addComputer.title')}
- {DAEMON_SETUP_COMMAND} + {daemonCommand} diff --git a/frontend/src/v2/components/V2AgentRuntimeGrants.tsx b/frontend/src/v2/components/V2AgentRuntimeGrants.tsx index ce8199f2bd..05e03b2c3f 100644 --- a/frontend/src/v2/components/V2AgentRuntimeGrants.tsx +++ b/frontend/src/v2/components/V2AgentRuntimeGrants.tsx @@ -4,6 +4,7 @@ import { useV2Api } from '../hooks/useV2Api'; import './V2AgentRuntimeGrants.css'; import V2AgentProviderKey from './V2AgentProviderKey'; import V2RuntimeControls, { RuntimeSettings, RuntimeUpdate } from './V2RuntimeControls'; +import { daemonSetupCommand } from '../utils/daemonSetupCommand'; export interface Runtime extends RuntimeSettings {} @@ -49,7 +50,6 @@ const WALKED_ADAPTERS: Record = { hermes: 'Hermes', dsh: 'DeepSeek Harness', }; -const DAEMON_SETUP_COMMAND = 'npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install'; export const describeRuntime = (runtime: Runtime): { mode: 'native' | 'hosted' | 'daemon' | 'cli' | 'unknown'; adapter: string | null } => { const type = String(runtime.runtimeType || '').toLowerCase(); @@ -198,7 +198,7 @@ const V2AgentRuntimeGrants: React.FC = ({ - :

{t('agentManage.runtime.noActiveMachine')} {DAEMON_SETUP_COMMAND}

+ :

{t('agentManage.runtime.noActiveMachine')} {daemonSetupCommand()}

)}
)} diff --git a/frontend/src/v2/utils/daemonSetupCommand.ts b/frontend/src/v2/utils/daemonSetupCommand.ts new file mode 100644 index 0000000000..b0c540aa2a --- /dev/null +++ b/frontend/src/v2/utils/daemonSetupCommand.ts @@ -0,0 +1,8 @@ +import getApiBaseUrl from '../../utils/apiBaseUrl'; + +// One-time computer setup (ADR-026 D1). `--instance` is explicit: a bare +// `commonly login` falls back to the CLI's built-in production default, so a +// dev or self-hosted page would register its computer against commonly.me. +// Connect builds its login line the same way (V2ConnectPage). +export const daemonSetupCommand = (apiUrl: string = getApiBaseUrl()): string => + `npm i -g @commonlyai/cli@latest && commonly login --instance ${apiUrl.replace(/\/+$/, '')} && commonly daemon register && commonly daemon install`; From 4e44ac1dc01bc28ce3b2dddd973d36f212b7b605 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:10:30 -0700 Subject: [PATCH 2/3] test(scout): the Add-a-computer copy follows the helper, not a page constant (TASK-547) The Scout prompt repeats the computer command, and its test read a constant that the helper replaced. The prompt now names the public instance explicitly, and the test derives the expected command from the helper's template. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../unit/config/nativeAgents.scoutKnowledge.test.js | 11 ++++++++++- backend/config/native-agents/scout.ts | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js index b595e1b3a3..2e230afbcf 100644 --- a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js +++ b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js @@ -14,6 +14,7 @@ const path = require('path'); const { scoutApp } = require('../../../config/native-agents/scout'); const BYO_PAGE = path.resolve(__dirname, '../../../../frontend/src/v2/components/V2AgentBYO.tsx'); +const DAEMON_HELPER = path.resolve(__dirname, '../../../../frontend/src/v2/utils/daemonSetupCommand.ts'); const constantFromPage = (name) => { const source = fs.readFileSync(BYO_PAGE, 'utf8'); @@ -22,12 +23,20 @@ const constantFromPage = (name) => { return match[1]; }; +// The computer command is built from the page's API base URL (TASK-547), so the +// Scout copy is checked against the public instance it describes. +const daemonCommandForPublicInstance = () => { + const source = fs.readFileSync(DAEMON_HELPER, 'utf8'); + const template = source.match(/`(npm i -g [^`]+)`/)[1]; + return template.replace(/\$\{apiUrl[^}]*\}/, 'https://api.commonly.me'); +}; + describe('Scout onboarding knowledge', () => { const prompt = scoutApp.systemPrompt; test('gives the exact install and machine commands the Bring-your-own page shows', () => { expect(prompt).toContain(constantFromPage('CLI_INSTALL_COMMAND')); - expect(prompt).toContain(constantFromPage('DAEMON_SETUP_COMMAND')); + expect(prompt).toContain(daemonCommandForPublicInstance()); expect(prompt).toContain('export COMMONLY_API_URL='); expect(prompt).toContain('export COMMONLY_AGENT_TOKEN='); expect(prompt).toContain('commonly agent run '); diff --git a/backend/config/native-agents/scout.ts b/backend/config/native-agents/scout.ts index 20cb215225..e7de4d30af 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -133,7 +133,7 @@ export const scoutApp = { + 'Do not propose a connect card for it, and do not say OpenRouter, or any model ' + 'provider, was the one it was walked on.\n' + ' * "Add a computer": `npm i -g @commonlyai/cli@latest && commonly login ' - + '&& commonly daemon register && commonly daemon install` registers their ' + + '--instance https://api.commonly.me && commonly daemon register && commonly daemon install` registers their ' + 'machine once; after that they add agents to it from this page with no ' + 'terminal.\n' // Linux run 38982219348 (2026-10-16): the sandbox that keeps a connected From b10bea5a7c03fd0f3a50770ce8937cb8d02c1d74 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:35:25 -0700 Subject: [PATCH 3/3] fix(byo): Scout and the docs site follow the instance, not a constant (TASK-547) Scout is installed on every instance, so its computer command names a placeholder for the instance's API URL, not commonly.me (CodeRabbit on #2727). The docs site describes commonly.me and names it. Both tests now read the command template from the helper through one shared fixture, not a constant the page no longer has. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: dart Agent-Model: claude/claude-haiku-5-5/default --- .../config/nativeAgents.scoutKnowledge.test.js | 15 ++++++--------- .../unit/scripts/docsSiteFacts.test.js | 4 +++- .../utils/daemonSetupCommandTemplate.js | 17 +++++++++++++++++ backend/config/native-agents/scout.ts | 4 ++-- docs-site/agents/connect.mdx | 2 +- 5 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 backend/__tests__/utils/daemonSetupCommandTemplate.js diff --git a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js index 2e230afbcf..ab38f08955 100644 --- a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js +++ b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js @@ -14,7 +14,7 @@ const path = require('path'); const { scoutApp } = require('../../../config/native-agents/scout'); const BYO_PAGE = path.resolve(__dirname, '../../../../frontend/src/v2/components/V2AgentBYO.tsx'); -const DAEMON_HELPER = path.resolve(__dirname, '../../../../frontend/src/v2/utils/daemonSetupCommand.ts'); +const { daemonSetupCommandTemplate } = require('../../utils/daemonSetupCommandTemplate'); const constantFromPage = (name) => { const source = fs.readFileSync(BYO_PAGE, 'utf8'); @@ -23,20 +23,17 @@ const constantFromPage = (name) => { return match[1]; }; -// The computer command is built from the page's API base URL (TASK-547), so the -// Scout copy is checked against the public instance it describes. -const daemonCommandForPublicInstance = () => { - const source = fs.readFileSync(DAEMON_HELPER, 'utf8'); - const template = source.match(/`(npm i -g [^`]+)`/)[1]; - return template.replace(/\$\{apiUrl[^}]*\}/, 'https://api.commonly.me'); -}; +// Scout runs on every instance, so its copy names the URL placeholder, never a +// production host (CodeRabbit on #2727). +const SCOUT_API_PLACEHOLDER = ''; describe('Scout onboarding knowledge', () => { const prompt = scoutApp.systemPrompt; test('gives the exact install and machine commands the Bring-your-own page shows', () => { expect(prompt).toContain(constantFromPage('CLI_INSTALL_COMMAND')); - expect(prompt).toContain(daemonCommandForPublicInstance()); + expect(prompt).toContain(daemonSetupCommandTemplate(SCOUT_API_PLACEHOLDER)); + expect(prompt).not.toMatch(/--instance https:\/\/api\.commonly\.me && commonly daemon register/); expect(prompt).toContain('export COMMONLY_API_URL='); expect(prompt).toContain('export COMMONLY_AGENT_TOKEN='); expect(prompt).toContain('commonly agent run '); diff --git a/backend/__tests__/unit/scripts/docsSiteFacts.test.js b/backend/__tests__/unit/scripts/docsSiteFacts.test.js index 1746883f38..0fc6ebe888 100644 --- a/backend/__tests__/unit/scripts/docsSiteFacts.test.js +++ b/backend/__tests__/unit/scripts/docsSiteFacts.test.js @@ -11,6 +11,7 @@ */ const fs = require('fs'); const path = require('path'); +const { daemonSetupCommandTemplate } = require('../../utils/daemonSetupCommandTemplate'); const ROOT = path.resolve(__dirname, '../../../..'); const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8'); @@ -42,7 +43,8 @@ describe('docs.commonly.me says what the product does', () => { test('the quickstart is the hosted path and the deeper connect page keeps the CLI commands', () => { const install = constantFromPage('CLI_INSTALL_COMMAND'); - const daemon = constantFromPage('DAEMON_SETUP_COMMAND'); + // The docs site describes commonly.me, so its command names that instance. + const daemon = daemonSetupCommandTemplate('https://api.commonly.me'); expect(quickstart).toContain('Run it here'); expect(quickstart).not.toContain('npm i -g'); expect(connect).toContain(install); diff --git a/backend/__tests__/utils/daemonSetupCommandTemplate.js b/backend/__tests__/utils/daemonSetupCommandTemplate.js new file mode 100644 index 0000000000..1b0d7d2dcb --- /dev/null +++ b/backend/__tests__/utils/daemonSetupCommandTemplate.js @@ -0,0 +1,17 @@ +/** + * The computer setup command, read from its one source: the frontend helper + * that builds it for the page's instance (TASK-547). Backend copy that repeats + * the command (Scout's prompt, the docs site) is checked against this template + * with the API URL left as a placeholder, so each copy keeps its own instance. + */ +const fs = require('fs'); +const path = require('path'); + +const HELPER = path.resolve(__dirname, '../../../frontend/src/v2/utils/daemonSetupCommand.ts'); + +const daemonSetupCommandTemplate = (apiUrl) => { + const template = fs.readFileSync(HELPER, 'utf8').match(/`(npm i -g [^`]+)`/)[1]; + return template.replace(/\$\{apiUrl[^}]*\}/, apiUrl); +}; + +module.exports = { daemonSetupCommandTemplate }; diff --git a/backend/config/native-agents/scout.ts b/backend/config/native-agents/scout.ts index e7de4d30af..0c18feeae1 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -133,8 +133,8 @@ export const scoutApp = { + 'Do not propose a connect card for it, and do not say OpenRouter, or any model ' + 'provider, was the one it was walked on.\n' + ' * "Add a computer": `npm i -g @commonlyai/cli@latest && commonly login ' - + '--instance https://api.commonly.me && commonly daemon register && commonly daemon install` registers their ' - + 'machine once; after that they add agents to it from this page with no ' + + '--instance && commonly daemon register && commonly daemon install` registers their ' + + 'machine once (on commonly.me the API URL is https://api.commonly.me); after that they add agents to it from this page with no ' + 'terminal.\n' // Linux run 38982219348 (2026-10-16): the sandbox that keeps a connected // agent to its own folder cannot start Claude Code on Linux or WSL, and diff --git a/docs-site/agents/connect.mdx b/docs-site/agents/connect.mdx index 7d48c918b3..0850ae14c6 100644 --- a/docs-site/agents/connect.mdx +++ b/docs-site/agents/connect.mdx @@ -53,7 +53,7 @@ These commands are verified on macOS. Windows is not verified yet. When your agent should survive restarts, add the computer once: ```bash -npm i -g @commonlyai/cli@latest && commonly login && commonly daemon register && commonly daemon install +npm i -g @commonlyai/cli@latest && commonly login --instance https://api.commonly.me && commonly daemon register && commonly daemon install ``` The daemon supervises the agents you add to that computer. Add them from