Repository navigation
feat(tripwire): screen outside tool results for prompt injection - #87
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createTurnTripwire screens a tool result window by window with the moderator role model (reasoning disabled), metered as moderator on the run; a window that errors or misses TRIPWIRE_TIMEOUT_MS passes and leaves a 'tripwire-fail-open' call entry on the run. A trip logs tool-output-tripwire and, for a configured agent, pushes the injection event. The mock answers <tool_result_to_classify> for any mock id. Deployment switch: tripwire.enabled (TRIPWIRE_ENABLED). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…spected injection withProvenance screens the text it is about to wrap (result, media text, stringified object, thrown failure) when given the turn's tripwire; a suspected result is thrown as the WITHHELD_RESULT placeholder, and an abort during screening propagates instead of passing unscanned text. modelTools screens open-world tools only (isScanned), code_mode and page tools always. The loop stops after a tripped step with stopReason injection-suspected (detail: the source), stores the trip record on the call's toolMetadata, counts classifier credits toward the run budget and returns the tripwire for late verdicts. Task runs count the stop as cut short. Service tools declare openWorldHint explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oved calls included An approved call of an earlier turn runs before the continuation's first step, where stopWhen never looks: its trip now still yields the injection-suspected stop reason and notice, and its stored part carries the trip record. Waiting and awaiting-approval keep precedence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its original length capForScan's cut now reaches the record like the recorder's own bound: toolMetadata.truncated.totalChars is the whole result as the model would have read it, envelope included, on a call of the turn and on an approved call of an earlier turn alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A task run's webhook payloads are screened before its first model call; a trip stops the run with no model call (status error, failed + injection-suspected). A verdict settling suspected after its timeout withholds the result: on the turn's own parts while the loop runs, then on the stored part by toolCallId once the target closed (withholdStoredResult). The turn is not stopped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bhook screen runs once - the driver closes the turn's tripwire (kept on the live run) after close or fail - a task's reminder pass does not screen the webhook payloads again - a late verdict's in-loop persist is awaited before the loop returns or throws - a late trip is recorded on the run (note tripwire-late-suspected) - mock seam FAIL_AFTER_RESULT: the step after a tool result fails mid-stream Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation One step of the spec now costs the assistant's call plus the tripwire's scan of its tool result (same mock model, moderator role). At the former price a single step crossed the per-run budget, so the spec tested the wrong ceiling; it now also asserts the cap is crossed after a first step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moderation.md gains the tool-output tripwire section (scan set, criterion, fail open, stop, events, late verdicts, known gaps, cost, sources); web-access, mcp-tools, code-mode and the overview index point to it. Two injection simulation cases (a web page, a dataset row) planted in the MCP fixture, with their premises unit-tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ested results - A withheld webhook payload, on time or late, is replaced by the placeholder in the run's stored prompt (replaceWebhookPayload, withholdStoredResult), so read_conversation of the run no longer carries it. - read_conversation's view keeps a script's nested calls (data-trace) without their results, which the tripwire never screened (withoutNestedResults); the export a person downloads keeps them. - One withheldErrorText helper for the placeholder error text. - Logged once per process when the tripwire classifies with the assistant model (tripwire-assistant-model). - The URL named in the stop notice is cut at 200 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wo stages - Policy-guide system prompt (gpt-oss-safeguard form) with a self-incrimination rule: content addressed to the classifier or any screening is an injection. - Sandwich: the task is restated after the content, which sits in a tag carrying a random per-call nonce the content cannot name; any tool_result_ tag is defused. - Two stages (after Claude Code auto mode): a one-word yes/no screen on every window that fails open, and on a yes a structured confirmation that clears a false alarm and counts as suspected when it fails (tripwire-confirm-failed). - Mock seam reads the stage from the instruction after the wrapper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, review docs - TRIPWIRE_STAGE1_MAX_TOKENS 5 -> 16: the OpenAI Responses API refuses fewer, which would have failed every stage-1 call open. - An unparseable stage-2 answer is charged from NoObjectGeneratedError.usage. - moderation.md: late verdicts up to ~60 s, no overclaim on the wrapper, our stage 2 runs without reasoning; the design spec notes it is superseded in part. - MID-RUN cap test priced at 400/M so a step stays far under the cap as prompts grow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…failed confirmations A trip is a security event: lib-node's alert level. A result passing unscanned was recorded on its run only, so a broken moderator model turned the tripwire off with no counter moving; it now logs a coded warn that df_logs counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nonce wrapper, the sandwich instruction, the two stage calls (one word at 16 tokens, then a structured verdict), reasoning disabled, temperature 0 and the metering of unparseable answers move to api/src/guard. The tripwire uses them with no behaviour change; moderation takes withReasoningDisabled from there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The judged message is always in a nonce-tagged wrapper, a first message included, its context in a second one, the task restated after both. The policy is a policy guide with the self-incrimination rule. Two stages: one word biased to allow, then the structured verdict on a flag only. Moderation fails open at both stages; a failed confirmation is recorded as the new fail-open-confirm event and counted in the fail-open rate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tic codes moderation-fail-open (warn, with its cause), moderation-block (warn, ids only) and moderation-cooldown (alert, when a strike update arms it), counted by df_logs like the tripwire's codes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/moderation/:type/:id/probe adds a tripwire array: a benign table with an x-agent column hint, an injected page and text addressed to the classifier, each run through the tripwire's two stages on the live moderator model, with the stage-1 answer, the verdict, latency and error. Moderation's probes now run its two stages too and report stage 1. The activity page lists both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One shared engine section, a side-by-side table of what differs, then each guard's own: moderation's wrappers, two stages, fail-open-confirm and log codes; the tripwire's hardening points moved to the engine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ldown - guard/service.ts: one stage-1-then-stage-2 sequence (classifyTwoStage) and one error-to-cause mapping (failureCause, describeFailure), used by moderation's classifyMessage, runTripwireProbe and the tripwire's confirmation step. Failure directions unchanged. - The moderation probe carries the provider's error message again, as the tripwire's does (mock-failing seam, api test). - Strikes: a block counted during a running cooldown neither extends nor re-arms it; the arming update is identified by an id, so each cooldown alerts once, even for two blocks counted in the same millisecond (api test, mutation-checked). - Probe: both sets run at the same time; the response says whether the deployment runs the tripwire (tripwireEnabled) and the page marks it off. - Docs: the deployment switch in the table, "on a confirmed block"; the operations.ts header states its guides.ts exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ped too abortRunsOfConversation (and abortRunsOfAgent) aborted the live turns, then wrote the stop on the running runs. A turn going live after the abort loop, whose initial stop read reached mongo before the write, missed both until its next poll 2s later: a hello and a prompt sent back to back got two answers (session-turn.api.spec.ts:350, ~3% failures). The abort loop now runs again once the write is acknowledged: a turn live by then is aborted in-process, one going live after it reads the stop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 1 errs toward flagging dev/guard-eval runs labelled tool results and messages through both guards' real two-stage calls (moved to config-free classifier.ts files the services use) and reports misses, false alarms, stage-2 rate and latency by style of content. On it, a stage 1 that erred toward allowing let a third of the off-platform requests through unseen by stage 2: stage 1 now screens toward yes and stage 2, biased to allow, decides. The policy gains off-platform examples distinct from the eval's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he turn A trip used to end the turn on a fixed English notice naming tools; every simulated person had to ask what it meant. One more step now runs with no tool (report_outcome alone in a task run, needs-attention), having seen only the placeholder, and explains in the person's language that the source seems tampered with and what to do instead, never suggesting to paste it. The fixed notice stays as the fallback and as a task run's error. A verdict landing after a result passed on the timeout now stops the turn at the next step, and no further call runs (NOT_RUN_AFTER_TRIP); after the last step the answer gets LATE_TRIP_NOTICE. Its placeholder says the model had read the result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a destructive tool injection-web-page-subtle asks automated summaries to misreport a closure without any "ignore your instructions"; injection-task-run plants the harbour row in a weekly task whose agent can reset the workspace. readTheWeb now counts reads made inside a code_mode script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec compared credits billed to a full-rate price in euros. At the 0.008 EUR/credit peg that only held while cache reads were over 99.2% of the input, so it broke once the turn's prompt grew past ~1 008 mock tokens. Priced like usage.api.spec.ts (one token, one credit), it now checks the exact uncached amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before the agent model reads a tool result from outside the service, an LLM classifier on the
moderatorrole screens it for text addressed to an AI agent. A result that trips it is withheld, and the turn stops. Input moderation now runs on the same hardened classifier engine: two sides of one coin, untrusted user input and untrusted tool output.openWorldHint: false(the MCP default), page tools,read_fileand webhook payloads. The service's own tools andread_skillare not scanned. Neither isread_conversation, since transcripts are people instructing the agent, and it never serves a withheld payload back.needs-attentionoutcome and ends in error, with stop reasoninjection-suspected.agents:injection-suspected:<agentId>, and the server logs atalertwith codetool-output-tripwire.api/src/guard/):warnon a fail-open, block or failed confirmation,alerton a trip or an armed cooldown.dev/guard-evalruns a labelled set (65 tool results, 44 messages) through both guards' real two-stage calls.tripwire.enabled/TRIPWIRE_ENABLED, on by default.Why: users will mix web and data exploration with destructive tools whatever the docs say. These are business-data agents, so their tool output has no legitimate reason to address the agent. A tripwire raises the cost of an attack, since an attacker who tries several payloads is likely to trip it. It is not a security boundary. Design and sources are in
docs/architecture/moderation.md.Measured on the dev bridge, with Claude Haiku as the moderator:
Heads-up:
dev/guard-evaland the simulations on each production moderator model before relying on these numbers.api/src/conversations/model-loop.ts: the explanation step, the stop reason, the late-trip gate, webhook screening;api/src/conversations/live-runs.ts: the abort ordering, shared stop-path code.