Skip to content

MCP upstream proxy — enforcing tools/call mode (P61e review-spec)

Status: shipped (assay v3.24.0, proxy-enforce); this review-spec is kept as the P61e design of record. This is the heaviest risk class in the proxy line. It extends the shipped, opt-in manifest-observation proxy (P61a–d, assay v3.23.0) from observe-only to enforcing: it forwards a privileged tools/call only after a fail-closed policy decision. Tracked as #1624. The binding decisions are agreed (§16, "Resolved decisions"); this is the design of record. The arc is complete and shipped: P61e-b (the deny-all enforcing run mode), P61e-c1 (caller-allowance), P61e-c2 (credential-scope), P61e-c3 (the drift gate + the first allow/forward path), and P61e-d (the assay.enforcement_decision.v0 per-call evidence record). The enforcing proxy forwards a privileged tools/call only after a clear allow from every gate (classification, caller-allowance, credential-scope, drift) — the narrowest such path — records every decision as evidence, and keeps the forwarded call's side effect asserted, never proven.

The one-line scope: the manifest-observation proxy answers "did this tool surface change?"; the enforcing proxy answers "should this specific privileged call be forwarded, right now, for this caller?" — and defaults to no.

The review sentence to keep: P61e turns tool-call forwarding into an enforcing, fail-closed decision point. It does not make the proxy an authorization server, does not prove side effects, and does not allow observe-only privileged forwarding.

0. Why this is its own arc

P61b–d deliberately never forward tools/call (not even observe-only) — a credential-bearing proxy that relays privileged calls without a blocking decision is the textbook confused deputy. P61e is the only slice that crosses that line, so it opens, all at once: caller authorization, upstream credential use on a caller's behalf, a policy decision before forwarding, confused-deputy prevention, proxy_denied semantics, the interaction with manifest drift, and the interaction with the side-effect evidence ladder. Each is specified below; none may be skipped.

1. The decision (Q1): enforce, or never forward?

Recommendation: forward a privileged tools/call only enforcing and fail-closed, never observe-only. Observe-only forwarding of a privileged call is exactly the trap P61a rejected and stays rejected. Manifest-observation mode remains the default and unchanged; enforcing mode is a separate, explicit opt-in run mode — not a flag on the same proxy, because a flag reads as a small variation and this is a different risk class.

Mode shape (decided): a distinct enforcing subcommand.

assay-mcp-server proxy observe ...   # manifest-observation (the shipped P61a–d mode)
assay-mcp-server proxy enforce ...   # enforcing tools/call (P61e)
Nested proxy observe / proxy enforce subcommands; if the clap layout makes nesting awkward, two top-level subcommands proxy-observe / proxy-enforce. Either way the enforcing mode is its own command, never implicit. A deployment that does not run the enforcing mode keeps P61b–d's behavior (every tools/callproxy_unsupported).

Implementation note (v0, P61e-b): the shipped CLI spelling is the top-level proxy-enforce subcommand (a sibling of the existing proxy), chosen so the shipped proxy --upstream-command … invocation is untouched and the enforcing slice stays the smallest possible change in a security-load-bearing PR. The design concept is unchanged — an explicit, separate enforcing proxy mode; folding it into nested proxy observe / proxy enforce is a later ergonomic change, not a v0 concern.

2. Claim, non-claims, boundary

Claim (when shipped): in an explicit enforcing mode, the proxy applies a deterministic, fail-closed policy decision before forwarding a privileged tools/call, denies the call when the decision is not a clear allow, and records the decision as evidence.

Boundary sentence: the enforcing proxy decides whether to forward a call against a declared policy; it does not prove the call's side effect occurred, does not verify the upstream's behavior, and does not classify maliciousness.

Non-claims: - not an authorization server: it mints no tokens and runs no OAuth flow of its own; - not a sandbox: an allowed call runs at the upstream with the upstream's own authority; - a forwarded-after-allow call's side effect stays asserted, never proven (the E9 ladder applies); - allow is a policy decision, not a safety guarantee; deny is fail-closed caution, not a maliciousness verdict; - no behavior verification, no descriptor semantic analysis, no LLM judge.

