{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://treeship.dev/schemas/session.v1.json",
"title": "session.v1",
"description": "A completed work session as a typed, signed record: the atom of an agent's work history. Minted by `treeship session close` from the session's own sealed evidence (never hand-written), and carried as the payload of a Treeship receipt artifact with kind=session.v1. See docs/specs/work-history.md.",
"type": "object",
"required": [
"session_id",
"actor",
"outcome",
"started_at",
"closed_at",
"attestation_class",
"receipt_digest"
],
"properties": {
"session_id": {
"description": "The session this record summarizes.",
"type": "string"
},
"actor": {
"description": "Actor URI the session ran as, e.g. agent://hermes.",
"type": "string"
},
"headline": {
"description": "One-line human narrative of the session, from the sealed receipt.",
"type": ["string", "null"]
},
"outcome": {
"description": "How the session ended: completed | abandoned | failed.",
"type": "string",
"enum": ["completed", "abandoned", "failed"]
},
"started_at": {
"description": "RFC 3339 session start.",
"type": "string"
},
"closed_at": {
"description": "RFC 3339 session close.",
"type": "string"
},
"duration_ms": {
"description": "Session wall-clock duration in milliseconds.",
"type": "number"
},
"harness": {
"description": "Capture surface(s) the session ran under: distinct tool runtime ids from the sealed receipt (e.g. 'claude-code'), or 'cli' when no tool runtime was involved.",
"type": "string"
},
"custody": {
"description": "Who held the signing key, when that is not the actor. ABSENT means self-custody -- the actor signed for itself, the default and the strong case. Present means a service signed on the actor's behalf: a materially weaker claim, so it is recorded rather than inferred. Deliberately a separate axis from attestation_class, which grades how EVIDENCE was captured; this grades who held the KEY. They vary independently -- a service-mediated room can have excellent runtime-captured evidence and still be custodially signed. Under self-custody, forging a participant's action needs that participant's key; under delegated custody, a compromised service can mint any history for every actor it signs for. Same receipt shape, different threat model.",
"type": "object",
"required": ["mode", "signer", "on_behalf_of"],
"additionalProperties": false,
"properties": {
"mode": {
"description": "Only 'delegated' is ever serialized. Self-custody is the absence of this whole object, so existing receipts stay byte-identical and 'no custody block' cannot be misread as 'custody unknown'.",
"type": "string",
"enum": ["delegated"]
},
"signer": {
"description": "The identity whose key actually produced the signature, e.g. svc://gateway-rooms. This is who a verifier is really trusting.",
"type": "string"
},
"on_behalf_of": {
"description": "The actor the signature is claimed to be FOR, e.g. agent://fizz. A verifier can confirm the signer signed; it cannot confirm this actor agreed, and must not present it as though it could.",
"type": "string"
},
"reason": {
"description": "Why the actor did not sign for itself, e.g. 'browser-mediated room; participants hold no local key'.",
"type": "string"
}
}
},
"attestation_class": {
"description": "How the record's evidence was captured, per the work-history ladder: self (the agent's own receipts, no external signal), runtime (events emitted by a harness hook or bridge the agent cannot forge or omit), countersigned (a second party signed evidence in the package, e.g. consumed human approvals). Labeled, never laundered; 'anchored' is a later, derivable state (publish + witness), not set at close.",
"type": "string",
"enum": ["self", "runtime", "countersigned"]
},
"action_count": {
"description": "Signed artifacts in the session chain.",
"type": "number"
},
"approval_count": {
"description": "Approval uses consumed during the session (each backed by a signed grant embedded in the package).",
"type": "number"
},
"handoff_count": {
"description": "Agent-to-agent handoffs during the session.",
"type": "number"
},
"event_count": {
"description": "Events in the sealed session event log.",
"type": "number"
},
"tools_exercised": {
"description": "Tool names actually invoked during the session, computed from captured events (never hand-written). Feeds capability cards' `exercised` grade.",
"type": "array"
},
"receipt_digest": {
"description": "Digest of the sealed session receipt (receipt.json) this record summarizes; binds the record to its evidence.",
"type": "string"
},
"receipt_merkle_root": {
"description": "Merkle root of the sealed .treeship package, when composed.",
"type": ["string", "null"]
},
"report_url": {
"description": "Published session report URL, when one exists.",
"type": ["string", "null"]
},
"room": {
"description": "Room this session hosted, when it was created with `treeship room create`. Mirrored here from session.json so `invitation_authority` -- which decides who may mint invitations -- is bound into the DSSE-signed payload rather than living only in unsigned local state. Absent (null) for ordinary, non-room sessions.",
"type": ["object", "null"],
"properties": {
"room_id": {
"description": "Stable room identifier, distinct from session_id.",
"type": "string"
},
"host_pubkey": {
"description": "Base64url-no-pad Ed25519 public key the room's invitations are issued under.",
"type": "string"
},
"invitation_authority": {
"description": "Who may mint invitations for this room.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["host_only", "delegated_to", "open"]
},
"delegates": {
"description": "Present only when kind is delegated_to.",
"type": "array",
"items": { "type": "string" }
}
},
"required": ["kind"]
},
"workflow_ref": {
"type": ["string", "null"]
},
"checkpoint_every_actions": {
"type": ["number", "null"]
},
"participants": {
"description": "Finalized (both-signed) participant artifact ids, in join order.",
"type": "array",
"items": { "type": "string" }
}
},
"required": ["room_id", "host_pubkey", "invitation_authority"]
}
}
}