Multi-hop delegation chains
A single hop is user → agent. A chain is user → A → B: agent A, already acting
for the user, hands the work to agent B.
Off by default. max_delegation_depth is 1, which refuses a subject_token
that already carries act. Set it to 2 or more to allow chaining.
Why it is safe, and where the risk actually is
Adding a hop can only narrow authority. The PDP evaluates the strict
intersection over the user and every actor, so:
effective = user ∩ A ∩ B ∩ …
B can never reach something A could not. There is no arrangement of a chain that
yields more authority than the shortest link allows.
The real cost is accountability, not authority: whoever authorised B is A, not
the user. The user consented to A. That is why the default is 1 — the mechanism is
correct by construction, but the governance question is one an installation should
answer deliberately rather than inherit.
The bug this had to avoid
Checking only the current actor would have made delegation a way to gain
authority. With a chain A→B, if the PDP asked only about B, an action A cannot
perform would go through as soon as A handed it to B. Every actor is queried, and
every actor must allow.
The same reasoning applies to the kill switch: freezing a mid-chain agent stops
the chain. If the freeze looked only at the last actor, it would be bypassable by
adding a hop.
The root grant governs the whole chain
Authority descends from the consent the user gave the innermost agent — the
first one delegated to. Downstream hops have no grant of their own and need none.
The consequence matters operationally: revoking the root grant stops the entire
chain, not just the last link. There is one thing to revoke, not N.
The claim on the wire
The current actor is outermost, per RFC 8693 §4.1:
{
"sub": "user:42",
"act": { "sub": "agent:B", "act": { "sub": "agent:A" } },
"pds_dgr": "dgr_01J9…",
"sid": "sess_…"
}
sid travels with the delegated token on purpose: every hop re-verifies that the
human’s session is still alive. Without it the second hop would have no way to
notice a logout, and the chain would lose its revocation hook exactly where it
gets longer.
What is refused
| Refusal | Why |
|---|---|
subject_token_already_delegated |
Depth is 1 — chaining is off |
max_delegation_depth_exceeded |
The chain is longer than configured |
delegation_chain_cycle |
A→B→A adds no accountability and hides a delegation loop as a legitimate chain |
invalid_scope |
Nothing survived intersecting every hop’s max_scopes — an agent cannot obtain by delegation a scope its own ceiling denies |
invalid_request on actor_token |
Refused even with multi-hop on: the acting party is already identified by client authentication, and RFC 8693 §2.1 allows omitting it precisely then. Accepting it would mean two sources for the actor’s identity, one of them unauthenticated |
Reading a delegated decision
checkDelegated() returns one sub-decision id per hop:
'sub_decisions' => [
'subject' => 'dec_…', // the user layer
'actor' => 'dec_…', // the current actor (unchanged for single-hop consumers)
'actors' => ['agent:B' => 'dec_…', 'agent:A' => 'dec_…'],
],
Because one denying layer denies everything, the useful question is which
one — each id replays on its own. The console’s decision playground renders
exactly this, one row per layer.
Configuration
// config/iam-agents.php
'max_delegation_depth' => 2,
Requires padosoft/laravel-iam-server ≥ 1.26 (the delegated token carries sid
forward through TokenIssuanceContext::setSessionId()).