TallwrightDocs

Writing a framework adapter for Cloud Proof

A contract for every package that connects an agent framework to Tallwright Cloud Proof (tallwright-<framework>), written before the first one is built (E11, 2026-10-08). It says what an adapter does at each event of a framework, and it is held to the one rule underneath all of it:

An adapter decides when to call Tallwright.capture and what content to hand it. Nothing else. The commitment, the line, the evidence directory, sending, resending, the receipt and the exhibit belong to tallwright-cloud (packages/tallwright-cloud) and the public verifier. An adapter that redoes any of them makes a second implementation of the evidence, and that second implementation is what a court would be asked to trust.

1. What an adapter is given, and what it exposes

from tallwright_cloud import Environments, Tallwright

client = Tallwright(ingest_url, archive_key, evidence)                  # built by the integrator
adapter = FrameworkAdapter(client, fail_closed=False)                   # the adapter's own class

environments = Environments(ingest_url, key_for, evidence)              # a vendor with many customers
adapter = FrameworkAdapter(client_for=lambda session: environments.client(session.customer_ref))
  • It is handed a Tallwright client, or a function that gives one per session, and never builds one. The key and the evidence are the integrator's: an adapter that reads a key from the environment or chooses where evidence lives has made a decision about a credential and about where salts live, and both belong to the person who runs the agent. A vendor whose one agent process serves many end customers hands the adapter client_for: the Environment of the customer a session is for, which only the vendor's own code knows.
  • submit by default, capture when fail-closed. An event is handed to client.submit, which returns once the evidence is safe and sends in the background, so the agent never waits on the network. With fail_closed=True an action is handed to client.capture before the tool runs, and a capture that was not confirmed stops the action.
  • One client per process. The client names its records <session>.<number> and mints the numbers under a lock, so the hooks of one process may share it. Whether a framework's concurrent hooks do so safely is shown by the adapter's own test (section 7), not assumed.
  • fail_closed, default False, with the meaning the self-hosted LangGraph middleware gives it. Best effort keeps the agent serving when a capture fails, and the failure is never silent (section 5). Fail-closed raises before the action runs, and a regulated deployment sets it.
  • pending() returns client.pending(), and flush() calls client.flush(), for the end of a session (a call hung up, a run finished). Those two are all an adapter adds to the client's surface; sending again what is pending is the client's own work (client.resend_pending()).

It never holds an export key. Reading exhibits is a separate object (Exhibits) in a separate process, and the process that records needs no key that reads.

2. Events, and what each one becomes

Two kinds of record exist, and the door derives the kind from one field: a line with action_type is ACTION_TRIGGERED, a line without one is CONTEXT_RECEIVED (docs/exhibit-format.md §3).

Framework event Record action_type actor_authority Content handed to capture (stays on the machine)
input: a message, prompt, retrieved document or tool list reaches the agent CONTEXT_RECEIVED none the rule it arrived under, if the framework names one an allowlist projection of what the agent received
intent no record none none none. No hook of a framework sees it, the line has no field for it, and an adapter does not invent one
action: the agent calls a tool, captured before the tool runs ACTION_TRIGGERED the kind of action (refund.approve), never a sentence the role or rule it runs under (role:payments-agent), if known the tool's name and its arguments
result: what the tool returned CONTEXT_RECEIVED none none the value returned, as the agent received it
approval: a person or a rule allowed or refused an action, and the agent is told CONTEXT_RECEIVED none the role or rule that decided (role:payments-supervisor), never a person's name the request and the decision as the framework holds them
error: a tool or the model failed, and the agent is told CONTEXT_RECEIVED none none the error as the agent received it: its type and message, never a traceback with local variables

An approval is not an action of the agent's, so it is not ACTION_TRIGGERED. The action it allowed is, in a record of its own, and that record names the same authority in actor_authority.

A capture that failed is not an error event. It is a hole, and section 5 says what happens to it.

3. The fields an adapter may set, and nothing more

