Skip to content

Let the policy see the Enter a type action presses - #104

Merged
davidmckayv merged 1 commit into
CopilotKit:mainfrom
beardthelion:fix/type-submit-policy-bypass
Aug 22, 2026
Merged

Let the policy see the Enter a type action presses#104
davidmckayv merged 1 commit into
CopilotKit:mainfrom
beardthelion:fix/type-submit-policy-bypass

Conversation

@beardthelion

Copy link
Copy Markdown
Contributor

Closes #101.

A form has three doors and the policy could see two. computer_type presses Enter when submit is set, and the gateway passed no key for it, so an agent refused at the button and refused at the keypress typed into the field with submit: true instead and the form went through. Both shipped copies of the "never submit" rule name computer_key alone, so the deployment following the product's own advice was the one with the door open.

What it does

  • gateway.type carries key: "Enter" when the call submits. That is the field policy.ts added for exactly this reason, and this path was not setting it, so a rule about Enter now decides on it and the audit row records it. Without the key the trail said a field had been filled in and not that the form had been sent.
  • intentOf reports that action as activate rather than type, folding computer_type into the branch computer_key already used. What Enter does is press whatever has focus, whichever tool asked for it.
  • Both shipped rules name computer_type alongside computer_key, in .env.example and in the Boundaries preset.

The clause still guards key behind a tool name. That guard is load-bearing for a reason unrelated to this: an unevaluable deny expression matches, so an unguarded rule naming a browser field refuses MCP tool calls, which server/tests/plugin-store.integration.test.ts:243 already covers. Dropping it to shorten the rule would have traded this bug for that one.

Verification

Five new cases in server/tests/computer-gateway.test.ts, driving the real gateway and the real policy engine.

Fails before, passes after: the submit is refused by a rule about Enter, the audit row carries key: Enter, and the submit is refused by a rule about intent == "activate".

Must not move, and did not: typing without submit is still permitted under a rule that refuses Enter. That is the case a careless fix breaks, by reporting every type as a keypress.

The shipped preset now has its own test, running the exact string through the button, the keypress and the submit flag, and confirming a Bot can still fill a form in and take a screenshot. It lives in two files, one of them a string in the browser bundle, and they drift silently otherwise.

I also reverted each half of the change with the tests live to confirm they fail: dropping the key fails three, dropping the intent change fails one.

server suite failure set is identical to main. bun run --filter server typecheck and bunx biome check clean on the changed files.

Scope

An operator who wrote their own rule naming computer_key still has to widen it, and the preset change does not reach a policy already configured in an environment variable. That is unavoidable: the deployment's rule is the deployment's. What changes for everybody is that the key and the intent are now true, so a rule written about either catches this call.

A form has three doors and the policy could see two. `computer_type` takes a `submit`
flag that presses Enter once the text is in, and the gateway passed no key for it, so
an agent refused on clicking "Submit order" and refused again on pressing Enter typed
into the field with `submit: true` and the order went through. The audit row said a
field had been filled in, because it had no key to say otherwise.

The deployment following the product's own advice was the one with the door open. Both
shipped copies of the rule, the example in `.env.example` and the "Never submit a form"
preset on the Boundaries page, named `computer_key` alone.

Three changes, and the last is the one that closes it for anybody who has not written
their own rule:

- the gateway carries `key: "Enter"` for a type that submits, so a rule about Enter
  decides on it and the trail records it;
- `intentOf` reports that action as `activate` rather than `type`, on the same reasoning
  the keypress path already used: what Enter does is press whatever has focus;
- both shipped rules name `computer_type` alongside `computer_key`. The clause still
  guards `key` behind a tool name so it short-circuits on actions that have no keypress
  in them, which is what keeps a browser rule from refusing MCP calls.

The preset's behaviour is now pinned by a test that runs the shipped string through the
button, the keypress and the submit flag, and confirms a Bot can still fill a form in.
Two strings in two files drift otherwise, and the drift is silent.
@beardthelion
beardthelion force-pushed the fix/type-submit-policy-bypass branch from 7360bf5 to 1f73a91 Compare August 21, 2026 20:52
@beardthelion beardthelion reopened this Aug 21, 2026
@davidmckayv

Copy link
Copy Markdown
Contributor

The hole is real and the diagnosis is exactly right: on main a computer_type with submit: true reaches the policy with key == "" and intent == "type", so it sails past the shipped "Never submit a form" preset. Making the implicit Enter visible as key: "Enter", with one decision and one audit row that now says the form was sent, is the right fix — keep that.

What blocks it is the intent reclassification (server/src/computer/gateway.ts ~:847): because intent is single-valued, a type-with-submit now reports activate and no longer reports type. That silently breaks the very next shipped preset, intent == "type" && contains(element.name, "password") (app/src/routes/_authed/admin/boundaries.tsx) — it stops matching when a Bot types a password and submits in the same call, which is the ordinary shape of a login. That's the same "spell the input differently" bypass this PR closes, reintroduced one preset down, and the new tests don't cover it. Keep the key field change; either leave computer_type's intent as type, or widen the password preset to catch both. With that it's a merge.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified against main at 4f072bf, which has moved a long way since this was cut. Merges clean; 1145 pass, 8 skip, 0 fail on the merged tree, typecheck and format clean.

The bypass is real and I confirmed it independently before reading this. type() hands govern only ref and snapshotId, so a call carrying submit: true reaches the policy with no key, and the shipped preset's tool.name == "computer_key" && key == "Enter" clause cannot match. The first clause does not save it either: on that path the element is the field, so element.name is the field's label rather than the button's.

I have a particular reason to care about this one. Earlier tonight I validated that preset by driving it: applied Never submit a form, told a Bot to press Submit order on httpbin.org/forms/post, and got a refusal naming the rule, with carriedOut: false on the row. That validation was incomplete, and this PR is why. I proved one door was shut while a second stood open, and would have signed the boundary off on that basis.

What makes the fix right rather than merely effective:

  • The key is set only when submit is. The obvious fix is to report every computer_type as a keypress, which would start refusing ordinary text under a rule about Enter. The test "typing without a submit is still typing" is the one that matters here, and it is present.
  • intent moves with it. A submit reports activate, so the clause the preset actually leads with catches it too, not only a rule someone thought to write about key.
  • Both shipped copies are updated, .env.example and the Boundaries preset. Fixing the engine and leaving the advice stale would have left every existing deployment carrying the hole.
  • The row carries the key. Without it the trail says a field was filled in, not that a form was sent, which is the second half of the same problem.
  • policy.ts is corrected rather than left flattering. "A form has two doors" becomes three, and the remaining limitation is still stated plainly.

One note for afterwards, not a blocker: the fallthrough from computer_key into computer_type is deliberate and reads correctly, but it is the kind of line a later edit can break silently by inserting a case between them. The tests would catch it, which is the answer.

Good find, and the write-up on #101 with the three-row table is what made it quick to confirm.

@davidmckayv
davidmckayv merged commit bd55d8c into CopilotKit:main Aug 22, 2026
10 of 12 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.

A deny rule that stops a form submission is bypassed by computer_type with submit:true

2 participants