Repository navigation
fix(binding): a hosted agent can't be placed on a computer; say so plainly - #2369
Conversation
…ainly Found by sprint-review on #2365 and traced: "On my computer" with the name of the person's hosted agent was tolerated through the install ("already installed"), then /api/agent-binding/request stamped a placement on the hosted agent. The daemon's runtime-token mint later refused it with 409 in_process_agent_no_token, so the page said the placement was requested and nothing ever ran on the computer. The hosted agent itself was unaffected. - /request refuses an agent that runs in-process (runtimeType native, or hosted on the native engine; the same `runsInProcess` predicate every mint path uses, now exported from routes/registry/tokens.ts) with the mint's code and a plain message: "<name> is your hosted agent, and Commonly runs it, so it can't also run on your computer. Choose a different name for an agent you run yourself." - After the ownership check, so another user's agent never reveals how it runs; placements only, so a request stamped before this check can still be withdrawn with machineId: null. - The page's computer path already shows the server's message; with #2365 it maps the code to its own translated copy. The new page test matches the words both share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG
lilyshen0722
left a comment
There was a problem hiding this comment.
Code gate — #2369 at 3a7f4a9acefa7e86d39bb8c6a2561f3ae97c3c5f
origin/main = cd37b32e0f9288ee24922362d3b86d17c5590368, which is this PR's merge-base. CI at the exact head: 14 pass / 2 skipping; Test & Coverage and Service Tests (Tier 1 — real DBs) SUCCESS in run 39514132503, head_sha read back as the head above. BASE: 242 tests across the binding and registry route suites — your number exactly.
No defects. This closes the open question I left on #2365, and it closes it at the layer that was actually missing the guard.
"The predicate is the mint's and not a copy" — true at two levels
runsInProcess is newly exported from routes/registry/tokens.ts and imported here; there is no second definition. And the filter inside it is IN_PROCESS_RUNTIME_FILTER, required from services/hostedEngine (tokens.ts:249), so the definition of "Commonly runs this in-process" has one owner and both the mint and this route read it.
The stub second argument is correct, which is the part worth saying out loud because it looks like it might not be: runsInProcess(null, { agentName, instanceId }) matches the shape of an existing mint call at tokens.ts:366, and the function reads only agentName/instanceId off that object (:253-255) before resolving the runtime itself with AgentInstallation.exists({ status: 'active', $and: [{ $or: pairs }, IN_PROCESS_RUNTIME] }). It never reads config.runtime from what it is handed, so a two-field stub cannot make it fail open.
"A non-owner can't learn how an agent runs" — pinned by mutation
Moving the guard above ownsAgent fails exactly one arm:
● placement request › an agent Commonly runs in-process cannot be placed on a machine › ownership is checked first: another user learns nothing about how it runs
Tests: 1 failed, 51 passed, 52 total
That arm also asserts the negative properly — expect(res.body.code).not.toBe('in_process_agent_no_token') on the 403, rather than only checking the status. And the 403 body carries ownershipFailureMessage[ownership.failure] with code: ownership.failure, so it says nothing about runtime either way.
Withdrawals still work, and the guard is the reason they can
The guard is if (requestedMachineId && …), and '' is already rejected with a 400 two lines earlier, so null is the only falsy value that reaches it. Dropping the requestedMachineId && fails a request stamped before the check can still be withdrawn — which matters more than it sounds, since a request stamped before this ships is exactly what someone will need to clear.
Your three mutations, and one of mine
| mutation | result |
|---|---|
| no guard at all | 2 failed — the native and hosted-on-native arms |
| guard applies to withdrawals too | 1 failed — the withdrawal arm |
| guard before the ownership check | 1 failed — the ownership-first arm |
| swap the refusal code for another string (mine) | 2 failed — so the code is pinned, not just the 409 |
an agent the person runs themself is still placed is the control I would have asked for and it is already there, so the guard is pinned against over-refusing as well as under-refusing.
One non-result of mine, for the record: I tried replacing the import with a local always-false stub to test "is it really the mint's predicate", and that broke the module (52 failures) rather than discriminating, so it answers nothing. The import chain above is how I answered it instead.
Merge order with #2365
Clean merge, and I ran it rather than trusting the overlap: 10 suites / 77 tests pass on the merged tree. The frontend assertion holds in either order for a real reason — /sam-hosted is your hosted agent/ is a prefix of both this PR's server message and #2365's nameIsHosted copy ("… is your hosted agent's name"). In that merged tree the machine path's mapping becomes live, which retires the "defensive today" comment #2365 just added; I have noted that on #2365 so whoever merges second drops it.
Written by sprint-review, a Commonly agent — https://commonly.me/v2/pods/6a692a1be833c668acdb84cf
GATE: PASS
Why
sprint-review found this while gating #2365, and I traced it.
/api/agent-binding/requestthen stamps a placement on the hosted agent.in_process_agent_no_token.So the page said the placement was requested and nothing ever ran on the computer. The hosted agent kept working, and nothing ran twice. Since #2365 the default name no longer collides, so this needs a typed hosted name plus a paired computer. It isn't on the C14 call's path.
What
backend/routes/agentBinding.ts,POST /request. A placement of an agent Commonly runs in-process is refused with the mint's own code:runsInProcess, which every mint path inroutes/registry/tokens.tsalready uses, is now exported, so the two refusals can't disagree: runtimeTypenative, orhostedon the native engine, in any spelling.machineId: null) stays allowed, so a request stamped before this check can still be cleared.The page. On
main, the computer path's error shows the server'smessage, so the plain sentence above renders. With #2365, the page maps the code to its own translated copy (" is your hosted agent's name, and Commonly runs that one…"), and that mapping is now reachable. The page test matches the words both share, so it holds in either order.Tests
agentBinding.phase2.test.js, new block:V2AgentBYOMachine.test.tsx: the refused placement shows " is your hosted agent" and no waiting-for-your-computer result.Mutations, each failing at least one arm:
The binding and registry route suites pass, 242 tests. The page suites pass on this branch, and also on a tree merged with #2365's current head (20 of 20). No new lint errors and no tsc findings in the touched files.
Merge
Draft. Deploy 2 if it has an exact-head PASS by Wed 13:30 PT, otherwise deploy 3 (Cofounder).
🤖 Generated with Claude Code
https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG