HTTP API · 2. Sessions and handoffs

Thirty-three registrations cover the life of an agent session: minting it, recording what it observed, journalling what it did, packaging it for the next agent, and reporting what it cost in tool calls.

Planes covered: Session handshake and invocation verify · Sessions (state, archive, observations) · Observations, mediation receipts and aggregate · Activity journal · Agent Workbench · Agent and MCP tool usage · Context surface.

Read chapter 0 first for the auth model, request-field notation and error shape.

Two facts that shape everything below:

  • Twelve of these thirty-three routes are the Agent Workbench, and eleven of them return 402 on a stock daemon. CORECRUXD_ENABLED_PRO_SERVICES is empty by default. See §2.5.
  • Two routes are outside /v1. POST /session and POST /invocation/verify are legacy rails, Public by contract, excluded from the daemon's own route manifest, and flagged in-source as candidates for a future /v1 migration.

2.1 Session handshake and invocation verify

Four registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/sessionsession.rs:358 post_sessionpublic · PublicSession handshake bodySession response500, 503-W · facts
POST/invocation/verifyinvocation.rs:79 post_invocation_verifypublic · PublicInvocation verify bodyVerification response400, 500, 503-W · receipt
GET/v1/sessions/activesession.rs:223 get_active_sessionsadmin:read · Read-keys sessions, count, bindings_by_passport, total_bindings-,R
GET/v1/sessions/{sessionId}/plansession.rs:248 get_session_planany-of sessions:read, admin:read · Readpath sessionIdkeys session_id, plan, plan_hash, contract, target_contract, legacy_contract, capability_graph_hash, principal_id, origin, status, closed, close_reason, minted_at, expires_at, schema403, 404, 500, 503-R · receipt

Both public rails are still classified by route-auth, so they do not become unreachable when CORECRUXD_ROUTE_AUTH=enforce is set, but Public means no scope is required in any mode. They mint session material without a credential. That is the contract as shipped, and it is the reason these two are the first thing to look at when hardening a network-reachable daemon.

2.2 Session state, archive and observations

Seven registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/sessions/{sessionId}/statefacts.rs:961 get_session_stateany-of query:read, admin:read · Readpath sessionIdStored session state401, 404-R
PUT/v1/sessions/{sessionId}/statefacts.rs:919 put_session_stateany-of sessions:write, admin:write · Writepath sessionId; body: free-form JSON200, stored401, 404, 500-W
POST/v1/sessions/{sessionId}/archivefacts.rs:1051 archive_sessionany-of sessions:write, admin:write · Writepath sessionId; body: reason (String, dflt)200, archived401, 404-W
POST/v1/sessions/{sessionId}/unarchivefacts.rs:1073 unarchive_sessionany-of sessions:write, admin:write · Writepath sessionId200, restored401, 404-W
GET/v1/sessions/{sessionId}/observationsobservations.rs:1685 get_observationsany-of query:read, admin:read, tenant-bound · Readpath sessionId; query: since (DateTime, dflt), limit (usize, dflt), provider (String, dflt)observations (Vec ObservationRecordV1), chain (ChainStatusJson)403, 500-R · receipt
POST/v1/sessions/{sessionId}/observationsobservations.rs:1094 post_observationany-of sessions:write, admin:write · Writepath sessionId; body: kind (String, req), provider (String, req), client_ts (DateTime, dflt), payload (JSON, dflt)Observation response403-W · receipt
POST/v1/sessions/{sessionId}/observations/batchobservations.rs:1115 post_observations_batchany-of sessions:write, admin:write · Writepath sessionId; body: items (Vec of the single-observation body above, req)items (Vec PostObservationResponse)403-W · receipt

The scope correction. PUT /v1/sessions/{sessionId}/state is documented in the repository's older docs/api-reference.md as needing query:read. It requires any-of sessions:write, admin:write (facts.rs:146). Archive and unarchive are the same. A read token will not write session state under enforce.

The chain field on the observations read is a chain-status report over the observation journal. It tells you whether the journal for that session is contiguous and hash-consistent. It does not attest to what the agent did, see chapter 4 for what the receipt chain does and does not prove.

2.3 Observations aggregate and mediation receipts

Two registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/observations/aggregateobservations.rs:1737 get_observations_aggregateany-of query:read, admin:read · Readquery: since (DateTime, dflt), provider (String, dflt), kind (String, dflt), session_id (String, dflt), limit (usize, dflt)observations, provider_counts, principal_counts, kind_counts, chains, matched, returned500-R
POST/v1/mediation/receiptsobservations.rs:1581 post_mediation_receiptany-of sessions:write, admin:write · Writebody: free-form JSON, dispatched on kindkeys observation_id, receipt, session_id, tenant_id, passport_id, principal, ts400, 422CORECRUXD_STREAM_RECEIPTS and CORECRUXD_FEATURE_USAGE_RECEIPTS, both default offW · receipt

POST /v1/mediation/receipts accepts several draft kinds and the two flags gate different ones. With CORECRUXD_STREAM_RECEIPTS off, stream and context receipt drafts are rejected by the legacy parse. With CORECRUXD_FEATURE_USAGE_RECEIPTS off, a usage_ping draft is rejected. Both default off, so on a stock daemon this route accepts only the legacy draft kinds and returns 422 for the rest.

The matched versus returned split on the aggregate is the honest one: matched counts everything the filter selected, returned counts what fitted under limit.

2.4 Activity journal

Four registrations, all gated. The activity journal is a per-turn record with an agent lane and a human lane.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/activityactivity.rs:94 post_activityany-of facts:write, admin:write, tenant-bound · FeatureGatedbody JournalInput: tenant_id (String, req), session_id (String, req), kind (JournalKind, req), turn_id (String, dflt), actor_passport (String, dflt), text (String, dflt), refs (JournalRefs, dflt), meta (JournalMeta, dflt), private (bool, dflt)Journal entry400CORECRUXD_FEATURE_ACTIVITY_LOG, default offW
GET/v1/activityactivity.rs:166 get_activityany-of facts:read, admin:read, tenant-bound · FeatureGatedquery parameters as a string map, including session_id, token_budget, cursorkeys rows, returned, has_more, next_cursor, session_id, all_sessions, token_budget, truncated400CORECRUXD_FEATURE_ACTIVITY_LOG, default offR
GET/v1/activity/turn/{turn_id}activity.rs:269 get_activity_turnany-of facts:read, admin:read, tenant-bound · FeatureGatedpath turn_id; query parameters as a string mapkeys entries, session_id, turn_id400CORECRUXD_FEATURE_ACTIVITY_LOG, default offR
GET/v1/activity/turn/{turn_id}/verifyactivity.rs:345 get_activity_turn_verifyany-of facts:read, admin:read, tenant-bound · FeatureGatedpath turn_id; query parameters as a string mapkeys entries, session_id, signer, turn_id400CORECRUXD_FEATURE_ACTIVITY_LOG, default offR

CORECRUXD_FEATURE_ACTIVITY_LOG_TTL_SECS sets a retention window when present (activity.rs:93). Unset means no TTL.

The truncated flag on the list read is the one to check in an agent loop: the journal honours token_budget and will silently cut the tail rather than overflow it.

2.5 Agent Workbench

Twelve registrations. Every one except GET /v1/workbench/contract returns 402 pro_service_not_enabled unless its capability is named in CORECRUXD_ENABLED_PRO_SERVICES, which is empty by default (workbench.rs:800).

The gate is two-stage (workbench.rs:776):

  1. Scope. admin:read on a GET or admin:write on a POST passes. Failing that, the tenant-scoped per-surface capability passes, checked against the request's tenant_id.
  2. Entitlement. Whichever way you passed stage 1, the capability must also appear in CORECRUXD_ENABLED_PRO_SERVICES, or the response is 402.

