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
402on a stock daemon.CORECRUXD_ENABLED_PRO_SERVICESis empty by default. See §2.5. - Two routes are outside
/v1.POST /sessionandPOST /invocation/verifyare legacy rails,Publicby contract, excluded from the daemon's own route manifest, and flagged in-source as candidates for a future/v1migration.
2.1 Session handshake and invocation verify
Four registrations.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
POST | /session | session.rs:358 post_session | public · Public | Session handshake body | Session response | 500, 503 | - | W · facts |
POST | /invocation/verify | invocation.rs:79 post_invocation_verify | public · Public | Invocation verify body | Verification response | 400, 500, 503 | - | W · receipt |
GET | /v1/sessions/active | session.rs:223 get_active_sessions | admin:read · Read | - | keys sessions, count, bindings_by_passport, total_bindings | - | , | R |
GET | /v1/sessions/{sessionId}/plan | session.rs:248 get_session_plan | any-of sessions:read, admin:read · Read | path sessionId | keys session_id, plan, plan_hash, contract, target_contract, legacy_contract, capability_graph_hash, principal_id, origin, status, closed, close_reason, minted_at, expires_at, schema | 403, 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/sessions/{sessionId}/state | facts.rs:961 get_session_state | any-of query:read, admin:read · Read | path sessionId | Stored session state | 401, 404 | - | R |
PUT | /v1/sessions/{sessionId}/state | facts.rs:919 put_session_state | any-of sessions:write, admin:write · Write | path sessionId; body: free-form JSON | 200, stored | 401, 404, 500 | - | W |
POST | /v1/sessions/{sessionId}/archive | facts.rs:1051 archive_session | any-of sessions:write, admin:write · Write | path sessionId; body: reason (String, dflt) | 200, archived | 401, 404 | - | W |
POST | /v1/sessions/{sessionId}/unarchive | facts.rs:1073 unarchive_session | any-of sessions:write, admin:write · Write | path sessionId | 200, restored | 401, 404 | - | W |
GET | /v1/sessions/{sessionId}/observations | observations.rs:1685 get_observations | any-of query:read, admin:read, tenant-bound · Read | path sessionId; query: since (DateTime, dflt), limit (usize, dflt), provider (String, dflt) | observations (Vec ObservationRecordV1), chain (ChainStatusJson) | 403, 500 | - | R · receipt |
POST | /v1/sessions/{sessionId}/observations | observations.rs:1094 post_observation | any-of sessions:write, admin:write · Write | path sessionId; body: kind (String, req), provider (String, req), client_ts (DateTime, dflt), payload (JSON, dflt) | Observation response | 403 | - | W · receipt |
POST | /v1/sessions/{sessionId}/observations/batch | observations.rs:1115 post_observations_batch | any-of sessions:write, admin:write · Write | path 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/observations/aggregate | observations.rs:1737 get_observations_aggregate | any-of query:read, admin:read · Read | query: 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, returned | 500 | - | R |
POST | /v1/mediation/receipts | observations.rs:1581 post_mediation_receipt | any-of sessions:write, admin:write · Write | body: free-form JSON, dispatched on kind | keys observation_id, receipt, session_id, tenant_id, passport_id, principal, ts | 400, 422 | CORECRUXD_STREAM_RECEIPTS and CORECRUXD_FEATURE_USAGE_RECEIPTS, both default off | W · 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
POST | /v1/activity | activity.rs:94 post_activity | any-of facts:write, admin:write, tenant-bound · FeatureGated | body 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 entry | 400 | CORECRUXD_FEATURE_ACTIVITY_LOG, default off | W |
GET | /v1/activity | activity.rs:166 get_activity | any-of facts:read, admin:read, tenant-bound · FeatureGated | query parameters as a string map, including session_id, token_budget, cursor | keys rows, returned, has_more, next_cursor, session_id, all_sessions, token_budget, truncated | 400 | CORECRUXD_FEATURE_ACTIVITY_LOG, default off | R |
GET | /v1/activity/turn/{turn_id} | activity.rs:269 get_activity_turn | any-of facts:read, admin:read, tenant-bound · FeatureGated | path turn_id; query parameters as a string map | keys entries, session_id, turn_id | 400 | CORECRUXD_FEATURE_ACTIVITY_LOG, default off | R |
GET | /v1/activity/turn/{turn_id}/verify | activity.rs:345 get_activity_turn_verify | any-of facts:read, admin:read, tenant-bound · FeatureGated | path turn_id; query parameters as a string map | keys entries, session_id, signer, turn_id | 400 | CORECRUXD_FEATURE_ACTIVITY_LOG, default off | R |
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):
- Scope.
admin:readon a GET oradmin:writeon a POST passes. Failing that, the tenant-scoped per-surface capability passes, checked against the request'stenant_id. - Entitlement. Whichever way you passed stage 1, the capability must also appear in
CORECRUXD_ENABLED_PRO_SERVICES, or the response is402.
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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/workbench/contract | workbench.rs:213 get_workbench_contract | any-of query:read, admin:read · Read | - | Per-surface capability, method, path and status (enabled or entitled_not_enabled) | - | not Pro-gated | R |
GET | /v1/workbench/brief | workbench.rs:220 get_agent_brief | admin:read, or tenant-bound agent_brief:pro · Read | query: 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, schema | 402 | CORECRUXD_ENABLED_PRO_SERVICES must list agent_brief:pro, empty by default | R · receipt |
POST | /v1/workbench/context-pack | workbench.rs:287 post_context_pack | admin:write, or tenant-bound context_pack:budgeted · Write | body: 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, schema | 400, 402, 500 | context_pack:budgeted | W · receipt |
POST | /v1/workbench/impact-preflight | workbench.rs:362 post_impact_preflight | admin:write, or tenant-bound impact:preflight · Write | body: 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, schema | 402, 500 | impact:preflight | W · receipt |
POST | /v1/workbench/command-ledger | workbench.rs:417 post_command_ledger | admin:write, or tenant-bound ledger:history · Write | body: 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, schema | 400, 402, 500 | ledger:history | W · receipt |
GET | /v1/workbench/command-ledger | workbench.rs:462 get_command_ledger | admin:read, or tenant-bound ledger:history · Read | query: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt) | keys entries, count, tenant_id, schema | 402 | ledger:history | R |
GET | /v1/workbench/audit-triage | workbench.rs:490 get_audit_triage | admin:read, or tenant-bound audit:triage · Read | query: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt) | keys queues, tenant_id, schema | 402 | audit:triage | R |
GET | /v1/workbench/reasoning-timeline | workbench.rs:523 get_reasoning_timeline | admin:read, or tenant-bound reasoning:timeline · Read | query: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt) | keys events, count, tenant_id, schema | 402 | reasoning:timeline | R |
POST | /v1/workbench/handoff-v2 | workbench.rs:551 post_handoff_v2 | admin:write, or tenant-bound handoff:v2 · Write | body: 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, schema | 400, 402, 500 | handoff:v2, plus CORECRUXD_HANDOFF_OBSERVATIONS (default off) for the observation write | W · receipt |
POST | /v1/workbench/route-probe | workbench.rs:625 post_route_probe | admin:write, or tenant-bound route_probe:lab · Write | body: 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, schema | 402, 404 | route_probe:lab | W |
GET | /v1/workbench/api-drift | workbench.rs:685 get_api_drift | admin:read, or tenant-bound api_drift:check · Read | query: tenant_id (String, req), project_id (String, dflt), limit (usize, dflt) | Drift report | 402 | api_drift:check | R |
POST | /v1/workbench/policy-simulation | workbench.rs:703 post_policy_simulation | admin:write, or tenant-bound policy:simulate · Write | body: action (ActionEnrichmentInput, req) | keys simulation, verdict, matched_constraints, proposal, receipt, status, tenant_id, schema | 400, 402, 500 | policy:simulate | W · 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/agents/{passport}/usage | agent_usage.rs:228 get_agent_usage | any-of sessions:read, admin:read, admin:write · Read | path passport; query: window_hours (u32, opt) | Usage report | 403, 500 | - | R |
GET | /v1/mcp/tools/usage | agent_usage.rs:436 get_mcp_tools_usage | any-of admin:read, admin:write · Read | query: window_hours (u32, opt) | Usage report | 500 | - | 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.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/context | context_surface.rs:492 get_context | any-of query:read, admin:read · FeatureGated | query: session_id (String, opt), entity (String, opt), query (String, opt), token_budget (usize, opt), render (String, opt) | context_bundle/v1 | 401, 404 | CORECRUXD_CONTEXT_SURFACE, default off | R |
POST | /v1/context | context_surface.rs:514 post_context | any-of query:read, admin:read · FeatureGated | body: same five fields as the GET | context_bundle/v1 | 401, 404 | CORECRUXD_CONTEXT_SURFACE, default off | W |
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
| Symptom | Most 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 token | Session state writes need sessions:write or admin:write, not query:read. |
404 on /v1/context | CORECRUXD_CONTEXT_SURFACE is off. It is off by default. |
404 on /v1/activity | CORECRUXD_FEATURE_ACTIVITY_LOG is off. It is off by default. |
422 from POST /v1/mediation/receipts on a draft that used to work | The draft kind is behind CORECRUXD_STREAM_RECEIPTS or CORECRUXD_FEATURE_USAGE_RECEIPTS, both default off. |
| Handoff succeeds but no observation appears | CORECRUXD_HANDOFF_OBSERVATIONS is off. The route does not fail; it just skips the write. |
truncated: true on GET /v1/activity | The journal hit your token_budget and cut the tail. Raise the budget or page with next_cursor. |
403 on GET /v1/sessions/{id}/observations | The observation read is tenant-bound. A non-admin: scope must be paired with a token whose tenant claim covers the session's tenant. |
Sources
- crates/corecruxd/src/http/session.rs:253, session plan scopes, any-of
sessions:read,admin:read - crates/corecruxd/src/http/facts.rs:146,
require_session_write_ctx - crates/corecruxd/src/http/observations.rs:1655, tenant-bound observation read
- crates/corecruxd/src/http/activity.rs:73, activity read and write scope sets
- crates/corecruxd/src/activity.rs:58,
CORECRUXD_FEATURE_ACTIVITY_LOG - crates/corecruxd/src/http/workbench.rs:46, the ten per-surface capability strings
- crates/corecruxd/src/http/workbench.rs:776,
require_surface, the scope stage - crates/corecruxd/src/http/workbench.rs:800,
require_surface_enabled, the402 - crates/corecruxd/src/http/agent_usage.rs:235, agent usage scopes
- crates/corecruxd/src/config.rs:1342,
CORECRUXD_CONTEXT_SURFACEdefault - crates/corecruxd/src/config.rs:1347,
CORECRUXD_HANDOFF_OBSERVATIONSdefault - crates/corecruxd/src/http/openapi.rs:130, the two legacy rails, excluded from the manifest

