Skip to content

Take the kind of human input from the path, not the body - #111

Merged
davidmckayv merged 1 commit into
CopilotKit:mainfrom
beardthelion:fix/human-input-kind-from-path
Aug 21, 2026
Merged

Take the kind of human input from the path, not the body#111
davidmckayv merged 1 commit into
CopilotKit:mainfrom
beardthelion:fix/human-input-kind-from-path

Conversation

@beardthelion

Copy link
Copy Markdown
Contributor

Closes #110.

The route checks :kind against the four gestures and then built the call as { kind, ...body }. The spread came second, so a body carrying its own kind replaced the value that had just been checked, and the gateway puts that value into the path it calls on the computer. A body of {"kind":"../computers/reset"} sent to /human/click wipes the profile; {"kind":"../exec","command":"..."} runs a command. Both carry the deployment's computer token.

What it does

Two layers, because they answer different questions.

The route spreads the body first and puts the checked kind last, so the value it verified is the value it sends. That is the whole of the root cause.

The gateway checks the gesture against a set before interpolating, because that is where the string becomes a path. kind is typed as a union of four and the route casts a parsed body to that shape, so the type is erased exactly where it would have helped. This is the layer that holds if another caller arrives later, and humanInput is worth guarding twice: it is the one acting method that writes no audit row, so a call that went somewhere else leaves nothing behind that says so.

Nothing else changes. The four gestures behave as before, and an unknown :kind in the path is still refused with 400 by the check that was already there.

Verification

Twelve tests across three files. Each layer was reverted on its own with the tests live: reverting the route turns the route tests red, reverting the gateway turns the gateway tests red.

Route (computer-routes.test.ts): a real gesture carries through with its coordinates; a body naming its own kind does not decide where the call goes; the same for the shell; an unknown path kind is still refused before the gateway is asked.

Gateway (computer-gateway.test.ts): four rejected values, asserting that nothing left the process rather than merely that something threw, since a request that goes out and comes back 404 also throws and that is the bug; plus all four gestures reaching their own endpoints.

End to end (human-input-end-to-end.test.ts, new): binds a socket, drives the real router through the real gateway and the real transport with no injected fetch, and asserts on what the far side received. This is the one that settles it. The .. is resolved by URL parsing inside fetch, so a test that reads back the URL string it passed in is describing its own argument rather than a request. With both layers reverted, the server receives /computers/reset, /exec and /health.

It stands in for agent-computer rather than running it, because that process opens Chromium at import time. What it reproduces is the part that matters: an HTTP API on the far end of the transport with endpoints beyond the four this route may reach.

server suite failure set is identical to main (integration tests wanting a Postgres). bun run --filter server typecheck and bunx biome check are clean on the changed files.

`/:botId/human/:kind` checks `kind` against the four gestures a person's mouse and
keyboard produce, and then built the call as `{ kind, ...body }`. The spread came
second, so a body carrying its own `kind` replaced the value that had just been
checked. `humanInput` puts that value straight into the path it calls on the computer
and the transport concatenates it onto the base address, so a fetch resolves the `..`
away and the request lands on a different endpoint entirely.

`{"kind":"../computers/reset"}` sent to `/bot-1/human/click` reaches `/computers/reset`,
which wipes the profile and every login in it. `{"kind":"../exec","command":"..."}`
reaches the shell. Both carry the deployment's computer token, because the transport
attaches it to everything it sends.

Three defences are not defeated so much as stepped around, since the request never
enters the paths that carry them. This route deliberately skips the policy decision and
the audit row, which is the price of a takeover: a person entering a password is
exactly what must not be recorded. The computer's own `humanMayDrive` check is keyed to
the four `/human/*` paths and never sees a request routed elsewhere. So the one route
that writes nothing down is the one that could be pointed anywhere.

The only gate in front of it is the Bot-access check every route under `/:botId/*` has,
so this is reachable by any signed-in person who may act as the Bot, and a Bot whose
visibility is public means everybody.

Fixed at both layers, because they answer different questions. The route spreads the
body first and puts the checked `kind` last, so the value it verified is the value it
sends. The gateway checks the gesture against the set before interpolating, because
that is where the string becomes a path: `kind` is typed as a union of four, and the
route casts a parsed body to that shape, so the type is erased exactly where it would
have helped.

Reverting either one turns tests red on its own.

The end-to-end test binds a socket and asks the far side what arrived, because the
stubbed-fetch tests either side of it asserted on a URL this process had just built.
The `..` is resolved by URL parsing inside fetch, so reading back the string passed in
describes the argument rather than the request. With both layers reverted, a real
server receives `/computers/reset`, `/exec` and `/health`.
@davidmckayv
davidmckayv merged commit 1cb9d7f into CopilotKit:main Aug 21, 2026
6 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.

The body of a human-input request picks which computer endpoint the server calls

2 participants