symfonic.capabilities.human.binding¶
binding ¶
Scope, session, and request binding.
The two hash formulas below are frozen. They are not chosen here — they were
chosen by the engine that minted every token currently in flight, and a token
minted before this extraction must validate after it. Both are reproduced
character for character, including the _ placeholder and the truncation to
sixteen hex digits.
Neither hash is a signature. Authenticity is the envelope's job (T2.3.6); these answer "is this the same scope / the same request?" once authenticity is already established.
ScopeBinder ¶
Checks the bindings a pause token carries. Holds no state.
call_id_of
staticmethod
¶
Which id this pause correlates on -- the split, in one expression.
ask_user joins on the reserved tool_call_id and a registered
interrupt on its own interrupt_id; a claim carries whichever its
family minted. Derived from the claims rather than from which subsystem
is asking, exactly as
:func:~symfonic.capabilities.human.checkpoints.payload_key derives the
metadata key.
Source code in src/symfonic/capabilities/human/binding.py
check ¶
check(claims: PauseClaims, *, scope: Any, session_id: str | None = None, run_id: str | None = None, call_id: str | None = None, cross_scope_allowed: bool = False) -> bool
Return whether this redemption crossed scopes. Raise when it may not.
Four axes, checked one at a time (HK2). Scope says which tenant, session says which conversation, run says which turn of it, and the call id says which of that turn's questions. They are four independent facts and each is separately refusable, because a redemption that satisfies three of them is a real, reachable mistake rather than a hypothetical: an operator with two paused runs open answers the wrong tab (run), an onboarding agent that asked twice gets the second answer filed against the first question (call), and a shared-inbox admin answers a colleague's session (session). Only the tenancy axis has ever had a bypass.
None means "not stated" for the last three, and a caller that does
not state them gets no check for them. That is why the resume route
states all four rather than trusting this default: the default is what a
transport left out, and a transport that forgets an axis must not be
the thing that decides the axis does not matter.
The session, run and call checks run regardless of
cross_scope_allowed: crossing scopes is about who redeems (a
tenant admin approving a sub-tenant's action), never about which run
gets resumed. Relaxing them all at once would let one opt-in reach every
paused run in the deployment.
Source code in src/symfonic/capabilities/human/binding.py
check_payload ¶
SEC-PTK-5 — the paused request is the one the token was minted for.
Source code in src/symfonic/capabilities/human/binding.py
payload_body_matches ¶
Whether a recorded body hashes to the token's request_hash.
request_body ¶
hash_payload ¶
hash_payload_body ¶
hash_scope ¶
The legacy PauseToken.hash_scope formula, unchanged.
Read structurally — the tenancy scope type lives outside this layer and must
not be imported into it. An object with no tenant_id is refused rather
than hashed as the empty tenant, which would make every unauthenticated
caller share one scope.
Source code in src/symfonic/capabilities/human/binding.py
payload_body ¶
The exact text :func:hash_payload hashes.
A model serialises itself; anything else goes through canonical JSON with
default=str so an unserialisable value degrades to a stable string
rather than raising in the middle of a mint.
Split out from the hash so the recorded payload can be the same bytes the
hashed payload was. Recording a differently-serialised body and re-hashing
whatever survives the round trip is how a model payload — and every token the
pre-extraction engine minted, which stored request.model_dump_json() —
ends up answering "the paused request was altered" at resume time.