3. Threat model (SOTA, June 2026)

  • Confused deputy. A proxy holding a powerful upstream credential is induced by an inbound caller to make a privileged upstream call the caller itself was not authorized for. The MCP-specific form is acute when a proxy uses a static client identity, allows dynamic client registration, and forwards without per-caller consent. Mitigation: §5 (per-caller authorization) + §8 (credential boundary).
  • Post-approval drift (the rug-pull threat). The tool the caller approved may have mutated since approval (OWASP MCP03 post-approval descriptor mutation via tools/list_changed; see mcp-manifest-drift.md and the E12 lifecycle experiment). A call to a tool whose contract digest drifted since approval is invoking something other than what was approved. Mitigation: §9 (drift-aware enforcement) — genuinely the novel part of this design.
  • Excessive agency / ambient authority. The proxy's reach must not silently become the caller's reach. Mitigation: §5 + §8.
  • Credential exfiltration / token passthrough. Inbound caller auth must never be forwarded to the upstream, and the upstream credential must never leak into evidence. Mitigation: §8.

4. Architecture: a policy decision point before forwarding

MCP client ──► assay-mcp-server (enforcing proxy) ──► upstream MCP server
                     │  observe   (P57 tool-decision classification; P60/E12 manifest-drift state)
                     │  DECIDE    (PDP: allow / deny, fail-closed, BEFORE forwarding)  ← P61e
                     │  forward   (only on allow; the upstream runs with its own credential)
                     └─ record    (enforcement decision evidence, separate carrier)

The PDP runs on every tools/call, not only on privileged-classifiable calls — "unclassified" is not "safe", so an unclassifiable call is exactly the fail-closed case. The decision logic:

  • the call is classified privileged (P57c) → evaluate the privileged policy gates (caller allowance, credential scope, drift) and forward only on a clear allow;
  • the call is unclassified, or classification is incomplete → deny unless an explicit non-privileged allow rule matches; an unclassified call never passes through by default.

Non-tools/call methods keep the manifest-observation behavior (handshake, ping, tools/list); everything else stays proxy_unsupported. For P61e-b (below) the whole tools/call surface is deny-all; the allow path arrives only in P61e-c.

5. Caller authorization (the confused-deputy core)

The decision to forward a privileged call must consider who is calling and what they are allowed to do — not merely "the proxy can reach the upstream."

  • Caller identity is declared, not inferred (decided). Over stdio there is no transport identity, so v0 is one caller per stdio session, declared via explicit config — never inferred from the transport, no multi-caller, no OAuth, no token introspection. The shipped --enforce-policy shape (c1 caller/allowances + the c2 upstream_credential block):
    caller:
      id: "ci-agent"
    upstream_credential:                      # c2: the single credential the proxy holds
      alias: "gh-deploy"                       # referenced in evidence by alias, never by value
      scopes: ["repo:deploy_key:write"]        # checked against the action's required scope
    allowances:
      - action_class: "github_deploy_key"      # the only class with a matcher so far
        targets:
          - { owner: "acme", repo: "prod-app" }   # exact owner+repo; no wildcard
    
    A missing caller.id fails startup (not a runtime deny — see §14), so unknown_caller cannot occur at runtime under the static-config model. Multi-caller is a later arc. The allowance uses targets: (not allowed_effects:); an action_class other than github_deploy_key has no verifiable matcher in c1 and is fail-closed (no_declared_allowance) until its own gate slice.
  • Per-caller consent/authorization. The proxy keeps a per-caller registry of allowed privileged action classes/targets (operator-declared, consistent with the declared tool surface, P58). A privileged call with no matching per-caller allowance is denied, never forwarded.
  • Ambient authority is not caller authority. The proxy's upstream credential never becomes the caller's authority: the allowance is keyed to the caller, not to the proxy's reach.
  • No silent surface mixing. Assay's own tools and the upstream's tools are never merged into one list a caller cannot distinguish.

