Skip to content

Every allowed action on a dry-run deployment's Audit page says it was not enforced #529

Description

@zopeVaibhav

On a deployment whose policy is in dry-run, the Audit page labels every allowed action "dry-run: recorded, not enforced", and it prints the same line under a tool call that content inspection actually stopped. The line exists to mark a refusal that dry-run let through, and today it cannot tell that case apart from either of the other two.

Three rows, one label

With { mode: "dry-run", deny: [], allow: ["true"] }, rendering the Audit page for these rows gives:

computer.action_refused  allowed: false  carriedOut: true   Blocked   dry-run: recorded, not enforced   (correct)
computer.action_allowed  allowed: true   carriedOut: true   Allowed   dry-run: recorded, not enforced
mcp.call_rejected        allowed: true   carriedOut: true   Blocked   dry-run: recorded, not enforced
  refusal: sensitive_tool_arguments

Only the first is a refusal that was not enforced. Nothing refused the second, and the third was stopped: the Bot got "The tool call was refused because its arguments contain credential material." and the vendor was never called.

Why it happens

The page draws the line when decision.mode === "dry-run" && decision.carriedOut (audit.tsx:381). carriedOut is the policy's forward, and an allowed action forwards in every mode (policy.ts:286), so every allowed row on a dry-run deployment qualifies. The browser gateway writes the field the same way (gateway.ts:1155).

The tool-call case is a second cause. Content inspection builds its refusal row by spreading the policy step's decided object (store.ts:3037-3039), which already holds carriedOut: verdict.forward (store.ts:2987). So the row for a call inspection refused says it was carried out, in enforce as well as dry-run. The changelog entry that introduced the field says it exists so "a reader can tell a call this deployment stopped from one dry-run recorded and let past", and this row reads the wrong way round.

Reproduction

  1. On a migrated test database, build a plugin store with the dry-run policy above, grant google-drive/search_files to a Bot, and call it with { query: "quarterly report" }. The row carries decision: { allowed: true, mode: "dry-run", carriedOut: true }.
  2. Call it again with { query: "quarterly report", nested: { apiKey: "..." } }. The call throws the credential refusal and writes mcp.call_rejected with refusal: "sensitive_tool_arguments" and decision.carriedOut: true.
  3. Render /admin/audit with those rows plus a dry-run deny that went ahead and an enforce refusal. The line appears under the first three and not under the enforce refusal.

Why it matters

Dry-run is how a rule is measured before it is switched on, and the Boundaries page promises "Nothing is stopped" in that mode. This line is how an operator reads what a rule would have refused. On every allowed row it stops meaning anything, and on a content refusal it tells them a Bot's call went out when it did not.

Severity

Low to medium. Enforcement is unaffected; only what the trail says is wrong. But the people who read these rows most carefully are the ones who switched dry-run on to read them.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions