Problem/Motivation
Coarse allow/deny behavior and mutable approval context cannot prove that a reviewer approved the exact action later executed. High-impact actions need typed decisions, stable reasons, obligations, and a single-use manifest that seals the actor, delegation, arguments, target, policy, and preconditions.
Proposed resolution
Introduce a typed decision contract and immutable, one-use action manifest for governed high-impact actions.
Acceptance criteria
- Decision outcomes include deny, allow, require approval, and allow with obligations, plus stable
- The manifest binds actor, delegation, normalized arguments, target and version, policy digest,
- Approval is separation-of-duties aware, grants no standing authority, and cannot be replayed or
- Reviewers see the exact diff/context and obligations before approval.
- Execution validates the sealed manifest and emits a correlated receipt with postconditions.
- Uncertain execution is reconciled without duplicate mutation.
- Tests cover tampering, replay, expiry, stale target version, concurrent execution, reviewer conflict,
reason codes.
approval identity, expiry, idempotency key, and preconditions.
widened.
break-glass constraints, and postcondition discrepancy.
Related work
Connector-side precondition and reconciliation work may be linked as supporting implementation, but the source-of-record decision and manifest contract remains authoritative here.
API changes
Adds versioned decision, action-manifest, approval, and execution-receipt schemas.
Comments
Comment #2
jmcerdaPublic working mirror: GitHub #111.
Drupal.org remains the authority for this work item; implementation discussion and pull-request linkage may occur in the mirror.
Comment #3
jmcerdaThis issue is large enough that a single implementation pass would be a monolith nobody can review, so here is a slice plan first — the same approach taken on #3616540, where splitting the work made both halves reviewable and let the first half ship while the second was still being designed.
WHAT ALREADY EXISTS, AND IS NOT BEING REBUILT
The mcp_sentinel_approval submodule already carries an approval request entity (requester, operation, target entity type and id, payload, status, decider, decided timestamp), an approval gate that decides which operations need approval, an executor that approves or denies, and a break-glass manager with time-boxed grants, expiry reaping and an unsafe-posture reaper. Version 2.8.0 added the evidence guard: correlation ids, a precommit appended inside the mutation's own transaction, execution receipts, and a reconciliation ledger for uncertain outcomes.
What this issue adds on top is the part that makes an approval provable: a typed decision vocabulary, and an immutable, single-use manifest binding what was approved to what executes. Today the approved payload is an unsealed string, so nothing prevents it from differing — by tampering, replay, or simple staleness — from the thing a reviewer actually saw.
PROPOSED SLICES
1. Typed decision contract. A decision value object and two enums: outcome (deny, allow, require_approval, allow_with_obligations) and stable reason codes, following the McpGovernanceReadinessReason precedent, where the code is the contract and the prose is only a hint. Internal gates return typed decisions; every call site behaves exactly as before. Ships dark, no behaviour change.
2. The sealed action manifest. A manifest record binding actor, delegation (consumer client id and the caller's request id), normalized arguments, target entity type, id and revision at request time, the active policy digest, preconditions, an expiry, and a single-use idempotency key — sealed with the audit chain's signing key so any later edit invalidates it. Reuses the key-resolution path the evidence guard already fails closed on. Created when a high-impact action needs approval; nothing requires one yet. Ships dark.
3. Approval binds to exactly one manifest. This is the behaviour change. A high-impact path refuses without a valid manifest; approval references one manifest and no other; execution re-validates the seal, the expiry, the target revision (a stale target refuses rather than executing against something the reviewer never saw) and the idempotency key, immediately before mutation. Separation of duties: the approver is not the requester and holds a distinct permission. Approval grants no standing authority — it authorises one manifest, once. The receipt is the existing evidence guard's, extended with postconditions.
4. Break-glass hardening. The existing grant becomes sealed, single-use, and unavailable to a standing superuser identity (uid 1 or any is_admin role), with its configuration and its use recorded as separately audited elevated events. Explicitly: break-glass authority cannot promote policy and cannot lift the no-agent-publish floor.
5. Reviewer context. The queue shows the exact diff the manifest encodes against the target's current state, plus any obligations, before a decision is possible. A reviewer who cannot see what they are approving is not a control.
6. Adversarial test pass. Threaded through every slice as it is built, then a final sweep across the matrix this issue names: tampering, replay, expiry, stale target version, concurrent execution, reviewer conflict, sealed and expired break-glass, superuser rejection, promotion separation, safety-floor preservation, postcondition discrepancy, and reconciliation of an uncertain execution without duplicate mutation.
Slices 1 and 2 ship dark and can land in one release; slice 3 is the one that changes what a governed high-impact action can do and deserves its own; 4 and 5 are independent of each other.
BOUNDARIES
The no-agent-publish floor and the source-authoritative denial stay exactly where they are: nothing in this issue can widen them, and slice 3's tests assert that an approval cannot. Hosted distribution of manifests, and any tenant-facing approval interface, are not in scope here.
Comments welcome on the split before implementation starts — particularly on whether slice 3 should also cover config-plane actions, or whether those follow separately as the evidence-required class did.
Comment #4
jmcerdaSlice 1 (typed decision contract) is in https://github.com/Wilkes-Liberty/mcp_sentinel/pull/139.
McpDecision plus outcome/reason enums. The approval gate can return the typed form (decide()) without changing who is gated. Ships dark: no production path requires a sealed manifest. Slices 2-5 are not in this PR.
Comment #5
jmcerdaSlice 2 (sealed action manifest) is in https://github.com/Wilkes-Liberty/mcp_sentinel/pull/140.
HMAC-sealed McpActionManifest is minted when a gated operation is queued, using the audit-chain signing key. A missing key stores nothing. Execution still follows the unsealed payload. Slice 3 is not in this PR.
Comment #7
jmcerdaSlice 3 (approval binds to one sealed manifest) is in https://github.com/Wilkes-Liberty/mcp_sentinel/pull/141, stacked on #140. Approving a gated operation now requires a valid HMAC-sealed manifest. The requester cannot approve their own request. Execution follows the sealed arguments. Slices 4-5 are not in this PR.
Comment #8
jmcerdaSlice 4 (break-glass hardening) is in https://github.com/Wilkes-Liberty/mcp_sentinel/pull/142, stacked on #141. A grant is HMAC-sealed and single-use. Uid 1 and any standing is_admin account cannot receive the role. An elevated operator cannot promote policy or lift the no-agent-publish floor. Slice 5 is not in that PR.
Comment #9
jmcerdaSlice 5 (reviewer context) is in https://github.com/Wilkes-Liberty/mcp_sentinel/pull/143, stacked on #142. The approve form shows the sealed action against the live target, plus obligations, before a decision is possible. A missing or invalid manifest hides Approve.
Comment #11
jmcerdaSlices 2-5 are merged to 1.x: GitHub #140 (sealed manifest), #141 (approval binds to one manifest), #142 (break-glass hardening), #143 (reviewer context). Adversarial coverage was threaded through those PRs. This issue stays open until a release is cut.
Comment #30
jmcerdaReleased in 2.13.0 (commit 180b6ae on 1.x). Marking Fixed.
Adversarial coverage now includes manifest expiry, a changed live revision, overlapping approve, and a still-pending second consume. The existing evidence receipt records postconditions. A sealed-vs-observed discrepancy is refused as postcondition_discrepancy. Approval execution emits that correlated receipt. Who is gated is unchanged. Uncertain-execution reconciliation stays on #3616539.
https://www.drupal.org/project/mcp_sentinel/releases/2.13.0
Comment #32
jmcerdaVersion set to 2.13.0 now that packaging listed it.