From 712d2ae7a2eb59531c50b0057042939475066a69 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:23:51 -0700 Subject: [PATCH 1/2] fix(byo): the default agent name follows where it runs C14 call rehearsal, 2026-10-20: after "Run it here" created `-agent`, "Use my own agent" pre-filled the same name, and "Get my connect command" was refused with "Commonly runs this agent, so it has no runtime token. To run an agent yourself, connect your own agent instead." That is what the person was doing, and it would have happened on the call right after the hosted step. - Hosted (and no choice yet) keeps `-agent`. Your own agent defaults to `-`, following the adapter; a computer placement to `-`. The default updates with the mode and the adapter only while the person hasn't typed a name, and a typed name is never replaced. - If a name still collides with the person's hosted agent, the `in_process_agent_no_token` refusal reads " is your hosted agent's name, and Commonly runs that one. Choose a different name for the agent you run yourself." (en, zh-CN), on both the own-agent and computer paths. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG --- frontend/src/i18n/locales/en.json | 1 + frontend/src/i18n/locales/zh-CN.json | 1 + .../__tests__/V2AgentBYOListenVerify.test.tsx | 5 +- .../v2/__tests__/V2AgentBYOMachine.test.tsx | 6 +- .../src/v2/__tests__/V2AgentBYOName.test.tsx | 133 ++++++++++++++++++ frontend/src/v2/components/V2AgentBYO.tsx | 47 +++++-- 6 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 frontend/src/v2/__tests__/V2AgentBYOName.test.tsx diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index db4438f0db..5e66a30db7 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1389,6 +1389,7 @@ "podRequired": "Pick a pod to install into.", "tokenEmpty": "Install succeeded but token issuance returned empty. Retry, or contact ops.", "installFailed": "Install failed.", + "nameIsHosted": "{{name}} is your hosted agent's name, and Commonly runs that one. Choose a different name for the agent you run yourself.", "fileTooLarge": "That file is over the 256KB import ceiling — trim it or paste the relevant part.", "importFailed": "Import failed.", "hostedCap": "You already have a hosted agent (beta allows {{cap}}). Connect your own runtime for more.", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 6bcc60ec01..d847e12fdd 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1449,6 +1449,7 @@ "podRequired": "请选择要安装到的 Pod。", "tokenEmpty": "安装成功,但令牌签发返回为空。请重试,或联系运维。", "installFailed": "安装失败。", + "nameIsHosted": "{{name}} 是你在 Commonly 上运行的托管智能体的名字。请给你自己运行的智能体换一个名字。", "fileTooLarge": "该文件超过 256KB 的导入上限——请精简,或只粘贴相关部分。", "importFailed": "导入失败。", "hostedCap": "你已有一个托管智能体(测试期允许{{cap}}个)。如需更多,请自带运行时。", diff --git a/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx index dc3fb13b2a..0a2c5bcf82 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx @@ -99,13 +99,14 @@ describe('V2AgentBYO listening verification', () => { await act(async () => { jest.advanceTimersByTime(4000); }); expect(screen.queryByTestId('byo-listen-ok')).not.toBeInTheDocument(); - // Second poll: the just-issued agent (sam-agent) authenticates. + // Second poll: the just-issued agent (sam-claude: your own agent defaults + // to `-`) authenticates. axios.get.mockResolvedValueOnce({ data: { issued: true, connected: true, lastUsedAt: new Date().toISOString(), - connectedAgent: { agentName: 'sam-agent', instanceId: 'default', podId: 'p1' }, + connectedAgent: { agentName: 'sam-claude', instanceId: 'default', podId: 'p1' }, }, }); await act(async () => { jest.advanceTimersByTime(4000); }); diff --git a/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx index 53b4a548ad..525bf00e39 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx @@ -156,13 +156,15 @@ describe('BYO on-my-computer mode', () => { fireEvent.click(screen.getByText('Add to this computer')); await waitFor(() => expect(screen.getByTestId('byo-machine-result')).toBeInTheDocument()); + // A computer placement defaults to `-`, never to the + // hosted agent's `-agent` (C14 rehearsal, 2026-10-20). expect(axios.post).toHaveBeenCalledWith('/api/registry/install', expect.objectContaining({ - agentName: 'sam-agent', + agentName: 'sam-local', podId: 'p1', config: expect.objectContaining({ runtime: { runtimeType: 'wrapper' } }), }), expect.anything()); expect(axios.post).toHaveBeenCalledWith('/api/agent-binding/request', { - agentName: 'sam-agent', instanceId: 'default', machineId: 'mach-a', + agentName: 'sam-local', instanceId: 'default', machineId: 'mach-a', }, expect.anything()); // Honest until the daemon reports it: waiting, not live. expect(screen.getByTestId('byo-machine-waiting')).toBeInTheDocument(); diff --git a/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx new file mode 100644 index 0000000000..683e51064e --- /dev/null +++ b/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx @@ -0,0 +1,133 @@ +// @ts-nocheck +// The default agent name follows where it runs (C14 rehearsal, 2026-10-20). +// "Run it here" takes `-agent`. When "Use my own agent" defaulted to the +// same name, a person who had just started a hosted agent was refused at the +// token step with "Commonly runs this agent, so it has no runtime token": the +// own agent shared the hosted one's name. +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import V2AgentBYO from '../components/V2AgentBYO'; +import { AuthContext } from '../../context/AuthContext'; + +jest.mock('../components/V2NavRail', () => () => null); +jest.mock('axios', () => { + const mock = { + get: jest.fn(), + post: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + defaults: { baseURL: '', headers: { common: {} } }, + interceptors: { + request: { use: jest.fn(), eject: jest.fn() }, + response: { use: jest.fn(), eject: jest.fn() }, + }, + }; + return { __esModule: true, default: mock, ...mock }; +}); + +const axios = jest.requireMock('axios').default; + +const authValue = { + currentUser: { _id: 'u1', username: 'sam' }, + user: { _id: 'u1', username: 'sam' }, + token: 'user-jwt', + loading: false, + error: null, + isAuthenticated: true, + register: jest.fn(), + login: jest.fn(), + logout: jest.fn(), + updateProfile: jest.fn(), +}; + +const pods = [{ _id: 'p1', name: 'Workspace', type: 'chat' }]; + +// Hosting on, so the page asks where the agent runs. +const mockGet = () => { + axios.get.mockImplementation((url) => { + if (url.startsWith('/api/hosted/availability')) { + return Promise.resolve({ data: { configured: true, caps: { agentsPerUser: 1, turnsPerDay: 30 } } }); + } + if (url === '/api/pods') return Promise.resolve({ data: pods }); + return Promise.resolve({ data: {} }); + }); +}; + +const renderAt = (path = '/v2/agents/byo') => { + window.history.pushState({}, '', path); + return render( + + + + + , + ); +}; + +const nameInput = () => screen.getByPlaceholderText('my-mcp-agent'); + +afterEach(() => { + jest.clearAllMocks(); + window.history.pushState({}, '', '/'); +}); + +describe('BYO default agent name follows where it runs', () => { + test('hosted keeps -agent; your own agent is -claude', async () => { + mockGet(); + renderAt(); + await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); + expect(nameInput()).toHaveValue('sam-agent'); + + fireEvent.click(screen.getByTestId('byo-mode-hosted')); + expect(nameInput()).toHaveValue('sam-agent'); + + fireEvent.click(screen.getByTestId('byo-mode-byo')); + expect(nameInput()).toHaveValue('sam-claude'); + + // And back: the defaults never leave the two modes on one name. + fireEvent.click(screen.getByTestId('byo-mode-hosted')); + expect(nameInput()).toHaveValue('sam-agent'); + }); + + test('a Codex link defaults your own agent to -codex', async () => { + mockGet(); + renderAt('/v2/agents/byo?adapter=codex'); + await waitFor(() => expect(screen.getByTestId('byo-mode-byo')).toBeInTheDocument()); + fireEvent.click(screen.getByTestId('byo-mode-byo')); + expect(nameInput()).toHaveValue('sam-codex'); + }); + + test('a name the person typed is never replaced by a mode switch', async () => { + mockGet(); + renderAt(); + await waitFor(() => expect(screen.getByTestId('byo-mode-byo')).toBeInTheDocument()); + fireEvent.change(nameInput(), { target: { value: 'my-reviewer' } }); + fireEvent.click(screen.getByTestId('byo-mode-byo')); + expect(nameInput()).toHaveValue('my-reviewer'); + fireEvent.click(screen.getByTestId('byo-mode-hosted')); + expect(nameInput()).toHaveValue('my-reviewer'); + }); + + test("a name that is the person's hosted agent's is explained, not 'has no runtime token'", async () => { + mockGet(); + axios.post + .mockResolvedValueOnce({ data: { ok: true } }) // /api/registry/install ("already installed" tolerance aside) + .mockRejectedValueOnce({ + response: { + status: 409, + data: { + code: 'in_process_agent_no_token', + error: 'Commonly runs this agent, so it has no runtime token. To run an agent yourself, connect your own agent instead.', + }, + }, + }); + renderAt(); + await waitFor(() => expect(screen.getByTestId('byo-mode-byo')).toBeInTheDocument()); + fireEvent.click(screen.getByTestId('byo-mode-byo')); + fireEvent.change(nameInput(), { target: { value: 'sam-agent' } }); + fireEvent.click(screen.getByRole('button', { name: /get my connect command/i })); + expect(await screen.findByText(/sam-agent is your hosted agent's name/)).toBeInTheDocument(); + expect(screen.queryByText(/has no runtime token/)).toBeNull(); + }); +}); diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index 904f5789f9..2a711cb4b1 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -30,6 +30,9 @@ const DEFAULT_SCOPES = [ 'posts:write', 'posts:read', 'memory:read', 'memory:write', ]; const DEFAULT_AGENT_NAME = 'my-mcp-agent'; +// The token routes refuse a name that belongs to an agent Commonly runs (the +// person's hosted agent): say that, not "has no runtime token". +const IN_PROCESS_NAME_CODE = 'in_process_agent_no_token'; const DEFAULT_POD_TYPE = 'chat'; // @latest matters: `agent run` learned to bootstrap from these env vars in // 0.1.9 — a machine with an older global install must upgrade, not skip the @@ -150,16 +153,20 @@ const V2AgentBYO: React.FC = () => { // shared literal default ("my-mcp-agent") 409s for every user after the // first one to accept it. Seed from the username so defaults never collide. const { currentUser } = useAuth(); - const defaultAgentName = (() => { - const u = (currentUser?.username || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); - if (personaCard) return u ? `${u}-${personaCard.key}` : personaCard.key; - return u ? `${u}-agent` : DEFAULT_AGENT_NAME; - })(); - const [name, setName] = useState(prefill.name || defaultAgentName); - // currentUser can resolve after mount — refresh the default if untouched. - useEffect(() => { - setName((prev) => (prev === DEFAULT_AGENT_NAME && defaultAgentName !== DEFAULT_AGENT_NAME ? defaultAgentName : prev)); - }, [defaultAgentName]); + const userSlug = (currentUser?.username || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); + // The default name follows where the agent runs (C14 rehearsal, 2026-10-20): + // "Run it here" takes `-agent`, so an own agent defaulting to the same + // name was refused at the token step once the hosted one existed. Your own + // agent defaults to `-`, a computer placement to + // `-`. A name the person typed is never replaced. + const defaultNameFor = (runMode: string | null, runner: string): string => { + if (personaCard) return userSlug ? `${userSlug}-${personaCard.key}` : personaCard.key; + if (!userSlug) return DEFAULT_AGENT_NAME; + if (runMode === 'byo' || runMode === 'machine') return `${userSlug}-${runner || 'local'}`; + return `${userSlug}-agent`; + }; + const [name, setName] = useState(prefill.name || defaultNameFor(null, '')); + const [nameTouched, setNameTouched] = useState(Boolean(prefill.name)); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [issued, setIssued] = useState<{ token: string; agentName: string; podId: string; issuedAt: number } | null>(null); @@ -251,6 +258,12 @@ const V2AgentBYO: React.FC = () => { // back to Claude or auto-detect, and vice versa. const [adapter, setAdapter] = useState(''); const [model, setModel] = useState(''); + // Re-default while the person hasn't typed a name: when the user resolves + // after mount, and when the mode or the runner changes. + const defaultAgentName = defaultNameFor(mode, mode === 'machine' ? adapter : listenAdapter); + useEffect(() => { + if (!nameTouched) setName(defaultAgentName); + }, [defaultAgentName, nameTouched]); const [placed, setPlaced] = useState<{ agentName: string; machineId: string; machineName: string } | null>(null); const [placedState, setPlacedState] = useState<'waiting' | 'running' | 'slow'>('waiting'); @@ -499,8 +512,10 @@ const V2AgentBYO: React.FC = () => { machineName: selectedMachineName, }); } catch (err) { - const e = err as { response?: { data?: { error?: string; message?: string } }; message?: string }; - setError(e.response?.data?.message || e.response?.data?.error || e.message || t('agentByo.errors.installFailed')); + const e = err as { response?: { data?: { error?: string; message?: string; code?: string } }; message?: string }; + setError(e.response?.data?.code === IN_PROCESS_NAME_CODE + ? t('agentByo.errors.nameIsHosted', { name: cleanName }) + : e.response?.data?.message || e.response?.data?.error || e.message || t('agentByo.errors.installFailed')); } finally { setSubmitting(false); } @@ -569,8 +584,10 @@ const V2AgentBYO: React.FC = () => { setMemoryError(null); } } catch (err) { - const e = err as { response?: { data?: { error?: string; message?: string } }; message?: string }; - setError(e.response?.data?.error || e.response?.data?.message || e.message || t('agentByo.errors.installFailed')); + const e = err as { response?: { data?: { error?: string; message?: string; code?: string } }; message?: string }; + setError(e.response?.data?.code === IN_PROCESS_NAME_CODE + ? t('agentByo.errors.nameIsHosted', { name: cleanName }) + : e.response?.data?.error || e.response?.data?.message || e.message || t('agentByo.errors.installFailed')); } finally { setSubmitting(false); } @@ -974,7 +991,7 @@ const V2AgentBYO: React.FC = () => { setName(e.target.value)} + onChange={(e) => { setNameTouched(true); setName(e.target.value); }} placeholder={DEFAULT_AGENT_NAME} className="v2-byo__input" /> From b337667396fc10f4f5240f1649e1c812fac79faf Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:11:16 -0700 Subject: [PATCH 2/2] test(byo): pin a persona link's name through mode clicks; mark the machine mapping defensive sprint-review on #2365 (PASS at 712d2ae7, two findings): - With ?persona= and no ?name=, only the persona branch's place in defaultNameFor keeps a mode click from renaming the agent; moving it below the mode branch stayed green. New arm; that mutation now fails it. - The machine path's in_process_agent_no_token mapping can't fire today: neither /api/registry/install nor /api/agent-binding/request returns the code. Kept, with a comment saying so. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG --- .../src/v2/__tests__/V2AgentBYOName.test.tsx | 16 ++++++++++++++++ frontend/src/v2/components/V2AgentBYO.tsx | 2 ++ 2 files changed, 18 insertions(+) diff --git a/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx index 683e51064e..7b44569dd8 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx @@ -9,6 +9,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import V2AgentBYO from '../components/V2AgentBYO'; import { AuthContext } from '../../context/AuthContext'; +import { PERSONA_CARDS } from '../agents/personaCatalogData'; jest.mock('../components/V2NavRail', () => () => null); jest.mock('axios', () => { @@ -98,6 +99,21 @@ describe('BYO default agent name follows where it runs', () => { expect(nameInput()).toHaveValue('sam-codex'); }); + // sprint-review on #2365: with ?persona= and no ?name=, nothing marks the + // name as chosen, so only the persona branch's place in defaultNameFor keeps + // a mode click from renaming it. + test('a persona link without ?name= keeps the persona name through both modes', async () => { + const card = PERSONA_CARDS.find((c) => c.availability === 'connect'); + mockGet(); + renderAt(`/v2/agents/byo?persona=${card.key}`); + await waitFor(() => expect(screen.getByTestId('byo-mode-byo')).toBeInTheDocument()); + expect(nameInput()).toHaveValue(`sam-${card.key}`); + fireEvent.click(screen.getByTestId('byo-mode-byo')); + expect(nameInput()).toHaveValue(`sam-${card.key}`); + fireEvent.click(screen.getByTestId('byo-mode-hosted')); + expect(nameInput()).toHaveValue(`sam-${card.key}`); + }); + test('a name the person typed is never replaced by a mode switch', async () => { mockGet(); renderAt(); diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index 2a711cb4b1..34e880d67e 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -513,6 +513,8 @@ const V2AgentBYO: React.FC = () => { }); } catch (err) { const e = err as { response?: { data?: { error?: string; message?: string; code?: string } }; message?: string }; + // Defensive: neither route this path calls returns the in-process code + // today (sprint-review on #2365). It reads right the day one does. setError(e.response?.data?.code === IN_PROCESS_NAME_CODE ? t('agentByo.errors.nameIsHosted', { name: cleanName }) : e.response?.data?.message || e.response?.data?.error || e.message || t('agentByo.errors.installFailed'));