# Session Handshake (Crux Daemon) Crux Daemon supports the VaultCrux Session Handshake v1 protocol — one endpoint and one MCP tool that together give an agent a receipted plan describing what it's allowed to do. This document is the operator-facing summary — where state lives on disk, what the wire formats look like, and how to verify a session plan or invocation receipt outside the daemon. The canonical protocol invariants are encoded in the `crux-session` crate ([crates/crux-session/src/lib.rs](../crates/crux-session/src/lib.rs)). ## What Crux Daemon Gives You - **One MCP tool:** `cuecrux_session`. Registered at the head of the `tools/list` catalogue; collapses per-service discovery into one call. - **One HTTP endpoint:** `POST /session` (unversioned path; local-daemon specific, matches master-plan §5.1). - **One verification endpoint:** `POST /invocation/verify`. Decodes a hex-encoded invocation receipt and returns the governance verdict. - **Durable session state** under `$CORECRUXD_DATA_DIR`: - `.install-uuid` — per-install random UUID; hashed into the principal. - `sessions/{session_id}.json` — one sealed plan per session. - `session-events.jsonl` — append-only sealed-event log (one JSON line per `SessionPlanSealed` or `InvocationReceipted` event). Plans on Crux Daemon run in `"local"` receipt mode: BLAKE3 hash over canonical CBOR, no ed25519 signature. Local mode covers integrity; signed plans are a hosted-only thing because the local-daemon threat model trusts the local machine. ## Opening a session ```http POST /session HTTP/1.1 Host: localhost:14800 Content-Type: application/json Accept: application/json { "client_id": "my-agent", "client_version": "1.2.3", "accepts": ["application/json"], "intent": "document_ingest", "hints": { "prefer_bulk": false } } ``` Response: ```http HTTP/1.1 200 OK Content-Type: application/json X-CueCrux-Session-Id: 0102030405060708090a0b0c0d0e0f10 X-CueCrux-Plan-Hash: b8e1... Cache-Control: no-store { ...SessionPlan... } ``` The response body is the canonical JSON form of the `SessionPlan`; the `X-CueCrux-Plan-Hash` header mirrors `receipt.hash`. All subsequent calls that should chain to this session include `X-CueCrux-Session-Id` in their headers (or the equivalent in MCP metadata). ## What's in a SessionPlan Fields an agent should care about: | Field | Meaning | |---|---| | `session_id` | Opaque 16-byte ULID; used as the bearer for subsequent calls. | | `capability_graph` | Array of `{cap, prefer, shape, min_tier, cost_class, impl_path}`. Everything the passport is entitled to invoke this session. | | `capability_graph_hash` | BLAKE3 over the canonical-CBOR of `capability_graph`. Exposed so MemoryCrux + audit tools can index by surface. | | `channels` | `{bulk, mcp}` — where to route calls. `bulk` is null on Crux Daemon until Layer 2 ships. | | `receipt.hash` | BLAKE3 over canonical-CBOR of the plan with `hash`/`signature`/`signer_kid` zeroed. | | `receipt.mode` | `"local"` on Crux Daemon. `"verified"` on hosted, with an `ed25519` signature over the hash. | | `intent_hint` | Echoes back the `intent` field from the request, if supplied. | The Crux Daemon capability graph is shaped by: 1. **Affinity** — the passport has `["*"]` so every catalogue entry passes the affinity filter. 2. **Tier** — Crux Daemon runs at tier `"local"`. Catalogue entries with `min_tier: "free"` or above are filtered out. Baseline free capabilities (`session_context`, `journal_append`, etc.) are in; `retrieve` / `proof_document` / `audit_replay` are hosted-only. ## Invocation receipts Every call made under an active session should produce an `InvocationReceipt` — a 200-byte receipt chained to the plan via `parent_plan_receipt_hash`. To verify one: ```http POST /invocation/verify HTTP/1.1 Host: localhost:14800 Content-Type: application/json { "invocation_id": "<16 bytes hex>", "session_id": "<16 bytes hex>", "parent_plan_receipt_hash": "<32 bytes hex>", "capability": "retrieve", "channel": "bulk", "invoked_at": 1745000001000, "completed_at": 1745000001100, "input_hash": "<32 bytes hex>", "output_hash": "<32 bytes hex>", "outcome": "ok", "receipt_hash": "<32 bytes hex>" } ``` Response: ```json { "verified": true, "integrity_ok": true, "capability_ok": true, "channel_ok": true, "governance_faults": [], "parent_plan_found": true, "parent_plan_principal_id": "ce:a4f3b1c2:tester" } ``` The endpoint returns `200` even when the verdict flags governance faults (wrong capability, wrong channel). Master-plan §8.2 — faults are evidence, not reasons to drop the receipt. The caller decides what enforcement to apply. ## On-disk format Crux Daemon is designed so that an operator can `jq` through state without spinning up a database: ```bash # Every session you've ever opened: ls "$CORECRUXD_DATA_DIR/sessions/" # Sealed events in write order (1 JSON line each): cat "$CORECRUXD_DATA_DIR/session-events.jsonl" | jq -r .event_type | sort | uniq -c ``` Session files are rewritten atomically (temp-file + rename) on every close/revoke; the event log is append-only and fsync'd per write. ## Verifying a receipt offline The `crux-session` crate's CBOR + BLAKE3 primitives are pure; a verifier can be built with zero I/O. Sketch: ```rust use crux_session::{plan_receipt_hash, verify_invocation_receipt, SessionPlan}; let plan = SessionPlan::from_canonical_cbor(&plan_cbor_bytes)?; // Plan-level integrity: assert_eq!(plan_receipt_hash(&plan), plan.receipt.hash); // Invocation-level chain: let verdict = verify_invocation_receipt(&receipt, &plan); assert!(verdict.verified_overall()); ``` The TypeScript mirror at `@cuecrux-shared/session` produces byte-identical canonical CBOR, so the same receipt verifies in either runtime. ## Feature flags Crux Daemon has no plan-level feature flags at M6 — everything is always on. On hosted the session feature is gated behind `FEATURE_SESSION_HANDSHAKE` per tenant; Crux Daemon ships with it enabled by default. ## What's missing (follow-ups) - **Layer 2 bulk channel.** Crux Daemon surfaces `channels.bulk: null`; the HTTP/2 + CBOR bulk transport is a separate plan. - **Automatic MCP interceptor.** Invocation receipts are minted by an agent that chooses to chain them; there is no built-in trap-door that silently mints one for every tool call. See the M4 deferred-to-follow-up note in the ExecPlan. - **Hosted import migration.** The import event type and the `imported_principal_map` table are pre-positioned for Phase 8; the actual upload pipeline is not yet wired. The persisted `ce:` principal prefix remains as a wire-compatibility identifier for existing local receipts.