docs: explain OpenRouter prompt-injection guardrail rejections - #1182
Merged
Merged
Conversation
OpenRouterAgent, PiAgent, and OpenCodeAgent with an openrouter/ model all use the key in OPENROUTER_API_KEY. When a guardrail that covers that key sets prompt-injection detection to Block, OpenRouter rejects each matching request with HTTP 403 "Request blocked: prompt injection patterns detected" before it reaches a model. #1149 blamed transcript-like examples in Ultrafuzz's prompts. OpenRouter's documented exact regexes match none of Ultrafuzz's prompt sources or the rendered prompts of a local run. They do match text Ultrafuzz does not write: OpenCode 1.18.18's default system prompt, used for models without a model-specific prompt (DeepSeek, Qwen, GLM), matches role_delimiter_injection, and so does ordinary Vyper or YAML source. Document the operator fix in docs/config.md. Set prompt-injection detection to Flag, or turn it off, on every guardrail that covers the key, because OpenRouter applies the most restrictive action across the workspace default and member or key guardrails. Do not use Redact, which forwards the request with each match replaced. Runtime behavior is unchanged: the rejection is an ordinary agent failure under the [retry] policy, and the doc points there instead of restating it. Closes #1149 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review follow-up for the OpenRouter guardrails section. - The section said the OpenCode openrouter/ model came from "the shipped profile". No OpenCode model profile ships: packages/config/defaults.toml and ultrafuzz.toml carry [agents.OpenCodeAgent] but no [models.opencode]. Drop the parenthetical, and correct the OpenCode agent section's "The default root config includes an opt-in OpenCode profile", the claim it repeated. The doctor paragraph's copy of the claim is left to #1179, which rewrites that paragraph. - OpenRouter's scan_scope for the prompt-injection builtin defaults to all_messages and can be set to user_only, so say that every message is scanned by default rather than always. - Say that the workspace default and member guardrails also cover other keys, that a workspace used only for the Ultrafuzz key confines the workspace-default change to that key, and that in an organization account only an organization admin can change guardrails. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every pull request in this batch inserts its entry at the same place in CHANGELOG.md, so each merge would conflict with the next. The entries are collected into one changelog update instead. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Problem
#1149 reports that OpenRouter rejects requests from the OpenRouter-backed OpenCode adapter with HTTP 403
Request blocked: prompt injection patterns detected. It blames transcript-like examples in Ultrafuzz's own prompt templates and proposes four things: a provider-specific prompt transform, snapshot pinning for that transform, a normalization record, and a live-provider fixture.Root cause
The 403 comes from OpenRouter's opt-in prompt-injection guardrail. It fires when a guardrail that covers the operator's key uses the Block action. Ultrafuzz's prompt sources and the rendered prompts of a local run match none of OpenRouter's 33 documented exact patterns. That check does not cover OpenRouter's typoglycemia, misspelling and encoding layers, which its docs describe but do not publish as exact rules.
What OpenRouter documents (prompt injection, guardrails, errors, guardrail API):
403before it reaches the model". The error message isRequest blocked: prompt injection patterns detected.scan_scopefor this detector "Defaults to all_messages" and can be set touser_only.Ultrafuzz's text does not match the exact patterns. I copied the 33 exact regexes from OpenRouter's documentation on 2026-09-28 and ran them over:
.ultrafuzz/prompts,packages/prompts/srcandpackages/runtime/src/templates, 88 files.None of them matched.
Text Ultrafuzz does not write does match:
v1.18.18inpackages/opencode/src/session/prompt/*.txt.default.txtandtrinity.txteach matchrole_delimiter_injection: the lineassistant: [runs ls and sees foo.c, bar.c, baz.c]followed by auser:line.SystemPrompt.provider()picksdefault.txtfor any model ID that is not Muse, GPT/o1/o3,gemini-, Claude, Trinity or Kimi, for example DeepSeek, Qwen or GLM.llm/request.tsputs it into the system prompt of every request, and Ultrafuzz never sees it.balances: HashMap[address, uint256]thenuser: address) and YAML (roles: [admin]thenuser: alice) both matchrole_delimiter_injection. I wrote these snippets myself; I did not find them in a real target.So rewording Ultrafuzz's prompts cannot prevent this. It is an operator setting.
Correction to the analysis this work item started from.
anthropic.txtis the prompt OpenCode uses foropenrouter/anthropic/claude-opus-4.8, the example OpenCode profile indocs/config.md(the default config ships no OpenCode model profile). It does not match any documented exact regex: its lines that end in]are followed by</example>, not by a role line. I do not know what tripped the reporter's request. The issue does not name the model.Change
Docs plus a CHANGELOG entry. In
docs/config.md:## OpenRouter guardrailssection, placed after the Pi agent section because it coversOpenRouterAgent,PiAgent, andOpenCodeAgentwithopenrouter/models. It:[PROMPT_INJECTION];[retry]policy. A retry on the same profile is rejected again when the match is in the task prompt or in the harness's system prompt.packages/config/defaults.tomlandultrafuzz.tomlhave[agents.OpenCodeAgent]but no[models.opencode], andgit log -G '\[models\.opencode\]'over both files is empty. The text now says to add a model profile. The doctor paragraph's "keeping the shipped[models.opencode]profile" is left alone, because perf(runtime): halve launch fsync work, and doctor stops failing on unused agent profiles #1179 rewrites that paragraph and removes the claim.I departed from the work-item spec in two places:
[retry] agentsfallback". On current main that is not what happens when the 403 repeats identically. The agent Task's retry policy (smithers.ts:7917) sets nomaxIdenticalFailures, so Smithers 0.35.0 falls back toDEFAULT_MAX_IDENTICAL_FAILURES = 3(@smthrs/scheduler/src/errorSignature.js). It marks the nodestalledon the third failure with the same error signature (makeWorkflowSession.js:878), before any later attempt or fallback profile runs. I read this in the pinned source; I did not run it. The new section points to the[retry]paragraph instead of restating it. That paragraph is itself wrong about identical failures on main, and fix(runtime): give agent retries a real wait, stop retrying deterministic failures, and label timeouts by code #1171 fixes it: it setsmaxIdenticalFailures: 0and states that "repeated identical failures do not end it before later attempts or fallback profiles run". The pointer is accurate only after fix(runtime): give agent retries a real wait, stop retrying deterministic failures, and label timeouts by code #1171 lands, so merge this PR after fix(runtime): give agent retries a real wait, stop retrying deterministic failures, and label timeouts by code #1171.Deliberately not built (and why)
AGENT_CONFIG_INVALID: that contradicts Add bounded error-agnostic agent retries with backoff and optional model fallback #572, which rules out matchers on provider error text. It would also stop the retry chain. Retrying can still succeed when the match came from content the failed session happened to read.Verification
There is no behavior change, so there is no discriminating test. A test that pins doc prose would prove nothing.
Run for the first commit:
BaseCliAgent,classifyQuotaErrorand the 8 non-retryable patterns (loaded from the pinned source) match neither the Codex-shaped message (unexpected status 403 Forbidden: {"error":{...}}) nor the bare message. Both therefore become a genericAGENT_CLI_ERROR.captureAgentFailureinpackages/runtime/test/generated-workflow-verifier.test.ts, reverted afterwards. It covered both message shapes, with and withoutAGENT_CLI_ERROR, on bothgenerateandpreflight. Every case came out with nocodeordetailsand the provider message intact. In the same run,402 gateway failures are promoted to Smithers quota parking controlsandagent failure normalization preserves only validated Smithers recovery controlspassed.session/retry.ts. A 403 with OpenRouter's documented body is not retryable there, and its internal retries are capped at 5, so OpenCode does not loop on it.Run for the review follow-up commit:
role_delimiter_injection, taken from OpenRouter's current page, over OpenCodev1.18.18default.txt,trinity.txtandanthropic.txt. It hitsdefault.txt:43andtrinity.txt:45once each andanthropic.txtnot at all. Re-readSystemPrompt.provider()at that tag.scan_scope"Defaults to all_messages" (PromptInjectionScanScopein the guardrail API reference).npx prettier --check docs/config.md CHANGELOG.md,node scripts/docs-check.mjs,node scripts/audit-profile-docs.mjs --check,node scripts/prompt-catalog-docs.mjs --check,CI=1 ESLINT_PLUGIN_DIFF_COMMIT=origin/main pnpm -w lint:strict:ci(a no-op, since no code files changed) andpnpm -w knip.git merge-treeagainst fix(runtime): give agent retries a real wait, stop retrying deterministic failures, and label timeouts by code #1171, fix(runtime): agent adapters never hang or reroute on telemetry and environment quirks #1173, refactor: delete dead prompt rename, config helpers, and CI scripts orphaned by #1131 #1175, perf(runtime): halve launch fsync work, and doctor stops failing on unused agent profiles #1179 and ci: validate every package on PRs, stop cancelling main runs, and add a global complexity ceiling #1184.docs/config.mdmerges cleanly with each.CHANGELOG.mdconflicts with all of them, because every PR adds to the same list.CI is red for a reason outside this PR's content.
External static analysisfails, andrelease-gatesfails because it depends on it. The cause is Super-Linter's markdownlint MD013 (lines over 400 characters) onCHANGELOG.md.docs/config.mdhas no markdownlint errors.CHANGELOG.mdfails the same way. ci: validate every package on PRs, stop cancelling main runs, and add a global complexity ceiling #1184 turns MD013 off.markdownlint-cli0.45.0 run with ci: validate every package on PRs, stop cancelling main runs, and add a global complexity ceiling #1184's.github/linters/.markdown-lint.ymlpasses on both files. Merge after ci: validate every package on PRs, stop cancelling main runs, and add a global complexity ceiling #1184, or re-run CI once it lands.Not verified:
instructionsfield. That field matters becauseOpenRouterAgentruns Codex withwire_api = "responses".Risk / compatibility
[retry]pointer, departure 2) and after ci: validate every package on PRs, stop cancelling main runs, and add a global complexity ceiling #1184 (CI).Closes #1149
🤖 Generated with Claude Code
The PR appears safe to merge based on the reviewed changes.
Summary
The PR documents how OpenRouter prompt-injection guardrails can reject agent requests and clarifies that operators must add an OpenCode model profile.
Reviews (3) · Last reviewed commit: "chore: move the changelog entry to the c..."