Skip to content

fix(binding): a hosted agent can't be placed on a computer; say so plainly - #2369

Merged
lilyshen0722 merged 1 commit into
mainfrom
fix/placement-refuses-hosted
Oct 21, 2026
Merged

lilyshen0722 merged 1 commit into
mainfrom
fix/placement-refuses-hosted

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Why

sprint-review found this while gating #2365, and I traced it.

  1. On Bring your own, the person picks "On my computer" and types the name of their hosted agent.
  2. The install answers "Agent already installed in this pod", which the page tolerates.
  3. /api/agent-binding/request then stamps a placement on the hosted agent.
  4. The daemon on that computer later asks for a runtime token and gets 409 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:

409 { code: 'in_process_agent_no_token',
      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." }
  • Same predicate. runsInProcess, which every mint path in routes/registry/tokens.ts already uses, is now exported, so the two refusals can't disagree: runtimeType native, or hosted on the native engine, in any spelling.
  • After the ownership check, so another user's agent never reveals how it runs.
  • Placements only. Withdrawing (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's message, 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:

  • refused: hosted on the native engine, and plain native, each give 409 with the code, nothing stamped, and nothing in the daemon's work list;
  • withdrawal: a stamped request can still be withdrawn;
  • ownership first: a non-owner gets 403, not the in-process code;
  • control: an agent the person runs themself is still placed.

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:

mutation result
no guard 2 failed
guard on withdrawals too 1 failed
guard before the ownership check 1 failed

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

…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 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@lilyshen0722
lilyshen0722 marked this pull request as ready for review October 21, 2026 19:13
@lilyshen0722
lilyshen0722 added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit b34fa03 Oct 21, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant