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)
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/call → proxy_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-policyshape (c1 caller/allowances + the c2upstream_credentialblock):A missingcaller: 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 wildcardcaller.idfails startup (not a runtime deny — see §14), sounknown_callercannot occur at runtime under the static-config model. Multi-caller is a later arc. The allowance usestargets:(notallowed_effects:); anaction_classother thangithub_deploy_keyhas 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
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_call → classification_incomplete → no_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 withcredential_scope_unknown, never silentlycredential_scope_insufficient(an unknown is not an insufficiency). This gate is P61e-c2 (shipped): it reads the policy'supstream_credential.scopesagainst the action'srequired_scope_for(category)through a deterministic lattice that matches the authoritative credential-scope contract exactly (repo:deploy_key:writeis covered only byrepo:deploy_key:writeandrepo: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.v0baseline (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-manifeststartup 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 isproxy_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-outis 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)
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_deniedis 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, alongsideproxy observe), never an--enforceflag 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_reviewis 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 reviewis 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 missingcaller.idfails startup (non-zero exit, before the proxy starts — see §14), so under this static one-caller-per-stdio-session modelunknown_callercannot occur at runtime. (Theunknown_callerreason 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.