PDP inputs (all deterministic, no LLM): caller identity; the P57c-classified action + projected target; the declared per-caller allowance (P58); the credential scope the call would use (P59 credential-scope); and the manifest-drift state of the invoked tool (§9).

6. Fail-closed (the core invariant)

If the PDP cannot reach a clear allow — policy load error, classifier failure, unknown caller, inconclusive manifest-drift state, missing per-caller allowance, insufficient credential scope — the privileged call is denied and not forwarded. A proxy that fails open is worse than no proxy. "Inconclusive" is a deny, never a silent allow.

7. proxy_denied semantics

proxy_denied (the code reserved in P61b, now used) is returned for a policy denial, distinct from upstream_error (the upstream said no), proxy_failed (the proxy could not complete), and proxy_unsupported (method not handled in this mode). It carries data.origin: "assay-proxy" and a machine reason from the pinned set:

unknown_caller                          classification_incomplete
unclassified_tool_call                  no_declared_allowance
credential_scope_insufficient           credential_scope_unknown
manifest_baseline_missing               manifest_current_observation_incomplete
manifest_observation_ambiguous          manifest_drifted_since_approval
policy_unavailable                      policy_error
A denied call never reaches the upstream. credential_scope_unknown is distinct from credential_scope_insufficient: when coverage cannot be determined the denial reason is unknown, not insufficient (see §8).

Reason history across the slices. P61e-b used a single enforcing_mode_deny_all; c1 superseded it with per-gate reasons (unclassified_tool_callclassification_incompleteno_declared_allowance), and while c1/c2 had no allow path a fully-cleared call denied with a temporary pdp_gate_unavailable. c3 removes both: enforcing_mode_deny_all and pdp_gate_unavailable are retired and no longer emitted — a call that clears every gate is now forwarded, and every deny is a real gate reason.

8. Credential boundary (locked from P61a, extended)

  • No token passthrough by default. Inbound caller auth is never forwarded to the upstream (the standing no-passthrough invariant from P61b).
  • Upstream credential from operator config only, referenced by alias in evidence, never by value; raw key material is never stored.
  • Least privilege on the upstream credential (ties to P59 credential-scope): the proxy should hold only the scope the deployment legitimately needs, and a call requiring more than the declared credential scope covers is denied (credential_scope_insufficient). Hard non-claim: declared credential scope coverage is policy metadata, not a provider-verified token grant — there is no token introspection. When coverage cannot be determined, the call is denied with credential_scope_unknown, never silently credential_scope_insufficient (an unknown is not an insufficiency). This gate is P61e-c2 (shipped): it reads the policy's upstream_credential.scopes against the action's required_scope_for(category) through a deterministic lattice that matches the authoritative credential-scope contract exactly (repo:deploy_key:write is covered only by repo:deploy_key:write and repo:admin). The enforcement gate is never broader than that documented contract — repo:write, for instance, does NOT cover, and any scope not in the contract is unrecognized → credential_scope_unknown. Overbroad (admin breadth) still covers and is a recommendation, never a block in v0; an absent credential, an unrecognized scope, and a non-covering scope deny.
  • Audience binding (concept-aligned, RFC 8707 / OAuth 2.1). The proxy must not let a token or credential issued for one audience be borrowed to reach another; it does not upgrade or broaden caller authority. v0 need not implement token minting, but it must state and enforce that the proxy does not broaden authority.

9. Drift-aware enforcement (the novel contribution) — P61e-c3, shipped

Enforcement is tied to the manifest-drift state, connecting P60 (manifest drift), E12 (tool lifecycle), and P61e (enforcement): a privileged call to a tool whose contract digest changed since the caller approved it is invoking something other than what was approved — the post-approval-mutation payoff. The drift gate needs both of two evidence inputs, and is fail-closed without either:

  • the approval-time baseline comes from a declared approved manifest artifact only — the assay.declared_mcp_manifest.v0 baseline (or a future approval record that references it), never the first observed complete manifest of the session. A first-observed baseline can already be a post-rug-pull state, so pinning it and calling it "approval-time" would pin the wrong state. The baseline is the required --declared-mcp-manifest startup input (a missing/invalid/wrong-schema baseline fails startup, like the policy); a baseline that lacks the invoked tool → proxy_denied (manifest_baseline_missing);
  • a current complete observed manifest is required — the proxy reads it from its own observed tools/list (P61c). If the session has no complete current observation, or the invoked tool is absent from a complete one, the drift state is proxy_denied (manifest_current_observation_incomplete), because you cannot compare against a baseline without a current complete view of that tool.

Two observation-integrity rules close the rug-pull window (both fail-closed): - a post-approval tools/list_changed invalidates the current complete observation — the surface may have mutated, which is exactly the rug-pull signal, so the proxy will not authorize against the prior manifest until a fresh complete tools/list is re-observed (manifest_current_observation_incomplete until then). Observing a clean manifest then receiving a list_changed must never leave a stale digest authorizing a privileged call; - a duplicate-name (ambiguous) observed manifest is inconclusive — when the observed tools/list has colliding tool names the manifest is status: ambiguous (its manifest_digest is withheld; see mcp-manifest-drift.md), so the gate denies manifest_observation_ambiguous rather than pick one of the colliding per-tool digests. Likewise, a duplicate-name declared baseline fails startup (an approval baseline must be unambiguous — no first-match-wins).

The comparison is the per-tool tool_digest, looked up by tool name in both the baseline and the current observed manifest (the same canonical P60 projection on both sides; server id is not part of the per-tool digest). It compares the tool's contract, never the call arguments (those are the allowance gate). An exact digest match clears the gate; any difference is manifest_drifted_since_approval.

With both in hand: - current contract digest equals the declared approved baseline → the drift gate is satisfied (other gates still apply); - digest drifted since approval → proxy_denied (manifest_drifted_since_approval) — a hard deny; - drift state inconclusive (partial/unobserved, per the E12 boundary) → fail-closed deny, because "no drift observed" is not "no drift."

pending_tool_manifest_review is review-layer language, not a runtime proxy response; v0 is hard-deny (see §16-C). The proxy checks runtime manifest evidence on the call path instead of relying solely on prior review.

10. Side-effect evidence interaction

A forwarded-after-allow call's side effect stays asserted unless independently verified — the E9 ladder (asserted < observed_confirmed < audit_record_bound) is unchanged. The enforcement record notes the proxy allowed and forwarded the call; it never claims the upstream performed or persisted the action. Allowing a call and proving its effect are different, and the proxy only does the first.

11. Enforcement decision record (separate carrier) — P61e-d, shipped

The assay.enforcement_decision.v0 artifact, emitted by the enforcing path and kept separate from the manifest-observation artifact (the standing observation/enforcement separation). It records, per tools/call: caller id, the P57c-classified action + projected target (sensitive ids hashed) + target digest, the decision (allow/deny), the machine reason, the fail_closed flag, the derived drift_state (satisfied on allow / the specific drift reason / not_evaluated when an earlier gate denied), and the credential alias (never the token or the declared scopes). It records the policy decision only — it does not assert the side effect, and it deliberately carries no transport-outcome field: because it is written before the forward (so an allowed call is never forwarded unrecorded), it cannot honestly claim delivery, so an allow is the decision to forward and never a claim that the call reached or was performed by the upstream. A forward that then fails surfaces to the caller as proxy_failed (§12), never as a delivery claim in the record.

Implementation (P61e-d): opt-in via --enforcement-decision-out <path>; the proxy appends one compact JSON record per decision (NDJSON), for both allow and deny, so a killed proxy still keeps the decisions recorded so far. The record is deterministic (no timestamp). The diagnostic tracing line from c1–c3 stays as operability output; this artifact is the canonical evidence carrier. Safety rule (see §12): a record-write failure on an allowed call fails closed — the call is not forwarded (proxy_failed / enforcement_record_write_failed), never a silent unrecorded forward; a deny stands regardless (a missing deny-record is a completeness gap, logged, not a safety gap).

Name discipline (no overlap with the existing carrier): - assay.enforcement_health.v0 = mechanism / runtime-capability state (was enforcement active, did the mechanism attach) — the existing carrier; - assay.enforcement_decision.v0 = the per-call proxy policy decision (allow/deny + reason) — this new one. They never overlap: one is "could the mechanism enforce", the other is "what did the proxy decide for this call".

11a. Denied-call observation record (separate carrier)

The assay.denied_call_observation.v0 artifact is an optional sibling carrier for the caller-visible proxy denial surface. It exists for post-hoc review of attribution claims such as "the caller saw an Assay proxy denial", without turning the observation into a policy verdict. The record carries the called tool name, the classified target digest when classification produced one, the proxy error code, origin: assay-proxy, the machine reason, and a sha256: digest of the exact JSON-RPC response line sent to the caller.

Implementation: opt-in via --denied-call-observation-out <path> in proxy-enforce; the proxy appends one compact NDJSON record for each answered proxy_denied tools/call. The carrier is not written for allowed calls, and with the flag absent no file is created. A write failure is logged but does not change the denial response: the call is already fail-closed, and a missing observation record is a completeness gap, not a reason to synthesize a different verdict.

Non-claims: this carrier does not decide policy, does not assert the upstream side effect, does not certify safety or maliciousness, and does not replace assay.enforcement_decision.v0. A consumer that wants to bind a caller-visible proxy denial to a policy decision must join this observation record to a digest-bound assay.enforcement_decision.v0 deny record for the same tool and target digest.

