Create ActionReceipts from Python.
Add Bulla to the application code that authorizes or sends an action. The model does not need to know about the receipt.
A consequential action may be a payment, permission change, durable write, or handoff to another provider. Put wrap_action in the API gateway, tool router, payment handler, or agent runtime that performs the action. Bulla writes one ActionReceipt file for the counterparty to keep.
Your application supplies the action and its declared context; Bulla creates the JSON. Start with bulla demo if you want to inspect the action log, receipt, changed copy, missing-receipt comparison, and offline checker before integrating it.
Wrap an action
As a context manager, the scope object carries handles to set a result and add evidence, and holds the emitted receipt afterward:
from bulla import wrap_action, verify_receipt
with wrap_action("payments.charge", {"amount": 200, "currency": "USD"}) as act:
act.set_result("sha256:...") # optional
act.add_evidence("counterparty_ack", "sha256:...", "counterparty_signed")
receipt = act.receipt # a dict
verify_receipt(receipt).ok # TrueA receipt is emitted even when the wrapped body raises: the outcome is markederror and the exception is re-raised, never swallowed. For a completed action with no body to wrap, receipt_for(action_type, subject) returns the receipt directly.
Decorate a tool
The same object decorates a tool function, so every call leaves a receipt:
from bulla import wrap_action
scope = wrap_action("fs.write", {"path": "/tmp/out.txt"})
@scope
def write_file(path, content):
...
write_file("/tmp/out.txt", "ok")
receipt = scope.last_receiptState who authorized the action
Pass a principal and policy when another party needs to authenticate who authorized the action and which policy applied. Add an operational challenge endpoint, rollback window, or named remedy only when the transaction requires it:
from bulla import wrap_action, operational_envelope
env = operational_envelope(
principal="did:web:acme#agent",
policy="policy://payments@sha256:aa",
forum_endpoint="https://log.example",
forum_root="ots:root",
)
with wrap_action("payments.charge", {"amount": 200}, envelope=env) as act:
...Pass a signer from the bulla[identity] extra when the counterparty needs to authenticate the receipt. An unsigned receipt can still be checked for internal consistency, but it does not authenticate its issuer.
Find the actions that left no receipt
A receipt checker can inspect only files that exist. To find actions with no receipt, compare the receipt set with a separate action log from your gateway, receiver, or test harness. event_coverage returns the unmatched actions:
from bulla import event_coverage
report = event_coverage(observed_actions, emitted_receipts, anchor="gateway-log")
report["unreceipted_delta"] # actions with no covering receiptThe comparison can find only actions present in the supplied log; it does not prove that the log contains every real action. A runnable covered example is in examples/wrap-your-agent. The coverage walkthrough adds an action with no receipt.