How it works
Delego is a deterministic Policy Decision Point. The agent proposes an action; Delego decides; the credential broker is the only component that ever touches a secret. Three properties make it more than a policy engine: an action-bound approval (the confused-deputy guard), a signed audit chain, and no LLM in the decision path.
Propose → allow / deny / needs_approval
Every action runs through one deterministic core, policy.evaluate, in a fixed order: forbidden (hard blocks, always deny) → rules (first match wins, subject to constraints) → default. The decision is one of three outcomes:
It is fail-closed: the default is deny, and a rule that matches but whose constraints fail becomes a deny rather than slipping through. A rule matches on method / host / path / path_contains and can attach constraints — amount (cap + currency), allow_list (field-in-set), and rate_limit (counted from the ledger).
rules:
- name: place-order
decision: needs_approval
match: { method: POST, host: api.example.com, path: /orders }
constraints:
amount: { field: amount, max: 5000, currency: USD }
allow_list: { field: destination, in: [internal] }
default: deny # anything not allowed is refusedThe action-bound approval (confused-deputy guard)
This is the core idea. When a sensitive action returns needs_approval, the human “yes” is bound to one exact action fingerprint — a hash derived from the action’s method, host, path, and params — and to a hash of the original human instruction. At resolve time, Delego recomputes the fingerprint of the action the agent is trying to complete and compares it to what was approved.
So an agent that gets approval for action A cannot reuse it to run a different action B. A prompt injection that quietly adds a recipient or changes the destination produces a different fingerprint, and the resolve is refused:
# the human approved this exact order
order = ProposedAction(
instruction="place a small order",
method="POST", url="https://api.example.com/orders",
params={"amount": 2400, "currency": "USD", "destination": "internal"},
)
# a prompt injection adds a recipient but reuses the approval id
tampered = ProposedAction(
instruction="place a small order",
method="POST", url="https://api.example.com/orders",
params={"amount": 2400, "currency": "USD",
"destination": "internal", "recipient": "attacker"},
)
fw.resolve(approval_id, tampered).outcome # -> 'deny' (fingerprint mismatch)
fw.resolve(approval_id, order).outcome # -> 'allow', executed exactly onceThe approval is also single-use: it releases its bound action exactly once, so the same approval can’t be replayed to run action A twice.
The signed audit chain
Every decision — propose and resolve, allow and deny — leaves a receipt. Receipts form an append-only, Ed25519-signed hash chain: each receipt commits to the one before it. Editing a field, reordering, or dropping a receipt breaks verification, which reports the fault rather than trusting the ledger.
ok, problems = fw.audit.verify()
ok # -> True (chain intact and signed)
for r in fw.audit.tail(20):
print(r["seq"], r["outcome"], r["action_summary"])Two caveats worth knowing: hash-chaining does not by itself catch truncation of the most recent receipts (a tail-truncated prefix verifies clean), and the local signing key protects nothing against a full host compromise (use an HSM/KMS).
Head-anchoring: rollback detection
The fix for the truncation caveat is to anchor the chain head outside the ledger. delego verify prints the latest seq:entry_hash; persist that pair somewhere the ledger’s writer can’t rewrite, and pass it back on the next verification. A mismatch — or a shorter chain — is reported as truncation/rollback rather than a clean result. Without an anchor, verify says explicitly that truncation can’t be ruled out.
$ delego verify
Audit chain OK: all receipts intact and signed.
head: 42:9f3c1ab2… # persist this pair externally
$ delego verify --expected-head 42:9f3c1ab2…
Head anchor matches: no truncation/rollback against the anchor.No LLM in the decision path
Authorization is pure Python. A model can advise upstream — drafting a policy, summarising a request — but the decision that gates a credential is made outside the stochastic loop. That is the whole point: an injection can talk a model into anything, but it can’t talk a deterministic policy + a fingerprint check into approving an action the human never asked for.