The 402 body carries {schema, status: "pro_service_not_enabled", capability, path, fallback: {reason_code, detail}}. It is a stable, parseable shape, use capability to tell the operator exactly which service to enable.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/workbench/contractworkbench.rs:213 get_workbench_contractany-of query:read, admin:read · Read-Per-surface capability, method, path and status (enabled or entitled_not_enabled)-not Pro-gatedR
GET/v1/workbench/briefworkbench.rs:220 get_agent_briefadmin:read, or tenant-bound agent_brief:pro · Readquery: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt)keys active_constraints, open_decisions, open_work, recent_receipts, sessions, tenant_memory, workspace, root_path, scan_id, sample, stats, count, local_mirror_state, sync_configured, sync_degraded, sync_degraded_reason, unresolved_routes, project_id, tenant_id, schema402CORECRUXD_ENABLED_PRO_SERVICES must list agent_brief:pro, empty by defaultR · receipt
POST/v1/workbench/context-packworkbench.rs:287 post_context_packadmin:write, or tenant-bound context_pack:budgeted · Writebody: tenant_id (String, req), query (String, req), token_budget (usize, dflt default_context_pack_budget), include_private (bool, dflt), source_labels (Vec String, dflt)keys pack, items, entity, key, text, fact_id, content_hash, source_receipt, source_label, source_labels, score_space, semantic_profile_id, local_semantic_profile, tokens, tokens_used, token_budget, query, receipt, status, tenant_id, schema400, 402, 500context_pack:budgetedW · receipt
POST/v1/workbench/impact-preflightworkbench.rs:362 post_impact_preflightadmin:write, or tenant-bound impact:preflight · Writebody: tenant_id (String, req), changed_paths (Vec String, dflt), routes (Vec String, dflt), selected_tests (Vec String, dflt), include_storyline (bool, dflt)keys preflight, changed_paths, requested_routes, impacted_routes, selected_tests, living_objects, entity, key, fact_id, fact_supersession_refs, supersedes, version, receipt, status, tenant_id, schema402, 500impact:preflightW · receipt
POST/v1/workbench/command-ledgerworkbench.rs:417 post_command_ledgeradmin:write, or tenant-bound ledger:history · Writebody: tenant_id (String, req), command (String, req), args (Vec String, dflt), cwd (String, dflt), exit_status (i32, dflt), duration_ms (u64, dflt), started_at_unix_ms (u64, dflt), completed_at_unix_ms (u64, dflt), stdout_hash (String, dflt), stderr_hash (String, dflt), linked_receipts (Vec String, dflt), project_id (String, dflt), work_id (String, dflt)keys record, command, args, cwd, exit_status, duration_ms, started_at_unix_ms, completed_at_unix_ms, recorded_at_unix_ms, stdout_hash, stderr_hash, linked_receipts, project_id, work_id, receipt, replay_note, status, tenant_id, schema400, 402, 500ledger:historyW · receipt
GET/v1/workbench/command-ledgerworkbench.rs:462 get_command_ledgeradmin:read, or tenant-bound ledger:history · Readquery: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt)keys entries, count, tenant_id, schema402ledger:historyR
GET/v1/workbench/audit-triageworkbench.rs:490 get_audit_triageadmin:read, or tenant-bound audit:triage · Readquery: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt)keys queues, tenant_id, schema402audit:triageR
GET/v1/workbench/reasoning-timelineworkbench.rs:523 get_reasoning_timelineadmin:read, or tenant-bound reasoning:timeline · Readquery: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt)keys events, count, tenant_id, schema402reasoning:timelineR
POST/v1/workbench/handoff-v2workbench.rs:551 post_handoff_v2admin:write, or tenant-bound handoff:v2 · Writebody: tenant_id (String, req), goal (String, req), session_id (String, dflt), project_id (String, dflt), source_agent (String, dflt), target_agent (String, dflt), evidence_refs (Vec String, dflt), next_actions (Vec String, dflt)keys package, goal, constraints, open_decisions, evidence_refs, next_actions, session_state, command_ledger_summary, source_agent, target_agent, session_id, project_id, created_at_unix_ms, receipt, status, tenant_id, schema400, 402, 500handoff:v2, plus CORECRUXD_HANDOFF_OBSERVATIONS (default off) for the observation writeW · receipt
POST/v1/workbench/route-probeworkbench.rs:625 post_route_probeadmin:write, or tenant-bound route_probe:lab · Writebody: route (String, req), include_storyline (bool, dflt), include_tests (bool, dflt)keys route, path, handler_file, handler_fn, handler_line, source_file, source_line, scope_hints, storyline, warnings, schema402, 404route_probe:labW
GET/v1/workbench/api-driftworkbench.rs:685 get_api_driftadmin:read, or tenant-bound api_drift:check · Readquery: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt)Drift report402api_drift:checkR
POST/v1/workbench/policy-simulationworkbench.rs:703 post_policy_simulationadmin:write, or tenant-bound policy:simulate · Writebody: action (ActionEnrichmentInput, req)keys simulation, verdict, matched_constraints, proposal, receipt, status, tenant_id, schema400, 402, 500policy:simulateW · receipt