Field When Rule
action_type action events only a kind or the name of a rule: 1 to 128 of A-Z a-z 0-9 . _ : / + -, starting with a letter or a digit, no space, no @. It travels in the clear and is sealed into every exhibit
actor_authority when the framework names a role or a rule the same characters. A role or a rule, never a person
timestamp only when the framework gives the moment of the event RFC 3339 with its offset. Left out, the door stamps the moment it first takes the record
record_id almost never Let the client name records: its names carry a sequence, and tallwright_cloud.gaps shows a hole in it. A name of the adapter's own carries none. Only a framework id that is stable and unique per event (a tool-call id) may be used, and then a second line under the same name is refused (RecordIdConflict)

These are the parameters of capture. There is no other field: the door refuses a line that carries a name it does not record, rather than seal the record without it. An adapter that wants to say more says it in the content, which stays on the machine.

4. What stays on the machine, and how it is committed

  • The content and its salt stay on the machine. capture gives the content its one byte form, draws 32 random bytes, and sends only the address of salt ‖ content. The pair that opens the address is written to the evidence directory, in the form the public verifier reads. The door never sees content or salt, and neither does any process of ours.
  • The commitment is tallwright_cloud.commit.Commitment.of, reached through capture. Bytes are taken as they are, text as UTF-8, and a JSON object or array as its RFC 8785 form (tallwright_core.canonical.canonicalize), so the same object commits to the same bytes whoever builds it. An adapter does not hash, salt or canonicalize anything for the line, and does not write to the evidence directory.
  • The projection is the adapter's one real decision. It hands capture a dict of the fields it names, an allowlist, and drops what it does not name: provider reasoning blocks, credentials in headers, a framework's internal state. A field nobody chose is a field nobody can defend. Content that is not bytes, text or JSON is refused by capture, and the refusal names the type, never the value.

5. When something fails

  • NotConfirmed: resend, never capture again. The line is on disk and listed by pending(). A second capture of the same event draws a new salt and records it twice, and nothing afterwards can tell two events from one recorded twice. The client sends the same bytes again on its own (submit) or on client.resend_pending(), and a record the door already holds is a confirmation too.
  • RecordIdConflict, Unauthorized, AccountClosed, Refused: surface them. Nothing of that capture was recorded, and a retry records nothing either.
  • Best effort is never silent. The failure is logged by type and record name, never by content, and the record stays in pending(). A capture the adapter skipped on purpose is still a hole in the sequence, and gaps shows it.
  • Fail-closed raises before the action runs. An action is captured before the tool runs, so a refused capture stops the action, not just its record.

6. What an adapter never does

  • It invents no verifier or proof semantics. It adds no check, no verdict and no field to an exhibit, and it says nothing a verifier does not say. The public verifier decides what an exhibit proves.
  • It calls a Capture a receipt: the door's confirmation that it holds a record committing to this address at this leaf. The proof is the exhibit made after a witness has seen that leaf in an anchor, checked with the disclosure kept here.
  • It does not describe what it captures as all of what the agent did. A framework call the adapter has no hook for, or a hook installed where another layer can still rewrite the call, is outside the record, and the adapter's README names those places for its framework.
  • It does not claim to know whether what the agent was told was true. It commits to what was received, not to whether it was right.

7. What every adapter's tests show

  1. Each event in section 2 becomes the record named there, against a real door: the line's action_type and actor_authority are what the table says, and the content is not on the wire.
  2. An intent-shaped hook, if the framework has one (a plan, a "thought" message), produces no record, and reasoning blocks are absent from the content handed to capture.
  3. A lost answer, sent again by the client, gives one record, not two.
  4. Best effort: a door that refuses leaves the agent running, the record in pending(), and a log line without content. Fail-closed: the same door stops the action before the tool runs.
  5. Concurrent hooks in one process give distinct records and no hole in the sequence.
  6. The exhibit of a captured record checks with the public verifier and the kept disclosure, and checks no more after one byte of the kept content is changed.