Quickstart

Five minutes, the whole loop: register an agent → user consents → exchange → delegated call →
revoke → the next exchange fails
. This assumes a running laravel-iam-server
host app.

1. Install next to the server

composer require padosoft/laravel-iam-agents
php artisan migrate

The module registers its RFC 8693 grant into the server’s own /oauth/token at boot — no core fork,
no second issuer. It also announces itself on the server’s GET /capabilities so consoles can show
their Agents/Delegations pages.

The default ConsentVerifier is the Null one: it refuses everything — an unconfigured module can
create zero grants. Pick a real verifier in config/iam-agents.php:

'consent' => [
    // Built-in (no extra packages): IAM-native step-up, real single-use claim
    'verifier' => \Padosoft\Iam\Agents\Consent\IamNativeConsentVerifier::class,
    // Or PSD2-grade (requires padosoft/laravel-rebel-step-up ^0.2):
    // 'verifier' => \Padosoft\Iam\Agents\Consent\RebelStepUpConsentVerifier::class,
    'session_resolver' => \App\Iam\SessionCookieResolver::class, // where YOUR app keeps the IAM sid
],

See Consent for what each verifier trades off.

3. Register and approve the agent

# Admin API (or use the Agents page in laravel-iam-console ≥ 1.2)
curl -X POST https://iam.example.com/api/iam/v1/agents \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name": "Order Copilot", "max_scopes": ["orders:read", "orders:draft"]}'

# Approval is the human gate: it pastes the agent's PUBLIC JWKS and creates the
# OAuth client — confidential, private_key_jwt, token-exchange grant ONLY.
curl -X POST https://iam.example.com/api/iam/v1/agents/{id}/approve \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
  -d '{"jwks": {"keys": [{"kty": "EC", "crv": "P-256", "x": "…", "y": "…"}]}}'

Give the agent its own permissions in the PDP (it is an ordinary subject, agent:{ulid}): this is
the agent half of the intersection.

4. The user consents

Two steps, both on iam/me/delegations (session-authenticated, your app’s guard):

# Step 1 — open the challenge, BOUND to these exact parameters
curl -X POST https://iam.example.com/iam/me/delegations/consent-challenge \
  -d '{"agent_id": "agt_…", "scopes": ["orders:read"], "ttl_seconds": 2592000,
       "purpose": "Draft weekly order proposals"}'
# → {"data": {"challenge_id": "…", "method": "…", "expires_at": "…"}}

# Step 2 — verify (e.g. the OTP the user received) and create the grant
curl -X POST https://iam.example.com/iam/me/delegations \
  -d '{"agent_id": "agt_…", "scopes": ["orders:read"], "ttl_seconds": 2592000,
       "purpose": "Draft weekly order proposals",
       "challenge_id": "…", "verification": {"code": "123456"}}'

Change any parameter between the two steps and the binding hash diverges: refused. The
confirmation is one-shot (consent_confirmation_id is UNIQUE).

5. Exchange

The orchestrator (never the LLM) holds the user’s token and the agent’s private key:

use Padosoft\Iam\Contracts\Delegation\{TokenExchanger, TokenExchangeRequest};

$delegated = app(TokenExchanger::class)->exchange(new TokenExchangeRequest(
    subjectToken: $userAccessToken,
    scopes: ['orders:read'],
    audience: 'mcp://crm-tools',
));
// sub = user, act = agent, TTL ≤ 300s, non-refreshable. Re-exchange when it expires:
// that re-check IS how revocation lands.

(TokenExchanger ships in laravel-iam-client ≥ 1.9;
the wire call is plain RFC 8693 §2.1 if you roll your own.)

6. The resource server enforces the intersection

Route::get('/orders', ListOrders::class)->middleware('iam.can.delegated:shop:orders.read');

iam.can.delegated (laravel-iam-client) verifies the bearer via mandatory introspection and
decides user ∧ agent ∧ grant-still-active. Both identities land in the request attributes, in
Laravel Context (so in every log), and in the delegation audit stream.

7. Revoke — and watch the next exchange fail

curl -X DELETE https://iam.example.com/iam/me/delegations/{grantId}   # user, one click, no step-up
# or org-wide: POST /api/iam/v1/delegation-grants/{id}/revoke         # admin kill-switch

The next exchange returns invalid_grant; the next checkDelegated citing that pds_dgr denies.
Maximum staleness = the delegated token’s TTL (≤ 5 minutes by default).