Run the example
From an Obversa checkout, run:Source
Request identity
The request digest covers the gate ID and version, the question text, the response schema, and the input bytes. The request ID contains that digest. The same question therefore keeps the same ID across process restarts. Changed question bytes produce a new ID. Presentation hints do not change either value.Claim, submit, and release
- Post:
postrecords the request as pending. - Claim:
claimassigns it to exactly one router and returns a claim token. The token is recorded in the replayable event history. - Submit:
submitaccepts an answer only from that router with that token and the posted digest. - Release:
releasereturns a claimed request to the pending list. ThedirectRouterhelper also releases the claim if its responder throws or submits an invalid response.
missing, claimed, answered, or superseded.
Submission refusals report missing, not-claimed, not-owner, stale, or
invalid. Release refusals report missing or not-owner.
Validation and storage limit
The built-in validator checks required top-level fields and the standard JSON types declared for top-level fields. It accepts a single type or a type union. It is not a full JSON Schema validator. A host that needs nested constraints must validate them before it submits the answer.createCallbackClient keeps its event history in memory. Replay proves the
history contains all callback state, but the host must store that history when
requests must survive a process exit.
Store callbacks with a run
createStoredCallbackClient(storage, runId) writes callback history into the
run’s event stream. Opening a new client on the same run rebuilds pending,
claimed, released, submitted, and superseded state from those events.
The stored client uses the event stream revision when it changes state. Two
routers that race for one request therefore produce one winning claim. A
storage failure does not leave an in-memory answer that was never recorded.
Stored replay accepts only the exact fields for each callback event. A
callback-submitted event contains kind, requestId, requestDigest,
routerId, and response. The request digest must be 64 lowercase
hexadecimal characters and match the posted request. A submission without
requestDigest, or with an extra field, does not replay.
The runtime does not convert that stored history.
The stored client records the answer. The host must turn that answer into a
graph node result. The graph executor does not do this.
Bind approval to exact bytes
createApprovalCallbackGate(definition, subject) adds the subject digest to
the request input before it creates the request ID. The subject names the input
artifact hashes, proof scope and artifact, proposed output bytes, effective
permissions, and a workspace anchor or null when no workspace is bound.
Changed subject bytes therefore create a different request.
Post that request with the same subject:
ApprovalSubjectError, whose code is SUBJECT_MISMATCH. A subject-backed
submission also needs caller-supplied actor data and an action decision of
allow, wait, or deny. The client validates the response, then writes the
callback submission and approval record in one event batch.
A repeated plain submission after the request is answered returns the typed
not-claimed result. For a subject-backed request, a missing actor or invalid
action decision returns the typed invalid result.
After a valid subject-backed answer, the same submission from the claim owner
returns success and adds no event. A different valid decision or actor from
that owner throws StorageError with REVISION_CONFLICT. A wrong claim owner
returns the typed not-owner result. A changed request digest from the claim
owner returns the typed stale result.
resolveApproval(storage, runId, { request, ...subject }) returns the stored
decision only while the request, run plan, graph, proof, inputs, proposed
output, permissions, and workspace anchor still match. Otherwise it returns a
wait decision. The actor data records what the caller supplied; this API does
not authenticate that actor. An approval record does not apply the proposed
change.