Call GET /v1/workbench/contract first. It is the only ungated route on the plane and it tells you, per surface, whether the status is enabled or entitled_not_enabled (workbench.rs:765). A client that probes the contract once at startup never has to discover a 402 mid-task.

handoff:v2 has a second, independent flag. Even with the Pro service enabled, CORECRUXD_HANDOFF_OBSERVATIONS (default off) controls whether the handoff also writes an observation. With it off you get the handoff package and no observation record, the call succeeds either way, so check the flag rather than the status code if you are depending on the observation.

POST /v1/workbench/route-probe returns handler_file, handler_fn and handler_line for a route on the running daemon. That is a development aid, and it means an enabled route_probe:lab capability discloses the daemon's source layout to anyone holding the capability.

2.6 Agent and MCP tool usage

Two registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/agents/{passport}/usageagent_usage.rs:228 get_agent_usageany-of sessions:read, admin:read, admin:write · Readpath passport; query: window_hours (u32, opt)Usage report403, 500-R
GET/v1/mcp/tools/usageagent_usage.rs:436 get_mcp_tools_usageany-of admin:read, admin:write · Readquery: window_hours (u32, opt)Usage report500-R

Usage here means counts of tool invocations, not spend. Cost attribution lives on a different plane, see chapter 8 §8.6, and is a supporting field, not a billing record.

2.7 Context surface

Two registrations, both gated off by default. This is the injection-bundle surface: give it a session, entity or query and a token budget, and it returns a context_bundle/v1 shaped for prompt injection.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/contextcontext_surface.rs:492 get_contextany-of query:read, admin:read · FeatureGatedquery: session_id (String, opt), entity (String, opt), query (String, opt), token_budget (usize, opt), render (String, opt)context_bundle/v1401, 404CORECRUXD_CONTEXT_SURFACE, default offR
POST/v1/contextcontext_surface.rs:514 post_contextany-of query:read, admin:read · FeatureGatedbody: same five fields as the GETcontext_bundle/v1401, 404CORECRUXD_CONTEXT_SURFACE, default offW

With the flag off, both return 404. CORECRUXD_ASSEMBLY_CACHE (default off) memoises the bundle; with it off every call assembles cold, which is the correct default for correctness and the wrong one for a tight agent loop.

2.8 Failure modes on this plane

SymptomMost likely cause
402 with status: "pro_service_not_enabled"The capability is not in CORECRUXD_ENABLED_PRO_SERVICES. That variable is empty by default. Read capability in the body to know which one.
403 on PUT /v1/sessions/{id}/state with a read tokenSession state writes need sessions:write or admin:write, not query:read.
404 on /v1/contextCORECRUXD_CONTEXT_SURFACE is off. It is off by default.
404 on /v1/activityCORECRUXD_FEATURE_ACTIVITY_LOG is off. It is off by default.
422 from POST /v1/mediation/receipts on a draft that used to workThe draft kind is behind CORECRUXD_STREAM_RECEIPTS or CORECRUXD_FEATURE_USAGE_RECEIPTS, both default off.
Handoff succeeds but no observation appearsCORECRUXD_HANDOFF_OBSERVATIONS is off. The route does not fail; it just skips the write.
truncated: true on GET /v1/activityThe journal hit your token_budget and cut the tail. Raise the budget or page with next_cursor.
403 on GET /v1/sessions/{id}/observationsThe observation read is tenant-bound. A non-admin: scope must be paired with a token whose tenant claim covers the session's tenant.

Sources