treeship-sdk (Python)
Python SDK for Treeship. Wraps the CLI binary for signing, verification, session reporting, and hub operations.
Install
pip install treeship-sdkThe Python SDK shells out to the treeship CLI binary. The CLI must be installed and initialized (treeship init) before using the SDK.
Verified against CLI 0.24.0 on 2026-08-18. Two methods do not currently work, because the CLI surface moved and the wrapper did not follow:
attest_approval()always fails — it sends no scope flag, and the CLI now refusesapproval has no scope (no --allowed-actor / --allowed-action / --allowed-subject / --max-uses).wrap()always fails —treeship wraphas no JSON output mode, so the wrapper cannot parse an artifact id out of it (treeship wrap --actor returned invalid JSON).
Everything else on this page was executed end to end and works:
attest_action, attest_decision, attest_handoff, verify, and
session_report (with a hub attached). For approvals and wrapping, shell out
to the CLI directly until the wrapper catches up.
Constructing a client
Treeship() takes keyword arguments only:
| Parameter | Type | Description |
|---|---|---|
cli_path | Optional[str | Path] | Explicit path to the treeship binary. Highest priority. |
bot_mode | bool | Bootstrap the CLI if it is missing — resolves $TREESHIP_BIN, then a cache, then a GitHub Release or npm install. |
timeout | Optional[int] | Default subprocess timeout in seconds. |
cwd | Optional[str | Path] | Working directory for CLI calls. |
env | Optional[Mapping[str, str]] | Environment for CLI calls. |
Resolution order for the binary is cli_path > bot_mode bootstrap > plain
treeship on PATH. Note that $TREESHIP_BIN is consulted only on the
bootstrap path: a default Treeship() runs whatever treeship your PATH
resolves, even if $TREESHIP_BIN points elsewhere.
Quick start
from treeship_sdk import Treeship
ts = Treeship()
# Attest an action
result = ts.attest_action(
actor="agent://my-agent",
action="tool.call",
)
print(result.artifact_id)
# Verify it
verified = ts.verify(result.artifact_id)
print(verified.outcome) # "pass"
# Upload a closed session's receipt and get the permanent public URL
report = ts.session_report()
print(report.receipt_url)Treeship class
Wraps the treeship CLI. All methods raise TreeshipError on CLI failure.
attest_action(actor, action, ...)
Create a signed action receipt.
result = ts.attest_action(
actor="agent://coder",
action="tool.call",
parent_id="art_abc123",
meta={"tool": "read_file", "path": "src/main.rs"},
)| Parameter | Type | Description |
|---|---|---|
actor | str | Actor URI |
action | str | Label for the action |
parent_id | Optional[str] | Parent artifact id for chain linking |
approval_nonce | Optional[str] | Nonce from an existing approval |
meta | Optional[Dict[str, Any]] | Arbitrary metadata |
Returns ActionResult(artifact_id).
attest_approval(approver, description, allowed_actions=None, allowed_actors=None, allowed_subjects=None, max_uses=None, unscoped=False, expires_at=None)
Create a signed approval receipt with a binding nonce.
Currently broken against CLI 0.24. The wrapper sends no scope flag, and the CLI refuses a scopeless approval outright. Until this is fixed, mint approvals through the CLI:
treeship attest approval --approver human://alice \
--allowed-actor agent://checkout \
--allowed-action stripe.charge.create \
--max-uses 1 --expires 2026-12-31T23:59:59Z --format jsonDespite the name, expires_in is passed straight through to --expires, so it
must be an RFC 3339 timestamp, not a duration. A value like "1h" is stored
verbatim and compares as already expired.
Replay is enforced by the local Approval Use Journal, which reserves a use before the action is signed — a reused nonce is refused at attest time, on this device or workspace. Distributed single-use across machines is still open.
A scope is required. Pass at least one of allowed_actions, allowed_actors, allowed_subjects or max_uses — or unscoped=True to mint a bearer approval deliberately. An approval that constrains nothing is a bearer token, and the CLI refuses to create one by omission.
expires_at is an RFC 3339 timestamp (2030-12-31T23:59:59Z), not a duration. It was named expires_in before, which invited "1h" — a value the CLI signed verbatim and then compared as text, producing an approval that was already expired.
approval = ts.attest_approval(
approver="human://alice",
description="approve payment max $500",
allowed_actions=["payments.charge"],
max_uses=1,
)Returns ApprovalResult(artifact_id, nonce).
attest_handoff(from_actor, to_actor, artifacts, approvals=None)
Create a signed handoff receipt between agents.
Returns ActionResult(artifact_id).
attest_decision(actor, model=None, tokens_in=None, ...)
Create a signed decision receipt capturing LLM reasoning context.
Returns ActionResult(artifact_id).
verify(artifact_id)
Verify an artifact and walk its chain.
result = ts.verify("art_abc123")
if result.outcome == "pass":
print(f"Chain length: {result.chain}")Returns VerifyResult(outcome, chain, target).
hub_push(artifact_id)
Push an artifact to the configured hub.
Returns PushResult(hub_url, rekor_index).
wrap(command, actor=None, *, timeout=None)
Wrap a shell command with a signed receipt. command accepts a string or a
sequence of arguments.
Currently broken against CLI 0.24. treeship wrap has no JSON output mode —
it streams the wrapped command's own stdout and prints a human-readable summary
— so the wrapper cannot parse an artifact id back out and raises
TreeshipError: treeship wrap --actor returned invalid JSON. This fails for
every command, including silent ones. Call the CLI directly:
treeship wrap --actor agent://ci --action test.run -- npm testReturns ActionResult(artifact_id).
session_report(session_id=None, *, timeout=None)
Upload a closed session's Session Receipt to the configured hub and return the permanent public URL.
# Upload the most recently closed session
result = ts.session_report()
print(result.receipt_url)
# https://treeship.dev/receipt/ssn_42e740bd9eb238f6
# Upload a specific session by id
result = ts.session_report(session_id="ssn_42e740bd9eb238f6")| Parameter | Type | Description |
|---|---|---|
session_id | Optional[str] | Session id to upload. Defaults to the most recently closed session's package under .treeship/sessions/. |
Returns SessionReportResult(session_id, receipt_url, agents, events).
This method shells out to treeship session report, which reads the .treeship package from disk, DPoP-signs a PUT to the hub, and prints the receipt URL. The Python wrapper parses the text output and returns the structured result.
The returned receipt_url is permanent and public. Share it freely; no token, no expiry, no auth required to fetch it. See the receipt API docs for the endpoint reference.
session_report() requires an attached hub. Without one the CLI returns
receipt_url: null alongside hub not attached -- run \treeship hub attach` to
publish; receipt verifies locally, and the wrapper raises TreeshipError:
session report JSON missing receipt_url. Run treeship hub attach --endpoint https://api.treeship.dev` first. The receipt still verifies offline without a
hub — publishing is what needs one.
Result types
All results are simple dataclasses exported from treeship_sdk.
ActionResult
@dataclass
class ActionResult:
artifact_id: strApprovalResult
@dataclass
class ApprovalResult:
artifact_id: str
nonce: strVerifyResult
@dataclass
class VerifyResult:
outcome: str # "pass", "fail", "error"
chain: int
target: strPushResult
@dataclass
class PushResult:
hub_url: str
rekor_index: Optional[int] = NoneSessionReportResult
@dataclass
class SessionReportResult:
session_id: str
receipt_url: str
agents: int = 0
events: int = 0Error handling
The SDK raises TreeshipError when the CLI exits non-zero, when the CLI is missing from PATH, or when the CLI output cannot be parsed.
from treeship_sdk import Treeship, TreeshipError
ts = Treeship()
try:
result = ts.session_report()
except TreeshipError as e:
print(f"session report failed: {e}")
print(f"CLI args used: {e.args_used}")