# ADR 0177: An approval response is authorized separately from the approval request

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0177-an-approval-response-is-authorized-separately.md

---

# ADR 0177 — An approval response is authorized separately from the approval request

## Context

Tool approval already had a durable request side: a tool marked `user-approval`
parks the run, `pendingApprovals` reads the parked request out of canonical
messages, and `respondToApproval` writes a durable `tool-approval-response`.
The AI SDK's signed request proves that an answer refers to *this* request and
that its tool call and input are unchanged.

It proves nothing about the responder. In a multi-tenant or multi-channel
deployment, the signature says the request is authentic; it does not say the
principal who answered the channel is allowed to decide. Treating the two as the
same guarantee leaves a hole exactly where authority matters.

## Decision

`createHeadlessAgentHarness` accepts an optional `authorizeApprovalResponse`
policy. It runs before a decision is written and receives the responder the
caller supplied, the conversation, the request (`approvalId`, `callId`,
`toolName`, `input`) and the proposed decision. It returns `allowed` or
`rejected` with a reason.

A rejection is a fact about the responder, not about the approval. It is
recorded as one durable `approval/response-rejected` ledger event with its
reason, it does **not** write a `tool-approval-response`, and the request stays
pending — so the initiating principal, or any other responder the policy admits,
can still answer it. Absent the policy, behaviour is unchanged: the signature
check alone authorizes the response.

The policy judges only identity the application supplies as the decision
context. The harness does not authenticate it because it is not where the
session principal lives; the application wires the same verified principal it
trusts for the channel. What the harness guarantees is that the decision is made
on that identity and not on a field inside the signed request.

## Alternatives

- **Fold authority into the signed request.** Rejected: a signature and a right
  are different guarantees, and one cannot carry the other without making the
  signing key an authority oracle.
- **Drop the pending request on refusal.** Rejected: that lets one unauthorized
  responder destroy an approval the initiator still needs.
- **A durable promise/registry keyed by approval id.** Rejected: the request is
  already durable in canonical messages; a second store would be a second source
  of truth for the same wait (ADR 0175).

## Consequences

A channel can now say "this responder is not allowed" without losing the
request, and the refusal is auditable with its reason. The policy is synchronous
with the decision and cannot be bypassed by the request body. Applications that
need no response authorization keep the old behaviour by omitting the policy.