Correlation (forward-looking, not shipped): v0 carries no request-id, so a record is correlated to its call and to an upstream observation by order + content. An optional, caller-supplied, opaque per-call correlation id — echoed by the proxy, never minted by it (so the record stays deterministic), validated to a bounded safe shape before it lands (so the id can't smuggle content into the evidence), and carrying no transport claim — is specified as the clean join-key in enforcement-decision-correlation-id.md. It is additive within v0, not a schema bump.

12. Failure semantics (extends P61a)

  • PDP cannot decide → proxy_denied (fail-closed), never forwarded;
  • upstream unreachable on an allowed call → proxy_failed (the decision stands; the forward failed);
  • malformed upstream response → never trusted (as P61a);
  • the enforcement record (P61e-d) is written for both allow and deny when --enforcement-decision-out is set; a per-call record-write failure on an allowed call fails that call closed (proxy_failed / enforcement_record_write_failed), so it is never a silent unrecorded forward; a denied call's record-write failure is logged but the deny still stands (the call is fail-closed either way). The per-call streaming record fails the individual call closed rather than exiting the whole session.

13. What enforcing-v0 is NOT

No token minting / OAuth authorization-server behavior; no sandboxing of the upstream; no behavior verification; no descriptor semantic analysis; no maliciousness classification; no LLM judge; no HTTP-upstream or multi-upstream (still stdio, one upstream, as P61a); no change to the manifest- observation artifact shape.

14. PR slicing

P61e-a  this review-spec (design doc, no code)
P61e-b  the enforcing run mode exists and is DENY-ALL: every tools/call -> proxy_denied
        (enforcing_mode_deny_all), the upstream receives nothing, the allowlisted list methods still
        forward, an unknown method is proxy_unsupported. NO PDP skeleton, NO inputs, NO allow path.
        (SHIPPED, superseded by c1.)
P61e-c  split gate-by-gate, each fail-closed and independently tested, NO forwarding before c3:
  c1    caller-allowance gate (deny-only): --enforce-policy with caller.id + per-caller allowances
        (P58); the PDP classifies each tools/call and denies with the first-failing-gate reason
        (classification gate before allowance); a fully-allowed call is still denied
        (pdp_gate_unavailable). github_deploy_key {owner, repo} only; no wildcard. (SHIPPED.)
  c2    credential-scope gate (deny-only): the call's required scope vs the declared credential scope
        (P59); credential_scope_insufficient / credential_scope_unknown. Still no forwarding. (SHIPPED.)
  c3    drift-aware gate (declared baseline + current complete manifest, P60/E12) + the FIRST allow /
        forward path: a call that clears every gate is forwarded; pdp_gate_unavailable is removed.
        (SHIPPED.)
P61e-d  the assay.enforcement_decision.v0 record + the side-effect-evidence interaction (asserted),
        kept separate from the observation artifact; a downstream consumer reads it separately. (SHIPPED:
        opt-in --enforcement-decision-out NDJSON, one record per decision, allow+deny, fail-closed on
        an unrecordable allow.)

Implementation note (P61e-c1): the enforcing subcommand now requires --enforce-policy <path> (a YAML file with caller.id and the caller's allowances). A missing, unreadable, or malformed policy — or one without caller.id — is a startup failure (non-zero exit), never a runtime deny: an enforcing proxy that cannot decide is a misconfigured service and must not start. Caller identity is the static caller.id only (no transport/env inference), so unknown_caller cannot occur at runtime. The PDP precedence is classification → allowance → pdp_gate_unavailable; the classification gate runs before allowance so a missing-target call reads as classification_incomplete, never as a target mismatch. The proxy emits a per-decision tracing line (caller, action class, a domain-separated target digest, reason) for operability only — it is a diagnostic log, not the canonical assay.enforcement_decision.v0 evidence artifact (that is P61e-d). c1 still forwards no tools/call.

Implementation note (P61e-c2): the PDP gains a third gate after the allowance match — the credential-scope gate. It reads the policy's upstream_credential.scopes against the action's required_scope_for(category) (P59) through a deterministic lattice that matches the authoritative credential-scope contract exactly — NOT the richer E10 measurement vocabulary. The enforcement gate is never broader than the documented contract (repo:deploy_key:write is covered only by repo:deploy_key:write and repo:admin; repo:write is unrecognized and does not cover), so broadening it is a deliberate, contract-and-fixture-backed change, never a guess. Precedence is now classification → allowance → credential-scope → pdp_gate_unavailable. Coverage maps to: covered (exact or admin-breadth — overbroad still covers, a recommendation not a block) → fall through; recognized but non-covering → credential_scope_insufficient; absent credential / unrecognized scope → credential_scope_unknown (an unknown is not an insufficiency). Fail-closed: an unknown required scope denies (credential_scope_unknown), never a silent pass. Only github_deploy_key has a documented lattice today; any other classified class is fail-closed credential_scope_unknown until its own contract slice lands (and is unreachable anyway — c1's allowance matcher only admits github_deploy_key). The policy grows an optional upstream_credential: {alias, scopes} block; the alias is reserved for the P61e-d evidence record and is not read by the gate. (In c2 a call that cleared the credential-scope gate was still denied with the temporary pdp_gate_unavailable; c3 replaces that with the drift gate and the allow path.)

Implementation note (P61e-c3): the PDP gains the fourth and final v0 gate — drift — and the first allow/forward path. The enforcing subcommand now ALSO requires --declared-mcp-manifest <path> (the assay.declared_mcp_manifest.v0 JSON baseline); a missing/unreadable/malformed/wrong-schema baseline fails startup, exactly like the policy — in enforcing mode both inputs are required, so the proxy never "enforces" without an approval baseline. The drift gate compares the invoked tool's approved baseline tool_digest against the current observed tool_digest (read from the proxy's own complete tools/list observation), denying manifest_baseline_missing (tool absent from the baseline), manifest_current_observation_incomplete (no complete current observation of the tool), or manifest_drifted_since_approval (digests differ). Precedence is classification → allowance → credential-scope → drift → ALLOW. A call that clears every gate is forwarded to the upstream and the upstream's reply is relayed verbatim — the single, narrowest allow path (exact allowance + covered credential + an approved baseline whose per-tool digest exactly equals the current complete observed digest), and the only place a privileged tools/call is forwarded. pdp_gate_unavailable is gone. The per-decision diagnostic tracing line now also carries decision=allow|deny and forwarded; it remains operability-only, NOT the canonical assay.enforcement_decision.v0 record (that is P61e-d).

P61e-b is deliberately just a deny-all enforcing proxy — no policy decision point, no inputs, no allow path. It lands the enforcement boundary so "fail-closed" and the proxy_denied-vs- proxy_unsupported distinction are testable the moment the mode exists (the same discipline as P61b's negative-forwarding test), without building any of the gate machinery yet.

P61e-b tests (mirror P61b's negative-first discipline):

proxy enforce: a tools/call -> proxy_denied (enforcing_mode_deny_all); the upstream receives nothing
proxy enforce: an unknown method -> proxy_unsupported (not proxy_denied)
proxy enforce: an allowlisted list method (tools/list) still forwards
proxy observe: a tools/call still -> proxy_unsupported (the modes stay distinct)
This keeps proxy_denied (policy denial, enforcing mode) cleanly separate from proxy_unsupported (method not handled).

15. Design rules (binding for the arc)

  • enforcing mode is explicit opt-in; the default stays manifest-observation;
  • the PDP runs before forwarding; an unclear decision is a deny;
  • ambient (proxy) authority never becomes caller authority; per-caller allowance required;
  • no token passthrough; no broadening of caller authority; upstream credential by alias only;
  • a privileged call into a tool that drifted since approval is denied (hard-deny in v0; review-mode is a later, explicit opt-in), never silently allowed, and the baseline is a declared approved manifest;
  • the PDP runs on every tools/call; an unclassified call is denied, never passed through by default;
  • a forwarded call's side effect stays asserted; allowing is not proving;
  • proxy_denied is distinguishable from upstream/proxy_failed/proxy_unsupported, with a machine reason;
  • enforcement evidence is a separate carrier from observation evidence.

16. Resolved decisions

  • A. Mode shape — decided: a distinct enforcing subcommand (proxy enforce, alongside proxy observe), never an --enforce flag on the same proxy. A flag reads as a small variation; this is a different risk class (§1).
  • B. Drift baseline source — decided: the approval-time baseline comes from a declared approved manifest artifact only (assay.declared_mcp_manifest.v0, or a future approval record referencing it), never the first observed session manifest. Enforcement also requires a current complete observed manifest; without either, the drift gate is a fail-closed deny (manifest_baseline_missing / manifest_current_observation_incomplete). You cannot enforce drift against a baseline you never established (§9).
  • C. Review-mode vs hard-deny — decided: hard-deny by default. pending_tool_manifest_review is review-layer language and is not a runtime proxy response in v0; the proxy is on the call path and must return a concrete decision. A --decision-mode review is a later, explicit opt-in, not v0 (§9).
  • D. Caller identity source — decided: one caller per stdio session, declared via explicit config (caller.id); no inferred transport identity, no multi-caller, no OAuth, no token introspection. A missing caller.id fails startup (non-zero exit, before the proxy starts — see §14), so under this static one-caller-per-stdio-session model unknown_caller cannot occur at runtime. (The unknown_caller reason stays reserved in the §7 pinned set for a future multi-caller arc where caller identity is per-request rather than static-config.) (§5, §14)
  • E. Scope of v0 — decided: stdio + one upstream + deny-all (P61e-b) then narrow allow (P61e-c); no token minting, no OAuth resource-server behavior, no HTTP upstream, no multi-upstream (§13).

One-line recommendation, now locked: explicit opt-in proxy enforce subcommand, fail-closed deny-all first (P61e-b), then a narrow allow gated by declared caller allowance + credential-scope + drift-since-approval against a declared approved baseline with a required current complete manifest, a separate enforcement-decision record (P61e-d), and the side effect kept asserted. P61e-a (this spec) is approved; P61e-b code is the next step, not part of this doc.