Treeship
SDK

treeship-sdk (Python)

Python SDK for Treeship. Wraps the CLI binary for signing, verification, session reporting, and hub operations.

Install

pip install treeship-sdk

The 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 refuses approval has no scope (no --allowed-actor / --allowed-action / --allowed-subject / --max-uses).
  • wrap() always fails — treeship wrap has 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:

ParameterTypeDescription
cli_pathOptional[str | Path]Explicit path to the treeship binary. Highest priority.
bot_modeboolBootstrap the CLI if it is missing — resolves $TREESHIP_BIN, then a cache, then a GitHub Release or npm install.
timeoutOptional[int]Default subprocess timeout in seconds.
cwdOptional[str | Path]Working directory for CLI calls.
envOptional[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"},
)
ParameterTypeDescription
actorstrActor URI
actionstrLabel for the action
parent_idOptional[str]Parent artifact id for chain linking
approval_nonceOptional[str]Nonce from an existing approval
metaOptional[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 json

Despite 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 test

Returns 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")
ParameterTypeDescription
session_idOptional[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: str

ApprovalResult

@dataclass
class ApprovalResult:
    artifact_id: str
    nonce: str

VerifyResult

@dataclass
class VerifyResult:
    outcome: str  # "pass", "fail", "error"
    chain: int
    target: str

PushResult

@dataclass
class PushResult:
    hub_url: str
    rekor_index: Optional[int] = None

SessionReportResult

@dataclass
class SessionReportResult:
    session_id: str
    receipt_url: str
    agents: int = 0
    events: int = 0

Error 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}")