symfonic.capabilities.human.binding¶
binding ¶
Scope, session, and request binding.
Scope binding v2 includes every canonical kind/id pair and the orthogonal namespace. Pre-v2 hashes are ambiguous and must be refused, even under an explicit cross-scope registration; ask the question again after upgrading. Payload hashing retains its existing wire contract. Neither hash authenticates a token: authenticity belongs to its envelope.
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 ¶
Bind the full root-first path and namespace without importing tenancy.
Structural callers must supply the canonical path, never only the lossy tenant/sub-tenant projection. JSON preserves kind/id and namespace boundaries (including None, empty and underscore); the prefix versions this contract.
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.