Skip to content

[Hackathon] varuni7: delegatable capability tokens with cascading, partition-tolerant revocation#52

Open
varuni7 wants to merge 1 commit into
projnanda:mainfrom
varuni7:hackathon/varuni7-delegatable-auth
Open

[Hackathon] varuni7: delegatable capability tokens with cascading, partition-tolerant revocation#52
varuni7 wants to merge 1 commit into
projnanda:mainfrom
varuni7:hackathon/varuni7-delegatable-auth

Conversation

@varuni7

@varuni7 varuni7 commented Jul 1, 2026

Copy link
Copy Markdown

[Hackathon] varuni7: delegatable capability tokens with cascading, partition-tolerant revocation

Problem 4 (auth). The default jwt plugin cannot delegate a subset of a
capability, and revokes by exact token string with no parent/child relationship.
This ships delegatable: macaroon-style tokens where a holder mints a strictly
narrower, shorter-lived sub-token for another agent offline (no issuer
round-trip), and revoking a token severs its entire subtree.

Design

  • Macaroon HMAC chain. A child seal is HMAC(parent_seal, caveat), so a
    holder can only add restrictions (subset scopes, shorter TTL, bound
    audience) — never widen them. delegate() also checks the scope subset and
    clamps child TTL ≤ parent.
  • Cascade by construction. Revocation is keyed on the seal, not the token
    string. verify() recomputes the chain, and a revoked ancestor seal severs
    every descendant — no per-child revocation list.
  • Partition-tolerant revocation. Revocation state is a grow-only set
    (G-Set CRDT). merge is the set union — commutative, associative,
    idempotent — so replicas converge by gossip regardless of order, duplication,
    or loss. Safety is monotone (a seal once observed revoked is never accepted
    again), and a verifier that has not yet heard a revocation still accepts until
    it merges one.
  • Determinism. No uuid, no time.time(). Token identity is the seal (a
    pure function of content + secret); all time comes from an injected clock.
    Same seed → byte-identical trace.

Attacks defeated (adversarial validator)

The delegated_auth scenario + validators PASS on delegatable and FAIL on
jwt. Three attack classes are blocked, each a typed error:

  1. Scope escalation — child requests a scope the parent lacks → ScopeEscalationError.
  2. Stale / revoked parent — delegate or verify through a revoked ancestor → RevokedAncestorError.
  3. Audience confusion — token presented by an agent that is not its audience → AudienceMismatchError.

Plus a convergence check: after the revoked seal is gossiped, the revoked
intermediary can no longer mint tokens while its siblings still can — proof the
revocation propagated via CRDT merge, not just on the coordinator.

Verify it

uv sync
nest run scenarios/delegated_auth.yaml -o traces/delegated_auth.jsonl
python -c "
from pathlib import Path
from nest_core.validators import validate_trace
for r in validate_trace(Path('traces/delegated_auth.jsonl'), 'delegated_auth'):
    print(('PASS' if r.passed else 'FAIL'), r.name, '-', r.detail)
"
# Swap `auth: delegatable` -> `auth: jwt` in the YAML and the same four validators FAIL.

Tests

  • 51 new tests: example-based + hypothesis property tests (scope-subset,
    cascade over random chains, CRDT merge laws, determinism) + the 3 attacks +
    partition convergence, for both the plugin and the validators.
  • Full suite: 592 passed, 1 skipped. make ci-local: all 5 checks pass
    (ruff, ruff format, pyright strict, pytest).

Files

  • packages/nest-plugins-reference/nest_plugins_reference/auth/delegatable.py — plugin
  • registered in plugins.py _BUILTINS and nest-plugins-reference/pyproject.toml entry point
  • packages/nest-core/nest_core/scenarios_builtin/delegated_auth.py + scenarios/delegated_auth.yaml
  • 4 validators in nest_core/validators.py
  • docs/layers/auth.md
  • tests: test_delegatable_auth.py, test_delegated_auth_scenario.py

— with Claude Code

…rtition-tolerant revocation

Problem 4 (auth). Adds `delegatable`, a macaroon-style auth plugin: a token
holder can mint a strictly narrower, shorter-lived sub-token for another agent
offline (no issuer round-trip), and revoking a token severs its whole subtree.

Revocation state is a grow-only-set CRDT (RevocationSet): merges are the set
union — commutative, associative, idempotent — so replicas converge by gossip
regardless of order, duplication, or loss. Safety is monotone (a seal once
observed revoked is never accepted again), and a verifier that has not yet
heard a revocation still accepts until it merges one.

Cascade is by construction: a child seal is HMAC(parent_seal, caveat), so
verify() recomputes the chain and a revoked ancestor seal severs every
descendant with no per-child bookkeeping.

Determinism: no uuid, no wall-clock. Token identity is the seal (a pure
function of content + secret); all time comes from an injected clock. Same seed
-> byte-identical trace.

Ships:
- auth/delegatable.py plugin (fully typed, pyright strict clean), registered in
  both plugins._BUILTINS and a nest.plugins.auth pyproject entry point.
- delegated_auth scenario (coordinator + 3 intermediaries + 12 leaves) and 4
  adversarial validators (tree_built, cascade, attacks_blocked, convergence)
  that PASS on delegatable and FAIL on jwt.
- 3 attack classes defeated: scope escalation, stale/revoked parent, audience
  confusion.
- 51 tests (example + hypothesis property-based + adversarial); full suite 592
  passed. `make ci-local`: all 5 checks pass.
- docs/layers/auth.md updated.

Tests written by Claude.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant