fix(risk): resolve run_payload's true risk on every gate surface#160
Merged
Conversation
run_payload's nominal classification is High, but its EFFECTIVE risk depends on the target path: a .sub transmit, a badusb .txt, or an evil_portal launch dispatches to a Critical operation. Only the interactive Run loop resolved this (resolveRunPayloadRisk -> escalate before the confirm gate). The other two dispatch surfaces gated on the static High classification, so a Critical payload could slip past the Critical gate: - RunTool (used by the rules automation engine and the campaign executor, and callable directly): computed max(spec.Risk, Classify)=High and never resolved the path, so with --confirm-risk critical a Critical .sub ran with NO confirm prompt. - MCP consent gate: classified by tool name only (High), so PROMPTZERO_MCP_ALLOW_HIGH=1 (without ALLOW_CRITICAL) executed a Critical run_payload — breaking the invariant that ALLOW_HIGH never unlocks Critical. The audit fail-closed floor (RequireOpen(High)) still held, so this was a confirm/consent downgrade, not an audit bypass — but it weakened two of the gates the project promises never to weaken. Fix: a single source of truth, risk.ResolveRunPayloadRisk(path), now used by all four surfaces (the Run loop, RunTool, the MCP consent handler, and the generator's genRunPayloadRisk). RunTool escalates from the path before the audit + confirm gates; the MCP handler resolves the effective level per call before consentDecision. The two duplicated copies (agent.resolveRunPayloadRisk, tools.genRunPayloadRisk) are removed/delegated so the surfaces cannot drift. Found via the internal audit sweep. Tests: - TestRunTool_RunPayloadCriticalGate: with confirm-risk=critical, a run_payload(.sub) via RunTool must hit the confirm gate at Critical. - TestServer_CallTool_RunPayloadCriticalNotUnlockedByAllowHigh: with ALLOW_HIGH=1 but not ALLOW_CRITICAL, run_payload(.sub) over MCP must be refused with the Critical-tier message. Both proven to FAIL on the pre-fix code and pass after. Verified: task ci green (lint 0 / vet / build / test:full 0 fail / govulncheck clean); task eval 17/17.
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.
Summary
A consent/confirm-gate downgrade for Critical
run_payloadoperations (category A, security).run_payload's nominal classification is High, but its effective risk depends on the target path — a.subtransmit, a badusb.txt, or anevil_portallaunch dispatches to a Critical op. Only the interactiveRunloop resolved this (resolveRunPayloadRisk→ escalate before the confirm gate). The other two dispatch surfaces gated on the static High classification:max(spec.Risk, Classify)= High, never resolved the path — so with--confirm-risk criticala Critical.subran with no confirm prompt.PROMPTZERO_MCP_ALLOW_HIGH=1(withoutALLOW_CRITICAL) executed a Criticalrun_payload, breaking the invariant that ALLOW_HIGH never unlocks Critical.The audit fail-closed floor (
RequireOpen(High)) still held, so this was a confirm/consent downgrade, not an audit bypass — but it weakened two gates the project promises never to weaken.Fix
A single source of truth —
risk.ResolveRunPayloadRisk(path)— now used by all four surfaces (Run loop, RunTool, MCP consent handler, generator). RunTool escalates from the path before the audit + confirm gates; the MCP handler resolves the effective level per call beforeconsentDecision. The two duplicated resolvers (agent.resolveRunPayloadRisk,tools.genRunPayloadRisk) are removed/delegated so the surfaces cannot drift.Found via the internal audit sweep.
Verification
TestRunTool_RunPayloadCriticalGate: confirm-risk=critical +run_payload(.sub)via RunTool must hit the gate at Critical.TestServer_CallTool_RunPayloadCriticalNotUnlockedByAllowHigh:ALLOW_HIGH=1(noALLOW_CRITICAL) +run_payload(.sub)over MCP must be refused with the Critical-tier message.task cigreen (lint 0 / vet / build /test:full0 fail / govulncheck clean);task eval17/17.Security fix — anchors v0.778.0.