Approval Workflow
Some writes shouldn’t be a flat allow or deny — they should wait for a human.
The canonical case is taint tracking: a
write that follows an untrusted read is exactly the shape a semantic
(content-driven) attack produces, and a hard deny is often too blunt — the
write might be legitimate, just unverifiable by the proxy alone. Approval
workflow adds needs_approval as a third policy outcome, backed by an
approve-and-retry queue an operator drives from the CLI/API or the
dashboard.
Enable with:
features:
approval_workflow: true
approval:
grant_ttl_seconds: 300 # how long an approval stays valid for the retry
A Rego policy opts into it by returning approval: true alongside allow
(see the Rego reference for the
full result contract and precedence rules) — pairing naturally with
input.tainted:
package wardline.authz
default allow = false
is_write { input.method == "tools/call" }
approval {
input.tainted
is_write
}
allow {
input.identity == "agent-abc123"
}
The flow
- A call the policy marks
needs_approvalis not forwarded upstream. Wardline enqueues a pending request and responds202 Acceptedwith a pending id — the caller must retry, not wait on an open connection. No held connections, no new async infrastructure: this is why it’s approve-and-retry, not approve-and-continue. - An operator lists pending requests and approves or denies:
Loopback-only by default, same posture asGET /approvals/pending POST /approvals/{id}/approve POST /approvals/{id}/deny/credentials/revoke. The dashboard (whenweb_uiis also on) shows the same queue with Approve/Deny buttons. - Approval mints a single-use, TTL-bounded grant for that exact
(tenant, identity, session, tool). The client’s retry consumes the grant and forwards upstream — the audit entry for that call is marked"approved via grant (pending id consumed)"so an operator can tell a pre-approved write from an ordinary allow. A second retry finds no grant left and re-enqueues: one approval authorizes exactly one write.
Fail-closed by default
If a policy returns needs_approval but approval_workflow is off, the call
is denied, never allowed — a policy author can’t accidentally get a soft
outcome silently upgraded to a hard pass just because an operator forgot to
flip the flag. Turning the feature on only ever adds a path forward for a
call that would otherwise be a flat deny; it never weakens anything already
enforced.
Boundary
Approval workflow is the human-in-the-loop half of the taint story: taint tracking flags a session as suspect, approval workflow is what you do about it instead of a flat deny. It doesn’t replace post-condition audit fields — an approved write can still silently no-op upstream, which is exactly what that feature’s claimed-vs- actual signal is for.