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

Status: **review-spec, no code.** A new arc and the heaviest risk class in the proxy line. It extends
the shipped, opt-in [manifest-observation proxy](mcp-upstream-proxy-mode.md) (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/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](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):
  ```yaml
  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](declared-tool-surface.md),
  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_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 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](credential-scope.md) 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](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".

**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](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; Plimsoll consumes it separately (later). (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](credential-scope.md) 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.

[#1624]: https://github.com/Rul1an/assay/issues/1624
