symfonic.capabilities.human.tokens¶
tokens ¶
The pause-token service: mint, authenticate, bind, consume.
What this module owns is an ordering and a bound. Authenticity, key lifecycle
and the pin-less legacy-artifact policy are consumed from T2.3.6 through
:class:~.contracts.EnvelopeSignerPort -- no second signature scheme here, and
a suite asserts the package imports neither hmac nor secrets.
The ordering, in the one place it exists: verify before any claim is read, so a forged token cannot steer its own decoding; decode, refusing a body this build cannot account for; expire, where a closed window is "ask again" and never "denied"; bind the four axes under the registration's posture; and claim, last and exactly once -- the one step an honest caller cannot retry.
PauseTokenService ¶
PauseTokenService(*, signer: Any, ttl: TTLPolicy, consumption: Any = None, ledger: Any = None, clock: Callable[[], float] = time.time, pinless_policy: Any = None, binder: ScopeBinder | None = None)
Mint, validate, and consume pause tokens against exactly one winner-seam.
Source code in src/symfonic/capabilities/human/tokens.py
authenticate ¶
Steps 1–3: verify, decode, expire. No state is touched.
Verification precedes decoding so a forged envelope cannot choose how it is read; expiry is checked after decoding because the expiry claim is part of what the signature covers, so trusting it earlier would let a forgery declare itself fresh.
Source code in src/symfonic/capabilities/human/tokens.py
bind ¶
bind(claims: PauseClaims, *, scope: Any, session_id: str | None = None, run_id: str | None = None, call_id: str | None = None, registration: Any = None) -> bool
Step 4 — the four axes (HK2). Returns whether scopes were crossed.
None is "not stated" for the last three; the binder says why each is
separately refusable. The registration is the only thing that can open
cross-scope redemption, and it is passed in -- rather than looked up, or
reduced to a boolean a caller could pass -- so a caller cannot bind
against a different posture than the one it validates the answer with.
Source code in src/symfonic/capabilities/human/tokens.py
consume
async
¶
Step 5 — the single atomic claim. Losing it is a replay, not a fault.
Source code in src/symfonic/capabilities/human/tokens.py
drain_proof
async
¶
CUT-AIR-3 — proof that legacy-pinned tokens have drained.
mint
async
¶
mint(*, pin: Any, scope: Any, run_id: str, session_id: str, thread_id: str, checkpoint_id: str, payload: Any, root_run_id: str = '', name: str = ASK_USER, tool_call_id: str = '', interrupt_id: str | None = None, ttl_seconds: float | None = None, legacy_pinned: bool = False) -> MintedPause
Bind a pause to this scope, session, and request, and sign it.
Source code in src/symfonic/capabilities/human/tokens.py
record_retirement_horizon
async
¶
SCP-FRZ-2 — the date past which nothing may be extended.
Source code in src/symfonic/capabilities/human/tokens.py
scope_hash ¶
validate
async
¶
validate(envelope: Any, *, scope: Any, session_id: str | None = None, run_id: str | None = None, call_id: str | None = None, at: float | None = None, registration: Any = None) -> ValidatedPause
Authenticate, then bind. Still consumes nothing.
Source code in src/symfonic/capabilities/human/tokens.py
validate_and_consume
async
¶
validate_and_consume(envelope: Any, *, scope: Any, session_id: str | None = None, run_id: str | None = None, call_id: str | None = None, at: float | None = None, registration: Any = None) -> ValidatedPause
The whole ordering, for a caller with nothing to check in between.