Receiver Autonomy
Receiver autonomy controls whether an agent may move an inbox message from received to accepted without interactive human confirmation. It governs message admission only — it does not grant runtime tool permissions and does not relax agent safety defaults. The auto_review mode arrived in v0.3.1; v0.3.2 added the scope envelope — first-class side-effect booleans, a post-acceptance threshold checkpoint, a pinned terminal-state taxonomy, and default-off continuation grants. v0.3.5 recalibrated the gates — sender-marked guardrails fences, negation and declaration-aware demotion, the allow_pr_artifacts policy action, a pinned reason-code taxonomy backed by executable conformance fixtures, schema-v2 audits with human-outcome recording, and hook-enforced envelope compilation. v0.4.0 made merge authority and issue-filing first-class declarables (merges_pr, files_issues), pinned completion_kind to a four-value enum, and instrumented checkpoints with paused_at_utc and breach_basis; v0.4.1 scoped envelope enforcement to the compiling session and made the evaluator persist its own audit records. v0.4.2 moves signature verification to message intake (under enforce, only a signed-verified message reaches the gates), bounds profileless admissions with a documented default scope envelope, names every envelope’s origin in scope_envelope_source, and binds receiver policy files to authorized signers via the audit policy_auth block. v0.4.3 extends continuation grants to the review loop — a human-approved grant can authorize in-scope follow-up review rounds — and specifies checkpoint re-authorization end to end: three ranked answer channels and a consumption rule binding each answer to the pause it clears.
Phase 1 ships two modes:
| Mode | Behavior |
|---|---|
always_pause | Default. The receiver pauses for human review before accepting any work. |
auto_review | The receiver may auto-accept a message only when it clears every deterministic gate below. Anything uncertain pauses. |
Sender trust is messenger-bound and is not part of autonomy v1. Sender identity is logged for traceability but never gates a decision — since v0.4.0, message signing can verify that identity cryptographically. In warn mode the verified result is telemetry only; under enforce (since v0.4.2) the gate invokes verification at message intake, before any evaluation — an unverified message is quarantined to dead_letter/ with an intake_rejected decision and never reaches the gates. Even then, a verified signature identifies the messenger; it grants no authority.
Receiver config
Section titled “Receiver config”Policy lives at agents/<receiver>/config.yaml:
autonomy: default_mode: always_pause auto_review_thresholds: max_estimated_minutes: 45 max_expected_files_touched: 5 destructive_ops: pause external_side_effects: allow_pr_artifacts auth_config_or_secrets: pause dependency_changes: pause public_visibility: pause git_push_or_deploy: pause allow_without_task_profile: - brainstorm_request private_repo_allowlist: - example-org/private-repo continuation_grants: enabled: falseWith no config file, the receiver behaves as always_pause. Malformed config also pauses, and oacp doctor surfaces it. Autonomy is opt-in: drop in a config to enable auto_review, remove it to revert. The continuation_grants block is default-off (see Continuation grants below).
Since v0.3.5, external_side_effects accepts three policy actions (every other knob is pause-only):
| Action | Behavior |
|---|---|
pause | Pause every declared external side effect. |
allow_pr_artifacts | Allow PR creation/update, review comments, issue comments — and, since v0.4.0, declared files_issues issue-filing — only when the declared target_repo appears in the receiver-controlled private_repo_allowlist; direct main pushes, merges, deploys, and publishes still pause. |
allow | Allow declared ordinary external side effects; non-demotable hard stops still pause. |
A private PR-artifact profile must set external_side_effects: true, target_repo: owner/repo, public_visibility: false, and at least one of creates_or_updates_pr / comments_on_github / files_issues (since v0.4.0) to true. The sender declaration is necessary but never sufficient — target_repo must also match the receiver’s own private_repo_allowlist. A profile that falls outside the private artifact class — an unlisted or missing target_repo, or declared external effects that do not satisfy the PR/review/issue-comment/issue-filing class — pauses with external_side_effects_not_pr_artifact; a contradictory profile (a declared PR, comment, issue, or commit alongside external_side_effects: false) pauses with declaration_error instead. A declared public_visibility: true never reaches the PR-artifact check — it pauses earlier with its own per-knob code, public_visibility_pause. Direct-main pushes and effective deploy/publish wording never reach that check either — Gate 3 hard-stops them first with hard_stop_external_side_effect. Merge authority is different since v0.4.0: a declared merges_pr is not a hard stop but always pauses at admission with its own code, merges_pr_pause (grant-coverable); undeclared merge wording still hard-stops. Branch commits and pushes needed to create the allowlisted artifact are folded into external_side_effects.
Message fields
Section titled “Message fields”A message may carry a top-level autonomy_hint: auto_proceed. It is advisory only — the receiver’s own config and evaluator are authoritative, and a hint never escalates a verdict.
Under auto_review, task_request and question messages must include a machine-parseable task_profile block in the message body:
task_profile: estimated_minutes: 20 risk_tier: P3 expected_files_touched: 3 destructive_ops: false external_side_effects: false touches_auth_config_or_secrets: false touches_dependencies: false public_visibility: false # side-effect booleans (v0.3.2) + target_repo (v0.3.5) target_repo: "" creates_or_updates_pr: false comments_on_github: false commits_changes: false sends_oacp_reply_only: true # capability granulars (v0.4.0) merges_pr: false files_issues: false continuation_grants: {}The core declaration is complete only when it includes both numeric fields, risk_tier, and all five legacy risk booleans. The granular side-effect booleans are optional but must agree with external_side_effects — a profile that declares a PR, comment, or commit while declaring external_side_effects: false pauses with declaration_error.
A missing block pauses with task_profile_missing; an unparsable one pauses with task_profile_unparsable — that is a pause, not a fatal schema error. Types listed in allow_without_task_profile (just brainstorm_request by default) may auto-accept without it. Since v0.4.2 that exemption is admission-only: a profileless admitted request receives a documented default scope envelope — 25 minutes, 2 files, reply-only, every capability and risk flag false, risk_tier mirroring the message’s declared priority (else P2) — instead of running unbounded, so the threshold checkpoint is always evaluable. A sender-attached voluntary profile on an exempt type replaces the default entirely, and every decision names its envelope origin in scope_envelope_source (task_profile / default_profileless).
The side-effect booleans added in v0.3.2 make the profile’s GitHub-facing footprint explicit: whether the work opens or updates a PR, comments on GitHub, commits changes, or only sends an OACP reply. The evaluator normalizes them into the scope envelope it checks in Gate 2, and the threshold checkpoint compares them against what actually happened after acceptance. A missing-profile message that obviously requests PR, GitHub, commit, push, or public work pauses with risk_obvious_no_profile rather than the generic task_profile_missing.
The capability granulars added in v0.4.0 — merges_pr and files_issues — make PR-landing authority and issue-filing declarable. A declared merges_pr always pauses at admission (merges_pr_pause) so merge authority passes a human at least once (a human-approved continuation grant may cover it); a declared files_issues joins the allow_pr_artifacts private-repository artifact class. Both are embedded in the compiled envelope and consulted by its gh classifier, and the declarable keys, grant-coverable keys, and the checkpoint’s side_effects_actual keys are the same set by construction — the checkpoint can never observe a capability the schema cannot declare.
Sender-marked guardrails
Section titled “Sender-marked guardrails”Since v0.3.5, senders may isolate non-operative safety language in a fenced body section:
```oacp-guardrailsDo not merge, deploy, publish, or touch credentials.```Gate 3 excludes well-formed oacp-guardrails fence contents from ordinary side-effect, auth/config/secrets, and ambiguous-scope pause classification, but records every matching term as a lexical_advisory — fenced text is never invisible to the audit. Destructive commands, direct main pushes, credential rotation, dependency installation, public-repository text, memory-SSOT text, and pricing/commercial content are scanned across the raw body and remain hard even inside the fence. An unclosed or differently labeled fence is not skipped.
Four-gate evaluator
Section titled “Four-gate evaluator”Under auto_review the receiver runs four gates in order and pauses on the first failure. If any required signal is missing or uncertain, it pauses.
- Message integrity — the message validates against the OACP schema, has not expired, and its raw-YAML
message_sha256is recorded before processing. The message ID must not already have been auto-accepted by this receiver (replay guard).autonomy_hintstays advisory. - Declared task profile — required for
task_requestandquestion. The profile is normalized into a scope envelope — time, files, risk booleans, side-effect booleans, and optional continuation grants.estimated_minutes(45-minute standard cap),expected_files_touched(5-file standard cap), destructive scope, sensitive scope, and side-effect scope are cross-checked againstauto_review_thresholds. Since v0.3.5, each breach emits its own pinned reason code (estimated_minutes_exceeds_threshold,dependency_changes_pause, …) rather than one generic code,allow_pr_artifactsis applied only to the declared private-repository artifact class whentarget_repomatches the receiver’s allowlist, and contradictory profile fields pause withdeclaration_error. - Receiver classification — the hard stops below, plus a pause whenever file scope is ambiguous or broader than the declared profile. For message types in
allow_without_task_profile, side-effect verbs in the body (deploy, publish, merge) are logged as notes rather than hard stops — destructive command tokens still pause. Path-like tokens such aspackets/deploy/are not treated as deploy verbs. Since v0.3.5, three demotion mechanisms reduce lexical false positives without weakening the always-hard categories: well-formedoacp-guardrailsfences are skipped (with every match logged as an advisory), demotable matches in clauses headed by no / not / never / do not demote tolexical_advisory_negated, and with a complete profile a side-effect or sensitive-scope match whose corresponding declaration isfalsedemotes to a logged advisory. Since v0.4.0, a complete profile declaringmerges_pr: trueroutes the lexicalmergeverb to the granularmerges_pr_pauseadmission pause instead of the blanket hard stop — undeclared merge wording still hard-stops. - Runtime / workspace — the worktree is clean or the task can be isolated to a fresh branch, no conflicting task is running on the same repo, and the required tools are available.
LLM judgment may only reduce false positives after all four deterministic gates pass. It can never override a hard stop.
Hard-stop override
Section titled “Hard-stop override”Regardless of mode — and regardless of any autonomy_hint — a receiver must pause when the message body contains:
- Destructive command tokens —
rm -rf,--force,--no-verify,--dangerously-skip-permissions. - Non-demotable side-effect actions — push to main, credential rotation, or dependency install.
- Non-demotable sensitive text — public repos or memory SSOT.
- Content sensitivity — pricing or commercial content, reported since v0.3.5 as its own
hard_stop_content_sensitivitycategory (content class, not action risk): no fence, negation, or declaration demotion applies.
For these always-hard categories the body wins over the declaration: the token pauses the message even if the task_profile marked the matching risk flag false, and even inside a guardrails fence. Ordinary side-effect verbs (deploy, publish, merge) and declared auth/config/secrets scope are hard by default but demotable since v0.3.5. The only standard external-side-effect exception is the configured allow_pr_artifacts private-repository class. Continuation grants never override these hard stops either — even when enabled, a grant may only cover declared external side effects for the scoped PR, comment, or commit fields it marks true.
Audit events
Section titled “Audit events”Every decision — accept or pause — writes one YAML file to agents/<receiver>/audit/autonomy_decisions/<YYYYMMDDTHHMMSSZ>_<message-id>.yaml. The write is mandatory; a partial record beats none.
schema_version: 2spec_version: "0.4.3"created_at_utc: "2026-05-12T13:23:25Z"receiver: codexsender: irismessage_id: msg-20260512132325-iris-de62message_type: task_requestmessage_subject: "Small docs cleanup"conversation_id: conv-20260512-iris-001parent_message_id: nullmessage_path: agents/codex/inbox/20260512132325_iris_task_request.yamlmessage_sha256: "..."decision: auto_acceptedmode: auto_reviewpolicy_path: agents/codex/config.yamlpolicy_sha256: "..."policy_auth: # v0.4.2 — binds policy_sha256 to an authorized signer status: verified signer_agent: codex signer_kid: "..." reason: nullscope_envelope_source: task_profile # v0.4.2 — task_profile | default_profilelessreason_codes: - task_profile_present - risk_threshold_passedco_occurring_reason_codes: [] # v0.4.0 — Gate-2 breaches masked by an earlier hard stop; [] means passed, never unevaluatedevaluator: # v0.4.0 — self-stamped provenance, copied verbatim from the gate source: scripts/autonomy_gate.py content_sha256: "..." executed: truebreached: []thresholds: max_estimated_minutes: 45 max_expected_files_touched: 5task_profile: estimated_minutes: 20 expected_files_touched: 3 destructive_ops: false # ... the full declared profile, including granulars when presentruntime: agent: codex model: gpt-5result: final_state: done completion_kind: auto_accepted actual_minutes: null actual_files_touched: null predicted_risk_materialized: false completed_at_utc: null envelope_enforcement: none threshold_checkpoint: evaluated: false actual_minutes: null actual_files_touched: null side_effects_actual: {} breached: false breached_fields: [] declaration_errors: [] breach_basis: null paused_at_utc: null action: not_evaluated human_outcome: recorded: false actor: null decision: null decided_at_utc: null decision_latency_seconds: null pause_reason_codes: [] grant: decision: not_recorded request_present: false reply_message_id: msg-... artifacts: []The result block records what actually happened: completion_kind, actual_minutes, actual_files_touched, predicted_risk_materialized, and a threshold_checkpoint sub-record. policy_path and policy_sha256 are null when the pause is caused by missing or malformed config; since v0.3.5 policy_sha256 is the SHA-256 of a canonical key-sorted serialization of the parsed policy, so comments and formatting don’t produce false drift. Audit schema_version: 2 (v0.3.5) adds thread identity and the structured result.human_outcome block; standing continuation grants trust only v2 records. The accompanying received -> accepted transition records accepted_by: autonomy_policy, human_confirmed: false, and the same reason_codes, so an auto-acceptance is always distinguishable from a human approval.
Since v0.4.0, result.completion_kind is a pinned four-value enum — auto_accepted, admission_paused, checkpoint_paused, config_malformed — naming the terminal shape of the evaluation only; the pause cause lives in reason_codes, the run state in result.final_state, and human decisions in result.human_outcome. Receivers copy the evaluator’s value verbatim. Two more v0.4.0 additions: the evaluator self-stamps its provenance into every decision, and when a message is signed, oacp verify --attach-audit records the canonical result.message_auth block — the one supported path for stamping signature outcomes into an audit. Since v0.4.1, the evaluator itself persists the complete, lock-serialized audit record to its --audit-dir destination. Since v0.4.2, the writer hardens the record further: scope_envelope: null on an admitted record is a schema violation it refuses to persist, result.completion_kind is validated against the pinned enum at write time, and runtime.model resolves at the writer — caller-first, then the OACP_RUNTIME_MODEL environment variable, normalized with provenance in model_source; a record with no signal carries an explicit model_unknown_reason, never a silent null. The policy_auth block records whether the consulted config.yaml was signed by an authorized signer — a tampered policy fails closed with policy_auth_invalid before anything reads it (see policy-file signing).
Human approval and decline outcomes
Section titled “Human approval and decline outcomes”When a paused task is approved, modified, or declined by a human, record the decision in the same audit file with oacp autonomy-outcome (v0.3.5):
oacp autonomy-outcome <audit.yaml> --decision approvedThe recorder copies the pause reason codes, computes decision latency from the audit’s created_at_utc, locks the read-modify-write, and refuses to overwrite a recorded outcome unless --replace is explicit. Grant handling is separate (--grant-decision), so a task approval never silently creates a standing grant. Since v0.4.0, checkpoint-paused records are accepted too — latency is measured from the checkpoint’s paused_at_utc rather than admission time — and the --actor convention is pinned to one canonical, whitespace-free handle per human, fleet-wide.
Worked example
Section titled “Worked example”A receiver with default_mode: auto_review receives:
## TaskClean up the build directory: `rm -rf dist/ && rebuild`.
task_profile: estimated_minutes: 5 expected_files_touched: 1 destructive_ops: falseDecision trace:
- Gate 1 passes — schema valid, not expired, hash recorded.
- Gate 2 passes — profile present and within thresholds.
- Gate 3 fails — the body matches
rm -rf. - Verdict:
paused, reasonhard_stop_destructive_command,matched_pattern: "rm -rf".
The receiver pauses before any action runs. The destructive_ops: false declaration is irrelevant — no mode overrides a hard stop.
Threshold-exceeded checkpoint
Section titled “Threshold-exceeded checkpoint”Auto-acceptance is not a blank cheque. If work expands past the declared scope envelope after acceptance, the receiver evaluates a threshold checkpoint — and self-pauses, notifying the sender with a canonical opener:
Blocked: autonomy threshold exceeded — files_touched expected 3, now 12Blocked: autonomy threshold exceeded — prompt was docs-only, now requires credential accessBlocked: autonomy threshold exceeded — task expanded into untyped/unconfigured capability
A breach is instrumented in the audit result:
result: final_state: paused completion_kind: checkpoint_paused actual_minutes: 25 actual_files_touched: 4 predicted_risk_materialized: true threshold_checkpoint: evaluated: true actual_minutes: 25 actual_files_touched: 4 side_effects_actual: creates_or_updates_pr: true comments_on_github: true commits_changes: true breached: true breached_fields: - actual_files_touched breach_basis: realized paused_at_utc: "2026-08-04T09:12:00Z" action: paused_for_reauthorizationSince v0.3.5, an undeclared side effect about to materialize — the profile said creates_or_updates_pr: false and the work now needs to open a PR — pauses with declaration_error, and threshold_checkpoint.declaration_errors identifies the offending field. This checkpoint is mandatory before performing any newly discovered capability or outward action.
Since v0.4.0, a breached checkpoint stamps paused_at_utc (the moment it fired — the basis for human decision latency) and breach_basis: declared_intent | realized, so a prospective declaration-correction pause — caught before the undeclared action materialized, declared via declared_intent_fields with every realized effect still false — is distinguishable from realized drift.
Since v0.4.3, re-authorization is specified end to end. Three channels can carry an answer to a paused checkpoint, and rank — never arrival order — decides conflicts: a receiver-side human (authoritative, recorded via oacp autonomy-outcome); a signature-verified sender reply, bounded by the receiver’s own admission policy — numerics are capped at the receiver’s thresholds, boundary actions only within the admission predicate, and merge authority is never sender-grantable; and GitHub comments, which are advisory-only. Clearing authority belongs to the first two alone — a GitHub comment never clears a checkpoint. A consumption rule binds each answer to the pause it clears: a scope-less approval clears exactly one checkpoint and is then spent (reuse against a later pause records checkpoint_reauthorization_stale), a scoped numeric budget stands for the rest of the task up to the granted value, and a boundary-action grant durably authorizes one granular capability until task completion — no answer shape waives future checkpoints wholesale. The audit surface separates the requested scope from the effective policy-capped grant, under the pinned codes checkpoint_reauthorized and checkpoint_reauthorization_stale.
Envelope compilation
Section titled “Envelope compilation”v0.3.5 turns the declared task_profile from reviewed intent into enforced runtime constraints. After a message is admitted, the receiver compiles the profile plus its own autonomy config into a runtime envelope:
oacp envelope compile <message.yaml> --receiver <agent>The envelope lands at agents/<receiver>/state/active_envelope.json and is enforced at the tool-call layer by a static runtime shim — for Claude Code, a PreToolUse hook (oacp-envelope-hook) registered once by oacp setup claude. Per-task constraints live only in the envelope file: no per-task settings mutation, effective mid-session, and a strict no-op while no envelope is active. The receiver compiles at task pickup and clears with oacp envelope clear at completion.
Compilation fails closed: a missing, unparsable, or invalid profile — or malformed receiver config — pauses the task with envelope_compile_error instead of executing unenforced. The compiler reuses the gate evaluator’s normalization and pattern constants directly, so admission spec and runtime enforcement cannot drift, and the receiver-side private_repo_allowlist is embedded at compile time — runtime enforcement never trusts sender declarations alone. A drift deny arrives with the canonical Blocked: autonomy threshold exceeded opener and is handled as a threshold checkpoint. The audit outcome block records envelope_enforcement: hooks | none — degradation on older runtimes is recorded, never silent. See oacp envelope.
Since v0.4.0, the completion-time oacp envelope clear executes from inside the enforced session — the hook sanctions it against the task’s newest audit record once result.final_state is terminal — and protocol-mandated bookkeeping (audit records, the receiver’s own inbox/outbox, the runtime scratchpad) no longer consumes the declared file budget. Since v0.4.1, enforcement is session-scoped: the envelope records the harness session that compiled it and no-ops for every other session, so a concurrent interactive session in the same repository neither inherits the task’s constraints nor consumes its budget.
Two v0.4.2 additions close the loop with message signing. Under signing.verify_mode: enforce, the compiler refuses to compile from a message that is not signed-verified, and the envelope’s message_sha256 names the verified snapshot bytes. And an admitted public_visibility: true task whose record carries a human admission approval gets an explicit, recorded enforcement branch: oacp envelope compile --audit <admission-record> deliberately does not compile — a compiled public envelope would deny the entire approved chain — and instead stamps envelope_enforcement: none with the named reason public_visibility_admission_approved into the audit record, under the audit lock. The degradation is a documented mode with the human as the bound, not a silent absence; unapproved public tasks and all private tasks keep the fail-closed compile path.
Continuation grants
Section titled “Continuation grants”Continuation grants are default-off — receivers ignore them unless their config sets autonomy.continuation_grants.enabled: true. The one supported grant kind is approved_thread_continuation, declared under task_profile.continuation_grants:
task_profile: estimated_minutes: 20 expected_files_touched: 1 external_side_effects: true creates_or_updates_pr: true comments_on_github: true commits_changes: true continuation_grants: approved_thread_continuation: scope: max_actual_minutes: 30 max_actual_files_touched: 3 creates_or_updates_pr: true comments_on_github: true commits_changes: trueA grant is considered only when all hold: receiver config enables continuation grants; the message has same-thread evidence via parent_message_id or conversation_id; the grant includes an explicit scope; and actual work stays inside that scope. If grants are disabled, the receiver logs continuation_grant_ignored_disabled and evaluates the message normally. If enabled but actual work drifts outside the grant, the receiver pauses at the threshold checkpoint. Grants never relax destructive, auth/secrets, dependency, public-scope, pricing, or memory-SSOT hard stops.
Since v0.3.5, a sender-declared grant block is a request, never authority: a standing grant resolves only from a prior schema-v2 audit in the same sender/thread with a human-recorded grant approval (via oacp autonomy-outcome). Without that recorded approval the receiver pauses with continuation_grant_missing_approval; a later denial revokes (continuation_grant_denied); and a declaration beyond the granted scope re-pauses with continuation_grant_scope_exceeded.
Since v0.4.3, a grant may also carry a review_loop scope — one repository, PR, and sender thread; the allowed lifecycle types; a round ceiling; a wall-clock expiry; and the permitted review side effects. With it, an in-scope same-thread follow-up review_request auto-invokes the reviewer instead of pausing for a per-round confirmation, recorded in a review_continuation audit block; everything else pauses under a pinned reason code — review_continuation_*, or review_loop_invalid for a malformed scope. The grant authorizes running a round, never its verdict — the live PR head stays authoritative, declared heads are compared by exact full-SHA equality, expiry and revocation bind at evaluation time, and only rounds that actually ran consume the round budget. See the review loop for the lifecycle the grant covers.
Terminal-state taxonomy
Section titled “Terminal-state taxonomy”Audit result.final_state is pinned to a fixed set — done, paused, blocked, superseded, error. The terminal shape of the evaluation lives in result.completion_kind, pinned since v0.4.0 to exactly four values — auto_accepted, admission_paused, checkpoint_paused, config_malformed — with the pause cause left to reason_codes. Pinning both enums keeps senders like iris and codex able to pattern-match outcomes across runtimes.
Mental model
Section titled “Mental model”auto_review is the OACP analogue of Claude Code’s acceptEdits mode: class-based pre-approval inside a local trust domain, bounded by bright-line hard stops. The analogy is about the user contract, not the mechanism — OACP decides pre-execution from message content and the declared task_profile, while runtime tools still enforce their own permissions at action time.
The canonical conformance fixtures — clean tasks, ambiguous scope, hard-stop triggers, and malformed config — live in tests/conformance/autonomy/.