mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-05 22:14:11 +00:00
77 lines
3.7 KiB
Markdown
77 lines
3.7 KiB
Markdown
# Interface: approval workflow
|
|
|
|
Contract for the approval service, for whoever is going to build a human-approval UI/integration.
|
|
Implementation: [`src/emcp_bus/approval/models.py`](../../src/emcp_bus/approval/models.py),
|
|
[`approval/service.py`](../../src/emcp_bus/approval/service.py). See
|
|
[`AS-BUILT.md`](../AS-BUILT.md#approval-workflow-ep-14).
|
|
|
|
## When an approval comes into play
|
|
|
|
The PDP returns `outcome=REQUIRE_APPROVAL` (see [`pdp-opa.md`](pdp-opa.md)) when a transaction
|
|
exceeds `ClientProfile.restrictions.approval_threshold`. The Gateway calls
|
|
`ApprovalService.check_and_consume` on every attempt of that operation; without a granted and
|
|
still-valid approval, it creates an `ApprovalRequest` and denies the call with
|
|
`REQUIRE_APPROVAL:<reason_code>:<approval_id>` (see [`mcp-gateway.md`](mcp-gateway.md)).
|
|
|
|
## `ApprovalRequest`
|
|
|
|
```python
|
|
ApprovalRequest(
|
|
approval_id="...", # uuid4
|
|
operation_hash="...", # sha256(client_profile, tool, canonical arguments)
|
|
client_profile="Finance.Payments",
|
|
tool="finance.payment.execute",
|
|
arguments_summary={"amount": 15000, "region": "BR-SP"}, # not redacted in this RI
|
|
reason_code="APPROVAL_THRESHOLD_EXCEEDED",
|
|
requested_at="2026-09-14T12:00:00Z",
|
|
expires_at="2026-09-14T12:05:00Z", # ttl_seconds=300 by default
|
|
status="pending", # pending | granted | denied | consumed
|
|
)
|
|
```
|
|
|
|
`operation_hash` binds the approval to the **exact triple** `(client_profile, tool, arguments)` —
|
|
canonical JSON with sorted keys, so the same logical call always produces the same hash
|
|
regardless of dict insertion order. An approval granted for one call never satisfies a different
|
|
call with the same `client_profile`/`tool` but different arguments.
|
|
|
|
## Lifecycle
|
|
|
|
```
|
|
request() → pending
|
|
grant() → granted (emits an approval_granted audit event)
|
|
deny() → denied
|
|
check_and_consume() → True (exactly once) and marks "consumed"; False otherwise
|
|
```
|
|
|
|
`request()` is idempotent per operation: a repeated attempt of the same call while an approval is
|
|
`pending` returns the same request, and does not create a second one for the approver to
|
|
evaluate. `check_and_consume()` is the only way an attempt is released, and is single-use by
|
|
construction — never "enqueue and auto-release later": a `granted` approval that passes
|
|
`check_and_consume` is immediately marked `consumed` and never satisfies a second attempt, even
|
|
of the identical operation (README.md §46, "approval replay"). This check is protected by a
|
|
`threading.Lock` — without it, two genuinely concurrent attempts could both observe `granted`
|
|
before either wrote `consumed` (a real finding fixed during EP-15-T04, see `AS-BUILT.md`).
|
|
|
|
## Building an approval UI/integration
|
|
|
|
1. An approver watches for `tool_call_require_approval` events (see
|
|
[`audit-events.md`](audit-events.md)) — `approval_id` is carried in `payload`.
|
|
2. The UI/integration calls `ApprovalService.grant(approval_id)` (or `deny`) out of band — today
|
|
via CLI/direct Python call, there is no HTTP endpoint in this RI (see
|
|
`../Production-Recommendations.md`).
|
|
3. The original caller retries the same call (`tools/call` with the same arguments) — the Gateway
|
|
consults `check_and_consume` again and, now `granted`, releases the execution.
|
|
|
|
## Errors
|
|
|
|
| Situation | Exception |
|
|
|---|---|
|
|
| `grant`/`deny` on a nonexistent `approval_id` | `ApprovalNotFoundError` |
|
|
| Unexpected failure inside `check_and_consume` (any exception) | Handled by the Gateway as a denial — never as an implicit approval (EP-14-T03: `except Exception: approved = False`). |
|
|
|
|
## Not implemented in this RI
|
|
|
|
Persistent store (today an in-memory `dict` — loses all pending-approval state on a process
|
|
restart) and a real UI/notification for the approver. See
|
|
`../Production-Recommendations.md`.
|