# llms-full.txt — generated by scripts/build-llms-full.sh. Do not edit by hand. # Expansion of llms.txt: the full content of every linked local document. # ============================================================ # BEGIN llms.txt # ============================================================ # Crux > Local-first, tamper-evident retrieval + memory engine for AI agents. Sealed segments are > BLAKE3 hash-chained; every receipt-bearing operation produces a signed, replayable CROWN > receipt that verifies offline. ## Agent docs - [AGENTS.md](AGENTS.md): entry point — reading order, trust-core tour, verify commands - [CODEMAP](docs/agent/CODEMAP.md): crate atlas (28 crates → purpose, symbols, deps) - [CLAIMS](docs/agent/CLAIMS.md): product claim → enforcing symbol → proving test - [INVARIANTS](docs/agent/INVARIANTS.md): cryptographic + data guarantees I1–I6 - [GLOSSARY](docs/agent/GLOSSARY.md): CROWN, CCXI, CRC-v1, cruxpack, lane, passport - [repo-manifest.yaml](docs/agent/repo-manifest.yaml): machine-readable index + CI assertions ## Specs & trust - [THREAT_MODEL](docs/THREAT_MODEL.md): trust boundaries and stated limitations - [receipt-v1 spec](docs/spec/receipt-v1.md): on-the-wire receipt format - [audit-bundle-v1 spec](docs/spec/audit-bundle-v1.md): BYO audit-trail export bundle - [Trust Contract](TRUST-CONTRACT.md): the promises the daemon makes, verifiable in this tree - [ADRs](docs/adr/): 001 append-only sealed segments, 002 CROWN receipts, 003 CPU-only daemon ## Use & integrate - [getting started](docs/getting-started.md): install paths + the first loop - [developer portal](docs/developer-portal.md): HTTP/MCP base URLs, OpenAPI, offline verify, SDKs - [API reference](docs/api-reference.md): route notes - [agent guide](docs/agent-guide.md): how agents call the daemon — budgets, sessions, handoffs - [MCP system prompt](docs/mcp-system-prompt.md): drop-in prompt covering the full tool surface - [architecture](docs/architecture.md): daemon planes and crate boundaries - [verify releases](docs/verify-release.md): cosign signatures, SBOMs, SLSA provenance - [usage receipts](docs/usage-receipts.md): the opt-in, metadata-only adoption signal ## Verify - [testing & coverage](docs/testing-and-coverage.md): how tests + the coverage gate work, what's excluded and why - [benchmarks](docs/benchmarks.md): pinned baselines, regression-gated in CI - `cargo test --workspace` - `corecruxctl verify-store --strict` — per-segment BLAKE3 + chain walk - `corecruxctl replay --strict` — recompute + compare - conformance vectors: crates/corecrux-receipts/vectors/ - fuzz: fuzz/fuzz_targets/ (segment_decode, storage_scan_frames, receipt_verify_cbor, rcx_canonical_token) ## Optional - [README](README.md): product overview - [CHANGELOG](CHANGELOG.md) # ============================================================ # BEGIN AGENTS.md # ============================================================ # AGENTS.md Crux is a local-first retrieval and memory engine whose storage and receipts are cryptographically tamper-evident. Every receipt-bearing operation produces a signed, replayable CROWN receipt; sealed segments (`.ccxseg`) are BLAKE3 hash-chained and independently verifiable offline. This file is an **index**, not the content. The detail lives in `docs/agent/`. Start here, then follow the reading order. Everything is anchored by symbol name (greppable) and CI-verified, so you can trust the references — but verify the claims yourself (below). ## Reading order (start here) 1. `docs/agent/CODEMAP.md` — what each of the 28 crates owns + key symbols. 2. `docs/agent/CLAIMS.md` — each product claim mapped to the code and the **test** that proves it. 3. `docs/agent/INVARIANTS.md` — the cryptographic + data guarantees (I1–I6) and where they hold. 4. `docs/THREAT_MODEL.md` — trust boundaries and stated limitations. 5. `docs/spec/receipt-v1.md` — the on-the-wire receipt format. Machine-readable index (parse this, no NL needed): `docs/agent/repo-manifest.yaml`. Link map: `/llms.txt`. Vocabulary (CROWN, CCXI, CRC-v1, cruxpack, lane, passport): `docs/agent/GLOSSARY.md`. **Live code map (dogfood).** A running daemon can serve this repo's AST-derived structure back to you: register the checkout (`POST /v1/repos` with `root_path`), then `GET /v1/repos/{id}/codemap?tenant_id=…&format=summary|full` — same scanner as `/console/codegraph`. The curated CODEMAP.md is the reading order; the endpoint is ground truth. Per-crate context lives in `crates//AGENTS.md` (nearest file wins). **Building against the daemon instead of modifying it?** Different docs: connect via MCP and read `docs/agent-guide.md` (budgets, sessions, handoffs) and `docs/mcp-system-prompt.md` (the full tool surface); HTTP/SDK integration starts at `docs/developer-portal.md`. ## Verify the claims yourself (do not trust this file) - `cargo test --workspace` - `corecruxctl verify-store --strict` — per-segment BLAKE3 re-derivation + chain walk - `corecruxctl replay --strict` — recompute + compare - receipt conformance vectors: `crates/corecrux-receipts/vectors/` - fuzz targets (nightly): `fuzz/fuzz_targets/` — `cargo fuzz run segment_decode` ## The 60-second trust-core tour To understand what makes this tamper-evident, read these symbols in order: 1. `build_segment_v1` / `seal_segment_v1_from_record_area` — `corecrux-segment` (seal a segment). 2. `decode_segment_v1` — `corecrux-segment` (re-derive + reject tampered bytes; invariant I1). 3. `SegmentSealMaterialV1` — `corecrux-storage` (the signed, hash-chained seal material; I2). 4. `verify_segment_hashes_all` — `corecrux-storage` (what `verify-store --strict` walks). 5. `verify_receipt_v1` / `verify_c2pa_manifest_v1` — `corecrux-receipts` (fail-closed verify; I3). 6. `mark_superseded` — `corecrux-memory` (non-destructive versioned facts; I4). 7. `verify_rfc6962_inclusion_proof_v1` — `corecrux-receipts` (external witness anchoring). ## Build / test - MSRV: **1.88.0** (`rust-toolchain.toml`); edition 2021; workspace version 0.5.65 (`[workspace.package]` in `Cargo.toml` is authoritative, and `scripts/check-agent-docs.sh` fails the build when this line disagrees with it). - `cargo build --workspace --locked` - `cargo test --workspace` - `cargo fmt --check` and `cargo clippy --workspace -- -D warnings` (both CI-gated) - CI: `.github/workflows/ci.yml` (lint + test + msrv + coverage), `fuzz.yml` (scheduled), `agent-docs.yml` (this doc set's symbol/test existence check). ## Boundaries **Always:** anchor doc references by symbol name; keep the Apache-2.0 licence header on every `.rs` file (`grep -rL "Licensed under" crates/**/*.rs` must return nothing); test and lint before committing. **Ask first:** changing any default port (HTTP `14800`, gRPC `4007`, MCP `14801`); changing receipt formats, seal material, or anything under `crates/corecrux-receipts/`, `-segment/`, `-storage/` that an invariant (I1–I6) names; adding a new on-disk artifact type (update all three wiring points: storage allowlist, projection registry, load-at-startup); instrumenting a new `crux.outcome` site — the set is curated, and the admission bar plus the two ways to get it silently wrong are in `docs/agent/outcome-instrumentation.md`. **Never:** GPU/CUDA code, `--features cuda`, or GPU readiness checks (this repo is CPU-only — ADR 003); references to proprietary crates (`corecrux-analytics`, `corecrux-decision`, `corecrux-coordinator` do not exist here); line-number anchors in agent docs; deleting live shard data by hand. ## Maintenance contract `docs/agent/repo-manifest.yaml` → `ci_assertions` is the canonical list of every symbol, test, crate, and fuzz target these docs reference. `scripts/check-agent-docs.sh` greps the tree for each on every PR; a rename that orphans a reference fails the build. Anchor new references by **symbol name, never line number**, and add them to `ci_assertions`. The same gate also enforces: every local `llms.txt` link resolves; every crate ships a nested `AGENTS.md` (≤60 lines); `llms-full.txt` matches its generator (`scripts/build-llms-full.sh --check`); and, in CI (`--exec`), the cheap documented commands actually run. Editing any doc linked from `llms.txt` requires regenerating `llms-full.txt`. # ============================================================ # BEGIN docs/agent/CODEMAP.md # ============================================================ # CODEMAP — the crate atlas > What each crate owns, its load-bearing public symbols, and who depends on it. > Anchored by **symbol name** (greppable), not line number (rots). Verified against > the tree by `scripts/check-agent-docs.sh` in CI. The workspace has **34 cargo crates** under `crates/`. (`crux-console-ui` is a built SPA — `dist/` only, not a cargo member — and is excluded below.) ## Trust core (read these first) | Crate | Purpose | Key public symbols | Used by | ~LOC | |---|---|---|---|---| | `corecrux-segment` | Sealed segment format (`.ccxseg`) — build / seal / decode + hash binding | `build_segment_v1`, `seal_segment_v1_from_record_area`, `decode_segment_v1`, `SegmentError` | corecrux-storage, corecrux-index, corecrux-projections, corecruxctl, corecruxd | 4.1k | | `corecrux-storage` | Append-only shard store; sealing, integrity scan, seal material | `append_batch`/`append_batch_with_stats`, `SegmentSealMaterialV1`, `integrity_scan_stats_all`, `verify_segment_hashes_all` | corecrux-projections, corecruxctl, corecruxd | 12.4k | | `corecrux-receipts` | Receipt formats, Ed25519 signing, strict verification, witness anchoring, export bundles | `verify_receipt_v1`, `build_c2pa_manifest_v1`/`verify_c2pa_manifest_v1`, `build_bundle_v1`/`verify_bundle_v1`, `build_external_anchor_body_v1`/`verify_rfc6962_inclusion_proof_v1`, `sign_stream_v1` | crux-integrations, crux-mcp, corecruxctl, corecruxd | 12.0k | | `corecrux-frame` | Canonical v1 frame encoding + hash helpers (header / payload / stream) | `canonical_header_bytes_v1`, `compute_header_hash`, `compute_payload_hash`, `decode_canonical_header_bytes_v1` | corecrux-{index,projections,storage,segment}, corecruxctl, corecruxd | 0.3k | | `rcx-capability-token` | RCX capability token v1.0 (schema-lock, CBOR/JSON mirror, validation) | `RcxCapabilityToken`, `verify_token`, `validate_basic`, `RcxTier`, `ReceiptClass` | crux-enterprise-shim, crux-mcp, crux-router | 1.1k | | `rcx-revocation` | CRL feed behind `Revocation.crl_url`; tri-state freshness so callers fail closed | `RevocationSnapshot`, `authorize_when_known`, `RevocationFeed`, `CrlTransport`, `CrlDocument` | (relay accept path — M5) | 0.5k | | `crux-escrow` | Vault key recovery: recovery-code wrapped DEKs, Shamir 2-of-3 escrow, delayed cancellable share release | `RecoveryCode`, `WrappedDek`, `wrap_dek`/`unwrap_dek`, `VaultSetup::acknowledge`, `split_escrow`/`combine_shares`, `release::ReleaseRequest` | corecruxd | 0.7k | ## Memory & retrieval | Crate | Purpose | Key public symbols | Used by | ~LOC | |---|---|---|---|---| | `corecrux-memory` | Versioned fact store + session store; decay, supersession, CROWN receipts | `Fact`, `FactStore`, `mark_superseded`, `DEFAULT_PRIVATE_PREFIXES`, `DAEMON_OWNED_ENTITY_PREFIXES`, `consolidate_facts_v1` | crux-lens-features, crux-mcp, crux-observe, corecruxctl, corecruxd | 11.7k | | `corecrux-index` | Companion inverted index (`.ccxi`) built at seal time; powers BM25 | `CcxiBuilder`, `CcxiReader`, `CcxiHeader`, `pfordelta_encode`/`pfordelta_decode` | corecrux-retrieval, crux-mcp, corecrux-storage, corecruxctl, corecruxd | 1.1k | | `corecrux-retrieval` | Fused retrieval — BM25 + graph-signal fusion over `.ccxi` | `fused_retrieve`, `IndexManager`, `FusionWeights`, `FusedHit` | corecruxd, crux-mcp | 2.1k | | `corecrux-projections` | Living-objects projections + snapshots (`.ccxsnap`) + deterministic parity harness | `ProjectionEventV1`, `CcxsnapProjectionId`, `build_cold_segment_v1`, `apply`/`apply_at` | corecrux-retrieval, corecruxctl, crux-mcp, crux-session, corecruxd | 12.4k | | `crux-lens-features` | Feature Registry lens over the memory substrate (coverage / gap reporting) | `compute_coverage_report`, `compute_gaps`, `compute_promise_coverage` | crux-mcp, corecruxd | 0.5k | ## Routing, identity, sessions | Crate | Purpose | Key public symbols | Used by | ~LOC | |---|---|---|---|---| | `crux-router` | RCX daemon router primitives (capability / credit / egress) | `RcxRouter` | crux-mcp, corecruxd | 2.2k | | `crux-session` | VaultCrux session handshake v1 (canonical CBOR + JCS mirror + Ed25519) | `plan_receipt_hash`/`verify_plan_signature`, `mint_invocation_receipt`/`verify_invocation_receipt`, `to_canonical_cbor` | rcx-capability-token, corecruxctl, crux-mcp, corecruxd | 7.2k | | `crux-enterprise-shim` | Enterprise customer-hosted RCX token-validation contract | `EnterpriseShim`, `validate_enterprise_trust_root`, `EnterpriseTrustRoot`, `EnterpriseShimDecision` | corecruxd | 0.4k | | `vaultcrux-local` | Daemon-local VaultCrux classification + content-loading policy (tier boundaries) | `load_content_manifest`/`validate_content_manifest`, `tool_tier`, `ContentManifest`, `ToolTier` | crux-mcp, corecruxd | 0.6k | ## Daemon, CLI, MCP | Crate | Purpose | Key public symbols | Used by | ~LOC | |---|---|---|---|---| | `corecruxd` | The Crux Daemon binary (HTTP / gRPC / MCP host, signing, auth) | *(binary)* — owns `build_segment_seal_receipt`, `sign_segment_seal_material` (`src/grpc.rs`) | — | 83.8k | | `corecruxctl` | CoreCrux CLI: `verify-store`, `replay`, `gaps` | *(binary)* — `verify_store`, `replay`, `gaps` modules (`src/`) | — | 33.1k | | `crux-mcp` | Agent-facing MCP server (JSON-RPC 2.0 + axum Streamable-HTTP); static-bearer/OAuth fail-closed auth; ~42 tool modules | `dispatch`, `router`/`with_rcx_router`/`with_agent_passports`, `mcp_authentication_configured`, `crc_v1` | corecruxd | 33.1k | | `crux-observe` | Self-observation layer: ops events + bootstrap docs → memory facts | `Redactor`/`redact_line`, `bootstrap_entity`/`ops_entity`, `self_observe_enabled` | crux-mcp, corecruxd | 2.7k | | `crux-observe-api` | Wire types for the agent audit-chain data contract | `NodeKind`, `RiskClass`, `StepStatus`, `ReasoningRef` | corecruxd | 0.8k | | `crux-integrations` | Declarative manifest contract for daemon integration packs | `IntegrationManifest`, `IntegrationEntry`, `EntryKind`, `SafetyPolicy` | corecruxctl, crux-mcp, corecruxd | 2.5k | ## Supporting / standalone | Crate | Purpose | Key public symbols | Used by | ~LOC | |---|---|---|---|---| | `corecrux-types` | Foundational shared event-store types | `DriftClass`, `BuildInfo`, `UpdateStatus`, `CompatContract` | corecrux-receipts, crux-mcp, corecruxctl, corecruxd | 2.5k | | `corecrux-proto` | Protobuf/gRPC types for the data plane (tonic/prost generated) | `dataplane_v1`, `observe_v1` (modules) | crux-integration-tests, corecruxctl, corecruxd | 0.1k | | `crux-config-wizard` | Composes `CLAUDE.md`/`AGENTS.md` config from versioned profile fragments | `Target` enum *(also a binary)* | crux-claude-hooks | 1.8k | | `crux-claude-hooks` | Claude Code lifecycle hook binaries (`crux-hook`, `crux-llm-shim`) | *(binaries + `crux_claude_hooks` lib)* | — | 5.0k | | `crux-contrib` | Contribution-manifest builder + Ed25519 envelope signing | `build_manifest`, `ContributionManifest`, `Provenance` | — | 0.2k | | `crux-cost` | Ground-truth token-burn cost lens: parses a Claude Code transcript for measured usage, attributes carried context cost per source; shared `CostReport` contract for CLI (producer) and daemon (reader) | `analyze_file`, `analyze_str`, `CostReport` | corecruxctl, corecruxd | 2.2k | | `crux-sync` | Offline-first outbox sync client to the VaultCrux API | `Outbox`/`OutboxEntry`, `push_contributions`/`query_commons` | — | 0.7k | | `crux-integration-tests` | Cross-crate integration tests (consumes corecrux-proto) | *(test-only crate)* | — | 2.2k | ## Notes / corrections to common assumptions - The seal-receipt **builders** (`build_segment_seal_receipt`, `sign_segment_seal_material`) are private fns in the `corecruxd` binary (`src/grpc.rs`), not a library crate. The reusable, signable material type is `SegmentSealMaterialV1` in **corecrux-storage**. - `witness_v1`, `audit_bundle_v1`, `c2pa_manifest_v1`, `stream_v1` are **modules** of `corecrux-receipts`, not functions — call the concrete `build_*_v1` / `verify_*_v1` fns. - `verify_strict` is the upstream `ed25519-dalek` `VerifyingKey::verify_strict` method (rejects malleable / non-canonical signatures), invoked by every per-format verifier. It is not a CueCrux-defined symbol. - Two distinct "coverage" subsystems exist: `crux-lens-features` (feature-registry gaps, `compute_gaps`) and `corecruxctl gaps` (segment-indexed-vs-not). Don't conflate them. # ============================================================ # BEGIN docs/agent/CLAIMS.md # ============================================================ # CLAIMS — product claim → enforcing code → proving test > The showcase artefact. Every claim points at the symbol that enforces it and a test > that proves it. **Don't trust this file — run the tests** (`cargo test --workspace`). > Symbols and test names here are verified to exist in the tree by > `scripts/check-agent-docs.sh` in CI; a rename that orphans a reference fails the build. Files are repo-relative; tests are named by their `fn`. | # | Claim | Enforced by | File | Proven by (test fn) | |---|---|---|---|---| | 1 | Receipt signatures are strongly unforgeable (malleable / non-canonical sigs rejected) | `verify_receipt_v1`, `verify_c2pa_manifest_v1`, `verify_bundle_v1` — each calls ed25519-dalek `verify_strict` | `crates/corecrux-receipts/src/verify_v1.rs`, `c2pa_manifest_v1.rs`, `audit_bundle_v1.rs` | `tampering_with_signature_breaks_signature_check`, `wrong_verifying_key_fails`, `verify_receipt_sig_payload_hash_mismatch`, `tamper_with_signature_breaks_verification` | | 2 | Sealed-segment content matches its footer hashes (1-bit tamper is caught) | `decode_segment_v1` re-derives header/record/toc/segment hashes | `crates/corecrux-segment/src/decoder.rs` | `detects_footer_corruption` | | 3 | `verify-store` does cryptographic (BLAKE3) checks, not just CRC | `verify_segment_hashes_all` re-decodes each segment (via `decode_segment_v1`) and compares the recomputed `segment_hash` to the manifest record | `crates/corecrux-storage/src/integrity.rs` | `strict_scan_verifies_segment_hashes_and_detects_manifest_mismatch` | | 4 | Each sealed segment is signed and hash-chained to its predecessor | `SegmentSealMaterialV1` (binds `segment_hash` + `previous_segment_hash`) + `sign_segment_seal_material` | `crates/corecrux-storage/src/lib.rs`, `crates/corecruxd/src/grpc.rs` | `segment_seal_receipt_signing_commits_to_segment_chain`, `append_batch_seal_receipt_links_previous_segment_hash` | | 5 | Chain heads can be externally witnessed / timestamped | `witness_v1`: external-anchor body, RFC-6962 inclusion proof, RFC-3161 timestamp | `crates/corecrux-receipts/src/witness_v1.rs` | `rfc6962_inclusion_proof_verifies_two_leaf_tree`, `external_anchor_body_binds_inclusion_proof`, `rfc3161_timestamp_body_binds_token_hash_and_imprint` | | 6 | Capability tokens are verified against a trusted issuer key | `verify_token` (`verify_strict` against `trust_root_pubkey`), `validate_basic` | `crates/rcx-capability-token/src/lib.rs` | `strict_verify_rejects_tampered_token`, `strict_verify_rejects_wrong_trust_root` | | 7 | Memory updates are non-destructive and versioned (history retained) | `store` → `supersede_prior_version` → `mark_superseded` (predecessor marked, not deleted) | `crates/corecrux-memory/src/fact_store.rs` | `store_same_key_auto_supersedes_prior_version_in_recall`, `consolidate_facts_v1_supersedes_targets_without_deleting_history` | | 8 | Capability coverage is provable (no silent gaps) | `compute_coverage_report`, `compute_gaps` | `crates/crux-lens-features/src/analytics.rs` | `gaps_identifies_critical_no_tests_for_shipped`, `coverage_report_tallies_per_system` | | 9 | Untrusted bytes never panic the parsers | fuzz targets (libfuzzer) | `fuzz/fuzz_targets/` | `segment_decode`, `storage_scan_frames`, `receipt_verify_cbor`, `rcx_canonical_token`, `escrow_recovery_code` | | 10 | `.cruxpack` export excludes private/reserved/deleted facts by default, and a pack carrying deleted facts fails verification | `CRUXPACK_SCHEMA_V1`, `build_manifest` (cruxpack) | `crates/corecrux-memory/src/cruxpack.rs` | `export_excludes_private_and_reserved_by_default`, `verify_rejects_pack_carrying_deleted_facts`, `valid_pack_verifies` | | 11 | Context export is a signed custody proof — any tampered component or signed field fails offline verification | `run_context_export`, `run_context_verify` | `crates/corecruxctl/src/export.rs` | `context_export_round_trips_and_is_a_signed_custody_proof`, `tampered_component_fails_verification`, `tampered_signed_field_fails_signature` | | 12 | Usage receipts carry metadata only (never content) and sign/verify round-trip | `usage_receipt_v1` module | `crates/corecrux-receipts/src/usage_receipt_v1.rs` | `body_carries_only_metadata`, `build_sign_verify_round_trip`, `assert_kind_rejects_content_bearing_body` | | 13 | Metered credit spend is signed and idempotent — a retried quote cannot double-debit | `post_credit_spend` | `crates/corecruxd/src/http/credit_meter.rs` | `spends_seeded_comped_wallet_and_returns_signed_receipt`, `quote_mutation_retry_conflicts_without_second_debit` | | 14 | Passports are issued `unverified` and tier rank is earned, strictly ordered | `handle_issue_passport` | `crates/crux-mcp/src/tools/passport.rs` | `issue_passport_creates_unverified`, `tier_rank_ordering`, `issue_passport_idempotent` | | 15 | Constraints are validated at declaration — unknown types/severities are rejected, not stored | `handle_declare_constraint` | `crates/crux-mcp/src/tools/constraint.rs` | `declare_constraint_basic`, `declare_constraint_invalid_type`, `declare_constraint_invalid_severity` | ## Caveats (kept honest) - **Claim 1 — `verify_strict`** is ed25519-dalek's `VerifyingKey::verify_strict` (strong unforgeability / non-malleability), called *by* the per-format `verify_*` fns above. It is not a CueCrux symbol; the CueCrux-defined entry points are the `verify_*_v1` functions. - **Claim 3** — there is no "strict vs CRC mode flag" on `verify_segment_hashes_all`; the function is unconditional and delegates the BLAKE3 re-derivation to `decode_segment_v1`. CRC is a separate, lower-tier check inside the decoder (the `CrcMismatch` path that backs claim 2). The claim holds — it *is* a cryptographic check — but the "strict mode" framing overstates a toggle that does not exist. - **Claim 8** — the proving tests live in `crux-lens-features` (feature-registry coverage). Do **not** use `corecruxctl`'s `gaps.rs` tests (`partial_coverage_reports_gap`, `full_coverage_no_gaps`) here: those test a *different* coverage concept (`.ccxseg`/`.ccxi` segment-indexed-vs-not), not feature-registry gaps. - **Claim 9** — fuzz targets run on the scheduled `fuzz.yml` workflow (nightly toolchain), not in the per-PR `cargo test` run. Reproduce locally with `cargo fuzz run `. # ============================================================ # BEGIN docs/agent/INVARIANTS.md # ============================================================ # INVARIANTS — the guarantees and where they hold > Each invariant: the statement as the code actually implements it, where it is > **established** (written), and where it is **checked** (verified). Symbols are greppable > and CI-verified. ADRs in `docs/adr/` carry the rationale. ## I1 — segment_hash binding ``` segment_hash = BLAKE3( header_hash ‖ record_hash ‖ toc_payload_hash ) where header_hash = blake3(canonical header bytes) record_hash = blake3(record area) toc_payload_hash = compute_toc_payload_hash(toc_payload) ``` Concatenation order is exactly header → record → toc_payload (verified against source). - **established:** `corecrux-segment/src/sealer.rs` (`seal_segment_v1_from_record_area`) and the mirror in `corecrux-segment/src/builder.rs` (`build_segment_v1`) — stored into `SegmentFooterV1.segment_hash`. - **checked:** `corecrux-segment/src/decoder.rs` (`decode_segment_v1`, re-derives all four hashes; `SegmentError::CrcMismatch` on divergence) and `corecrux-storage/src/integrity.rs` (`verify_segment_hashes_all`, used by `corecruxctl verify-store --strict`). ## I2 — segment chain ``` seal(N).previous_segment_hash == seal(N-1).segment_hash (within a shard) seal(0).previous_segment_hash == None (first segment) ``` - **established:** `corecrux-storage/src/lib.rs` (`SegmentSealMaterialV1`, fields `segment_hash` / `previous_segment_seq` / `previous_segment_hash`) — populated on the append/seal path (`corecrux-storage/src/append.rs`), which links each seal to the highest-seq prior segment in the same shard. The receipt is signed by `sign_segment_seal_material` (`corecruxd/src/grpc.rs`). - **checked:** `corecrux-storage/src/tests.rs` (`append_batch_seal_receipt_links_previous_segment_hash`); the `verify-store --strict` chain walk; `build_segment_seal_receipt` gates on both previous-link fields being present. ## I3 — fail-closed verification ``` report.ok ⟺ canonical_hash_match && signature_valid && content_hash_match ``` A verification report is `ok` only if **all** component checks pass. A non-Ed25519 signature path yields `signature_valid = false` (fail-closed), not a skipped check. - **enforced:** `corecrux-receipts/src/c2pa_manifest_v1.rs` (`verify_c2pa_manifest_v1`, report `C2paVerificationReportV1`). The same boolean-AND shape recurs in the MCP verify tools (`crux-mcp/src/tools/receipt_verify.rs`, `observations.rs`) and `corecruxctl/src/c2pa_x509.rs` (adds `&& chain_pass`). ## I4 — monotonic fact versioning ``` A new value for (entity, key) gets version = prev.version + 1, supersedes = prev.fact_id; the predecessor is marked superseded_by (not deleted) and remains in history. First version = 1. ``` - **established:** `corecrux-memory/src/fact_store.rs` — `build_fact` assigns the monotonic version + `supersedes` link; `store` → `supersede_prior_version` → `mark_superseded` flips the predecessor's `superseded_by` marker (journaled for restart survival). Both rows are retained in the store. - **checked / observed:** recall filters on `superseded_by.is_none()` by default (`crux-mcp/src/tools/facts.rs`); `include_superseded=true` / `memory_view` expose the full chain. Tests: `store_same_key_auto_supersedes_prior_version_in_recall`, `auto_supersede_survives_replay`, `consolidate_facts_v1_supersedes_targets_without_deleting_history`. ## I5 — external witness anchoring ``` external_anchor_body commits to (field set includes): receipt_id, tenant_id, anchor_id, actor_passport, transparency_log, log_url, leaf_hash, log_index, tree_size, root_hash, and the RFC-6962 inclusion proof ``` An anchored CROWN chain head is bound to an external transparency-log entry: the inclusion proof must verify leaf → root, and the anchor body commits to the proof, so neither can be swapped independently. - **established:** `corecrux-receipts/src/witness_v1.rs` (`build_external_anchor_body_v1`). - **checked:** `verify_rfc6962_inclusion_proof_v1`; tests `rfc6962_inclusion_proof_verifies_two_leaf_tree`, `external_anchor_body_binds_inclusion_proof`, and the RFC-3161 variant `rfc3161_timestamp_body_binds_token_hash_and_imprint`. ## I6 — custody-proof export An exported context bundle (facts + sessions + signed journal + receipt refs) is passport-signed; verification is offline and fail-closed — any tampered component or signed field fails the check. - **established:** `corecruxctl/src/export.rs` (`run_context_export`). - **checked:** `run_context_verify` (offline, no network); tests `context_export_round_trips_and_is_a_signed_custody_proof`, `tampered_component_fails_verification`, `tampered_signed_field_fails_signature`. The export → offline-verify → tamper-rejection cycle is a release-blocking CI gate. ## I7 — an absent signal is never a pass ``` value(check could not run) ≠ value(check ran and passed) ``` A check that **could not be evaluated** must never return the value of a check that ran and succeeded. This is the single most-repeated defect in this codebase: a two-round coverage sweep in July 2026 found 28 pre-existing defects, and **19 of them were this one bug** (ExecPlan `crux-pinned-defect-remediation-2026-07-31`). It reappears because the lossy form is always the shorter one to write — `.unwrap_or(true)`, `is_none_or(...)`, `.ok()`, an early `return Ok(())`, or simply not emitting the check at all. Three sanctioned shapes, in order of preference: 1. **Add an explicit skipped state** — `checks_skipped: Vec` on the report, listing checks that were *requested but not evaluable*. `ok` keeps its meaning, so an operator who deliberately verifies without an input is not broken; a caller that needs every requested check to have actually run gates on `ok && checks_skipped.is_empty()`. This is the house idiom. Use it wherever an operator could reasonably have omitted the input. 2. **Fail closed** — return an error. Correct only when the absent signal has *no* legitimate cause (a truncated log, an empty sample where the neighbouring call already hard-errors, a pagination field the protocol requires). 3. **Reconcile sent against acknowledged** — where a count exists on both sides, compare them rather than copying one across. Two rules keep the mechanism honest: - **Absent ≠ skipped.** An input class that is *wholly absent* means the caller never asked for that check; it must NOT be listed. A `checks_skipped` that is non-empty on every ordinary run is useless as a gate. Every fix in the ExecPlan above ships with a control test asserting the absent-input case is not flagged. - **Only what the operator configured.** Tightening a check must not start requiring an input the operator never opted into — see `verify_jwt_hs256`, where `iss`/`aud` become required *only* when they are pinned. - **established:** `checks_skipped` on `EvidenceVerifyReportV1`, `ControlVerifyReportV1` and `SnapshotVerifyReport` (`corecruxctl/src/evidence.rs`, `snapshot.rs`); `X509VerifyReport::checks_skipped` (`corecruxctl/src/c2pa_x509.rs`, the originating precedent — commit `5dd75fc`, `incident:2026-07-28`); `ScanDiagnostics::parse_failures` (`corecruxd/src/workspace_scan.rs`) for the scanner form; `AuthMode::parse` and `required_spec_claims` (`corecruxd/src/auth.rs`) for the fail-closed form. - **checked:** the pin-inversion tests named in the ExecPlan's milestones, each paired with an absent-input control. **Review checklist item.** On any verification, scan, or auth path, treat these as requiring a written justification in review: `unwrap_or(true)`, `is_none_or(...)`, `.ok()` discarding an error, `.filter(|_| …exists())`, an early `return Ok(())` on missing input, and a check that is simply not pushed when its input is absent. A clippy lint was considered and **rejected** as too blunt: every one of these is correct in the majority of its uses (`is_none_or` on an optional filter bound, `.ok()` on a best-effort log write), so a lint would train reviewers to `#[allow]` it rather than read it. The signal is the *path*, not the combinator. --- *The I1–I4 formulas are quoted from current source (the audit's guessed forms for I1 and I3 matched the implementation verbatim; I2/I4 field names confirmed exact). I5–I6 statements are derived from the named builders/verifiers and their proving tests, not quoted formulas. I7 is a codified defect class, not a code-derived formula.* # ============================================================ # BEGIN docs/agent/GLOSSARY.md # ============================================================ # GLOSSARY — internal vocabulary → meaning → defining symbol > One line each, mapped to the symbol/file that defines it. Removes guesswork for a cold > agent. `(inferred)` marks a meaning assembled from usage rather than an explicit definition. ## Core concepts | Term | Meaning | Defined at | |---|---|---| | **CROWN** | **C**ryptographic **R**eceipt for **O**peration **W**ith **N**on-repudiation — the Ed25519-signed envelope minted on a receipt-bearing state mutation. | `docs/adr/002-crown-receipts.md`; label const in `corecrux-receipts/src/c2pa_manifest_v1.rs` (`CUECRUX_CROWN_RECEIPT_LABEL`) | | **CRC-v1** (`crc_v1`) | **Crux Response Contract v1** — the daemon's negotiated MCP tool-output envelope (kinds `search` / `addressed` / `fact`). **Default ON**; callers opt out to legacy with a `contract` arg. | `crates/crux-mcp/src/crc_v1.rs` (module doc) | | **passport** | An agent-identity credential carried into the session handshake; the `actor` attribution on substrate writes and the signing key for `.cruxpack` export. Gated by `CORECRUXD_AGENT_PASSPORTS`. | `Passport` in `crates/crux-session/src/`; `CRUX_PASSPORT_ID` env | | **lane** | A pluggable retrieval-scoring signal fused by the engine: dense vector, BM25, graph (topology), frame, navtree. Each toggleable; non-BM25 lanes default-OFF on the CPU daemon. | `corecrux-retrieval/src/fused.rs` (`FusionWeights`, `dense_lane_active`) | | **RCX** | The capability / credit / egress routing plane — capability tokens (`rcx-capability-token`) decided by `RcxRouter` (`crux-router`). | `crates/crux-router/src/lib.rs` (`RcxRouter`) | | **RcxTier / ReceiptClass** | The tier ladder and receipt-class vocabulary carried by an RCX capability token — which backends may be called and which receipt class a call must emit. | `rcx-capability-token/src/lib.rs` (`RcxTier`, `ReceiptClass`) | | **engram** | A distilled, versioned procedure resolved by declared intent at session start — instinct before retrieval — gated by passport tier, with provenance hashes back to source chunks. | `corecruxd/src/http/engrams.rs`; route `/v1/memory/engrams/resolve` (`corecruxd/src/product.rs`, `engram_resolution`) | | **observation** | A captured, redacted record of agent/vendor activity (including MCP handoffs), with provider breakdowns; verifiable via `verify_observation`. *(inferred)* | `corecruxd/src/http/observations.rs`; `crux-mcp/src/tools/observations.rs` | | **credit meter** | Default-off comped-wallet spend rail: pinned quote → signed spend receipt, idempotent on retry. Enabled by `CORECRUXD_CREDIT_METER`. | `corecruxd/src/http/credit_meter.rs` (`post_credit_spend`) | ## On-disk file types | Extension | Meaning | Defined at | |---|---|---| | **`.ccxseg`** | A sealed, append-only segment (header + frames + TOC + footer). The unit of I1's `segment_hash`. | `corecrux-segment/src/builder.rs`, `decoder.rs` | | **`.ccxi`** | **C**oreCrux **C**ompanion **I**ndex — the seal-time inverted index (vocab table + PForDelta postings + per-doc table) that powers BM25. Built when `CORECRUXD_BUILD_CCXI=1`. | `crates/corecrux-index/src/ccxi.rs` | | **`.ccxsnap`** | Projection snapshot (living-row / hot-pointer / relations / cold-segment blocks), magic `CSNP`. **Not** a segment companion — it deliberately sits outside the `ccx` namespace, which is reserved for CoreCrux lane companions. Was `.ccxs` until 2026-08-08, which collided with CoreCrux's subject-profile/traits companion. | `crates/corecrux-projections/src/ccxsnap.rs` | | **`.ccxe`** | Dense-vector companion, CoreCrux format — model-keyed as `.ccxe@`, with the authoritative `model_id` in the **header, never the filename**. Written at seal time beside its `.ccxseg` and served by the local cosine `DenseProvider`. Was the CE-only bespoke `.ccxv` until 2026-08-08. | `crates/corecrux-index/src/ccxe.rs`; writer `corecruxd/src/local_ingest.rs` | | **`.ccxprof`** | Embedder-profile sidecar (JSON `SemanticProfile`) recording tokenizer / prompt-template / normalisation / fingerprint — the parts of the profile the `.ccxe` header does not carry. Was `.ccxp` until 2026-08-08, which collided with CoreCrux's projection companion. | `corecruxd/src/local_ingest.rs` (`write_ccxprof`) | | **`.ccxatt`** | Detached CROWN attestation over a segment's whole companion bundle — binds segment identity plus a per-companion blake3 digest. Resolves to provenance `platform` / `local` / `invalid` / `none`; `invalid` refuses to load in every mode. | `crates/corecrux-index/src/ccxatt.rs` | | **`.cruxpack`** | A passport-signed, BLAKE3-hash-anchored memory-portability transfer envelope (schema `crux.cruxpack.v1`) — a snapshot of one daemon's memory for export/import. | `crates/corecrux-memory/src/cruxpack.rs` (`CRUXPACK_SCHEMA_V1`) | ### Platform lane companions (reader-only in the CE) Computed by the CueCrux platform and shipped to a customer daemon, which reads them locally. The CE ports the `Ccx*Reader` half only — constraint C7 of ExecPlan `crux-companion-vocabulary-unification-2026-08-08`, enforced by `scripts/assert-reader-only-companions.sh`. A reader is inert without artifacts, and only the platform produces artifacts. Provenance: `crates/corecrux-index/VENDORED_FROM.md`. | Extension | Meaning | Defined at | |---|---|---| | **`.ccxs`** | **Subject-profile / traits** companion — `(subject_kind, subject_id) → [(predicate, object)]`, binary-searched by `xxh64(kind_byte ‖ subject_id)`. | `crates/corecrux-index/src/ccxs.rs` | | **`.ccxse`** | **Subject-trait embeddings** — one vector per `.ccxs` trait, keyed by the same subject hash; the header's `source_ccxs_crc` pins which `.ccxs` it was built from. | `crates/corecrux-index/src/ccxse.rs` | | **`.ccxdi`** | **Document index** for the `indexing` lane — per-doc regions and read-pointers with Q8.8 salience. Schema v2 stamps a per-doc `tenant_hash`. | `crates/corecrux-index/src/ccxdi.rs` | | **`.ccxal`** | **Vernacular atoms** for the `vernacular` lane — D0 pointer crystals (back to source spans) and D1 claim-graph atoms, plus an OOV surface pool. | `crates/corecrux-index/src/ccxal.rs` | | **`.ccxn`** | **Entity matrix** — `canonical_name → [(session_id, doc_id, frame_offset)]` plus `canonical_name → EntityType`. Callers must normalise through `canonicalise` before lookup. *(Superseded the earlier inferred "graph topology lane" entry, which was wrong.)* | `crates/corecrux-index/src/ccxn.rs` | | **`.ccxf`** | **Reverse frames** — short canonical-form questions a session uniquely answers, keyed back to `(session_id, doc_id)`. Lane id `topology`. *(Superseded the earlier inferred "query↔question embeddings" entry, which was wrong.)* | `crates/corecrux-index/src/ccxf.rs` | | **`.ccxev`** | **Extracted events** — verb-anchored `(verb_class, object, time, modality)` records. A scoring signal, not a candidate generator. v2 adds `record_off`, the stable physical join key. | `crates/corecrux-index/src/ccxev.rs` | | **`.ccxp`** | **Structured-fact projections** — `UserAction` / `UserAttribute` / `TemporalEvent` / `CountState` / `UserPreference` facts keyed to a source frame. | `crates/corecrux-index/src/ccxp.rs` | | **`.ccxst`** | RAPTOR **navtree** lane file. **Not ported** — 0% deployed coverage, explicitly excluded by the ExecPlan. | CoreCrux only | ## Receipt subtypes (modules in `corecrux-receipts/src/`) | Term | Meaning | Defined at | |---|---|---| | **witness_v1** | External-witness + RFC-3161 trusted-timestamp receipt classes (anchor a CROWN chain head to an external authority via an RFC-6962 inclusion proof). | `witness_v1.rs` | | **c2pa_manifest_v1** | C2PA content-provenance manifest for AI-generated output: BLAKE3 content hash + Ed25519/X.509 signature, bound to a CROWN receipt id. | `c2pa_manifest_v1.rs` | | **audit_bundle_v1** | "BYO Audit Trail" export bundle — packages receipts + scope into a verifiable, replayable archive (golden vectors in `vectors/audit-bundle-v1/`). | `audit_bundle_v1.rs` | | **stream_v1** | Streaming + context-injection receipt classes — receipts over injected-context bodies and stream-end state. | `stream_v1.rs` | | **memory_use_v1** | "Acknowledged Memory Use" receipt — proof that a memory was retrieved/used. | `memory_use_v1.rs` | | **usage_receipt_v1** | Opt-in, metadata-only usage-ping receipt (adoption signal) — deterministic body, sign/verify round-trip, content-bearing bodies rejected; submission is consent-gated. | `usage_receipt_v1.rs`; `docs/usage-receipts.md` | | **ConsolidationReceiptV1** | Receipt from the fact-store consolidation pass, listing the `superseded_fact_ids` it retired. | `corecrux-memory/src/fact_store.rs` | > Other `*_v1.rs` receipt modules exist (`store_v1`, `forget_v1`, `crypto_shred_v1`, > `export_v1`, `identity_v1`, `keyring_v1`, `approval_decision_v1`, `audit_gap_v1`, …); > each is a receipt class named by its file. *(meanings inferred from filename)* # ============================================================ # BEGIN docs/agent/repo-manifest.yaml # ============================================================ schema: crux-agent-manifest/v1 # Machine-readable index of the Crux workspace. No prose interpretation required. # The `ci_assertions` block at the bottom is verified against the tree by # scripts/check-agent-docs.sh — every symbol/test/crate/fuzz-target named there must # still exist, or CI fails. Anchor by symbol name, never line number. summary: Local-first, tamper-evident retrieval + memory engine with signed, replayable receipts. msrv: "1.88.0" edition: "2021" workspace_version: "0.5.65" verify_commands: - cargo test --workspace - corecruxctl verify-store --strict # per-segment BLAKE3 + chain walk - corecruxctl replay --strict # recompute + compare - cargo fuzz run segment_decode # (nightly; one of 5 targets) docs: reading_order: - docs/agent/CODEMAP.md - docs/agent/CLAIMS.md - docs/agent/INVARIANTS.md - docs/THREAT_MODEL.md - docs/spec/receipt-v1.md glossary: docs/agent/GLOSSARY.md conformance_vectors: crates/corecrux-receipts/vectors/ crates: - name: corecrux-segment purpose: sealed segment format (.ccxseg) — build/seal/decode + hash binding entry_symbols: [build_segment_v1, seal_segment_v1_from_record_area, decode_segment_v1] - name: corecrux-storage purpose: append-only shard store, integrity scan, seal material entry_symbols: [integrity_scan_stats_all, verify_segment_hashes_all, SegmentSealMaterialV1] - name: corecrux-receipts purpose: receipt formats, signing, strict verification, witness anchoring entry_symbols: [verify_receipt_v1, verify_c2pa_manifest_v1, verify_bundle_v1, verify_rfc6962_inclusion_proof_v1] - name: corecrux-memory purpose: versioned fact store, decay, supersession, CROWN receipts entry_symbols: [Fact, mark_superseded, ContradictionCandidateV1, ConsolidationReceiptV1] - name: corecrux-index purpose: companion inverted index (.ccxi) — seal-time, powers BM25 entry_symbols: [CcxiBuilder, CcxiReader] - name: corecrux-retrieval purpose: fused retrieval — BM25 + graph-signal fusion entry_symbols: [fused_retrieve, FusionWeights] - name: crux-lens-features purpose: feature-registry coverage / gap reporting entry_symbols: [compute_coverage_report, compute_gaps, compute_promise_coverage] - name: rcx-capability-token purpose: RCX capability token v1.0 (schema-lock + validation) entry_symbols: [RcxCapabilityToken, validate_basic] - name: crux-router purpose: RCX router primitives (capability / credit / egress) entry_symbols: [RcxRouter] - name: crux-cost purpose: ground-truth token-burn cost lens (transcript parse + carried-context attribution) entry_symbols: [analyze_file, analyze_str, CostReport] claims: - claim: receipt signatures are strongly unforgeable symbol: verify_receipt_v1 test: tampering_with_signature_breaks_signature_check - claim: sealed-segment content matches its footer hashes symbol: decode_segment_v1 test: detects_footer_corruption - claim: verify-store does cryptographic (BLAKE3) checks symbol: verify_segment_hashes_all test: strict_scan_verifies_segment_hashes_and_detects_manifest_mismatch - claim: each sealed segment is signed and chained symbol: sign_segment_seal_material test: segment_seal_receipt_signing_commits_to_segment_chain - claim: chain heads can be externally witnessed symbol: verify_rfc6962_inclusion_proof_v1 test: rfc6962_inclusion_proof_verifies_two_leaf_tree - claim: capability tokens verified against trusted issuer symbol: validate_basic test: strict_verify_rejects_tampered_token - claim: memory updates non-destructive and versioned symbol: mark_superseded test: store_same_key_auto_supersedes_prior_version_in_recall - claim: coverage is provable (no silent gaps) symbol: compute_gaps test: gaps_identifies_critical_no_tests_for_shipped - claim: cruxpack export excludes private/reserved/deleted by default and verifies symbol: CRUXPACK_SCHEMA_V1 test: verify_rejects_pack_carrying_deleted_facts - claim: context export is a signed custody proof, tamper fails offline verify symbol: run_context_export test: tampered_component_fails_verification - claim: usage receipts are metadata-only and sign/verify round-trip symbol: usage_receipt_v1 test: body_carries_only_metadata - claim: metered credit spend is signed and cannot double-debit on retry symbol: post_credit_spend test: quote_mutation_retry_conflicts_without_second_debit - claim: passports issue unverified with a strictly ordered earned tier ladder symbol: handle_issue_passport test: issue_passport_creates_unverified - claim: constraints are validated at declaration (invalid type/severity rejected) symbol: handle_declare_constraint test: declare_constraint_invalid_severity invariants: - id: I1 statement: segment_hash = BLAKE3(header_hash || record_hash || toc_payload_hash) checked_by: verify_segment_hashes_all - id: I2 statement: seal(N).previous_segment_hash == seal(N-1).segment_hash within a shard checked_by: append_batch_seal_receipt_links_previous_segment_hash - id: I3 statement: report.ok iff canonical_hash_match && signature_valid && content_hash_match checked_by: verify_c2pa_manifest_v1 - id: I4 statement: new (entity,key) value supersedes prior; history retained checked_by: consolidate_facts_v1_supersedes_targets_without_deleting_history - id: I5 statement: anchored chain head binds to an external transparency-log entry; inclusion proof verifies leaf-to-root and the anchor body commits to the proof checked_by: external_anchor_body_binds_inclusion_proof - id: I6 statement: exported context bundle is passport-signed; tampered component or signed field fails offline verification checked_by: tampered_signed_field_fails_signature fuzz_targets: [segment_decode, storage_scan_frames, receipt_verify_cbor, rcx_canonical_token, escrow_recovery_code] # --------------------------------------------------------------------------- # CI ASSERTIONS — scripts/check-agent-docs.sh greps the tree for each of these. # This is the canonical machine-checked superset of every name cited across the # docs/agent/ set. A rename that orphans a reference here fails the build. # --------------------------------------------------------------------------- ci_assertions: crate_dirs: - corecrux-segment - corecrux-storage - corecrux-receipts - corecrux-memory - corecrux-index - corecrux-retrieval - corecrux-projections - corecrux-frame - crux-lens-features - rcx-capability-token - crux-router - crux-session - crux-mcp - crux-observe - corecruxd - corecruxctl - crux-cost symbols: - verify_receipt_v1 - verify_c2pa_manifest_v1 - build_c2pa_manifest_v1 - verify_bundle_v1 - build_bundle_v1 - verify_rfc6962_inclusion_proof_v1 - build_external_anchor_body_v1 - decode_segment_v1 - build_segment_v1 - seal_segment_v1_from_record_area - verify_segment_hashes_all - integrity_scan_stats_all - SegmentSealMaterialV1 - sign_segment_seal_material - build_segment_seal_receipt - Fact - mark_superseded - ContradictionCandidateV1 - ConsolidationReceiptV1 - compute_coverage_report - compute_gaps - compute_promise_coverage - fused_retrieve - RcxCapabilityToken - validate_basic - RcxRouter - CcxiBuilder - CcxiReader - RcxTier - ReceiptClass - analyze_file - analyze_str - CostReport - CRUXPACK_SCHEMA_V1 - run_context_export - run_context_verify - usage_receipt_v1 - post_credit_spend - handle_issue_passport - handle_declare_constraint - write_ccxe - BootstrapSeeder # outcome dimension (docs/agent/outcome-instrumentation.md) - SpanOutcome - OUTCOME_FIELD - record_outcome - record_outcome_through - OutcomeExt - TRACE_CAPTURE_ENV - CruxSpanLayer - capture_spans - capture_spans_async - outcomes_of - load_latest_workspace_blocking - list_dossier_ids_internal - list_storybook_versions_internal - list_extensions - list_grants_for_extension - list_grants_for_passport - list_links - get_plane_layers - get_project_layers - read_project_layers - read_plane_layers - latest_storybook_ts - query_prefix - count_facts_with_prefix tests: - tampering_with_signature_breaks_signature_check - wrong_verifying_key_fails - verify_receipt_sig_payload_hash_mismatch - tamper_with_signature_breaks_verification - detects_footer_corruption - strict_scan_verifies_segment_hashes_and_detects_manifest_mismatch - segment_seal_receipt_signing_commits_to_segment_chain - append_batch_seal_receipt_links_previous_segment_hash - rfc6962_inclusion_proof_verifies_two_leaf_tree - external_anchor_body_binds_inclusion_proof - rfc3161_timestamp_body_binds_token_hash_and_imprint - strict_verify_rejects_tampered_token - strict_verify_rejects_wrong_trust_root - store_same_key_auto_supersedes_prior_version_in_recall - consolidate_facts_v1_supersedes_targets_without_deleting_history - gaps_identifies_critical_no_tests_for_shipped - coverage_report_tallies_per_system - export_excludes_private_and_reserved_by_default - verify_rejects_pack_carrying_deleted_facts - valid_pack_verifies - context_export_round_trips_and_is_a_signed_custody_proof - tampered_component_fails_verification - tampered_signed_field_fails_signature - body_carries_only_metadata - build_sign_verify_round_trip - assert_kind_rejects_content_bearing_body - spends_seeded_comped_wallet_and_returns_signed_receipt - quote_mutation_retry_conflicts_without_second_debit - issue_passport_creates_unverified - issue_passport_idempotent - tier_rank_ordering - declare_constraint_basic - declare_constraint_invalid_type - declare_constraint_invalid_severity # outcome dimension (docs/agent/outcome-instrumentation.md) - record_outcome_is_inert_when_the_span_never_declared_the_field - the_workspace_scan_loader_records_whether_it_found_anything - a_corrupt_scan_fact_records_empty_not_found - the_dossier_lister_records_whether_it_returned_anything - the_storybook_version_lister_records_whether_it_returned_anything fuzz_targets: - segment_decode - storage_scan_frames - receipt_verify_cbor - rcx_canonical_token - escrow_recovery_code # ============================================================ # BEGIN docs/THREAT_MODEL.md # ============================================================ # Threat Model This document describes the trust boundaries, adversary model, and security assumptions for the Crux Daemon. ## Trust Boundaries ``` +-----------+ Untrusted | Client | HTTP / gRPC (external) +-----------+ | ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ | ─ ─ ─ ─ ─ ─ ─ TLS termination boundary | Trusted (internal) v +-----------+ +-------------+ | corecruxd |------>| Data dir | | (daemon) | | (segments) | +-----------+ +-------------+ | v +-----------+ | CROWN | | receipts | +-----------+ ``` ### Boundary 1: Client to Daemon - **Network:** The daemon binds to `127.0.0.1` by default. Non-loopback binding with `DevScopes` or `Off` auth modes is blocked unless `CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1` is explicitly set. - **TLS:** The Crux Daemon does **not** implement TLS natively. Deployments exposed beyond localhost **must** use a TLS-terminating reverse proxy (Caddy, nginx, Envoy). - **Auth:** Controlled by `CORECRUXD_AUTH_MODE` (required, no default): - `off` - No authentication. All scope checks bypassed. Use only for local development. - `dev_scopes` - Scopes provided by client via HTTP headers. No cryptographic verification. Emits a startup warning. Suitable for development and testing only. - `jwt_hs256` - HS256 JWT verification with `exp`, `nbf` validation and 30s leeway. - `jwt_jwks` - JWKS/OIDC JWT verification. Production-recommended. ### Boundary 2: Daemon to Storage - Segments are stored as files in `CORECRUXD_DATA_DIR`. - BLAKE3 hashes ensure **integrity** (tamper detection), not **confidentiality**. - Data at rest is **not encrypted**. Use filesystem-level encryption (LUKS, dm-crypt) if required by your threat model. - Daemon control records (passports, grants, work/gates, coordination, receipts, tenant metadata, and related internal state) occupy reserved fact namespaces. Generic HTTP/MCP writes and deletes, candidate promotion, extension/WASM writes, remote sync, and CruxPack import reject those namespaces. Only the owning typed daemon workflow may write them through the low-level store. - The canonical namespace policy lives in `corecrux_memory::fact_privacy::{DEFAULT_PRIVATE_PREFIXES, DAEMON_OWNED_ENTITY_PREFIXES, GENERIC_CREATE_RESERVED_PREFIXES}`. Control records remain born-private even if a runtime sharing override names their prefix. The `__agent::::` prefix is a daemon-assigned physical wrapper: clients mutate the owner-visible logical entity and cannot create the wrapper directly. - This boundary is prospective. Reclassifying a legacy row as private during replay does not authenticate who originally wrote it. Operators upgrading a store that may have accepted generic writes into a control namespace must inventory and reissue those rows through their typed governance API before relying on them as authoritative. - Engram overlays are control records: generic fact writes cannot address `__engram__::`; authenticated `PUT /v1/engrams/{name}` with `admin:write` validates the typed object and stamps daemon-owned actor, time, tenant, privacy, and provenance fields. - JWT modes default wired HTTP fact-backed surfaces to real tenant stamping/filtering. Multi-tenant and wildcard tokens on tenant-implicit routes require an authorized `X-Corecrux-Tenant-Id`; an explicit route/body tenant also selects the tenant and must agree with the header. Missing, ambiguous, or mismatched claims fail closed. Operators may explicitly set `CORECRUXD_TENANT_WRITE_STAMP=off` or `shadow` only while migrating historical shared-`default` rows. This flag does not cover MCP or stores with independent entity/session/projection tenant models. - Decision-tool rows deliberately remain compatibility annotations. Their BLAKE3 value is a content identifier, not a signature or append-only proof; consumers must require the `integrity: "untrusted_annotation"` contract and must not use these rows as authorization decisions. ### Boundary 3: CROWN Receipts - Ed25519 signatures provide non-repudiation and tamper evidence for appended events. - The signing key is loaded from `CORECRUXD_RECEIPTS_KEYRING_PATH` or `CORECRUXD_RECEIPTS_KEYRING_JSON`. - Key material is held in process memory. Compromise of the daemon process exposes the key. - Store verification is performed by `corecruxctl verify-store`; CROWN receipt signature verification is performed by the receipt verifier and the `/v1/receipts/` API endpoints. ## Adversary Model ### What CROWN receipts protect against - **Post-write tampering:** Any modification to sealed segments invalidates the BLAKE3 chain and Ed25519 signatures. - **Selective omission:** The chain structure makes it computationally infeasible to remove events from sealed segments without detection. - **Receipt forgery:** Without the Ed25519 private key, an adversary cannot produce valid receipts for fabricated events. ### What CROWN receipts do NOT protect against - **Key compromise:** If the signing key is exfiltrated, all future receipts can be forged. Rotate keys and audit access to keyring files. - **Pre-seal tampering:** Events in the active (unsealed) head segment are not yet covered by the sealed chain. The window is bounded by the seal interval. - **Side-channel attacks:** The daemon does not implement constant-time signing; timing attacks on the Ed25519 operations are not mitigated. ## Auth Mode Implications | Mode | Verification | Production-safe | Use case | |------|-------------|-----------------|----------| | `off` | None | No | Local development, air-gapped | | `dev_scopes` | None (header pass-through) | No | Integration testing | | `jwt_hs256` | Symmetric JWT (exp/nbf/iss/aud) | Qualified | Small deployments with shared secret | | `jwt_jwks` | Asymmetric JWT via JWKS/OIDC | Yes | Production with identity provider | ## Network Assumptions 1. The daemon assumes the network between reverse proxy and daemon is trusted (loopback or private network). It only consumes `Forwarded` / `X-Forwarded-For` for rate-limit keying from peers listed in `CORECRUXD_TRUSTED_PROXY_CIDRS`. 2. gRPC replication between nodes should use authenticated channels (`CORECRUXD_REPLICATION_AUTH_BEARER`). 3. Prometheus metrics (`/metrics`) and health endpoints (`/healthz`, `/readyz`) are unauthenticated. Restrict access at the network level if exposing beyond localhost. 4. MCP Streamable HTTP requests fail closed whenever either registered bearer tokens or hosted-client OAuth introspection is configured. This includes server-info GET, SSE stream creation, and JSON-RPC POST; only an MCP daemon with neither authentication rail configured permits anonymous access. 5. Public `/v1/version` is redacted. Full operational version details live at `/v1/admin/version` behind `admin:read`. 6. `GET /v1/receipts/signing-keys` is unauthenticated and serves only the receipt-signing *public* key. It is discovery, not trust: a key fetched from the daemon that signed a receipt proves nothing if that daemon or the channel is compromised. Verifiers pin the key at enrolment (or cross-check a witness) and treat a changed key as an alarm. ## Capability Token Trust and Revocation - **Local-token trust invariant.** The capability router (`crux-router`) skips signature verification for the `local` backend. This is sound only because the token reaching the router is daemon-minted (self-minted local token in `corecruxd` startup) and never client-injected — the local token does not cross a trust boundary. Hosted/customer backends are always signature-verified against a configured trusted issuer key; the local short-circuit cannot be leveraged to authorise a hosted lane. A future change that routes a client-supplied token through the router must construct it with a trusted issuer pubkey. This is pinned by the `local_signature_bypass_does_not_extend_to_hosted_backend` regression test. - **Revocation is modelled but not yet enforced.** Tokens carry `crl_url` and `push_channel` revocation hints, but the router does not yet consult them, so a revoked-but-unexpired token is still authorised within its validity window. To avoid misleading downstream auditors, the router mode stamp carries `revocation_checked: false`; an authorised decision does **not** imply the token was checked against a CRL or revocation timestamp. Mitigation today: keep token lifetimes short. Revocation IO (CRL/timestamp consult) is a planned later phase. ## Key Escrow and Recovery Scope: the `crux-escrow` crate. A vault's data encryption key (DEK) never leaves the customer's control in plaintext. This section is the M0 artefact of the key escrow and recovery plan and is the contract the crate's tests assert against. ### Recovery modes | Mode | What the server holds | Reconstructs the DEK | Customer artefacts | |---|---|---|---| | **Layer 0 — recovery code** (default) | wrapped DEK ciphertext only | the recovery code alone | one 256-bit code, shown once | | **Layer 1 — Shamir 2-of-3** (opt-in) | wrapped DEK ciphertext + **share C** | any **two** of A/B/C | device share A, printed share B | Layer 0 is not escrow: we hold ciphertext and no key-derivation input. Layer 1 adds one share, deliberately one short of a threshold. ### Adversaries | Adversary | Capability assumed | Defeated by | Residual | |---|---|---|---| | **Us (honest)** | full read of every server-side store | share C is 1 of 3 in a 2-of-3 scheme; Layer 0 stores no key input at all | none — insufficiency is arithmetic, not policy | | **A breached us** | offline copy of everything at rest, including share C | same threshold argument; share C is stored non-exportable and its release is an operation, not a read | an attacker who *also* phishes share A or B reconstructs. Two independent compromises required | | **An account thief** (valid passport, no device) | authenticated as the account holder | release of share C is delayed (default 72h), notified to every registered device, and cancellable by any of them | a thief who holds the account **and** all registered devices **and** waits out the window succeeds. This is the account-recovery floor for any online system | | **A network observer** | full transcript of client↔server traffic | only wrapped ciphertext and share-C *release decisions* cross the wire; unwrapping is client-side | traffic analysis reveals *that* a recovery occurred, not the key. Metadata is receipted anyway | | **A coerced operator** (court order, insider under duress) | can compel production of everything we hold | share C alone reconstructs nothing; there is no server-holds-the-key mode to compel | we can be compelled to hand over a useless share, and to say so. Published in the M5 transparency statement | | **A malicious client** | crafted shares, replayed or forged release requests | shares are integrity-tagged (corruption is detected, not silently mis-reconstructed); release requests are passport-bound and receipted | a client that already holds 2 valid shares is the legitimate owner by construction | ### Constraint mapping Each constraint from the plan maps to a named defence, or is an accepted risk: 1. **Boring** — CSPRNG (`rand`), BLAKE3 in KDF mode (`derive_key`) to turn the recovery code into a wrapping key, XChaCha20-Poly1305 AEAD to wrap the DEK, and a published Shamir implementation over GF(2^8). All four are existing workspace dependencies or a vetted addition; no novel construction. A password KDF (Argon2id, scrypt) is deliberately **not** used. The recovery code is 256 bits of CSPRNG output, not a user-chosen password: there is no low-entropy input for a memory-hard function to defend, and it would only add a dependency and a tuning parameter. This is the same reasoning recorded for the relay device-credential secret. 2. **Insufficient by construction** — enforced by the threshold, and asserted by the `one_share_yields_nothing` and `server_dump_yields_nothing` tests. Neither test can pass if the server's holdings ever become sufficient. 3. **Redundant without weakening** — share C may be replicated freely (Vault + sealed offline backup) *because* it is insufficient alone. Replication count does not appear in any security argument, so operational care is not load-bearing. 4. **Receipted** — every wrap, escrow opt-in, release request, notification, cancellation and completion emits a CROWN receipt into the customer's own timeline. ### Unrecoverable by design **Losing both user shares (A and B, or the Layer 0 recovery code) is unrecoverable.** This is not a gap to close later. The only way to make it recoverable is for our holdings alone to reconstruct the DEK — which is Constraint 2 inverted, and would mean a breach of us is a breach of every customer's data. We accept permanent loss in the rare case in order to make catastrophic loss impossible in the common one. Product consequence: the customer must be told this *before* opting in, in the UI, not in a help article afterwards. That is the M6 launch gate. ### Not defended against - **A compromised client device at wrap time.** If the endpoint that generates the DEK is owned, escrow is irrelevant — the plaintext is already there. - **A user who photographs their recovery code into cloud storage.** Out of scope; the code is rendered for transcription, with print/download offered once. - **Rubber-hose against the *customer*.** They hold 2-of-3 by design; they can be compelled to use it. No key-holder scheme defends against this. ## Error Response Policy Error responses for shard routing errors (`SHARD_UNAVAILABLE`, `WRONG_SHARD`, `SHARDMAP_VERSION_MISMATCH`) are sanitised by default and do not include internal topology details (gRPC addresses, shard map versions). Set `CORECRUXD_DEBUG_ERRORS=true` to include full details for debugging. ## Rate Limiting The Crux Daemon implements coarse client-IP rate limiting and request caps. Use your reverse proxy (Caddy `rate_limit`, nginx `limit_req`, etc.) for route-specific resource protection. `X-Corecrux-Passport-Id` is not trusted as a pre-auth rate-limit key; unauthenticated callers cannot rotate it to obtain independent buckets. Global request bodies default to 16 MiB. Bulk/import endpoints that need a larger envelope have explicit route-specific limits. The console embedding probe is an authenticated admin write operation and rejects metadata, link-local, private, multicast, unspecified, and DNS-rebound targets by default unless the target matches the configured embedding endpoint or an explicit local-probe override is set. ## Route Authorization Proof The CI test gate `route_auth_matrix_is_complete` parses the live Axum router source and fails when a route lacks one of these classes: public, read, write, admin read, admin write, internal replication, or feature-gated. Companion tests pin representative scope contracts and high-risk HTTP boundary routes. ## Dependency Security - `cargo deny check` is run in CI to detect known advisories, bans, licence drift, and source-policy violations. - `cargo audit` is run in CI as a secondary RustSec advisory check. It currently surfaces the yanked `aes 0.9.0` warning through `zip 8.6.0`; this is visible but non-blocking until the upstream dependency path can move. - Known RustSec ignores in `deny.toml` must carry owner and expiry comments; CI enforces this metadata. - Container images are scanned with Trivy before push. Emergency skips require a structured waiver with owner, expiry, reason, commit SHA, run ID, and image reference, uploaded as a 90-day artifact. - Parser/verifier fuzz targets run on the scheduled workflow and as bounded PR runs when fuzz, frame, receipt, router, or lockfile paths change. Crash and corpus artifacts are uploaded for follow-up. - `cargo deny`'s `wildcards` policy is `deny`: no workspace crate may declare a `"*"` version. `multiple-versions` remains `warn` because two RustCrypto generations coexist (the stable `digest 0.10` / `der 0.7` stack from `ed25519-dalek` and `p256`/`ecdsa`, and the `digest 0.11` / `der 0.8` stack from `cms`, `x509-cert`, and `zip`). This duplication is tracked, not silently accepted; the crypto subset will move to a targeted `deny` once upstreams converge. - **Pre-release crypto in witness verification.** `corecrux-receipts` depends on `cms 0.3.0-pre.2` and `x509-cert 0.3.0-rc.4`. These parse RFC3161 timestamp tokens for the optional witness/co-signature path only — they are not in the core CROWN Ed25519 receipt signing/verification path, which uses stable `ed25519-dalek 2.x`. `cms` is the only Rust CMS/PKCS#7 parser and is still pre-release upstream, so it cannot be replaced with a stable equivalent today. Treat witness-timestamp parsing as a defence-in-depth signal, not a primary trust anchor, until these crates reach a stable release. # ============================================================ # BEGIN docs/spec/receipt-v1.md # ============================================================ # Receipt v1 Receipt v1 is bytes-first: receipt bodies are stored and verified as opaque canonical bytes. Producers are responsible for body canonicalization; verifiers must not reserialize a receipt body and then verify the new bytes. ## COSE_Sign1 SCITT export Crux can export a compatible CROWN retrieval receipt as a COSE_Sign1 signed statement conforming to the published [CROWN SCITT Application Profile v0.2](https://github.com/CueCrux/ResearchCrux/blob/main/protocol/scitt-compat/crown-scitt-profile.md) and its normative [CROWN receipt CDDL](https://github.com/CueCrux/ResearchCrux/blob/main/protocol/scitt-compat/crown-receipt.cddl). The `.cose` file is an export representation for SCITT interoperability; it does not replace the stored, bytes-first receipt body and detached signature described below. Export a receipt from JSON and verify the resulting signed statement offline: ```bash corecruxctl receipts export-cose receipt.json \ --out receipt.cose \ --key-file ed25519.key \ --iss https://crux.local \ --kid crux-export-v1 corecruxctl receipts verify-cose receipt.cose --pubkey-b64 ``` `export-cose` accepts exactly one of `--key-b64`, `--key-file`, or `--gen-dev-key`; `--iss` defaults to `https://crux.local`. `verify-cose` accepts an optional `--pubkey-b64`; omitting it selects only the fixed development key. The deterministic `--gen-dev-key` is public test material for fixtures and local interop checks. Never use it to sign production receipts. The CDDL's float16 fields must already be exactly representable as float16; export rejects lossy conversion rather than changing a value committed by `receipt-hash`. `verify-cose` verifies the COSE signature, profile structure, typed payload, and CWT subject binding. Legacy canonical-JSON `receipt-hash` recomputation and SCITT Transparency Service inclusion verification remain separate checks. `export-cose` covers retrieval receipts only: the profile requires `snap-id`, `answer-id`, `fusion`, `retrieval`, `selection`, and `timings`, which `model_invocation` and the other stream receipts do not carry, and a COSE signature cannot reuse the daemon's signature over the raw body bytes. ## Offline stream-receipt verification Stream receipts (`model_invocation`, `context_injected`, `stream_completed`, `stream_aborted`) are signed by the daemon's passport key over the canonical CBOR body bytes. Verify one offline from its observation record (an element of `GET /v1/observations/aggregate?kind=model_invocation` `observations[]`, or its bare `payload`) against a pinned public keyring: ```bash corecruxctl receipts verify-stream-receipt record.json --keyring keyring.json \ --expect-receipt-id --kind model_invocation ``` `keyring.json` is `{"v":1,"keys":[{"keyId":"","pubKeyBase64":""}]}`. The daemon serves exactly this shape, unauthenticated, at `GET /v1/receipts/signing-keys` (each key also carries `publicKeyHex`, `alg: "ed25519"` and `use: "receipt-signing"`, which the keyring parser ignores). That route is key *discovery*, not trust: pin the key when you enrol the daemon, and treat a later change as an alarm rather than refetching. The command checks the daemon's own signature (nothing is re-signed and no key is read from the record) and exits non-zero unless the signature is valid, the body hash matches, the signed body names the tenant (`--tenant-id`, default `local`) and the receipt id, and the signed `kind` is one of the four stream kinds. Other receipt classes the daemon signs with the same key and tenant (e.g. `usage_ping`) share the record shape and are rejected on `kind`. `--expect-receipt-id` and `--kind`, when given, must match the signed body. On success it prints, as JSON on stdout, the fields decoded from the signed body: `kind`, `receipt_id`, `tenant_id` and, for `model_invocation`, `invocation_id`, `provider`, `model_id`, `model_version`, `provider_request_id`, `prompt_hash`, `retrieval_set_hash`, `output_hash` (absent optional fields are omitted). Only these printed fields are signature-verified. The record's top-level `kind` and the payload's own `invocation_id`, `prompt_hash`, `retrieval_set_hash`, `output_hash`, `session_id`, `stable_hash` and `injected_stable_hash` are unsigned copies: read the command's output, not `payload.output_hash`. ## Body Event The generic receipt body event uses: | Field | Value | |---|---| | Stream type | `receipt` | | Event type | `receipt.body.v1` | | Content type | `application/cbor; profile=cuecrux-receipt-body-v1` | Typed receipt bodies include at least: | Field | Type | Notes | |---|---|---| | `schema` | string | Current family: `cuecrux.receipt.body.v1`. | | `kind` | string | Type discriminator such as `memory_use`, `stream_completed`, or `approval_decision`. | | `receipt_id` | string | Stable receipt id. | | `tenant_id` | string | Tenant scope. | Additional fields are kind-specific and must be documented by the module that builds the kind. ## Audit II Receipt Kinds Audit II gap closure adds these typed bodies in `corecrux-receipts`: | Kind | Builder | Purpose | |---|---|---| | `model_invocation` | `build_model_invocation_body_v1` | AI-call provenance: binds provider/model metadata, prompt hash, retrieval-set hash, output hash, and request timing. | | `chain_reanchor` | `build_chain_reanchor_body_v1` | Body-hash-algorithm migration: records old/new chain heads, hash algorithms, receipt window, and linked migration receipts. | | `chain_signature_reanchor` | `build_chain_signature_reanchor_body_v1` | Signature-algorithm migration (G6): re-anchors a chain head originally signed under alg A under a new alg B without invalidating the original. Verified under **both** algorithms; optional hybrid two-signature mode. See [`crypto-migration-v1.md`](crypto-migration-v1.md). | | `redaction` | `build_redaction_receipt_body_v1` | Crypto-shred erasure: records subject scope, CEK commitment, destruction timestamp, and before/after content commitments without storing plaintext. | | `consolidation` | `build_consolidation_body_v1` | Memory consolidation: records canonical fact, superseded fact ids, source receipts, and deterministic strategy. | | `coverage_attestation` | `build_coverage_attestation_body_v1` | Reproducible coverage/eval: binds corpus, run id, commit, lane flags, score, floor, gaps hash, and report hash. | | `external_anchor` | `build_external_anchor_body_v1` | External anchoring: binds a Rekor/Sigstore/private transparency-log UUID, RFC6962 leaf/root/tree metadata, and an inclusion proof. | | `rfc3161_timestamp` | `build_rfc3161_timestamp_body_v1` | Trusted timestamping: binds TSA metadata, RFC3161 TimeStampToken DER bytes, token hash, message-imprint algorithm, and message-imprint hash. | Each kind uses the same `cuecrux.receipt.body.v1` schema family and the same detached `ReceiptSigV1` signature envelope. Kind-specific assertions are cheap post-verification checks; they do not replace generic signature verification. Offline witness verification: | CLI | Check | |---|---| | `corecruxctl receipts verify-external-anchor --body ` | Recomputes RFC6962 inclusion path against the stored leaf hash, log index, tree size, and root hash. | | `corecruxctl receipts verify-rfc3161-timestamp --body [--expected-imprint-hash ]` | Recomputes the stored timestamp-token SHA-256 and optionally checks the expected message imprint hash. | | `corecruxctl receipts verify-rfc3161-timestamp --body --tsa-root-cert [--expected-policy-oid ] [--expected-nonce-hex ]` | Enables strict RFC3161 validation: parses the TimeStampToken CMS, checks TSTInfo content type, signed attributes, message imprint, policy, nonce, CMS signature, TSA time-stamping EKU, signer validity at `genTime`, and a certificate chain to the supplied TSA trust anchor. | | `corecruxctl receipts witness-smoke [--witness-enabled --witness-provider rekor --rekor-url ] [--tsa-enabled --tsa-url --tsa-root-cert ]` | Local-only preflight for default-off live witness/TSA rollout. It does not submit to Rekor or a TSA; it checks required settings, readable Rekor public key paths when provided, and DER/PEM TSA trust roots. | | `corecruxctl receipts verify-chain-reanchor --body ` | Checks a `chain_reanchor` body has the expected kind, non-empty old/new chain heads, distinct heads, supported algorithm labels, non-zero receipt count, and non-empty linked receipt IDs. | Migration attestation: | CLI | Output | |---|---| | `corecruxctl receipts chain-reanchor-attest ...` | Writes a structurally verified `chain_reanchor` body and, when `--out-sig` plus `--signing-key-b64` are provided, a detached `receipt.sig.v1`. | Witness attestation: | CLI | Output | |---|---| | `corecruxctl receipts external-anchor-attest ...` | Writes an `external_anchor` body from transparency-log inclusion proof fields after deterministic RFC6962 verification. | | `corecruxctl receipts rfc3161-timestamp-attest ...` | Writes an `rfc3161_timestamp` body from a TSA token file after token-hash and message-imprint binding checks. | Daemon live witness submission is controlled by default-off env knobs: `CORECRUXD_WITNESS_ENABLED`, `CORECRUXD_WITNESS_PROVIDER`, `CORECRUXD_WITNESS_TIMEOUT_MS`, `CORECRUXD_REKOR_URL`, `CORECRUXD_REKOR_PUBLIC_KEY_PATH`, `CORECRUXD_TSA_ENABLED`, `CORECRUXD_TSA_URL`, `CORECRUXD_TSA_ROOT_CERT_PATH`, and `CORECRUXD_TSA_POLICY_OID`. The daemon exposes the same local-only preflight at `GET /v1/witness/smoke`. Staged crypto-shred support: | Artifact | Schema / method | Notes | |---|---|---| | Crypto-shred envelope | `cuecrux.crypto_shred.envelope.v1` / `xchacha20poly1305-subject-cek-v1` | Non-destructive staging artifact. The envelope stores ciphertext, nonce, AAD hash, plaintext/ciphertext hashes, and a CEK commitment. It never stores CEK bytes. | | Redaction receipt | `kind = "redaction"` | Hash-only receipt body that records subject scope, `subject_cek_id`, `subject_cek_commitment`, optional `cek_destroyed_at`, and linked forget/redaction receipts. | | Destroy marker | `cuecrux.crypto_shred.destroy_marker.v1` | Non-destructive CEK lifecycle artifact. It links a redaction receipt, subject CEK id/commitment, idempotency key, actor passport, optional wrapped-key registry reference, and optional human-gated destruction attestation. It never stores CEK bytes and never deletes keys by itself. | `corecruxctl receipts redaction-attest` can build a redaction receipt body. When called with `--crypto-shred-staged`, it can also seal a local plaintext fixture into a crypto-shred envelope so tests can prove retained ciphertext is unreadable without the per-subject CEK. Production CEK destruction is not part of this command and remains a separately gated operation. `corecruxctl receipts crypto-shred-destroy-marker` writes a JSON destroy marker for migration dry-runs and cutover evidence. With no `--destroyed-at`, the marker state is `destroy_requested` and reports that a human gate is still required before any destructive CEK action. With `--destroyed-at`, callers must also provide `--human-gate-receipt`; the marker state becomes `destroy_attested`, but the command still performs no CEK deletion. ## Signature Event The detached signature event uses: | Field | Value | |---|---| | Stream type | `receipt` | | Event type | `receipt.sig.v1` | | Content type | `application/cbor; profile=cuecrux-receipt-sig-v1` | `ReceiptSigV1` fields: | Field | Type | Notes | |---|---|---| | `schema` | string | `cuecrux.receipt.sig.v1`. | | `receipt_id` | string | Must match the body receipt id. | | `alg` | string | Currently `ed25519`. | | `key_id` | string | Keyring lookup id. | | `signed_at` | string | Stable verification timestamp. | | `signature` | bytes | 64-byte Ed25519 signature over the body bytes. | | `signed_payload_hash` | bytes | 32-byte BLAKE3 hash of the body bytes. | ## Verification A conforming verifier must: 1. Hash the stored body bytes and compare to the event header payload hash. 2. Parse body bytes as CBOR only for validation and trace extraction. 3. Parse signature bytes as `ReceiptSigV1`. 4. Reject unsupported signature algorithms. 5. Verify `receipt_id` and `signed_payload_hash` match the body. 6. Resolve `key_id` in the supplied keyring. 7. Verify Ed25519 over the original body bytes. Trace checks are derived metadata. The stored body bytes and detached signature remain canonical truth. # ============================================================ # BEGIN docs/spec/audit-bundle-v1.md # ============================================================ # Audit Bundle v1 `audit-bundle-v1` is the offline BYO audit-trail bundle family produced by `corecruxctl audit-export` and verified by `corecruxctl audit-verify`. New bundles use `bundle_format_version: 3`; the verifier retains v1 and v2 support. ## Container The production bundle is `tar.zst` with these members: | Path | Encoding | Required | |---|---|---| | `manifest.json` | UTF-8 JSON | yes | | `events.jsonl` | UTF-8 JSON Lines | yes | | `receipts.cbor` | CBOR array | yes | Unknown tar members are ignored for forward compatibility. Verification is offline: no daemon, network, or key fetch is allowed. ## Manifest `manifest.json` is signed Ed25519 over canonical JSON bytes. For v3 the signing input is `cuecrux.audit_bundle.v3\0` followed by the manifest object with `signature_b64` set to the empty string, serialized without extra whitespace and with object keys sorted recursively. Field declaration order is not load-bearing. The signed manifest fields are: 1. `bundle_format_version` 2. `bundle_id` 3. `since` 4. `until` 5. `generated_at` 6. `scope` 7. `fact_count` 8. `receipt_count` 9. `events_jsonl_sha256` 10. `receipts_cbor_sha256` 11. `signer_public_key_b64` 12. `signer_key_id` 13. `key_class` 14. `signature_b64` `bundle_format_version` is `3` for new bundles. `key_class` is one of `persistent`, `env`, or `ephemeral`: `persistent` means the issuer key was generated once and stored owner-only in the daemon data directory; `env` means the operator supplied `CORECRUXD_AUDIT_EXPORT_SIGNING_KEY_B64`. The embedded `signer_public_key_b64` remains the key used for offline cryptographic verification; `key_class` records signed provenance rather than replacing that check. `ephemeral` is **read-only**: writers refuse to produce it (play03 D2). A key minted for the length of one export cannot be pinned by any verifier and cannot tie two exports to one issuer, so an ephemeral-signed bundle is a signature without an identity. `resolve_audit_export_signing_key` and `build_bundle_v1` both reject it; the variant remains in the schema so bundles written before that refusal still parse and verify. Compatibility is fail-closed and version-specific: v1 uses declaration-order compact JSON without a domain tag; v2 uses recursively key-sorted compact JSON prefixed by `cuecrux.audit_bundle.v2\0`; and v3 uses the equivalent v3 domain and requires `key_class`. Verifiers reject any other version. `scope` records the export slice. Its current fields are: | Field | Type | Notes | |---|---|---| | `entity_prefix` | string, optional | Included only when the export was prefix-scoped. | | `include_reserved` | boolean | Whether reserved-prefix entries were included. | | `caller` | string, optional | Passport/operator label supplied by the caller. | ## Data Members `events.jsonl` contains one exported fact event per line. Each line is an object matching `AuditEventV1` in `corecrux-receipts`: | Field | Type | |---|---| | `fact_id` | string | | `entity` | string | | `key` | string | | `value` | string | | `source_receipt` | string, optional | | `confidence` | number | | `stored_at` | RFC3339 string | | `tokens` | integer | | `deleted` | boolean | | `version` | integer | | `supersedes` | string, optional | `receipts.cbor` is a CBOR array of `AuditReceiptRefV1` objects: | Field | Type | |---|---| | `fact_id` | string | | `receipt_id` | string | ## Verification A conforming verifier must: 1. Load exactly the required members. 2. Reject unsupported `bundle_format_version`, and reject v3 without a valid `key_class`. 3. Recompute SHA-256 over the raw `events.jsonl` bytes. 4. Recompute SHA-256 over the raw `receipts.cbor` bytes. 5. Decode `signer_public_key_b64` as a 32-byte Ed25519 public key. 6. Decode `signature_b64` as a 64-byte Ed25519 signature. 7. Verify the signature over the canonical manifest signing bytes. `ok=true` only when all checks pass. ### Signer identity (pinning) Steps 5-7 verify the bundle against the key the bundle itself carries. That proves internal consistency, not origin: an edited bundle re-signed with a newly generated key passes all of them. Identity is therefore a separate, caller-supplied check rather than a format rule, and the verifiers in this workspace report it as `signer_pin` — the result of comparing `signer_public_key_b64` against an expected signer the caller supplies out of band: | `signer_pin` | Meaning | Effect on `ok` | |---|---|---| | `pinned` | The embedded signer is the expected key. | unchanged | | `unpinned` | No expected key was supplied. | unchanged | | `mismatch` | The embedded signer is a different key. | forced to `false` | The pin comes from `--expect-pubkey-hex` / `?expect_pubkey_hex=` where the surface offers one, otherwise from `CRUX_EXPORT_VERIFY_PUBLIC_KEY_HEX` — the audit-export signer. (`corecruxctl context verify` pins the daemon *passport* instead, from `CRUX_CONTEXT_VERIFY_PASSPORT_HEX`; the two identities are different keys.) An `unpinned` report carries `trust_label` = `internally consistent, UNPINNED — trust unproven`, so a green verdict is not read as proof of custody. ## Conformance Vectors Text-friendly vector directories live under `crates/corecrux-receipts/vectors/audit-bundle-v1/`. The v1, v2, and v3 valid vectors preserve compatibility across all supported signing inputs. Each vector contains the three bundle members plus `expected.json`, and a packed `audit-bundle.tar.zst` holding the same three members in the production archive layout. `tools/gen_audit_bundle_vectors.py` emits both forms (the unpacked members and the `.tar.zst`); the byte-canonical archive is also reproducible via the Rust example `cargo run -p corecrux-receipts --example gen_audit_bundle_archive_vectors`. The independent verifier `tools/verify_audit_bundle_v1.py` accepts either an unpacked vector directory or an `audit-bundle.tar.zst` and compares the verdict against `expected.json`. The generator and verifier need the `cryptography` (Ed25519) and `zstandard` (zstd) Python modules; the verifier and generator also accept the `zstd` CLI as a fallback when the module is unavailable. CI exercises every vector in both forms in `.github/workflows/audit-vectors.yml`. # ============================================================ # BEGIN TRUST-CONTRACT.md # ============================================================ # CueCrux Trust Contract v1.0 The Crux daemon is expected to satisfy the CueCrux Trust Contract: - free-tier operation is local-first, CPU-only, accountless, and offline-capable - wire egress is capability-token gated - local documents remain on the daemon unless a token explicitly authorises the relevant data class - every call produces a receipt with the token-selected receipt class - community mods cannot bypass token policy This file is the canonical Trust Contract text. Release bundles include it next to `LICENSE` so operators inherit the same posture every Crux daemon claims to satisfy. ## Repository scope The contract above is what the *local daemon* in this repository guarantees. Every clause is verifiable by reading the source — the receipt verifier, the capability-token spine, the JWT auth path, the `vaultcrux-local` content-signature gate, and the `crux-router` decision matrix all live in this tree. The hosted VaultCrux API that `crux-sync` optionally pushes to is operated by CueCrux Ltd and is **not** in this repository. Self-hosted deployments without the hosted backend run in local-only mode (`DegradedLocal` decisions on hosted-tier capabilities); the trust contract above still holds end-to-end for the local half. Audit the hosted half via the published Trust Contract and the receipts your daemon emits, not via this source tree. # ============================================================ # BEGIN docs/adr/001-append-only-sealed-segments.md # ============================================================ # ADR-001: Append-Only Storage with Sealed Segments **Status:** Accepted **Date:** 2026-04-01 ## Context CoreCrux stores events for long-term memory retrieval. The storage engine must support high-throughput appends, crash recovery, and cryptographic integrity verification. Traditional mutable databases allow in-place updates, which complicates integrity proofs and replay auditing. ## Decision Events are written to an append-only log within **segments**. Once a segment reaches its size threshold, it is **sealed**: a BLAKE3 hash of the entire segment is computed, a trailer index is written, and the segment becomes immutable. Sealed segments are never modified. Key properties: - **Append path:** New events are written to the head (unsealed) segment. Commit markers record durable write boundaries. - **Seal path:** When the head segment exceeds `segment_target_bytes`, it is sealed and a new head segment is created. - **Crash recovery:** On startup, the daemon replays from the last commit marker. Partially-written frames after the marker are discarded. - **Integrity:** `corecruxctl verify-store` checks manifest/frame structure; `--strict` walks sealed segments and recomputes BLAKE3 hashes. Any mismatch is flagged. ## Consequences - Events cannot be deleted or modified after write. This is intentional — it ensures the CROWN receipt chain is verifiable. - Storage grows monotonically. Operators must provision sufficient disk or use segment rotation policies. - Replay is deterministic — given the same sealed segments, any instance will produce identical state. # ============================================================ # BEGIN docs/adr/002-crown-receipts.md # ============================================================ # ADR-002: CROWN Receipts for Receipt-Bearing Operations **Status:** Accepted **Date:** 2026-04-01 ## Context In retrieval-augmented systems, users need assurance that query results are authentic and complete. Without receipts, a compromised intermediary could silently omit results or forge responses. We needed a mechanism that provides non-repudiable proof of every operation without requiring a trusted third party at query time. ## Decision Receipt-bearing operations produce a **CROWN receipt**: an Ed25519-signed envelope containing: - The operation's BLAKE3 content hash - A chain hash linking to the previous receipt (forming an ordered sequence) - A timestamp and operation metadata - The signer's key ID AppendBatch also returns write-confirmation material, and when an append seals a segment it includes a segment-seal receipt signed over the segment hash plus previous sealed segment hash. These response receipts are not the same as stored CROWN receipt streams. Receipts are stored alongside events and are independently verifiable via `corecruxctl receipts verify` or the `/v1/receipts/{id}/verification` API endpoint. The name "CROWN" stands for **C**ryptographic **R**eceipt for **O**peration **W**ith **N**on-repudiation. ## Consequences - Receipt-bearing writes have signing overhead (~50us for Ed25519). Acceptable for our throughput targets. - The receipt chain is append-only. Gap detection is trivial: any missing sequence number indicates tampering or data loss. - Key management is the operator's responsibility. The signing key lives in daemon memory; compromise exposes future signing capability but does not retroactively invalidate existing receipts. - Receipt export (TAR/ZST bundles) enables offline audit without access to the live daemon. # ============================================================ # BEGIN docs/adr/003-cpu-only-crux-daemon.md # ============================================================ # ADR-003: CPU-Only Crux Daemon **Status:** Accepted **Date:** 2026-04-07 ## Context CoreCrux has dataplane-enabled distributions with GPU-accelerated operations (append, read, replay) via CUDA. The Crux Daemon is open source under Apache-2.0 and must be fully functional without GPU hardware or CUDA dependencies. ## Decision The Crux Daemon ships CPU-only. All GPU/CUDA code has been stripped: 1. **`dataplane_store.rs` and `pool.rs`** are type-compatible stubs. They provide the type definitions (`AppendError`, `DataPlanePool`, etc.) needed for API compatibility, but `DataPlanePool` is unconstructable and `DataPlaneStore` methods are `unreachable!()`. 2. **gRPC data-plane RPCs** (AppendBatch, ReadStream, etc.) return `Status::UNIMPLEMENTED` with a clear data-plane-required message. 3. **HTTP handlers** behind `if let Some(pool)` guards return 501 Not Implemented. 4. **Crux Daemon features** work without the data-plane: fact store, session store, BM25 text search (via `.ccxi` companion indexes), CROWN receipts, Prometheus metrics, and the MCP tool server. The `gpu_id: Option` field in shard routing metadata is retained — it's an architectural routing concept, not a CUDA dependency. ## Consequences - Contributors cannot reconstruct the hosted data-plane from the Crux Daemon source. - The gRPC proto contract is identical between Crux Daemon and dataplane-enabled deployments — clients can target either without code changes. - CPU-only text search via BM25 is functional but does not include the GPU-accelerated vector paths available in dataplane-enabled distributions. - The stub pattern means `Option` appears throughout the codebase; this is intentional scaffolding, not dead code. # ============================================================ # BEGIN docs/getting-started.md # ============================================================ # Getting started with Crux From zero to a **verifiable receipt** in about ten minutes: install (≈5 minutes), boot, connect your first agent harness, store a fact, and prove the receipt catches tampering. Crux is a single local-first binary — no account, no telemetry, no outbound connections (verify that yourself: `scripts/assert-no-phone-home.sh`). Pick one install path, then continue at [First boot](#2-first-boot). ## 1. Install (~5 minutes) Release binaries are cosign-signed; native installer targets also carry SLSA provenance. Whatever path you choose, verify the artifact **before** anything runs — copy-paste commands and the cross-build provenance boundary live in [verify-release.md](verify-release.md). ### Option A — installer script (macOS, Linux, WSL2) Resolve the exact release, download the installer and its signature, verify it, then read and run it. We never ask you to pipe a URL into a shell, and the installer refuses to install binaries that fail signature verification (it needs [cosign](https://docs.sigstore.dev/cosign/system_config/installation/)). ```bash REPO=CueCrux/Crux TAG="$(curl -fsSL -o /dev/null -w '%{url_effective}' \ "https://github.com/${REPO}/releases/latest" | sed 's|.*/tag/||')" BASE="https://github.com/${REPO}/releases/download/${TAG}" curl -fsSLO "${BASE}/install.sh" \ -O "${BASE}/install.sh.sig" \ -O "${BASE}/install.sh.pem" cosign verify-blob \ --certificate install.sh.pem \ --signature install.sh.sig \ --certificate-identity \ "https://github.com/${REPO}/.github/workflows/release.yml@refs/tags/${TAG}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ install.sh less install.sh bash install.sh --version "${TAG}" # add --with-service for a service unit ``` Installs `crux`, `corecruxd` (same binary, service-manager name), `corecruxctl`, and `crux-hook` into `~/.local/bin`, and creates a private data dir (`~/.local/share/crux`, mode 0700). Nothing is auto-started. Re-run the verified installer to upgrade the complete set together. Packaged installs refuse daemon-only `crux self update` so the CLI and hook cannot be left on an older version. ### Option B — Docker / Podman Pull-only compose stack with a pinned version, non-root runtime, and a healthcheck: [examples/quickstart/](../examples/quickstart/). Or minimally: ```bash docker run -d --name crux \ -p 127.0.0.1:14800:14800 -p 127.0.0.1:14801:14801 \ -v crux-data:/data \ -e CORECRUXD_AUTH_MODE=dev_scopes \ -e CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1 \ -e CORECRUXD_HTTP_HOST=0.0.0.0 -e CORECRUXD_MCP_HOST=0.0.0.0 \ ghcr.io/cuecrux/crux-daemon:latest ``` `:latest` tracks the newest **release tag**, not `main`. Verify the image signature by digest first ([verify-release.md §4](verify-release.md)). ### Option C — Debian / Ubuntu (.deb) ```bash TAG=v0.5.0 # pick the release you want curl -fsSLO "https://github.com/CueCrux/Crux/releases/download/${TAG}/crux-daemon_${TAG#v}_amd64.deb" # verify its cosign signature first — verify-release.md §1, same flags sudo dpkg -i "crux-daemon_${TAG#v}_amd64.deb" ``` Ships a systemd unit that is installed but **not** enabled; start it when you choose: `sudo systemctl enable --now crux` (data in `/var/lib/crux`). ### Option D — Homebrew ```bash brew tap cuecrux/tap && brew install crux ``` (Available once the tap is published alongside the first public release.) ### Windows Native Windows builds are post-v1. Use WSL2 with Option A — it is a first-class, continuously-dogfooded path. WSL2 specifics are in [Troubleshooting](#troubleshooting) below. ### Build from source Rust 1.88+, `protobuf-compiler`, then `cargo build --locked --release`. See the [README Quickstart](../README.md#quickstart). #### Cargo feature flags The default build is the Community Edition daemon. All optional surfaces are off by default; enable them with `--features`: | Feature | Default | What it adds | |---|---|---| | `hosted-surfaces` | off | Hosted-service HTTP surfaces: the Pro GPU-1 compute bridge (`/v1/gpu1/*`) and the Pro cloud access posture (`GET /v1/cloud/access-contract`). Compiled entirely out of the default Community Edition binary — the routes 404 and their handler code is absent — so a stock build advertises and serves neither. | | `wasm-extensions` | off | wasmtime-backed host for `kind: wasm` community extensions. | | `otel` | off | OpenTelemetry OTLP export. | Runtime env flags (e.g. `CORECRUXD_CONTEXT_SURFACE`, `CORECRUXD_QUOTA`) gate routes that are always *compiled in* but return 404 until enabled; the `hosted-surfaces` cargo feature is different — it removes the routes and handlers from the binary at build time. ## 2. First boot If you installed a service unit, start it (`systemctl --user enable --now crux` / `launchctl load ...` / `sudo systemctl enable --now crux`). Otherwise, foreground: ```bash CORECRUXD_AUTH_MODE=dev_scopes CORECRUXD_DATA_DIR=~/.local/share/crux crux ``` `CORECRUXD_AUTH_MODE` must be set explicitly — there is no default. `dev_scopes` is right for a single-user loopback install; use `off` only for throwaway local experiments, and `jwt_hs256`/`jwt_jwks` for anything shared (see [config.example.env](../config.example.env)). Now open **** — the embedded console (it ships inside the binary; works fully offline) runs a one-time setup: auth posture, health check. Or probe from the shell: ```bash curl -sf http://127.0.0.1:14800/readyz # {"ok":true} curl -sf http://127.0.0.1:14800/v1/version | jq '.version, .product.tier' ``` Prefer a guided tour? `corecruxctl quickstart` walks config → health → first fact → query → cleanup interactively. ## 3. Connect Claude Code The daemon's MCP server listens on `http://127.0.0.1:14801/mcp`: ```bash claude mcp add --transport http crux http://127.0.0.1:14801/mcp ``` Start a new Claude Code session and the `mcp__crux__*` tools (`store_fact`, `query_facts`, `query`, `save_session`, …) are available. If the daemon runs with agent tokens (`CRUX_AGENT_TOKEN`), pass the matching `Authorization: Bearer` header — see [examples/mcp-configs/](../examples/mcp-configs/) for header-carrying configs. Optional but worth it: the [Claude Code observation hooks](../integrations/claude-code/) capture every session lifecycle event as an Ed25519-signed observation — verifiable evidence, not a self-reported log. ## 4. The first receipt (the point of all this) Store a fact — from the Claude Code session you just connected ("store a fact that the rollout completed"), or by hand: ```bash curl -s -X PUT http://127.0.0.1:14800/v1/facts \ -H "Content-Type: application/json" \ -H "X-Corecrux-Scopes: facts:write,facts:read" \ -d '{"entity":"getting-started","key":"first_fact","value":"stored at minute ten"}' | jq . ``` Every state mutation produces a signed CROWN receipt. See it in the console (Facts → entity timeline), then prove the signature actually protects you — this 60-second demo seeds a receipt, verifies the store, flips one byte on disk, and watches verification fail: ```bash bash scripts/demo-receipt-tamper.sh ``` That failure is the product: your agent's memory is evidence, not vibes. ## 5. Connect a second harness The same daemon serves every harness on this machine — that's the continuity story (one memory, many agents): - **Codex CLI**: [integrations/codex-cli/](../integrations/codex-cli/) — stdio MCP shim + observation hooks. - **Claude Desktop / Cursor**: ready-made configs in [examples/mcp-configs/](../examples/mcp-configs/). - Anything that speaks MCP: point it at `http://127.0.0.1:14801/mcp`. Facts stored from Claude Code are immediately queryable from the second harness (`query_facts` on the same entity). ## Troubleshooting **WSL2** (this is our daily test bed): - Backgrounding the daemon from a script: a bare `&` is fragile under WSL tty-detach. Use `setsid nohup crux < /dev/null > crux.log 2>&1 &` then `disown` — or just use the systemd user unit (`install.sh --with-service`), WSL2 ships systemd by default these days. - Windows-side browsers reach the console via `http://127.0.0.1:14800` (localhost forwarding is automatic; if you've disabled it, use the WSL interface IP from `ip addr show eth0`). - Keep the data dir on the Linux filesystem (`~/.local/share/crux`), not under `/mnt/c` — 9p file locking will hurt you. **Port already in use**: something else owns 14800/14801. Override with `CORECRUXD_HTTP_PORT` / `CORECRUXD_MCP_PORT` (don't change them without reason — 14800 is the documented Crux port). **401/403 on `/v1/*`**: your auth mode wants scopes or a JWT. For `dev_scopes`, send `X-Corecrux-Scopes` as in the examples; for JWT modes see [session-handshake.md](session-handshake.md). **"Pro required" / 501 on some routes**: the free local daemon is CPU-only and some capabilities are hosted-only (GPU rerank/enrich, managed sync). The boundary is machine-readable: `curl -s localhost:14800/v1/version | jq '.product.capability_catalog'`. Free-tier routes never require an account. **More**: [troubleshooting.md](troubleshooting.md), [ops-guide.md](ops-guide.md). ## Upgrade Upgrades are always explicit — the daemon never updates automatically: ```bash bash install.sh --version vX.Y.Z # re-verifies signatures; data is kept # or: brew upgrade crux / dpkg -i the new .deb / bump CRUX_VERSION in compose ``` Previous-version binaries stay downloadable forever (releases are superseded, never deleted), so pinning back is the same command with an older tag. ## Uninstall ```bash bash install.sh --uninstall # removes binaries + service unit ``` Your data is **never** deleted by uninstall. Export it first if you're leaving (console → Settings → Export, or `GET /v1/facts/export`), then remove the data dir yourself: `rm -rf ~/.local/share/crux`. Docker: `docker compose down -v` deletes the volume — same rule, your explicit `-v`. # ============================================================ # BEGIN docs/developer-portal.md # ============================================================ # Developer Portal This page is the stable entrypoint for building against the local-first Crux daemon. Start here when you need the HTTP API, MCP surface, SDK lifecycle, or offline receipt verification path. ## API Contract - HTTP base: `http://127.0.0.1:14800` - MCP base: `http://127.0.0.1:14801/mcp` - OpenAPI: `GET /v1/openapi.json` - Human route notes: [api-reference.md](api-reference.md) - Error format: [RFC 7807 Problem Details](api-reference.md#error-format) The launch-default single-user loopback rail is `dev_scopes`. Local examples use explicit scope headers so the same requests fail closed when you switch to JWT auth: ```bash curl -s http://127.0.0.1:14800/v1/openapi.json | jq '.info.title' curl -s "http://127.0.0.1:14800/v1/facts?query=hello&token_budget=500" \ -H "X-Corecrux-Scopes: query:read" | jq . ``` Every retrieval call should carry an explicit `token_budget`. Use `500` for small confirmations and raise it only when the caller needs more context. ## Agent And MCP Integration - [Agent guide](agent-guide.md) covers install UUIDs, bootstraps, fact writes, retrieval, and handoff posture. - [MCP system prompt](mcp-system-prompt.md) is the recommended runtime protocol for agents connected to the daemon. - [Session handshake](session-handshake.md) documents the plan and invocation receipts that bind an agent session to its local identity. - [LLM shim](llm-shim.md) documents the local shim for context injection and mediation receipts. ## Receipt Verification Crux verification is offline-first. The CLI can check the on-disk store, inspect individual receipts, and verify typed receipt bodies without calling a hosted service. ```bash corecruxctl verify-store --data-dir ./data --scope recent corecruxctl verify-store --data-dir ./data --scope all --mode full --strict corecruxctl inspect-receipt --data-dir ./data corecruxctl receipts verify-external-anchor --body receipt-body.cbor corecruxctl receipts verify-rfc3161-timestamp --body receipt-body.cbor corecruxctl receipts verify-chain-reanchor --body receipt-body.cbor ``` The bytes-first receipt contract is in [spec/receipt-v1.md](spec/receipt-v1.md). Release artifact verification is separate and lives in [verify-release.md](verify-release.md). ## SDKs The in-repo SDKs are the supported public SDK surface: - [sdks/python](../sdks/python) - [sdks/typescript](../sdks/typescript) - [sdk-release-lifecycle.md](sdk-release-lifecycle.md) Generated clients should be pinned to the daemon release they target and regenerated from `/v1/openapi.json` when the HTTP API moves. ## Contracts - [contracts/README.md](contracts/README.md) documents the CRC-v1 response envelope. - [contracts/crc-v1.schema.json](contracts/crc-v1.schema.json) is the JSON schema used by the contract fixtures. - [error-catalogue.md](error-catalogue.md) lists stable problem details. # ============================================================ # BEGIN docs/api-reference.md # ============================================================ # API Reference Crux Daemon exposes three network surfaces by default: - HTTP API on `14800` - gRPC API on `4007` - Built-in MCP server on `14801` when `CORECRUXD_MCP_ENABLED=true` (default) ## HTTP Endpoints ### Infrastructure | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/healthz` | Health check with build metadata | None | | GET | `/readyz` | Readiness probe (lock held, routing loaded, capacity OK) | None | | GET | `/metrics` | Prometheus metrics | None | | GET | `/v1/version` | Build version, feature flags, sync posture, and cached git update status (`current`, `behind`, `ahead`, `diverged`, `disabled`, `unavailable`, or `error`) | None | ### Query & Retrieval | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/query/text-search` | BM25 text search with token budget and coverage scoring | `query:read` | | POST | `/v1/query/text-search/expand` | Progressive retrieval — expand scan results with full content | `query:read` | | POST | `/v1/query/graph-expand` | Graph traversal from seed artifacts with budget | `query:read` * | | POST | `/v1/query/time-range` | Temporal range query over artifact state changes | `query:read` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. #### Segment coordinates on a result Each `/v1/query/text-search` result (and each `/expand` chunk) carries two segment coordinates, which are **not the same number**: | Field | Meaning | |---|---| | `segment_index` | Position in the loaded-reader list, ascending by sequence. **0-based, global, and mutable** — see the warning below. Pairs with `doc_id` to form `result_id`, and is the value `/expand` takes back. | | `segment_seq` | The sealed segment's own sequence — the `segment_seq` the `/v1/local/ingest` receipt returned. **1-based and stable for the life of the segment.** | To join a query result back to the ingest that produced it, **join on `segment_seq`**. **Never derive one from the other.** `segment_index` is a position in the daemon's loaded-reader list, so it moves when *any* segment is loaded, sealed or erased — including segments belonging to other tenants, written by someone else, while your query runs. Measured on one host on 2026-08-07: `segment_seq - segment_index` was **1** in the morning, **18** that afternoon and **17** minutes later, uniformly across every tenant. There is no offset to store, not even a freshly measured one. A consumer that translates between them scores a plausible, uniform **0%** instead of erroring, because every hit unmaps at once and nothing raises. That is how the BEAM-100K mapping was wrong for a full run before anyone noticed. If you are on a daemon old enough not to return `segment_seq`, do not guess the offset: discover the tenant's live segment ids through the same query path you score with, and pair them in ascending order with your ingest batches — segment sequences are allocated monotonically, so ascending live order matches ingest order whatever the absolute numbers have become. ### Local prose ingest | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/local/ingest` | Seal pre-chunked prose into a local segment (BM25 + optional dense) | `admin:write` (tenant-scoped) | The 202 response reports the dense lane explicitly, because a corpus that failed to embed still indexes over BM25 and otherwise looks healthy: | Field | Meaning | |---|---| | `dense_vectors` | Vectors actually persisted to the segment's `.ccxv` companion | | `dense_expected` | Vectors this ingest expected — every chunk when the node embeds server-side, the caller-vectored subset otherwise | | `dense_status` | `ok`, `partial`, `skipped`, `not_configured` (no vectors expected — BM25-only by configuration), or `not_applicable` (idempotent re-ingest, nothing sealed) | **Assert `dense_status == "ok"`** (or `dense_vectors == dense_expected`) after every ingest that expects semantic recall. `skipped` means the embed step failed and the segment sealed lexical-only; the daemon logs `local-ingest-dense-gap-sealed` at WARN with the segment sequence, and the cause in the preceding `local-ingest-embedding-failed` line. Both fields are additive — a client that ignores them sees the response shape it saw before ([local_ingest.rs](../crates/corecruxd/src/http/local_ingest.rs)). ### Fact Store | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | PUT | `/v1/facts` | Store or update a shared fact (entity + key + value + confidence) | `query:read` | | PUT | `/v1/facts/bulk` | Bulk-store multiple facts | `query:read` | | GET | `/v1/facts` | Query facts by text with token budget | `query:read` | | GET | `/v1/facts/{factId}` | Retrieve a specific fact by ID | `query:read` | | DELETE | `/v1/facts/{factId}` | Delete a fact | `query:read` | | GET | `/v1/facts/entity/{entity}` | List all facts for an entity | `query:read` | HTTP fact writes do not support `private=true`. Private facts and per-agent visibility are MCP-only features. ### Work and Orchestrators Work and orchestrator records are authority-sensitive, tenant-scoped surfaces. In JWT modes, creator/updater/commenter identity and tenant come from verified claims; matching body fields are compatibility constraints, not an impersonation mechanism. A caller cannot list, read, mutate, comment on, attach members to, or resolve gates for another tenant. In local `off`/`dev_scopes` mode, an explicit passport header or matching body assertion is recorded as `operator:unverified:`. It is not a verified human identity: work state changes always queue for review. Human gate decisions in authenticated modes require `facts:write`, a canonical JWT `passport_id`, and the work tenant; MCP agent tokens and `sub`-only JWTs cannot approve or reject. An unmapped MCP agent token is attributed as `agent:` and is gated as automation; only an explicit `CRUX_AGENT_PASSPORTS` mapping may resolve it to a real passport id. The generic `/v1/entities/{kind}/{id}` and MCP `entity_*` APIs reject governed `orchestrator` records and omit them from unfiltered listings. Use the typed `/v1/orchestrators` routes so tenant and actor checks cannot be bypassed. ### Session Store | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | PUT | `/v1/sessions/{sessionId}/state` | Store session state (JSON blob) | `query:read` | | GET | `/v1/sessions/{sessionId}/state` | Retrieve session state | `query:read` | ### Event Append | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/admin/append` | Append events to a stream (`/v1/append` compatibility alias) | `admin:write` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. ### CROWN Receipts | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/receipts/{receiptId}` | Retrieve a CROWN receipt body | `events:read` | | GET | `/v1/receipts/{receiptId}/signature` | Retrieve receipt Ed25519 signature | `events:read` | | GET | `/v1/receipts/{receiptId}/verification` | Verify receipt signature and chain | `events:read` | | GET | `/v1/receipts/signing-keys` | Receipt-signing public keyring (`Ed25519KeyRingV1`) for offline verifiers | public | ### Credits | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/credits/spend` | Default-off comped-wallet spend rail. Requires `CORECRUXD_CREDIT_METER=1`; consumes a pinned quote, reserves/spends seeded credits idempotently, and returns a signed `crux.credit_spend_receipt.v1`. Does not mint fiat credits or call Paddle. | `admin:write` | When `CORECRUXD_CREDIT_METER=1`, successful RCX-verified `POST /v1/gpu1/rerank` calls also reserve and spend 3 comped-wallet credits. The pinned `crux.credit_quote.v1` rides at `options.credit_quote`; the `crux.gpu1.compute_response.v1` envelope adds `credit_spend_receipt`, `credits_spent`, and `wallet_balance`. Failed/degraded compute releases the reservation and emits no spend stamp. ### BYOK Provenance (default off) | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/provenance/sign` | Sign an asset with a request-scoped caller P-256 key and leaf-first certificate chain | `provenance:write` or `admin:write` | | POST | `/v1/provenance/verify` | Verify envelope integrity and optional asset binding without retaining a record | `provenance:write` or `admin:write` | | POST | `/v1/provenance/verify-record` | Verify and retain a passport-signed record; supports `Idempotency-Key` | `provenance:write` or `admin:write` | Set `CORECRUXD_FEATURE_PROVENANCE_API=1` to mount the routes. They require an explicit authorized tenant and a safe transport posture; see the [BYOK provenance quickstart](provenance-byok-quickstart.md). Exact leaf pins can establish a narrow operator-selected identity policy, but the beta does not perform CA-chain/root validation. Metering remains a no-op until the fractional-credit contract is ratified and implemented. `CORECRUXD_PROVENANCE_RETENTION_DAYS=1..3650` enables activity-driven retained-record expiry; unset means no automatic deletion. Sweeps preserve active tenant-wide or `provenance::verification_record::`-scoped legal holds and mint count-only governance receipts. A non-empty sweep reports its receipt status/id and deletion count in `X-Cuecrux-Retention-*` response headers. See the quickstart for the exact lifecycle and fail-closed behavior. All three routes share a tenant-scoped 120-request/minute budget keyed by the verified stable JWT `sub` or `passport_id`, so token rotation and switching operations do not create fresh allowance. A rejection is `429` with `Retry-After: 60`. This sits behind the daemon-wide effective-client-IP token bucket, body limits, and concurrency/load-shed layer. The principal table is process-local: a multi-replica hosted deployment must additionally enforce a shared limit at its edge or gateway. Configure `CORECRUXD_TRUSTED_PROXY_CIDRS` before relying on forwarded client addresses; loopback is exempt by default. ### Replay Exports | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/replay/exports/receipts/{receiptId}` | Export receipt bundle (ZIP/TAR+ZST) | `events:read` | | GET | `/v1/replay/exports/answers/{answerId}` | Export answer bundle | `events:read` | | GET | `/v1/replay/exports/actions/{actionId}` | Export action bundle | `events:read` | | GET | `/v1/replay/exports/streams/{streamType}/{streamId}` | Export stream bundle | `events:read` | ### Self-Observation (crux-observe) | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/ops/facts` | Query operational facts | `admin:read` | | GET | `/v1/ops/errors` | Query recent errors since timestamp | `admin:read` | | GET | `/v1/ops/health` | Operational health summary | `admin:read` | | POST | `/v1/bootstrap/pull` | Pull bootstrap facts with token budget | `admin:read` | | GET | `/v1/bootstrap/status` | Check bootstrap seeded state | `admin:read` | ### Projections | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/projections/entity/count` | Entity count by tenant | `query:read` * | | GET | `/v1/projections/entity/timeline` | Entity state timeline | `query:read` * | | GET | `/v1/projections/entity/current-state` | Current entity state | `query:read` * | | GET | `/v1/admin/projections/meta` | Projection cursor metadata per shard | `admin:read` * | | POST | `/v1/admin/projections/rebuild` | Trigger projection rebuild | `admin:write` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/state` | Artifact living state | `admin:read` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/relations` | Artifact relations | `admin:read` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/dependents` | Artifact dependents | `admin:read` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/pressure-events` | Artifact pressure events | `admin:read` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. ### Repos & Code Map AST-derived code structure for registered repositories. Registration with a `root_path` runs a one-shot scan (Rust natively; TS/TSX/Vue/Python via tree-sitter) and persists it; the repo watch loop re-indexes on change. The codemap endpoint is the read side — the daemon serving its own code understanding back to agents. | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/repos?tenant_id=…` | List registered repos for a tenant | `admin:read` | | POST | `/v1/repos` | Register a repo (`root_path` scans now; `clone_url` defers) | `admin:write` | | GET | `/v1/repos/{repoId}?tenant_id=…` | One registration | `admin:read` | | DELETE | `/v1/repos/{repoId}?tenant_id=…` | Unregister (stops watch) | `admin:write` | | GET | `/v1/repos/{repoId}/codemap?tenant_id=…&format=summary\|full` | AST code map: `summary` = stats + per-crate rollup; `full` = files, symbols, deps, routes | `admin:read` | | POST | `/v1/workspace/scan` | Scan the daemon's own workspace (`CORECRUXD_WORKSPACE_PATH`) | `admin:write` | | GET | `/v1/workspace/scan` | Latest self-scan in full | `admin:read` | | GET | `/v1/workspace/storyline?format=tree\|json` | Per-route call trees from the self-scan | `admin:read` | ### Routing & Shards | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/shards` | List shards with store status | `admin:read` | | GET | `/v1/shard-map` | Current shard map (shard → node assignment) | `admin:read` | | GET | `/v1/route` | Route a stream to its owning shard | `admin:read` | | GET | `/v1/routing/route` | Debug route resolution | `admin:read` | | GET | `/v1/routing/status` | Routing table version and reload status | `admin:read` | | GET | `/v1/gpus` | GPU inventory | `admin:read` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. ### Admin & Operations | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/admin/shard-map` | Update shard map | `admin:write` | | GET | `/v1/admin/control` | Current control state | `admin:read` | | POST | `/v1/admin/restart` | Request daemon process restart | `admin:write` | | GET | `/v1/admin/ops-log` | Structured operations log | `admin:read` | | POST | `/v1/admin/valves` | Set valve states (throttle, pause, emergency brake) | `admin:write` | | GET | `/v1/admin/replication/status` | Replication topology status | `admin:read` * | | POST | `/v1/admin/actions` | Submit admin action (seal, scrub, verify, rebalance) | `admin:write` | | GET | `/v1/admin/actions/{actionId}` | Get admin action status | `admin:read` | | POST | `/v1/admin/stream-meta` | Update stream metadata | `admin:write` * | | GET | `/v1/admin/tenants/{tenantId}/footprint` | Segments, docs and bytes a tenant occupies in the retrieval corpus | `admin:read` † | | POST | `/v1/admin/forget-tenants` | Erase the named tenants' retrieval corpora (`/v1/admin/forget-tenant` is the singular alias) | `admin:write` † | | DELETE | `/v1/admin/forget-tenants/{tenantId}` | Lift a mask-only erasure | `admin:write` † | | POST | `/v1/internal/replication/segments` | Receive replicated segments | `replication:write` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. † Requires `CORECRUXD_TENANT_ERASURE=1`; the routes 404 while it is unset. #### Tenant corpus erasure `POST /v1/admin/forget-tenants` takes `{"tenant_ids": ["…"], "reclaim": false}`. Empty ids are dropped and duplicates collapsed in first-seen order; the batch is capped at 4096, `__`-prefixed (reserved) tenant ids are refused, and `admin:write` is checked for **every** named tenant before anything is mutated — a partially authorised batch is `403` and erases nothing. Two layers: - **Layer 1 (default).** Segments sealed up to the current `watermark_segment_seq` become invisible to that tenant, and the mask is persisted to `/forgotten-tenants.json` before the response returns. Reversible via the `DELETE` route. Anything ingested afterwards is served normally, so a corpus can be erased and re-paved under the same tenant id. - **Layer 2 (`"reclaim": true`).** Additionally deletes the file group of every segment whose documents all belong to that tenant. **Irreversible** — recovery is restore-from-backup only. Segments shared with another tenant are never deleted; they stay masked and are reported as `mixed_segments_retained`. Both routes re-read the shard directories from disk before answering, so a segment sealed since the last scan is inside the blast radius rather than silently outside it. Segments are found by their `.ccxseg` file, not by a companion: a segment holding fact records has no `.ccxi` to key off, and it is still erasable. Tenant membership comes from the `.ccxi` doc table where one exists and from the segment's own frame headers where it does not. A segment that is on disk but cannot be read at all is reported as `unattributable_segments` on both routes, and is neither masked nor reclaimed — deleting a segment whose owner cannot be established risks a co-tenant's data. A non-zero count means an erasure is incomplete and needs an operator. Scope is the **retrieval corpus only**. A tenant's facts, sessions and activity rows are untouched, so the response says `corpus_erased`, not `tenant_forgotten`. Each tenant's erasure mints a signed governance receipt carrying counts, the watermark and the scope — never document content. --- ## gRPC Services Default port: `4007`. Proto files in `proto/`. ### CoreCruxDataPlaneV1 All RPCs return `UNIMPLEMENTED` in Crux Daemon and require a dataplane-enabled deployment. | RPC | Request | Response | Description | |-----|---------|----------|-------------| | `AppendBatch` | `AppendBatchRequest` | `AppendBatchResponse` | Append events with deduplication | | `ReadStream` | `ReadStreamRequest` | stream `ReadStreamResponse` | Read events from a stream | | `ReadStreamBatched` | `ReadStreamBatchedRequest` | stream `ReadStreamBatchResponse` | Batched read with configurable limits | | `ReadStreamBatchedUnary` | `ReadStreamBatchedRequest` | `ReadStreamBatchResponse` | Unary batched read | | `ReadManyBatchedUnary` | `ReadManyBatchedRequest` | `ReadManyBatchedResponse` | Read multiple streams in one call | | `ReadManyFramesBatchedUnary` | `ReadManyFramesBatchedRequest` | `ReadManyFramesBatchedResponse` | Read raw frames from multiple streams | | `ReadFramesBatchedUnary` | `ReadStreamBatchedRequest` | `ReadFramesBatchRawResponse` | Raw frame read | | `ReplaySession` | stream `ReplaySessionRequest` | stream `ReplaySessionResponse` | Bidirectional streaming replay | | `ReadFrames` | `ReadFramesRequest` | stream `ReadFramesResponse` | Stream raw frame bytes | ### CoreCruxExportV1 | RPC | Request | Response | Description | |-----|---------|----------|-------------| | `ExportReceiptBundle` | `ExportReceiptBundleRequest` | stream `ExportChunk` | Stream large export bundles | Returns `UNIMPLEMENTED` in Crux Daemon. ### CoreCruxObserveV1 | RPC | Request | Response | Description | |-----|---------|----------|-------------| | `QueryOpsFacts` | `QueryOpsFactsRequest` | `QueryOpsFactsResponse` | Query operational facts | | `QueryOpsErrors` | `QueryOpsErrorsRequest` | `QueryOpsErrorsResponse` | Query error log | | `GetOpsHealth` | `GetOpsHealthRequest` | `GetOpsHealthResponse` | Health summary (JSON) | | `BootstrapPull` | `BootstrapPullRequest` | `BootstrapPullResponse` | Bootstrap fact pull with token budget | | `GetBootstrapStatus` | `GetBootstrapStatusRequest` | `GetBootstrapStatusResponse` | Seeded state and fact count | --- ## MCP (JSON-RPC over HTTP) Endpoint: `GET/POST http://:14801/mcp` - `GET /mcp` returns server info and protocol metadata. - `POST /mcp` serves JSON-RPC 2.0 requests such as `tools/list` and `tools/call`. - If `CRUX_AGENT_TOKEN` or `CRUX_AGENT_TOKENS` is configured, MCP requests must include `Authorization: Bearer `. - `Accept: text/event-stream` opens a Streamable HTTP SSE stream. SSE streams use the same bearer-token rule, validate `Mcp-Session-Id`, and are capped by `CRUX_MCP_SSE_MAX_SESSIONS` and `CRUX_MCP_SSE_MAX_SESSIONS_PER_OWNER`. - Private facts, agent-scoped sessions, and handoff workflows are available through MCP tools, not the HTTP `/v1/facts` surface. - `sync_status` tells agents whether the node is local-only, sync-enabled, or degraded before they attempt hosted-platform integration. - `update_status` tells agents whether the local checkout is current, behind, ahead, diverged, disabled, unavailable, or erroring before they propose an upgrade or restart. See [agent-guide.md](/home/myles/CueCrux/Crux/docs/agent-guide.md) and [examples/mcp-configs/README.md](/home/myles/CueCrux/Crux/examples/mcp-configs/README.md) for JSON-RPC examples and client configs. --- ## Authentication Configured via `CORECRUXD_AUTH_MODE`: | Mode | Description | Use Case | |------|-------------|----------| | `off` | No authentication | Local development only | | `dev_scopes` | Scopes parsed from header, no signature verification | Development/testing | | `jwt_hs256` | JWT with HMAC-SHA256 signature | Simple production setups | | `jwt_jwks` | JWT with JWKS key rotation | Production with key management | Scopes are passed via `Authorization: Bearer ` header. Required scopes are listed per endpoint above. `X-Corecrux-Passport-Id` is only an unverified local assertion in `off` and `dev_scopes`; production authority must come from verified token claims. ### HTTP fact tenant isolation (`CORECRUXD_TENANT_WRITE_STAMP`) In `jwt_hs256` and `jwt_jwks` modes, the default is `on`: affected HTTP fact-backed writes stamp the verified JWT tenant and reads filter to the same tenant. A token with one tenant needs no selector. On tenant-implicit routes, a token with multiple tenants or a wildcard tenant must send `X-Corecrux-Tenant-Id`; an explicit route/body tenant is itself a selector and must agree with that header when both are present. A missing tenant claim, ambiguous selection, mismatch, or unauthorized selection is rejected. The separately documented raw-admin fact reads remain intentionally cross-tenant. The policy is parsed once at startup, and an invalid value aborts startup. `off` is a deliberate legacy migration override: reads and writes use the shared `default` tenant even when JWT claims differ. `shadow` preserves that same storage behaviour while logging requests that `on` would move or reject. Historical `default` rows are not migrated automatically. This switch covers wired HTTP fact-backed surfaces, including generic and console facts, context recall, engram overlays, memory candidates, result envelopes, replay capsules, and their paired HTTP audit/export reads. It is not a universal daemon tenant switch: the MCP compatibility plane still uses `default`, while entity, edge, session, projection, and other control stores retain their own tenant contracts. ### Route authorization gate (`CORECRUXD_ROUTE_AUTH`) Independently of `CORECRUXD_AUTH_MODE`, the daemon runs a deny-by-default route authorization layer as HTTP middleware, in front of (and in addition to) the per-handler scope checks. It maps every routed request to a route contract — the accepted (any-of) scope set for that route template and method — and is controlled by `CORECRUXD_ROUTE_AUTH` (read once at startup): | Value | Behaviour | |-------|-----------| | `off` | Pass-through; the middleware does nothing. | | `shadow` | Evaluates the contract and logs a structured `route_auth_shadow_mismatch` warning on any would-deny, but never blocks. It is the derived default only for auth-off, loopback-only operation; otherwise it is an explicit migration override. | | `enforce` | Public routes (`/healthz`, `/readyz`, `/metrics`, `/session`, `/invocation/verify`, `/v1/openapi.json`, `/v1/version`, `/v1/receipts/signing-keys`, `/v1/witness/smoke`, and the `/v1/auth/*` bootstrap rails) pass with no auth headers. Every other route requires one of its contract scopes via the same primitive the handlers use. A route with **no** contract entry — or a request axum could not match to a route template — **fails closed with `403`**. | With the variable unset, authentication enabled or a non-loopback listener selects `enforce`; only auth-off plus loopback derives `shadow`. An empty or unknown explicit value also selects `enforce` and emits a startup warning. The gate authorizes scopes only; feature-flag gating for optional surfaces stays in the handler. When `CORECRUXD_AUTH_MODE=off`, the scope check is a no-op (there is nothing to enforce), but `enforce` still fails closed on uncontracted routes. --- ## Error Format All HTTP errors use [RFC 7807 Problem Details](https://www.rfc-editor.org/rfc/rfc7807): ```json { "type": "https://errors.cuecrux.com/bad-request", "title": "Bad Request", "status": 400, "detail": "query must not be empty" } ``` See `docs/error-catalogue.md` for the full error code reference. # ============================================================ # BEGIN docs/agent-guide.md # ============================================================ # Using Crux as Your Memory & Retrieval Backend CoreCrux gives agents three distinct surfaces: - HTTP on `14800` for the shared REST API (`/v1/query/*`, `/v1/facts`, `/v1/sessions/*`, health/metrics). - gRPC on `4007` for the data plane. - MCP on `14801` for agent-facing tools such as private facts, agent-scoped sessions, and handoff. Use HTTP for shared application data. Use MCP when you need agent identity, private memory, or handoff between agents. ## Your First 3 Calls New to CoreCrux? Start here: ``` 1. get_bootstrap("patterns", token_budget=500) → Learn token reduction, scan-expand, fact memory, and session patterns. 2. store_fact(entity="test", key="hello", value="world") → Store your first fact. Confirm you get a fact_id back. 3. query_facts(query="hello", token_budget=500) → Retrieve it. You should see your fact with confidence=1.0. ``` (Pass `token_budget` on every retrieval call, including these — it is the convention this guide teaches throughout.) That's it — you're connected and working. Read on for authentication, tool selection, and advanced patterns. This guide covers the core memory loop. The wider tool surface — constraints (`declare_constraint` / `check_constraints`), passports (`issue_passport`), `.cruxpack` portability, substrate entities/edges, the coordination board, and per-session token accounting (`session_token_usage`) — is catalogued in [`mcp-system-prompt.md`](mcp-system-prompt.md). From a shell, `corecruxctl start` is the canonical zero-to-first-loop on-ramp. ## Platform Availability and Onboarding Crux Daemon is local-first. The local fact store and session store are available even when no upstream CoreCrux platform is online yet. When you are connecting Crux to another system, start with: ``` 1. sync_status() → Inspect whether this node is running local_only, manual_sync, sync_enabled, or degraded. 2. get_bootstrap(topic="docs", query="integration") → Pull the current onboarding playbooks from the seeded bootstrap docs. 3. update_status() → Check whether this checkout is current, behind, ahead, diverged, disabled, or unavailable before proposing maintenance work. ``` Use the returned mode like this: - `local_only`: keep working against the local HTTP + MCP surfaces and tell the human that remote sync is optional follow-up work. - `degraded`: keep working locally, surface the degraded reason, and ask the human to fix the remote sync config only if they actually need upstream sync. - `manual_sync` or `sync_enabled`: proceed with automated integration and verify end-to-end behavior. Use `update_status` like this: - `behind`: a newer tracked commit exists; pull the backup and upgrade playbooks before changing the node. - `ahead` or `diverged`: do not ask for a blind pull; switch to a human-reviewed upgrade flow. - `disabled`, `unavailable`, or `error`: tell the human how to configure the git update-check inputs, but keep serving traffic locally. The compact onboarding starter pack is compiled into `BootstrapSeeder` in `crates/crux-observe/src/bootstrap.rs` and seeded only when the local fact store has no existing `__bootstrap__::` facts, so agents can pull it at runtime without a hosted dependency. Set `CORECRUXD_SEED_BOOTSTRAP=0` to leave a cold store unseeded. ## First Connect Endpoint Selection When you are connecting to Crux Daemon for the first time, discover endpoints in this order: 1. Same host: HTTP `http://127.0.0.1:14800`, MCP `http://127.0.0.1:14801/mcp`, gRPC `127.0.0.1:4007`. 2. Tailnet host: use the Tailscale MagicDNS name or `100.x.y.z` tailnet IP with the same ports. 3. Remote HTTPS: use an operator-provided reverse proxy only when the daemon is intentionally exposed outside the local host or tailnet. Do not inline bearer tokens into URLs, MCP config files, or hook definitions. Resolve them from `CRUX_AGENT_TOKEN`, `CRUX_AGENT_TOKENS`, a token CSV such as `~/.config/cuecrux/crux-tokens/MCP_AGENT_TOKENS_CSV`, or the host's secret manager. Run these checks before assuming integration is complete: ``` 1. GET /readyz on the HTTP endpoint. 2. GET /v1/version on the HTTP endpoint. 3. MCP initialize, then tools/list. 4. call cuecrux_session to get the typed capability plan. 5. call sync_status(), update_status(), and get_bootstrap(topic="patterns", token_budget=500). 6. store_fact followed by query_facts(token_budget=500) for an end-to-end memory check. ``` If the agent host supports native streamable HTTP MCP, configure the MCP URL and bearer-token environment variable directly. If Codex CLI cannot complete the Crux HTTP MCP startup handshake, use the first-party stdio bridge under `integrations/codex-cli/crux-mcp-stdio.py`; it speaks Codex stdio JSON-RPC on stdin/stdout and forwards calls to the Crux HTTP MCP endpoint selected by `CRUX_MCP_URLS`, `CRUX_MCP_URL`, `~/.config/cuecrux/env`, or localhost. ## Authentication HTTP and MCP use different authentication models: - HTTP auth is controlled by `CORECRUXD_AUTH_MODE`. - MCP auth is controlled by `CRUX_AGENT_TOKEN` / `CRUX_AGENT_TOKENS`. ### HTTP Set `CORECRUXD_AUTH_MODE` on the server. If the selected mode requires bearer tokens, send `Authorization: Bearer ` on HTTP requests. ### MCP If the server has no MCP agent tokens configured, MCP requests may run as `anonymous`. If the server sets `CRUX_AGENT_TOKEN` or `CRUX_AGENT_TOKENS`, every `POST /mcp` request and authenticated SSE stream must include the matching bearer token. Streamable HTTP SSE sessions are capped globally and per agent or client IP via `CRUX_MCP_SSE_MAX_SESSIONS` and `CRUX_MCP_SSE_MAX_SESSIONS_PER_OWNER`. ```bash export CRUX_AGENT_TOKEN="your-token-here" curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_agent_identity","arguments":{}}}' ``` ## Unified login (`crux login`) One command authenticates a client to a daemon wherever it lives, auto-selecting the lowest-friction *secure* rail. The shipped binary is `corecruxctl`: ```bash corecruxctl login # discover + auto-select a rail corecruxctl login --url https://crux.example.com corecruxctl login --token # CI / headless / air-gapped corecruxctl login --device # force the browser device-grant flow corecruxctl whoami # show stored credential posture per daemon corecruxctl logout --url # revoke + clear (or --all) ``` `login` discovers the daemon (`--url` → `~/.config/cuecrux/env` → localhost), probes `/readyz` + `/v1/version` for reachability and a Read route for the auth posture, picks a rail, persists the credential to `~/.config/cuecrux/credentials.json` (mode `0600`), registers the MCP endpoint in `~/.config/cuecrux/env`, and verifies `tools/list` + a `store_fact`→`query_facts` round-trip. The four rails, in auto-selection preference order: | Rail | When | Credential | |---|---|---| | 1 — loopback | same host, `auth=off` | none | | 2 — tailscale | verified tailnet identity (`tailscale serve`) | daemon-minted scoped JWT | | 3 — device | no host/env access (remote) | device-grant access + refresh token | | 4 — static token | `--token` / CI / air-gapped | operator-provided named token | An explicit `--token` or `--device` overrides auto-selection. Short-lived access tokens (≤5 min JWTs) auto-refresh; the device rail's refresh credential is named and revocable (`logout` revokes it). Off-host rails require encrypted transport (WireGuard for tailnet, TLS for remote) — a plaintext non-loopback `auth=off` bind is refused. **One credential for HTTP + MCP.** HTTP (`:14800`) and MCP (`:14801`) use different auth systems (signed JWT vs the `CRUX_AGENT_TOKENS` registry). To unlock both with a single login, set `CORECRUXD_HTTP_ACCEPT_AGENT_TOKENS=1`: under a JWT mode the HTTP API then *also* accepts a registered MCP agent token (mapped to `CORECRUXD_AGENT_TOKEN_HTTP_SCOPES` / `CORECRUXD_AGENT_TOKEN_HTTP_TENANT`). Then `corecruxctl login --token ` stores one long-lived token that works on both ports, survives daemon restarts, and works with native MCP clients that send a fixed bearer. An unmapped token acts as the namespaced automation principal `agent:`; only an explicit `CRUX_AGENT_PASSPORTS` entry maps it to a real passport. Default off, so HTTP stays JWT-only unless you opt in. Daemon-side rails 2 and 3 are opt-in and default off (see `config.example.env`: `CORECRUXD_TS_IDENTITY_ENABLED`, `CORECRUXD_DEVICE_GRANT_ENABLED`). Issuance mints HS256 JWTs, so the daemon must run in `jwt_hs256` mode. The issued `tenant_id` and scopes are always set by the approving identity (allowlist or device approver), never by the requesting client — this is what keeps cross-tenant issuance closed (threat ref T.1). Rail 2 works with **any** proxy that authenticates humans, not just Tailscale. `CORECRUXD_IDENTITY_HEADER` names the header your proxy sets; it defaults to the `tailscale serve` preset, so an existing deployment is unaffected. | proxy | `CORECRUXD_IDENTITY_HEADER` | |---|---| | `tailscale serve` (default) | `tailscale-user-login` | | oauth2-proxy | `x-forwarded-email` (or `x-auth-request-email`) | | Cloudflare Access | `cf-access-authenticated-user-email` | | Authelia | `remote-email` | | Authentik | `x-authentik-email` | > **The proxy must strip-then-set that header on every forwarded request.** A > trusted CIDR proves the proxy *sent* the request, not that it *authored* the > header — a proxy that forwards a client-supplied value hands any client a free > identity. `tailscale serve` and Cloudflare Access overwrite by construction; > oauth2-proxy and Authelia depend on how you configure them. If you cannot > guarantee it, use the device-authorization grant (rail 3) instead, which > derives identity from an approver rather than a header. Rail 2 also answers the **proxied console**. When the console sits behind an authenticating proxy that injects one shared admin JWT (no `passport_id`) and forwards each operator's verified login in the identity header, the human-decision routes — work-gate approve/reject and passport-mint approve/reject — consult the rail directly: a trusted proxy peer (loopback or `CORECRUXD_TS_TRUSTED_PROXY_CIDRS`) plus an allowlisted login whose entry carries `#` resolves the decision *as that passport*, and the receipt envelope records `approver_source` so the provenance is auditable. Without the `#` suffix the login is known but unbound and the decision is refused exactly as before; agent tokens, passport-header overrides and untrusted peers are refused unchanged. ## IX / Infra: machines, hooks, config & session sync Onboarding is login-driven and observable. `corecruxctl login` does three things: authenticates, installs the Claude Code hooks, and registers the machine. The console's **IX (Infra)** section surfaces it all. ```bash corecruxctl login # auth + hooks + machine capture (skip: --no-hooks / --no-register) corecruxctl machine list # machines logged into the daemon corecruxctl hooks install # (re)install Claude Code hooks; --user for ~/.claude, else project corecruxctl hooks status # what's wired # Carry a known-good Claude config across machines (secrets redacted): corecruxctl config push myles-pc # capture ~/.claude → daemon corecruxctl config pull myles-pc # deploy onto another machine (re-run `login` to re-fill secrets) corecruxctl config list # Share session-state snapshots across machines (event-driven, not real-time): corecruxctl session push --file state.json corecruxctl session pull corecruxctl session list ``` Storage model: machines, config bundles, and session snapshots are **public facts** under `__infra__::machines` / `__infra__::configs` / `__infra__::sessions` on the shared daemon, so every machine reads the same view. Config bundles carry *structure* (settings, `.mcp.json`, `CLAUDE.md`, `commands/` + `agents/`), not secrets — values under secret-looking keys and token-shaped strings are replaced with `${REDACTED}` and re-resolved per machine via `login`. They are readable by any `admin:read` caller on the daemon, so don't push data you wouldn't share within the daemon's tenant. Console: the IX section reads `GET /v1/console/infra/summary` (auth `admin:read`). On a remote `jwt_hs256` daemon, set a bearer once in the browser: `setConsoleToken("")` (stored in `localStorage`), then open the **IX** pill → Onboarding / Machines / Auth rails / Config bundles / Session sync. ## Human-Guided vs Automatic Integration Choose the integration style that matches the environment: - Human-guided: pull `get_bootstrap(topic="docs", query="Human-Assisted Integration")`, then tell the operator which endpoint, token, and verification call they need to run. - Automatic: pull `get_bootstrap(topic="docs", query="Automatic Integration")`, write the HTTP/MCP config into the host system, reload it, and verify with `/v1/version`, `tools/list`, `store_fact`, and `query_facts`. - Mixed mode: use HTTP for shared application state and MCP for private facts, agent-scoped sessions, and handoff. ## Upgrades and Backups Use the same central bootstrap docs for maintenance: - Human-guided upgrade: call `update_status()`, then pull `get_bootstrap(topic="docs", query="Human-Assisted Upgrade")`. - Automatic upgrade: only when `update_status().state == behind`, the repo is writable, and a backup path exists. Pull `get_bootstrap(topic="docs", query="Automatic Upgrade")`. - Backup options: pull `get_bootstrap(topic="docs", query="backup")` before changing the checkout or restarting the service. Current Crux Daemon backup rails are operator-level rather than one-click automation: - Filesystem or volume snapshots of `CORECRUXD_DATA_DIR` - `corecruxctl verify-store --data-dir ./data --scope recent` before and after upgrades - Replay or receipt exports from `/v1/replay/exports/*` - Git branch or tag markers before pulling when the service runs from a checkout ## Tool Decision Tree Choose the right query endpoint based on what you need: ``` Do you need full document content right now? ├── YES → POST /v1/query/text-search (standard query) │ Returns scored results with full content, coverage report. │ └── NO → Do you need metadata first to decide what to fetch? ├── YES → POST /v1/query/text-search with mode=scan │ Returns titles, scores, token counts — no content. │ Then: POST /v1/query/text-search/expand with result_ids │ to fetch full content for selected results only. │ └── Do you need related/connected documents? └── YES → POST /v1/query/graph-expand Traverses entity relationships with a hop budget. Use when you need "what else is related to X?" ``` **Summary:** | Endpoint | Use when | Returns | |---|---|---| | `text-search` | You know what to search for | Full results + coverage | | `text-search` (mode=scan) + `expand` | Large corpus, token-conscious | Metadata first, then selected content | | `graph-expand` | Exploring relationships | Connected documents via entity graph | | `time-range` | Temporal queries | Events within a time window | ## Fact Store Patterns The fact store is a receipted key-value memory scoped by entity. Each fact costs about 12 tokens versus replaying the original conversation. ### Shared facts over HTTP ```bash curl -X PUT http://localhost:14800/v1/facts \ -H "Content-Type: application/json" \ -d '{ "entity": "project-alpha", "key": "status", "value": "Phase 1 complete, 12 milestones delivered", "confidence": 0.95 }' ``` Response: ```json { "fact_id": "f_01J...", "receipt_id": "rcpt_01J...", "created_at": "2026-04-03T10:00:00Z" } ``` ### Query facts ```bash curl "http://localhost:14800/v1/facts?query=project+status&token_budget=500" ``` Returns facts matching the query, fitted within your token budget. HTTP `/v1/facts` is a shared surface. It does not support `private=true`. ### Citing Quality + Threat refs in design facts CueCrux uses a small, stable taxonomy so design decisions cite a fixed set of IDs instead of free-text rationale. The taxonomy is defined in [docs/quality-threat-refs.md](./quality-threat-refs.md) under "Quality refs" and "Threat refs". Refs themselves never change — `QC.1` means the same thing across every ExecPlan and every commit body — so they survive renaming, refactors, and time. When storing a design fact, embed the refs in the value JSON **and** write sibling tag facts so `query_facts` can retrieve "every design citing QC.3" cheaply: ```bash # Main decision fact. store_fact( entity="design:passport-routing", key="decision:tenant-isolation", value={ "rationale": "...", "qc_ref": ["QC.3"], "threat_ref": ["T.1"], "commit_sha": "abc123" } ) # Sibling tags — one per cited ref. Cheap (~12 tokens each) and queryable. store_fact(entity="design:passport-routing", key="qc_ref:QC.3", value={"cited_in": "decision:tenant-isolation"}) store_fact(entity="design:passport-routing", key="threat_ref:T.1", value={"cited_in": "decision:tenant-isolation"}) ``` Retrieval: ```bash # Every design fact citing QC.3, across all design entities. query_facts(entity_prefix="design:", key="qc_ref:QC.3", token_budget=2000) ``` This pattern uses the existing `query_facts` (no retrieval-layer change required) and stays compatible with `query_expand`'s `segment_index:doc_id` shape. The drift-check script `Crux/scripts/check-execplan-drift.sh` flags ExecPlans that cite a `decision:` whose sibling tags are absent — that's the integrity gate. ### Private facts over MCP Set `private: true` with the `store_fact` MCP tool to scope a fact to your authenticated agent only. Other agents cannot see it, and handoff packages do not export private facts. ```bash curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "store_fact", "arguments": { "entity": "my-agent", "key": "internal_state", "value": "Waiting for user confirmation on budget", "confidence": 1.0, "private": true } } }' ``` ## Session Persistence Patterns Sessions store structured state such as decisions, open questions, and constraints. ### Shared sessions over HTTP ### Save session state ```bash curl -X PUT http://localhost:14800/v1/sessions/session-42/state \ -H "Content-Type: application/json" \ -d '{ "decisions": ["Use PostgreSQL for primary store", "Deploy to eu-west-1"], "open_questions": ["Which caching layer?"], "constraints": ["Budget < $500/mo", "GDPR compliant"], "context": "Architecture review for Project Alpha" }' ``` ### Resume a session ```bash curl http://localhost:14800/v1/sessions/session-42/state ``` Returns the full session state, ready to inject into your context window. ### Agent-scoped sessions over MCP When you access sessions through MCP, authenticated agents read and write inside their own session namespace: ```bash curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "save_session", "arguments": { "session_id": "session-42", "state": { "decisions": ["Use PostgreSQL for primary store"], "open_questions": ["Which caching layer?"], "context": "Architecture review for Project Alpha" } } } }' ``` ## Multi-Agent Handoff Workflow (MCP Only) When Agent A finishes a task and needs to pass context to Agent B: ### Agent A: Create handoff package ```bash PACKAGE_JSON=$(curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "create_handoff", "arguments": { "session_id": "session-42", "include_facts": true, "target_agent": "agent-b", "message": "Completed architecture review. Three decisions made, one open question remaining." } } }' | jq -r '.result.content[0].text') ``` Pass `PACKAGE_JSON` to Agent B. The package is server-authenticated and includes only relevant non-private facts. ### Agent B: Accept handoff ```bash curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d "$(jq -nc --arg package "$PACKAGE_JSON" '{ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "accept_handoff", arguments: { package: $package } } }')" ``` Agent B now has full session state and facts from Agent A's work. If handoff packages must survive daemon restarts or move between replicas, set the same `CRUX_MCP_HANDOFF_SECRET` on every server instance. ## Coverage Awareness Every `text-search` response includes a `coverage` object: ```json { "coverage": { "score": 0.33, "gaps": ["quantum", "entanglement"] } } ``` **What to do when `score < 0.5`:** 1. **Broaden your query.** Try synonyms or more general terms. 2. **Check gaps.** Call `GET /v1/gaps` to see what the corpus is missing. 3. **Inform the user.** If the corpus genuinely does not cover the topic, say so rather than hallucinating an answer. 4. **Store a gap fact.** Record the coverage gap so future agents know: ```bash curl -X PUT http://localhost:14800/v1/facts \ -d '{"entity":"coverage-gaps","key":"quantum-physics","value":"Corpus has no quantum physics content","confidence":1.0}' ``` ## Token Reduction Strategies ### Use `token_budget` instead of `top_k` ```bash curl -X POST http://localhost:14800/v1/query/text-search \ -H "Content-Type: application/json" \ -d '{"query": "deployment strategy", "token_budget": 4000}' ``` The engine returns the best results that fit within 4000 tokens. This reduces prompt cost by 60-80% compared to fixed `top_k`. ### Progressive retrieval (scan then expand) For large corpora, use the two-pass pattern: 1. **Scan** — get metadata only (titles, scores, token counts): ```bash curl -X POST http://localhost:14800/v1/query/text-search \ -d '{"query": "deployment", "mode": "scan", "top_k": 20}' ``` 2. **Expand** — fetch full content for the results you actually need: ```bash curl -X POST http://localhost:14800/v1/query/text-search/expand \ -d '{"result_ids": ["r_01", "r_05", "r_12"], "token_budget": 3000}' ``` This avoids pulling content you will discard. ### Pace self-paced loops under the prompt-cache TTL A `/loop` (or any self-paced wakeup) that sleeps an hour pays for the sleep. Claude Code caches the prompt prefix with a **1-hour TTL measured from request start**, so a 3600-second delay *plus* the time the previous turn took to answer lands past the TTL, and the next prompt re-writes the entire prefix at the 2x (1h) cache-write rate. Nothing about the loop looks wrong when this happens — the work still completes, it just costs double on that turn. **Cap a self-paced wakeup interval at ~50 minutes** (`/loop 45m`, not `/loop 60m`), or decide deliberately to eat the rewrite. Measured on corpus `drivew-host-claude-transcripts-2026-09` (the 27 most recent Claude Code sessions with >= 5 API turns on drivew-host, read 2026-09-17): idle TTL expiry caused **77.4%** of all rewritten cache tokens — 76 events, 29.0M tokens — and **7 of those 76 were 60-65 minute gaps**, i.e. a wakeup that missed the TTL by minutes and is free to recover by shortening the interval. To see the clock rather than guess at it, read `GET /v1/sessions/{sessionId}/cache`, which reports `ttl_seconds_remaining` and the `est_rewrite_tokens` a lapse would cost. The Crux statusline renders the same countdown (`cache 34m`, amber under 10 minutes, `cache COLD ~180k` once it has lapsed) at zero prompt tokens, because statusline text is not part of the prompt. ## Example Workflow: Research, Store, Save, Handoff A complete agent workflow showing all patterns together: ```bash # 1. Research: search the corpus curl -X POST http://localhost:14800/v1/query/text-search \ -H "Content-Type: application/json" \ -d '{"query": "database migration best practices", "token_budget": 4000}' # 2. Store facts extracted from results curl -X PUT http://localhost:14800/v1/facts \ -d '{"entity":"db-migration","key":"rollback-strategy","value":"Always use reversible migrations with down scripts","confidence":0.9}' curl -X PUT http://localhost:14800/v1/facts \ -d '{"entity":"db-migration","key":"zero-downtime","value":"Use expand-contract pattern for schema changes","confidence":0.85}' # 3. Save session state curl -X PUT http://localhost:14800/v1/sessions/research-42/state \ -d '{ "decisions": ["Use expand-contract pattern", "Require down migrations"], "open_questions": ["Which migration tool?"], "context": "Researching DB migration strategy for Project Alpha" }' # 4. Handoff to implementation agent via MCP curl -s -X POST http://localhost:14801/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $CRUX_AGENT_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "create_handoff", "arguments": { "session_id": "research-42", "include_facts": true, "target_agent": "implementer", "message": "Research complete. Two key decisions made. See facts for details." } } }' ``` ## Rate Limiting Crux has coarse built-in HTTP request caps and client-IP rate limiting. For production deployments, place Crux behind a reverse proxy (Caddy, nginx) with route-specific rate limiting configured. Set `CORECRUXD_TRUSTED_PROXY_CIDRS` only for proxy peers that strip inbound `Forwarded` / `X-Forwarded-For` and rewrite them from the real client address. The gRPC append path has built-in per-tenant throttling via `CRUX_TENANT_THROTTLE_*` env vars. Every HTTP endpoint has a **30-second request timeout** enforced by Tower middleware. Requests exceeding the deadline receive a `408 Request Timeout` response. # ============================================================ # BEGIN docs/mcp-system-prompt.md # ============================================================ # Crux Memory Backend — System Prompt You have access to Crux, a receipted memory and retrieval backend. Use these tools: ## Retrieval - `query(tenant_id, query, token_budget)` — Search with BM25. Always set token_budget to control cost. - `query_scan(tenant_id, query)` then `query_expand(tenant_id, result_ids)` — Two-pass: scan metadata first, then expand only what you need. ## Memory - `store_fact(entity, key, value)` — Store key insights. Private facts require an authenticated agent identity. - `query_facts(query, token_budget)` — Retrieve stored facts. - `delete_fact(fact_id)` — Clean up obsolete facts. ## Sessions - `save_session(session_id, state)` — Save structured state between turns in your agent namespace. - `get_session(session_id)` — Resume from where you left off in your agent namespace. - `list_sessions()` — See the active sessions visible to you. ### Name your sessions: `state.title` + `state.summary` Two **conventional optional keys** inside `state` are read by the console (Work › Sessions and Meters › Token Burn) and by any other session lens. Set them on every `save_session`: | Key | Type | What it is | | --- | --- | --- | | `state.title` | string | A short human name for the session — "M21 console round 10", not "session-42". | | `state.summary` | string | One paragraph on what happened and where the work stands. | ```json { "session_id": "console-m21", "state": { "title": "Console round 10 (M21)", "summary": "Accordion icons + graph LOD anchor fixed; cost bars next.", "decisions": ["LOD switch preserves the pointer anchor"] } } ``` Both are optional and the daemon never requires them. When `title` is absent the console falls back to the first conventional state line (`decisions[0]` / `note` / `context_summary` / `summary`) and then to a short id, and labels the row **untitled** — so an unnamed session is visibly unnamed rather than silently anonymous. Everything else in `state` stays opaque and is preserved verbatim. ### Session identity is stamped for you `save_session` records the authenticated writer on the session itself (`actor`) — you do not pass it. Resolution matches `store_fact`'s `actor`: the agent token-name, upgraded to a passport_id when `CORECRUXD_AGENT_PASSPORTS=1`. A daemon with no `CRUX_AGENT_TOKENS` configured has no identity to stamp, so `actor` is absent there; it is never guessed, and there is no backfill for sessions written before the stamp existed. Note that `actor` is **not** a plan link. `passport_id` / `execplan_slug` on a console session row come from session *bindings* and live coord *intents*, both keyed by the sealed-plan `session_id_hex` minted by `POST /session`. Agent `save_session` ids live in a disjoint key space (`__agent_session::::`), so they never match those joins — announce with `coord_announce` if you want your session to appear against an ExecPlan. ### PreCompact crash recovery When the `crux-claude-hooks` PreCompact subcommand is installed (see [crates/crux-claude-hooks/README.md](../crates/crux-claude-hooks/README.md)), the harness calls `save_session` automatically before context compaction with `session_id = "hook:session:"`. The persisted `state.recovery` object captures the anchors needed to resume mid-ExecPlan: ```json { "hook_event": "PreCompact", "trigger": "auto|manual", "cwd": "/path/to/repo", "transcript_path": "/path/to/jsonl", "snapshot_ts": 1747776000, "recovery": { "last_commit_sha": "", "branch": "", // present unless on a detached HEAD "active_milestone": "M3" // present if .agent/current-milestone exists } } ``` To recover after a session restart: `list_sessions()` with a prefix filter on `hook:session:`, sort by `snapshot_ts` descending, then `get_session(session_id="hook:session:")` on the most recent. `recovery.last_commit_sha` lets you verify the working tree against the snapshot; `recovery.active_milestone` re-anchors the ExecPlan. To name the current milestone for the hook, operators write a short label to `.agent/current-milestone` at the start of each milestone (e.g. `echo "M3: shell_pattern constraints" > .agent/current-milestone`). The file is read up to 256 bytes; longer content is truncated. Absent file → field omitted. ## Coordination - `create_handoff(session_id, include_facts, target_agent?)` — Bundle session state plus relevant non-private facts for another agent. - `accept_handoff(package)` — Receive and verify a server-authenticated handoff package. ### Live-session board (requires `CORECRUXD_COORD=1` on the daemon) For concurrent sessions sharing one source tree. Liveness is automatic (presence heartbeat + session binding); only the *focus declaration* is yours to make. - `coord_status(project_id?)` — Who else is live right now: per-session passport, heartbeat, declared focus (execplan/milestone/paths), punchcard leases held, plus work items in flight. Call at session start (the `session-start` hook injects a digest automatically) and before editing files another session may be touching. - `coord_announce(session_id, project_id, execplan_slug?, milestone?, paths?, note?, ttl_seconds?)` — Declare what this session is working on. Re-announce on focus change (it replaces); `ttl_seconds: 0` clears on the way out; default TTL 4 h. Stored as a private `__coord__::` fact attributed to your session's passport. Protocol for multi-session work: announce your focus at boot and on every execplan/milestone switch → check `coord_status` before multi-file edits → take a `punch_in` lease (`tree://` or `file://`) for paths you'll mutate → prefer `create_handoff` over letting leases/intents time out. Conflicts are advisory: a peer's intent or lease on your target path is a signal to coordinate via work-item comments, not a lock. ## Observability - `sync_status()` — Check whether this node is local-only, sync-enabled, or degraded before planning remote integration work. - `update_status()` — Check whether this checkout is current, behind, ahead, diverged, disabled, or unavailable before proposing an upgrade. - `get_gaps(query)` — Check what the corpus doesn't know. - `get_bootstrap(topic, query?, token_budget?)` — Learn best practices and pull onboarding playbooks (topics: "patterns", "docs", "errors"). ## Substrate (M1–M2) The substrate hosts arbitrary domain data as `(kind, id, payload)` entities + labelled edges between them. Lens crates register kinds at daemon startup; agents can read and write via these generic tools. - `entity_upsert(kind, id, payload)` — Upsert. Payload is validated against the registered kind's JSON-Schema. - `entity_get(kind, id, include_deleted?)` — Fetch one entity. - `entity_list(kind?, limit?, include_deleted?)` — List entities, optionally filtered by kind. - `entity_delete(kind, id)` — Soft-delete. The version chain is preserved. - `entity_history(kind, id)` — Full version chain (oldest → newest); receipt-grade audit trail. - `edge_upsert(from_kind, from_id, edge_kind, to_kind, to_id, payload?)` — Upsert a labelled directed edge. - `edge_get`, `edge_list`, `edge_delete` — Edge CRUD. - `kind_list()`, `kind_get(kind)` — Discover registered kinds and their schemas. ## Features lens (M3) Domain lens for the PlanCrux Feature Registry on top of the substrate. Capabilities live as `entity:capability:`; `depends_on` edges form the dependency graph. - `feature_file_search(path)` — Find capabilities whose `files` list contains the given substring. - `feature_coverage_report()` — Per-system totals (capabilities, tested, audited, shipped) and maturity breakdown. - `feature_trigger_audit(id, status, auditor?, notes?)` — Record an audit on a capability. Status ∈ {audited, gap, waived, blocked}. - `feature_suggest_next(limit?)` — Suggest next-best capabilities to work on, derived from gap analysis + weakest-promise heuristic. ## Code intelligence (M4–M8) Code health findings + context chains, ingested by `corecruxctl code-health` (ingest-not-analyze) — never re-read the codebase to re-derive these. - **Findings** live as facts under `entity="codehealth:"` (keys `dead:`/`unused-dep:`/`stub:`/`todo:`/`dark:<…>`, value `{class,file,line,message,tool,commit_sha}`; one `run:` summary). Query current findings (resolved ones are retired, never returned): - `query_facts(entity="codehealth:Crux", token_budget=500)` — all current findings + latest run summary for a repo. - `query_facts(entity_prefix="debt:", token_budget=500)` — the **debt ledger**: `crux-min:` shortcuts projected into `entity="debt:/"`, value `{ceiling, upgrade_trigger, file_line, commit_sha}`. Same findings as the `debt:` keys under `codehealth:`, queryable on their own. - `query_facts(query="codehealth stub", token_budget=500)` — stub/`todo!()` findings across repos. - **Chains** live as `codechain` entities (id = slugified route/fn; payload `{root, steps:[{name,qualified,file,line,depth,kind}], terminations}`). Answer "what does this route touch?" without re-reading code: - `entity_list(kind="codechain", limit=50)` — all extracted chains. - `entity_get(kind="codechain", id="v1-work-gate-actionId-approve")` — one chain's steps + terminations. - **File context** lives as `code::` facts (key `context`); the `code-context` PreToolUse(Read) hook injects them automatically when enabled (`CRUX_HOOK_CODE_CONTEXT=1`). To read one directly: `query_facts(entity="code:Crux:crates/corecruxd/src/work.rs", token_budget=500)`. - **Watched repos + AST graph (M7–M8).** Register a repo the daemon should keep current with `register_repo` (or `POST /v1/repos`); list with `list_repos`. Scans use the AST scanner when `CORECRUXD_AST_SCAN=1` (Rust via `syn`; TS/TSX/Vue/Python via `tree-sitter`), and `CORECRUXD_REPO_WATCH=1` keeps them fresh incrementally. With `CORECRUXD_CODEGRAPH_EDGES=1`, a repo's code structure is emitted as typed relation edges — traverse "what calls / imports / defines this?" via `POST /v1/relations/expand` filtered to edge types `calls` / `imports` / `defines` / `depends_on` (these are grounded, `Extracted`-confidence edges — prefer them over re-reading source). The console view `/console/codegraph` renders the same typed graph for a human. Always pass `token_budget` (500 default). Findings carry a `volatile` horizon — prefer a fresh query over a remembered count. ## Rules 1. Always use `token_budget` to control context size. 2. Store important findings as facts — don't rely on conversation memory. 3. Check `coverage.score` after queries — if < 0.5, inform the user. 4. Use `get_bootstrap("patterns")` on first connection to learn optimal usage. 5. Before remote integration or sync work, call `sync_status()`. If the mode is `local_only` or `degraded`, keep working against the local store and pull onboarding guidance with `get_bootstrap(topic="docs", query="integration")`. 6. Before maintenance or version changes, call `update_status()`. If the state is `behind`, pull `get_bootstrap(topic="docs", query="upgrade")` and `get_bootstrap(topic="docs", query="backup")`. If the state is `ahead` or `diverged`, stop and switch to a human-reviewed upgrade flow. # ============================================================ # BEGIN docs/architecture.md # ============================================================ # Architecture ## Crate Dependency Graph ```mermaid graph TD cruxmcp[crux-mcp
MCP Server
28 token-filtered tools] observe[crux-observe
Self-Observation] sync[crux-sync
Outbox Sync] contrib[crux-contrib
Contributions] corecruxd[corecruxd
HTTP + gRPC Daemon] corecruxctl[corecruxctl
CLI Tool] retrieval[corecrux-retrieval
BM25 + Graph Fusion] memory[corecrux-memory
Fact + Session Store] projections[corecrux-projections
Entity State] receipts[corecrux-receipts
CROWN Receipts] storage[corecrux-storage
Shard Store] index[corecrux-index
.ccxi Indexes] segment[corecrux-segment
Sealed Segments] frame[corecrux-frame
Frame Encoding] types[corecrux-types
Core Types] proto[corecrux-proto
gRPC Proto] cruxmcp --> memory cruxmcp --> retrieval cruxmcp --> observe observe --> memory corecruxd --> cruxmcp corecruxd --> observe corecruxd --> memory corecruxd --> retrieval corecruxd --> projections corecruxd --> receipts corecruxd --> storage corecruxd --> proto corecruxctl --> storage corecruxctl --> receipts retrieval --> index retrieval --> projections projections --> storage projections --> segment storage --> segment storage --> frame index --> segment index --> frame segment --> frame receipts --> types frame --> types ``` ## Data Flow ### Append Path 1. Client sends events via HTTP `POST /v1/admin/append` (or compatibility alias `POST /v1/append`) or gRPC `AppendBatch`. 2. `corecruxd` routes to shard via `stream_hash(tenant_id, stream_type, stream_id)`. 3. Events encoded as frames (`corecrux-frame`) and appended to shard (`corecrux-storage`). 4. When segment fills, it is sealed with BLAKE3 integrity hash. 5. `.ccxi` companion index built at seal time (`corecrux-index`). 6. AppendBatch returns write-confirmation material. If a segment is sealed, the response also includes a segment-seal receipt signed over the current segment hash and previous sealed segment hash. Stored receipt streams use `corecrux-receipts` for Ed25519-signed CROWN bodies/signatures. In the default Crux Daemon runtime, the dataplane is disabled, so append requires a dataplane-enabled deployment. ### Query Path 1. Client sends query via HTTP or MCP tools. 2. BM25 scoring across all `.ccxi` indexes (`corecrux-retrieval`). 3. Optional graph signal fusion from entity relations (`corecrux-projections`). 4. Results filtered by `tenant_hash_lo16` for isolation. 5. Token budget applied to trim results. 6. Coverage score and gaps computed from query token match rates. ### Memory Path 1. Shared facts are stored via `PUT /v1/facts`; private or agent-scoped facts are stored via MCP `store_fact`. 2. Facts indexed in-memory (`corecrux-memory` FactStore). 3. BM25 keyword search over fact values. 4. Token-budgeted retrieval of facts. ### Fleet coordination The local coordination plane (`/v1/coord/*`) ships in the free daemon and is enabled by default. It provides the operator's presence-joined session board and advisory local claims; it is not tier-gated. Governance packaging applies only to hosted fleet-wide aggregation, attribution, and policy enforcement in CruxEngine, outside this repository. A local daemon therefore keeps its full coordination surface whether or not it is connected to hosted services. ## Crate Responsibilities ### Core Storage Layer - **corecrux-types** -- Shared types, error codes, evidence structs. - **corecrux-frame** -- Frame encoding (header + payload, BLAKE3 hashed). Wire format: `CRX1` magic, version, header length, payload length, header bytes, payload bytes, CRC32 checksum. - **corecrux-segment** -- Sealed segment format. Active segments accept frame writes until sealed. Sealed segments contain a 4096-byte header (`CCS3` magic), record area (LZ4 compressed frame blocks), table of contents (`TOC1` magic with 64-byte entries), optional trailer index (block metadata + bloom filters + TOC-by-offset), and a 256-byte footer (`CCF3` magic) with BLAKE3 integrity hashes and CRC32C checksums. - **corecrux-storage** -- Shard storage engine. Manages append, seal, and crash recovery across multiple shards. Routes events to shards by stream hash. ### Retrieval Layer - **corecrux-index** -- `.ccxi` companion index. Built at seal time alongside each sealed segment. Contains a vocabulary table (token hash to posting list offset), PForDelta-compressed posting lists with per-token term frequencies, and a per-document table (frame offset, document length in tokens, tenant hash). v2 format extends doc entries to 16 bytes with full 64-bit tenant hash for exact tenant isolation. - **corecrux-retrieval** -- BM25 scoring with optional graph signal fusion. Reads `.ccxi` indexes, scores candidate documents, applies tenant filtering, and computes coverage gaps. - **corecrux-projections** -- Entity and relationship state derived from events. Provides graph signals for retrieval fusion. ### Application Layer - **corecrux-memory** -- In-memory fact store and session store. Facts are key-value pairs with entity, confidence, and timestamps. BM25 keyword search over fact values with token budgets. - **corecrux-receipts** -- CROWN receipt generation and verification for receipt-bearing streams. Ed25519-signed receipts bind receipt IDs to payload hashes. - **corecrux-proto** -- gRPC protocol buffer definitions for the `AppendBatch`, `ReadStream`, and query RPCs. ### Server Layer - **corecruxd** -- HTTP (axum, port 14800), gRPC (tonic, port 4007), and built-in MCP (port 14801 by default) daemon. Manages shard lifecycle, routes requests, serves Prometheus metrics at `/metrics`, and provides health/readiness endpoints. - **corecruxctl** -- CLI tool with subcommands: `verify-store` (structural integrity, plus sealed-segment BLAKE3 with `--strict`), `replay` (deterministic replay with drift classification), `receipts` (receipt tooling and export), `ccxi` (companion index inspection), `projections` (projection state management). ### Agent And Extension Layer - **crux-mcp** -- MCP router and token-filtered tool surface for agent interaction. Handles agent identity, tool routing, update awareness, constraints, passport, sync, and handoffs. - **crux-observe** -- Self-observation subsystem. Provides ops monitoring, bootstrap sequencing, and cold gate logic. - **crux-sync** -- Outbox sync with VaultCrux. Manages event replication to external systems. - **crux-contrib** -- Contribution manifest builder. Tracks and packages community contributions. ## Segment File Layout ``` +---------------------------+ | Segment Header (4096 B) | CCS3 magic, version, shard_id, epoch, segment_seq, CRC32C +---------------------------+ | Record Area | Frame data (optionally LZ4 block-compressed) | Frame 0 | CRX1 magic, header bytes, payload bytes, CRC32 | Frame 1 | | ... | +---------------------------+ | Table of Contents | TOC1 magic, entry_count, 64-byte entries sorted by (stream_hash, seq) | TOC Header (128 B) | Each entry: stream_hash, seq, record_off, frame_len, payload_len, | TOC Entry 0 (64 B) | event_id_hash16, header_digest8, payload_digest8 | TOC Entry 1 (64 B) | | ... | +---------------------------+ | Trailer Index (optional) | Block metadata (BLK1), TOC-by-offset (TBO1), trailer summary (TSI1) | Block Meta per block | Per-block: offsets, sizes, codec, frame count, bloom filter | TOC-by-offset entries | | Trailer Summary | +---------------------------+ | Segment Footer (256 B) | CCF3 magic, version, flags, file_len, area offsets/lengths, | | min/max stream_hash, min/max seq, BLAKE3 hashes, CRC32C +---------------------------+ ``` ## .ccxi Index File Layout ``` +---------------------------+ | CCXI Header (256 B) | CCXI magic, version 2, shard_id, segment_seq, epoch, | | vocab_size, total_postings, total_frames, tokenizer_version +---------------------------+ | Vocab Table | vocab_size x 16 bytes: token_hash (u64), postings_offset (u32), | | postings_len (u32). Sorted by token_hash for binary search. +---------------------------+ | Postings Area | PForDelta-compressed doc_id lists + raw u16 term frequencies | | per vocab entry. +---------------------------+ | Doc Table | total_frames x 16 bytes: frame_offset (u32), | | doc_length_tokens (u16), tenant_hash_lo16 (u16), | | tenant_hash_full (u64). +---------------------------+ | CCXI Footer (64 B) | BLAKE3 hash of vocab table + BLAKE3 hash of postings area. +---------------------------+ ``` # ============================================================ # BEGIN docs/verify-release.md # ============================================================ # Verifying a Crux release Every Crux release (tag `v*`) ships with a verifiable supply chain — threat ref **T.5** in [THREAT_MODEL.md](THREAT_MODEL.md): | Artifact | What proves it | |---|---| | Native release binaries (`corecruxd-*`, `crux-*`, `corecruxctl-*`, `crux-hook-*` for `linux-amd64` and both Darwin targets) | cosign keyless signature (`.sig` + `.pem` per file) + SLSA v1.0 provenance | | Cross Linux release binaries (the same four prefixes for `linux-musl-amd64` and `linux-arm64`) | cosign keyless signature; not yet included as SLSA subjects | | `RELEASE-MANIFEST-.txt` | signed sha256 list covering every file in the release package (licences, docs, content) | | `install.sh` | cosign keyless signature (`install.sh.sig` + `install.sh.pem`) and coverage by `RELEASE-MANIFEST-linux-amd64.txt` | | `crux-daemon-.cdx.json` | signed CycloneDX 1.6 SBOM of the workspace, per target | | `multiple.intoto.jsonl` | SLSA provenance for native-target binaries, SBOMs, and manifests, generated by the SLSA L3 builder | | Container image `ghcr.io/cuecrux/crux-daemon` | cosign keyless signature + CycloneDX SBOM attestation, both digest-addressed | All signatures are **keyless** (Sigstore): short-lived certificates bound to the GitHub Actions workflow identity, logged in the Rekor transparency log. There is no long-lived signing key that can leak — what you verify is "this exact artifact was built by the release workflow of `CueCrux/Crux` at this tag". Tooling: [`cosign`](https://docs.sigstore.dev/cosign/system_config/installation/) and [`slsa-verifier`](https://github.com/slsa-framework/slsa-verifier#installation). Set the tag you are verifying once: ```bash TAG=v0.5.0 # the release you downloaded REPO=CueCrux/Crux ``` ## 1. Verify a release binary Download the binary, its `.sig` and `.pem` from the GitHub Release, then: ```bash cosign verify-blob \ --certificate corecruxd-linux-amd64.pem \ --signature corecruxd-linux-amd64.sig \ --certificate-identity "https://github.com/${REPO}/.github/workflows/release.yml@refs/tags/${TAG}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ corecruxd-linux-amd64 ``` Expected output: `Verified OK`. Repeat for `crux-`, `corecruxctl-`, `crux-hook-`, the SBOM and the manifest (same flags, different filenames). Before running the installer, verify `install.sh` with the same command and workflow identity, using `install.sh.pem` and `install.sh.sig`. ## 2. Verify the whole package via the signed manifest The licence/docs/content files are not individually signed; they are covered by the signed manifest: ```bash cosign verify-blob \ --certificate RELEASE-MANIFEST-linux-amd64.txt.pem \ --signature RELEASE-MANIFEST-linux-amd64.txt.sig \ --certificate-identity "https://github.com/${REPO}/.github/workflows/release.yml@refs/tags/${TAG}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ RELEASE-MANIFEST-linux-amd64.txt sha256sum --check --ignore-missing RELEASE-MANIFEST-linux-amd64.txt ``` Manifest entries use the flat public release-asset filenames. Download the assets you want to verify into the same directory as the manifest; nested repository staging paths do not need to be recreated. The release workflow rejects duplicate basenames before publishing, so every checksum maps to one unambiguous public asset. The linux-amd64 manifest includes `install.sh`; the checksum command therefore provides a second, manifest-rooted check when both files are present. ## 3. Verify SLSA provenance `multiple.intoto.jsonl` (a release asset) attests that each native-target binary was built from this repository at this tag by the SLSA L3 trusted builder: ```bash slsa-verifier verify-artifact corecruxd-linux-amd64 \ --provenance-path multiple.intoto.jsonl \ --source-uri "github.com/${REPO}" \ --source-tag "${TAG}" ``` Expected output ends with `PASSED: SLSA verification passed`. The `linux-musl-amd64` and `linux-arm64` cross-build legs are cosign-signed but are not yet subjects in `multiple.intoto.jsonl`; do not claim a SLSA pass for those files. Their signatures and signed release manifests remain verifiable with steps 1 and 2. ## 4. Verify the container image Always verify by digest, never by floating tag: ```bash IMAGE=ghcr.io/cuecrux/crux-daemon:${TAG#v} DIGEST_REF=$(cosign triangulate --type digest "${IMAGE}" 2>/dev/null || docker buildx imagetools inspect "${IMAGE}" --format '{{json .Manifest.Digest}}' | xargs -I{} echo "ghcr.io/cuecrux/crux-daemon@{}") cosign verify \ --certificate-identity "https://github.com/${REPO}/.github/workflows/docker.yml@refs/tags/${TAG}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ "${DIGEST_REF}" ``` And the SBOM attestation attached to the image: ```bash cosign verify-attestation \ --type cyclonedx \ --certificate-identity "https://github.com/${REPO}/.github/workflows/docker.yml@refs/tags/${TAG}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ "${DIGEST_REF}" \ | jq -r '.payload' | base64 -d | jq '.predicate.metadata' ``` Note: images built from `main` merges (the `edge` tag) are signed with identity `…/docker.yml@refs/heads/main` instead of the tag ref. ## 5. Inspect the SBOM ```bash jq '.components | length' crux-daemon-linux-amd64.cdx.json # dependency count jq -r '.components[] | "\(.name) \(.version)"' crux-daemon-linux-amd64.cdx.json | sort ``` Feed it to any CycloneDX-aware scanner (`trivy sbom crux-daemon-linux-amd64.cdx.json`, Dependency-Track, etc.) to re-scan the exact dependency set that shipped. ## What failure looks like - A tampered binary fails step 1 with `Error: searching log query matched zero entries` or a hash mismatch. - A binary rebuilt outside CI fails step 3 (`FAILED: SLSA verification failed`) because no provenance exists for its hash. - An image re-pushed by an attacker with `packages: write` fails step 4 — they cannot mint a certificate for the `docker.yml` workflow identity. If any verification fails, do not run the artifact; report it via [SECURITY.md](../SECURITY.md). # ============================================================ # BEGIN docs/usage-receipts.md # ============================================================ # Usage receipts (opt-in adoption signal) > **CueCrux usage receipts (opt-in, off by default).** When you enable this, the daemon submits a > *signed, metadata-only* usage receipt — receipt id, the receipt's own metadata body (CBOR-encoded), > receipt-body hash, your passport fingerprint, your passport's public key, event class, and > timestamp — to the collector endpoint you configure. **No fact content, query text, or corpus data > is ever included.** This is the only outbound signal the daemon sends. It requires you > to (1) set an `https://` collector endpoint and (2) explicitly record consent > (`CORECRUXD_USAGE_RECEIPTS_CONSENT_AT`). Revoke anytime by unsetting > `CORECRUXD_USAGE_RECEIPTS_SUBMIT`. It exists so CueCrux can count adoption; it is never required to > use the daemon. Crux's trust posture is **no phone-home**: with default configuration the daemon makes zero non-loopback network calls, and a release gate (`scripts/assert-no-phone-home.sh`) boots the daemon with default environment and fails the build on any egress attempt. Usage receipts are the single sanctioned exception, and they are gated behind an explicit, three-part opt-in so that a fresh install — or any install where the operator has not deliberately turned this on — dials nothing. ## What it is The daemon already mints a **local, signed `usage_ping` receipt**: a deliberately metadata-only CROWN receipt that records "a thing happened" (a session opened, a query ran, the daemon started) without disclosing *what* happened. That receipt is persisted locally like any other signed observation. When — and only when — you opt in, the daemon additionally **submits a metadata-only copy** of that receipt to a collector endpoint you control. The submission carries just enough to let the collector verify the Ed25519 signature and count distinct daemon instances (by passport fingerprint) toward an adoption number: the receipt's own metadata body (so the signed message can be reconstructed) and the daemon's public key (so the signature can be checked). It never carries fact content, query text, or corpus data — the receipt body it does carry is metadata-only by construction. ## The three-part opt-in gate The submitter performs **zero** network I/O unless **all three** of the following hold: | Env var | Default | Meaning | |---|---|---| | `CORECRUXD_USAGE_RECEIPTS_SUBMIT` | `false` | Master enable. Must be `1`/`true` to submit. | | `CORECRUXD_USAGE_RECEIPTS_ENDPOINT` | *(unset)* | The `https://` collector URL. **There is no hardcoded default** — if unset, nothing is sent. Plaintext `http://` endpoints are rejected. | | `CORECRUXD_USAGE_RECEIPTS_CONSENT_AT` | *(unset)* | Your recorded consent act. Set it to an RFC3339 timestamp, or to the literal `yes` to stamp the current time. The submitter refuses to fire unless this is set. | If any one of the three is missing, the submitter is a no-op. It is also never wired into the boot path or a background timer — it is triggered **only** by an explicit `usage_ping` mint on the `/v1/mediation/receipts` surface, and only *after* the local signed receipt has been persisted. > Note: submitting also requires the `CORECRUXD_FEATURE_USAGE_RECEIPTS=1` feature flag so that the > daemon accepts `usage_ping` drafts at all. Without it, no `usage_ping` is minted and there is > nothing to submit. ## What is sent The wire payload is metadata only. Exactly these fields, and nothing else: | Field | Example | What it is | |---|---|---| | `receipt_id` | `r_9f1c…` | The signed receipt's id. | | `body_cbor_hex` | `a8667363…` | The receipt's own metadata fields, CBOR-encoded, so the collector can verify the signature; never fact/query/corpus content. | | `body_hash` | `blake3:5e88…` | A hash of the canonical receipt body — a digest, not the body. | | `passport_fpr` | `p_1a2b…` | The daemon's passport fingerprint (the adoption unit counted). | | `public_key_hex` | `9a3f…` | Your passport's public key, used to verify the signature. | | `event_class` | `session` | One of the closed set `session` / `query` / `daemon_start`. | | `created_at` | `2026-07-03T00:00:00Z` | The receipt timestamp. | | `sig` | `{alg, key_id, signed_at, signature_hex}` | The Ed25519 signature envelope, so the collector can verify the ping. | ## What is NOT sent - **No fact content** — not the value, not the key, not the entity. - **No query text** and no prompt text. - **No corpus identity** or corpus data. - **No content in the receipt body** — the metadata-only body *is* sent (as `body_cbor_hex`) so the signature can be verified, but by construction it holds only receipt metadata (id, kind, tenant, passport fingerprint, event class, count, timestamp), never a fact, query, or corpus. - No general telemetry, no host metrics, no environment, no IP-derived data beyond what the transport layer inherently exposes to the endpoint you chose. ## How to enable ```bash # All three legs — a fresh install would dial nothing without these. export CORECRUXD_FEATURE_USAGE_RECEIPTS=1 # mint usage_ping receipts locally export CORECRUXD_USAGE_RECEIPTS_SUBMIT=1 # master enable for submit export CORECRUXD_USAGE_RECEIPTS_ENDPOINT="https://collector.example.com/usage" export CORECRUXD_USAGE_RECEIPTS_CONSENT_AT="yes" # or an explicit RFC3339 timestamp ``` ## How to revoke Unset (or set to `0`/`false`) `CORECRUXD_USAGE_RECEIPTS_SUBMIT` and restart the daemon. The submitter is immediately inert again; local `usage_ping` receipts (if `CORECRUXD_FEATURE_USAGE_RECEIPTS` is still on) continue to be minted and stored locally, but nothing is submitted. Clearing the endpoint or the consent timestamp also disables submission — the gate requires all three. # ============================================================ # BEGIN docs/testing-and-coverage.md # ============================================================ # Testing & coverage How the Crux Daemon is tested, how coverage is measured and gated, and an honest account of why the gated number sits where it does. > **Snapshot (2026-07-30, `main`):** **6,671** test functions; **90.11%** gated > region coverage (ungated, whole tree: **88.81%**). > CI is the source of truth — the live numbers are printed in > the [Coverage job](../.github/workflows/ci.yml) of every run and attested by > [`coverage-attestation.yml`](../.github/workflows/coverage-attestation.yml). ## Running the tests ```bash cargo test --workspace # the full suite (6,671 test functions) cargo test -p corecruxd # one crate (corecruxd is a binary crate — no lib target) cargo fmt --check # formatting gate cargo clippy --workspace -- -D warnings # lint gate (lib + bins; not #[cfg(test)] code) ``` Coverage locally (matches CI exactly — same ignore regex): ```bash RX='(.*/corecruxd/src/pool\.rs|.*/corecruxd/src/dataplane_store\.rs|.*/corecruxd/src/http/dataplane\.rs|.*/corecruxd/src/main\.rs|.*/corecruxctl/src/main\.rs|.*/crux-claude-hooks/src/main\.rs|.*/crux-claude-hooks/src/bin/crux_llm_shim\.rs|.*/crux-config-wizard/src/main\.rs|.*/crux-config-wizard/src/interactive\.rs)$' cargo llvm-cov --workspace --ignore-filename-regex "$RX" --summary-only ``` (Needs `cargo-llvm-cov` + the `llvm-tools-preview` component, and a C toolchain for linking — `apt install build-essential` on a bare box.) ## What the gate measures The CI gate uses **region coverage** — column 4 of the `cargo llvm-cov` `TOTAL` line — not line coverage. Region coverage is the stricter of the two (it counts distinct branch regions, so a half-executed line doesn't read as fully covered). The [`Coverage` job in `ci.yml`](../.github/workflows/ci.yml) enforces: | Scope | Floor | Actual (snapshot) | Notes | |---|---|---|---| | **Workspace total** | **89%** | 90.11% | the headline gate; ratchet target **90** | | `corecrux-memory` | 93% | 93.5% | ratchet target **95** (see below) | | `crux-sync` | 98% | 98.4% | | | `crux-contrib` | 99% | 100% | | | `corecrux-receipts` | 88% | 91.2% | trust core (CROWN receipts); ratchet available | | `corecrux-segment` | 85% | 93.8% | trust core (sealed `.ccxseg`); ratchet available | | `corecrux-storage` | 79% | 89.9% | trust core (append-only store); ratchet available | Floors are set **at current-rounded-down** ("ratchet from reality"): they prevent regression today and are raised as coverage improves. The job also prints the **ungated** total (full tree, no exclusions) next to the gated one, so the exclusion list below can never quietly hide low-coverage code from review. > **Note on the memory floor.** The per-crate floor check was historically a > no-op (the awk summed the file-path column → always "100.0"), so the > long-stated `corecrux-memory` ≥95% target was never actually enforced. The > check is now correct; `corecrux-memory` is 93.5% today, so the enforced floor > is 93 with **95 as the ratchet target**. ### What is excluded from the gate — and why The ignore list is deliberately narrow: only code that is **not meaningfully unit-testable**. - **Binary entry points** — `main.rs` (`#[tokio::main]` bootstrap / clap dispatch) for `corecruxd`, `corecruxctl`, `crux-claude-hooks`, `crux-config-wizard`, plus the `crux_llm_shim` bin. - **The interactive config wizard** — `crux-config-wizard/src/interactive.rs` (stdin-driven prompts). - **The dataplane layer**, which is an **unconstructable typecheck stub** in the CPU-only CE build: [`pool.rs`](../crates/corecruxd/src/pool.rs) (`DataPlanePool { _private: () }`, every method `unreachable!()`, never constructed), `dataplane_store.rs`, and [`http/dataplane.rs`](../crates/corecruxd/src/http/dataplane.rs) (`PoolBackedHttpDataplane` whose `pool` is always `None`). The real dataplane — and its append→read→verify integration coverage — lives in the dataplane-enabled (CoreCrux) distribution, not this repo. The CE handlers' contract is covered against the `FakeHttpDataplane` test double in [`http/tests.rs`](../crates/corecruxd/src/http/tests.rs). Everything else — including the critical append, query, receipt, projection, and admin HTTP surfaces — is gated. ## Tests per crate (top) | Crate | Tests | | Crate | Tests | |---|---:|---|---|---:| | `corecruxd` | 2671 | | `corecrux-receipts` | 275 | | `corecruxctl` | 1174 | | `corecrux-storage` | 223 | | `crux-mcp` | 798 | | `crux-claude-hooks` | 222 | | `corecrux-memory` | 288 | | `crux-session` | 89 | | `corecrux-projections` | 276 | | `corecrux-segment` | 62 | ## Why the gated number is ~90%, not higher This is the honest part. 90.11% is what an *accurate* gate over the *meaningfully-testable* tree reports — it is not a target someone padded up to. 1. **The trust-core crates used to pull the average down, and no longer do.** `corecrux-storage` (89.9%), `corecrux-segment` (93.8%) and `corecrux-receipts` (91.2%) are large and full of deep I/O and error branches; they sat at 80/86/89% as recently as 2026-06-18. The **security-critical** paths in them *are* covered — tamper-rejection in [`corecrux-segment/tests/corruption_matrix.rs`](../crates/corecrux-segment/tests/corruption_matrix.rs) (magic / version / CRC / record-hash / TOC corruption all rejected) and fail-closed signature verification in [`corecrux-receipts/src/verify_v1.rs`](../crates/corecrux-receipts/src/verify_v1.rs) (`assert!(!report.signature_valid)`). What's *un*covered is mostly exhaustive error/IO-branch fan-out, not the invariants (see [`docs/agent/INVARIANTS.md`](agent/INVARIANTS.md)). 2. **`corecruxd` is an ~87k-LOC surface.** Most handlers are covered. The three that this doc previously recorded at 0% — `http/events.rs`, `http/infra.rs`, `http/policy.rs` — were closed on 2026-07-30 and now sit at **95.4%**, **98.8%** and **98.7%** region coverage respectively. They were *gated* (not excluded) throughout, which is why they showed up as debt rather than staying hidden. Remaining `corecruxd` debt is tracked per-file in the Coverage job log. A second sweep on 2026-07-30 took the nine largest remaining concentrations across `corecruxd` and `corecruxctl` — `http/replay.rs` (1032 missed regions → 66), `corecruxctl/audit_pack.rs` (1164 → 228), `corecruxctl/login.rs` (1035 → 159), `repo_watch.rs` (456 → 125), `storybook.rs` (411 → 35), `context_graph.rs` (296 → 19) and `http/incidents.rs` (379 → 211). **What is left is mostly not testable without a production change.** The clearest example is `integrations_github_sync.rs` (532 → 421) and `integrations_github.rs` (253 → 215): every fetch builds a `ureq::Agent` inline against a hard-coded `https://api.github.com/...` URL, with no base-URL or transport injection seam, so the pagination and status-code branches cannot be reached without hitting the real network. Adding a seam is the prerequisite for covering them — not more test effort. A related trap worth knowing: **no CI job runs `cargo test -- --ignored`**, so an `#[ignore]`d test contributes nothing to the gate. Two exist today — `sse_session_survives_30s_idle` (>35s wall clock) and `witness_submit`'s live Rekor probe. Both are ignored for good reasons, but the SSE endpoint's *only* test was one of them, which is how a whole handler sat at 0% while looking tested. Prefer a fast handler-level test alongside any long-running or network-dependent one. 3. **The denominator includes the test code itself.** `#[cfg(test)]` regions count toward the total, so each new test batch raises coverage by less than its raw covered-region count. 4. **Coverage measures execution, not assertion strength.** Region coverage proves a line ran; it does not prove a test would catch a regression. Mutation testing fills that gap on the trust-core crates (`corecrux-receipts`, `corecrux-segment`, `corecrux-storage`): - Nightly [`mutants.yml`](../.github/workflows/mutants.yml) runs `cargo-mutants` sharded 8 ways, merges the shard reports into a per-crate mutation-score table in the job summary, and **ratchets** against [`mutants-baseline.txt`](../.github/mutants-baseline.txt): a *new* survivor (a mutant no test kills that isn't in the baseline) turns the nightly red; survivors that become caught are listed so the baseline can be shrunk. Neither is a required PR check. - PR-time [`mutants-diff.yml`](../.github/workflows/mutants-diff.yml) runs `cargo mutants --in-diff` over the PR's changes to the trust-core crates, so assertion-free new code is caught at review time in minutes instead of overnight. - To burn a survivor down: write a test that kills it, run `cargo mutants --file -p --timeout 120` to confirm, and delete its line from the baseline. Only genuinely inert mutations (logging, metrics) belong in the baseline long-term — prefer `#[mutants::skip]` with a comment for those so the baseline shrinks. ## Maintaining the gate - **To raise a floor:** add tests for the crate, confirm the new per-crate number in the Coverage job log, then bump the `pair` floor in [`ci.yml`](../.github/workflows/ci.yml) (keep it ≤ actual). Three per-crate floors are currently well below reality and are the standing opportunities: `corecrux-storage` (79 → 89), `corecrux-segment` (85 → 93) and `corecrux-receipts` (88 → 91). They were left alone on 2026-07-30 because part of that headroom comes from `corecruxctl` tests exercising those crates *transitively*, so the numbers should be confirmed stable across a couple of main runs before being locked in. `corecrux-memory` remains at 93.5% against its 95 target. - **Leave headroom when ratcheting the workspace floor.** It is set one point below the measured total, not at it: a floor equal to reality turns main red on the first feature PR that adds uncovered code. - **Keep the regex in lock-step:** the `COVERAGE_IGNORE_REGEX` is duplicated in `ci.yml` and `coverage-attestation.yml` — change both together. - **Adding an exclusion is a reviewed decision:** justify it inline (the only acceptable classes are entry points, interactive surfaces, and platform-inert/unconstructable code). Prefer testing over excluding. ## See also - [`AGENTS.md`](../AGENTS.md) — the 60-second trust-core tour + "verify the claims yourself". - [`docs/agent/CLAIMS.md`](agent/CLAIMS.md) — each product claim → code → the test that proves it. - [`docs/code-health-harvest.md`](code-health-harvest.md) — the code-health (dead-code / stub / unused-dep) harvester. # ============================================================ # BEGIN docs/benchmarks.md # ============================================================ # Benchmarks ## Evidence Status This repository carries reproducible local daemon baselines and gate fixtures. Any benchmark number used in trust, release, or marketing material must name: - corpus or workload name - `commit_sha` - run ID or artifact path - lane flags / feature flags - hardware class and operating system - pass/fail threshold when the number is used as a gate The LongMemEval-style hosted retrieval result mentioned in the README is an internal preliminary CoreCrux/AMR result, not a bare local-daemon claim. Treat it as unpublished until a public evidence pack with corpus, run ID, lane flags, and commit SHA is available. ## Criterion Benchmarks The `corecrux-retrieval` crate includes a Criterion benchmark for BM25 scoring: ```bash cargo bench -p corecrux-retrieval --bench bm25_bench ``` Results are written to `target/criterion/` with HTML reports. ### Latest Local Results Measured on the development host (WSL2, Linux 5.15). Times are wall-clock per iteration (lower is better). These historical figures should be refreshed and recorded with a `commit_sha` before being used as current release evidence. #### BM25 Single-Segment Search (`bm25_search`) | Corpus size | Median | 95 % CI | |---|---|---| | 100 docs | 583 ns | [581 ns, 586 ns] | | 1,000 docs | 2.62 us | [2.62 us, 2.65 us] | | 10,000 docs | 23.1 us | [23.1 us, 23.8 us] | #### BM25 Multi-Segment Scoring (`bm25_score_multi`) | Segments | Median | 95 % CI | |---|---|---| | 2 | 8.75 us | [8.74 us, 8.77 us] | | 4 | 23.5 us | [23.1 us, 23.5 us] | | 8 | 72.2 us | [72.0 us, 72.4 us] | Scaling is roughly linear with segment count (2x segments ~ 2.7x time), confirming no contention overhead in the multi-reader merge path. ## Performance Regression Gates The `tests/bench/` directory contains JSON gate files used by CI to detect performance regressions: ``` tests/bench/ perf_regression_gates/ # Startup and append latency baselines replay_gates/ # Deterministic replay throughput baselines replay_many_gates/ # Multi-stream replay baselines ``` Each directory contains: - **`baseline_*.json`** — The accepted performance baseline (established by a known-good build) - **`candidate_*_pass.json`** — A candidate result that meets the gate threshold - **`candidate_*_fail.json`** — A candidate result that fails the gate (used for gate validation) ### Gate Thresholds #### Perf Regression Gates (`baseline_startupfix_v3`) **Append:** | Metric | Baseline | Threshold | |---|---|---| | Throughput | 25,422 events/sec | Min 85 % of baseline | | p95 latency | 9.98 ms | Max 125 % of baseline | | Fence wait (avg) | 1.7 ms | Max 135 % of baseline | | Lane wait (avg) | 5.0 ms | Max 135 % of baseline | | Store lock wait (avg) | 3.9 ms | Max 135 % of baseline, abs max 0.5 ms | **Replay (3 workload profiles):** | Profile | Throughput (reads/sec) | p95 (ms) | Min throughput ratio | Max p95 ratio | |---|---|---|---|---| | A | 21,328 | 0.52 | 0.85 | 1.25 | | B | 15,376 | 0.74 | 0.80 | 1.35 | | C | 19,750 | 0.57 | 0.85 | 1.25 | All replay profiles require `eventsReadRatio >= 0.98` and `sessionUseFrames = false`. #### Replay Gates (`baseline_append`) | Metric | Baseline | |---|---| | Total events | 600,000 | | Batches | 10,000 | | p50 latency | 3.4 ms | | p95 latency | 9.1 ms | | Throughput | 10,000 events/sec | #### Replay-Many Gates (multi-stream baselines) | Profile | Throughput (reads/sec) | p95 (ms) | Avg reads/RPC | |---|---|---|---| | A | 3,200 | 3.4 | 8.0 | | B | 850 | 12.0 | 8.0 | | C | 2,900 | 4.2 | 8.0 | ### Gate File Format ```json { "metric": "append_p99_ms", "baseline": 2.1, "candidate": 2.3, "threshold_pct": 15.0, "pass": true } ``` A candidate passes if `(candidate - baseline) / baseline * 100 <= threshold_pct`. ## Running Benchmarks Locally ```bash # BM25 scoring benchmark (Criterion) cargo bench -p corecrux-retrieval --bench bm25_bench # Replay benchmark (requires a data directory with sealed segments) CORECRUXD_DATA_DIR=./data cargo run --release --bin corecruxctl -- replay --scope full # Verify-store integrity benchmark CORECRUXD_DATA_DIR=./data cargo run --release --bin corecruxctl -- verify-store --scope all --mode full --strict ``` ## Adding a New Benchmark 1. For micro-benchmarks: Add a `[[bench]]` entry to the relevant crate's `Cargo.toml` and create a Criterion benchmark file in `benches/`. 2. For regression gates: Add baseline + candidate JSON files to the appropriate `tests/bench/` subdirectory. # ============================================================ # BEGIN README.md # ============================================================
CueCrux arc-loop mark

CRUX ### One binary. Your whole memory. **Crux is a local-first memory daemon for AI agents** — a versioned fact store, token-budgeted BM25 + graph retrieval, and an Ed25519-signed receipt for every write. 100+ MCP tools, agent passports, portable `.cruxpack` export. No account. No API keys. Nothing leaves your machine. [**Up and running in 60 seconds**](#up-and-running-in-60-seconds) · [The console](#the-console) · [How it works](#how-it-works) · [MCP tools](#100-mcp-tools-memory-first) · [Platform](#standalone-by-design-platform-by-choice) · [Docs](docs/README.md) [![CI](https://github.com/CueCrux/Crux/actions/workflows/ci.yml/badge.svg)](https://github.com/CueCrux/Crux/actions/workflows/ci.yml) [![Coverage](https://img.shields.io/badge/coverage-CI%20gated-green)](https://github.com/CueCrux/Crux/actions/workflows/coverage-attestation.yml) [![MCP](https://img.shields.io/badge/MCP-native-blue)](#100-mcp-tools-memory-first) [![Licence: Apache-2.0](https://img.shields.io/badge/licence-Apache--2.0-blue)](LICENSE) Crux console Overwatch view: a gate queue of decisions that need you, a live fleet of agent sessions, and a rolling receipted activity ticker Overwatch — the gates that need you, the live fleet, every action receipted. Ships inside the daemon at localhost:14800 — no account, no cloud dashboard.
## Why Crux - **Memory that ages honestly.** Facts carry confidence, provenance and a freshness horizon (volatile ~24 h · medium ~35 d · stable ~1 y). A deterministic decay engine downranks stale claims instead of serving them as truth — reversible via `memory_reverify`, with scoped forget + dry-run for GDPR Art. 17. - **Retrieval on a token budget.** `token_budget` is a first-class parameter, not a hope. BM25 + graph fusion returns metadata and token counts first, content only inside budget — **60–80% fewer tokens** than naive top-K context stuffing. Dense lanes are bring-your-own and never required. - **Receipts, not vibes.** Every mutation emits a **CROWN receipt**: Ed25519-signed, hash-bound, independently verifiable offline. Store integrity is checked separately with `corecruxctl verify-store --strict`. - **Identity agents earn.** Every agent carries a passport with a five-tier reputation ladder; verified receipts accrue as track record, and capability tokens expire instead of leaking. - **Policy over memory.** Declare organisational constraints once; proposed actions are checked against them — pass, warn, or block — before they execute. - **Your memory follows you.** Export everything as a self-certifying `.cruxpack` — signed, offline-verifiable, importable into any other daemon. Leaving is a command, not a ticket. - **Know what every session cost.** `corecruxctl session cost` and the console's token-burn page show how much context every model call re-read, where it went, and what to change. ## Up and running in 60 seconds The lowest-friction install is to just ask your agent. Paste this into any coding agent — it reviews the source, installs and runs the daemon, then Crux's setup wizard wires it into your session: the MCP surface connected, a passport minted, a first fact round-tripped. ```text Review and install the Crux Daemon from github.com/CueCrux/Crux — read the README and LICENCE first, run it locally, then use its setup wizard to connect the daemon to this session and confirm the console is up at http://localhost:14800. ``` **Prefer to do it yourself?** Bring the daemon up on any of three rails: ```bash # Docker (recommended) git clone https://github.com/CueCrux/Crux.git && cd Crux docker compose up -d ```
From a release binary, or from source **From a release** (binaries are cosign-signed with SBOMs — [verify them](docs/verify-release.md)): ```bash curl -sSL https://github.com/CueCrux/Crux/releases/latest/download/crux-linux-amd64 -o crux chmod +x crux CORECRUXD_AUTH_MODE=dev_scopes CORECRUXD_DATA_DIR=./data ./crux ``` **From source** (Rust 1.88+, `protobuf-compiler`): ```bash git clone https://github.com/CueCrux/Crux.git && cd Crux cargo build --release CORECRUXD_AUTH_MODE=dev_scopes CORECRUXD_DATA_DIR=./data ./target/release/corecruxd ``` `CORECRUXD_AUTH_MODE` is required; `dev_scopes` is the single-user loopback quickstart rail. Use `off` only for throwaway local development.
HTTP binds `127.0.0.1:14800`, MCP binds `127.0.0.1:14801`. Open `http://127.0.0.1:14800` — the embedded console walks you through one-time setup — then get live with one command: ```bash corecruxctl start ``` `start` is the canonical on-ramp: it detects the daemon, authenticates on the lowest-friction secure rail, wires the MCP endpoint + Claude Code hooks, round-trips a first fact, and prints a single "you're live" summary. **Using Codex or Cursor?** One command each — `--agent` wires that client's own config too: | Agent | Command | What it writes | |---|---|---| | Claude Code | `corecruxctl start --agent claude` | `~/.claude/settings.json` (hooks + statusline) | | Codex CLI | `corecruxctl start --agent codex` | `~/.codex/config.toml` + the stdio bridge | | Cursor | `corecruxctl start --agent cursor` | `~/.cursor/mcp.json` | Every write is a **merge, not an overwrite** — your other MCP servers and settings survive — and re-running is safe. No bearer token is ever written into an agent config; it stays in `~/.config/cuecrux/env` (0600) and is resolved at runtime. Codex gets the stdio bridge rather than the HTTP endpoint because Codex CLI fails the daemon's streamable-HTTP handshake ([details](integrations/codex-cli/README.md#in-session-mcp-tools)). Longer install path: [docs/getting-started.md](docs/getting-started.md) · guided rails: `corecruxctl login`, `corecruxctl quickstart` · ready-made MCP connector configs for Claude Code, Claude Desktop and Cursor: [`examples/mcp-configs/`](examples/mcp-configs/). > [!NOTE] > Crux receipts and BLAKE3 chains prove **integrity, not confidentiality**. The daemon data > directory is not encrypted by Crux; use filesystem encryption (LUKS, FileVault, BitLocker) > when the machine, volume, or backup location is outside your trusted boundary. Per-event-class > assurance (what is captured, what can bypass capture, what a receipt does and does not prove): > [docs/assurance-coverage-matrix.md](docs/assurance-coverage-matrix.md). ## The console Every daemon ships a local operator console at `localhost:14800` — watch your fleet, your plans, and your memory, every action receipted.
Crux console Canvas board: a size-adaptive tile board of sessions, plans and gates that can be dragged, panned and expanded Crux console relation graph: sessions, work items, gates, projects and passports as draggable nodes with flow-traced edges
Canvas — your whole operation as a size-adaptive board the relation graph — sessions, work, gates and passports, flow-traced
Crux console ExecPlans kanban: planned, in-progress, blocked and shipped plan cards with risk class, current milestone and progress Crux console Sessions view: saved sessions for resume and audit, each with its attached plan, passport, token usage and progress
plans as living documents — milestones, gates, risk, progress sessions that survive restarts — resume, audit, attribute token spend
## Don't believe this README Every claim about integrity is checkable on your own machine, offline, in about 60 seconds: ```bash git clone https://github.com/CueCrux/Crux.git && cd Crux cargo build --release --bin corecruxctl bash scripts/demo-receipt-tamper.sh ``` The script seeds a CROWN receipt into a tmp data dir, runs `corecruxctl verify-store --mode full --strict` (expects `ok: true`), **flips one byte on disk**, re-runs verification, and asserts the failure is detected. Read the script first — it's ~150 lines of bash using only documented subcommands. The verifier is ~1,250 lines of Rust at [`crates/corecrux-receipts/src/verify_v1.rs`](crates/corecrux-receipts/src/verify_v1.rs): `ed25519-dalek::verify_strict` (rejects malleable signatures and small-order keys), with the signature bound to both receipt ID and payload hash so it can't be transplanted. Tamper tests live in [`crates/corecrux-receipts/src/tests.rs`](crates/corecrux-receipts/src/tests.rs). And you can run **the same exit test we run in CI** — export everything the daemon knows (facts + sessions) and everything it did (signed journal + receipt refs) into one passport-signed bundle, verify that bundle **offline**, and watch the verifier reject a tampered byte: ```bash corecruxctl context export --data-dir --out ./bundle # signed=true corecruxctl context verify ./bundle --json # ok=true, offline, no network ``` Those checks verify the bundle against the key the bundle carries, which proves it is internally consistent, not who produced it — an unpinned pass is reported as `UNPINNED — trust unproven`. Add `--expect-pubkey-hex <64 hex>` (or set `CRUX_CONTEXT_VERIFY_PASSPORT_HEX`) to also require the signer to be the passport you expected; a bundle re-signed by anyone else then fails. There is no lock-in: the answer to *"can you export it?"* and *"can you prove what it saw and did?"* is a command, not a support ticket. This export → offline-verify → tamper-rejection cycle is a release-blocking CI gate, so every published build has passed its own exit test. Release artifacts are signed and attested end-to-end — cosign keyless signatures, CycloneDX SBOMs, SLSA provenance ([docs/verify-release.md](docs/verify-release.md)). Found something? Report it privately per the [security policy](SECURITY.md).
3D receipt chain in the Crux console: a green receipt block linked to a chain of further receipt blocks, with an inspector showing the gate verdict and linked chain receipts as a literal chain — click any block in the console, inspect the proof
**Testing & coverage:** 6,000+ tests and **~90%** CI-gated region coverage, with per-crate floors on the trust core (`corecrux-receipts` / `-segment` / `-storage`) and the ungated total reported alongside so exclusions can't hide low-coverage code. How it's measured and exactly what's excluded: [docs/testing-and-coverage.md](docs/testing-and-coverage.md). ## Your context window is the scarce resource Most memory layers measure recall. Crux also measures what recall *costs*: every retrieval call takes a hard `token_budget`, and the daemon trims to fit — metadata first, content only when it earns its tokens. Benchmark spread, four panels. Tokens returned per query: naive top-K about 18,400; token budget 4000 returns 3,900; budget 2000 returns 1,940; budget 500 returns 480 — budgets enforced server-side. Same answer, a fraction of the tokens: vendor-native memory scored 97/100 using 8,428 context tokens, Crux scored 95/100 using 1,263 — 15% of the tokens for 98% of the score (ScoreCrux context benchmark, sonnet-5 arm). More context makes it worse: with 3,346 docs and 2M+ tokens, stuffed recall fell 44% to 28% on one model and 28% to 8% on another while tool-mediated retrieval held 80–100% (ScoreCrux scale benchmark). Daemon CI perf gates: 25,422 events/sec append at p95 9.98 ms, 21,328 replay reads/sec at p95 0.52 ms, BM25 median 583 ns at 100 docs and 23.1 µs at 10,000, 60–80% fewer tokens than top-K stuffing All daemon numbers are reproducible from [`docs/benchmarks.md`](docs/benchmarks.md) with pinned baselines, regression-gated in CI. ScoreCrux methodology, negative controls and per-model runs: [scorecrux.com/context](https://scorecrux.com/context) · [scorecrux.com/scale](https://scorecrux.com/scale). Early hosted-retrieval evals via the CoreCrux substrate score ~91% under strict scoring¹. ¹ Internal runs via the CoreCrux retrieval substrate (paid tier; see [the platform section](#standalone-by-design-platform-by-choice)), not the bare local daemon. Strict scoring, no partial credit. Treat this as preliminary until a public evidence pack with corpus, run ID, lane flags, and commit SHA is published. ## How it works One daemon, three planes — memory, retrieval, receipts — on an append-only spine. The colours are the console's own: violet for memory, amber for retrieval, mint for receipts. No cloud in this picture; that's the point. Crux architecture: your agent speaks MCP, HTTP or gRPC to one corecruxd binary; three planes — memory (facts, sessions, handoffs), retrieval (BM25 + graph fusion, .ccxi indexes, repo code maps) and receipts (CROWN, Ed25519, offline verify) — sit on an append-only spine of BLAKE3-chained sealed segments More detail: [`docs/architecture.md`](docs/architecture.md). ## Passports, and how the line is drawn API keys say *something with this string called us*. A passport says *this agent, at this trust tier, with these grants, made this call* — identity and constraint, receipted as a verifiable record. What a receipt proves (and what it deliberately does not) is spelled out per event class in the [assurance & coverage matrix](docs/assurance-coverage-matrix.md); forgery-resistant *evidence* of model traffic comes from the [mediated witness path](docs/llm-shim.md), whose key the agent never holds. 1. **Bind** — the session handshake binds every connection to a passport. No anonymous writes; unattributed calls are operator-tagged, never silently allowed. 2. **Carry** — every tool call rides the passport. RCX mints short-lived capability tokens against it — scoped to tools, tenant and tier — so access expires instead of leaking. 3. **Earn** — verified receipts accrue to the passport as reputation; five tiers climb the capability ladder and unlock more autonomy. Trust is a ledger, not a checkbox. 4. **Answer** — any receipt, any time later, resolves back to the passport that produced it: who acted, at what tier, under which grants. The same daemon binary runs on every tier. What changes is a signed **RCX capability token** — the policy layer that says which backends the daemon may call and what data may cross the wire. **Enforcement is a property of the wire, not a crippled binary.** Tokens are self-issued locally, verifiable offline with the same machinery as everything else, and refusals are fail-closed with a signed `RefusalReceipt` carrying a reason code — never a bare 403 or a silent downgrade. ## What you get | Capability | Local daemon | Bring your own | Hosted / managed | |---|:---:|:---:|:---:| | Append-only event store with BLAKE3 integrity | yes | | | | CROWN receipts and offline receipt verification | yes | | | | Versioned fact store with freshness decay + `memory_reverify` | yes | | | | Scoped forget with dry-run (GDPR Art. 17) | yes | | | | Sessions, checkpoints and cross-session handoffs | yes | | | | ExecPlans + live work board | yes | | | | Decision records + constraint governance | yes | | | | `.cruxpack` export + offline import | yes | | | | Built-in MCP server, 100+ token-filtered tools | yes | | | | Agent passports (five-tier reputation) + RCX capability tokens | yes | | | | Live multi-session coordination board | yes | | | | Typed action traces (reasoning refs — never raw CoT) | yes·flag | | | | C2PA output attestation | yes·flag | | | | HTTP, gRPC, health, readiness, and metrics | yes | | | | `corecruxctl` verification and replay tooling | yes | | | | BM25 text search with `.ccxi` companion indexes | yes | | | | AST code maps: register a repo, get a typed code-structure graph (Rust · TS · Python · Vue) | yes | | | | Dense fact retrieval via embeddings | | Ollama, vLLM, TEI, llama.cpp, LiteLLM | | | Hosted team sync, multi-device passports, billing | | | yes | | GPU/CUDA fused retrieval + AMR routing | | | yes | | LLM entity/relation extraction + better-dense rerank | | | yes | | Fleet governance: attribution, policy gates, revocation | | | yes | `yes·flag` = ships in this repo behind a default-off feature flag — your traces and attestations are opt-in, like everything else. `/v1/version` reports which features are active. ## Standalone by design, platform by choice Crux is complete on its own. When you want more, it's the memory spine of the CueCrux platform — same receipts, same passports, every hop attributable. Crux runs inside a dashed boundary labelled your machine. CruxEngine (sync off by default), CoreCrux, ScoreCrux and your planning repo connect from one side; WikiCrux (live code maps served by this daemon) and the RCX environment from the other — every call rides a passport **Free forever** — the full local daemon: receipts, retrieval, passports, console. **Pro** (£19/user·mo) adds hosted sync, multi-device passports, and better-dense rerank. **Governance** (£49/user·mo) adds fleet-wide attribution, policy gates, and revocation that propagates with proof. Metered **credits** (~£0.01/Cr) buy the heavy lifts — dense rerank, extraction, attestations. The local dense lane is **never** metered: credits buy the step-up, not your own machine. Every credit spend mints a signed receipt (`crux.credit_spend_receipt.v1`) — even the meter is auditable. The local daemon is the acquisition, not the upsell. Full ladder: [memorycrux.com/pricing](https://memorycrux.com/pricing). **You cannot pay us to hold your data — there is no such product.** Paid frontier ingest and extraction is processed in flight, never parked: enrichment lands back in *your* store — process, return, delete. Every hop carries a receipt. Local-first isn't a pricing tier; it's the architecture. The promises are written down in the [Trust Contract](TRUST-CONTRACT.md). **The lane stack** (each lane is a different way of remembering): | Lane | What it remembers | Tier | |---|---|---| | lexical | exact words, BM25-class — microseconds on CPU | free · local | | dense | semantic similarity — meaning, not spelling | BYOE / managed | | graph / topology | entity links walked at query time | CoreCrux | | entity & trait | who/what-keyed recall — ask about a person, get their dossier | CoreCrux | | event | time-anchored recall — what happened, when, in what order | CoreCrux | | navigational | summary-tree descent for corpora too large to scan | CoreCrux | | verbatim pointers | exact-quote recall without duplicating content | free · local | | *+ several more* | *the ones we don't blog about* | — | **AMR — Adaptive Manifest Routing** reads the lane manifest and routes each query automatically — fusing the lanes that earn their tokens, skipping the ones that don't, learning from per-request outcomes. No knobs. It switches on with a subscription and stays off otherwise. Without one you still run the full local daemon — lexical lane, graph fusion, token budgets, receipts. That's not a demo; it's the same engine the paid lanes plug into. ## EU AI Act The Act asks for risk management, logging, transparency and human oversight. Most stacks will answer with PDFs. Crux answers with receipts — the compliance machinery is the same machinery everything else runs on. | Article | The Act asks for | Crux gives you | |---|---|---| | **Art. 9** — risk management | documented risk processes | risk classes on plans/work items; high-risk transitions demand a passport-attributed human gate, and the gate becomes a signed fact | | **Art. 10** — data governance | PII discipline | private facts never leave the machine; reserved prefixes are private at ingest; observation capture redacts; reasoning stored as references, never raw chain-of-thought | | **Art. 12** — record-keeping | automatic logging | every state mutation emits a signed CROWN receipt + timeline row; hash-chained, so gaps are detectable, not deniable | | **Art. 13** — transparency | attributable outputs | agent passports attribute every mutation; AI-authored commits and PRs carry agent attribution | | **Art. 14** — human oversight | meaningful gates | destructive actions need explicit, current consent; gated transitions can't auto-approve past a timeout | | **Art. 15** — accuracy & robustness | foresight + integrity | consequence enrichment before high-risk operations; published benchmark packs carry corpus + commit; tamper-evident store | | **Art. 50** — output transparency | "an AI made this" | C2PA Content Credentials via `output_attest` — a verifiable claim, not a footer | | *GDPR Art. 17* — erasure | right to be forgotten | `memory_forget` with dry-run: see what would be deleted, then delete it auditably | > These are engineering controls aligned with the Act — not a legal opinion, and conformity > assessment for your deployment remains your responsibility. What Crux gives you is the evidence > layer: when the auditor asks, you answer with receipts, not screenshots. ## Engrams — memory that compiles Retrieval answers questions. Engrams skip them. When the same lesson keeps getting re-learned from raw history, it's distilled into a named, versioned procedure that arrives **before** the agent acts — instinct, not search. Engrams carry provenance hashes back to the source chunks they were learned from (you can always audit *why* the instinct exists), resolve by declared intent at session start, and are gated by passport tier. Local-first: a built-in catalog plus fact-backed overlays serve the same contract with zero cloud dependency. ## 100+ MCP tools, memory-first Built MCP-first, not MCP-wrapped: every retrieval tool takes a token budget, every mutation emits a receipt, identity rides a passport. The server lives at `http://localhost:14801/mcp`. > [!TIP] > **AI agents exploring this codebase: start at [`AGENTS.md`](AGENTS.md)** — a crate atlas, > a claims-to-code-to-tests matrix, and the cryptographic invariants, all anchored by > greppable symbol names and verified in CI. | Capability area | Representative tools | |---|---| | Memory & retrieval | `store_fact` · `query_facts` · `query` · `query_scan` · `query_expand` · `fact_history` · `memory_view` · `get_bootstrap` | | Freshness & consolidation | `memory_freshness` · `memory_contradictions` · `memory_consolidate` · `memory_reverify` · `memory_pin` | | Portability & erasure | `memory_forget` · `memory_forget_dry_run` · `output_attest` · `receipt_verify` | | Identity & passports | `issue_passport` · `get_passport` · `revoke_passport` · `passport_link_device` · `resolve_principal` | | Handoff, sync & coordination | `create_handoff` · `accept_handoff` · `sync_status` · `coord_status` · `coord_announce` | | Sessions & artefacts | `save_session` · `get_session` · `session_checkpoint` · `list_sessions` · `artefact_put` · `artefact_get` | | Decisions & constraints | `record_decision` · `declare_constraint` · `get_constraints` · `check_constraints` | | Observability | `list_observations` · `get_observation` · `verify_observation` · `session_token_usage` | …and more across substrate entities & edges, work & coordination, and orchestration — 100+ tools today, and counting. The catalogue is token-filtered: a local token sees local tools; hosted-authorised tokens also see hosted-gated tools (descriptions are marked `[local]` / `[hosted]`). Recommended first calls: `cuecrux_session` → `get_bootstrap(topic="patterns")` → `store_fact(...)` → `query_facts(...)`. Full guidance: [`docs/agent-guide.md`](docs/agent-guide.md) · API surface: [`docs/developer-portal.md`](docs/developer-portal.md). ## Operations reference
Requirements & configuration | Path | Requirements | |---|---| | Docker | Docker or Docker Desktop. | | Build from source | Rust 1.88+, `protobuf-compiler`, and a C toolchain. | | Shell examples | `curl`; `jq` recommended. | | Embeddings | Optional local or remote embedding endpoint. | Config via environment variables or YAML (`config.example.env`, `config.example.yaml` → `$XDG_CONFIG_HOME/crux/config.yaml`). Core settings: | Variable | Default | Description | |---|---|---| | `CORECRUXD_AUTH_MODE` | required | `off`, `dev_scopes`, `jwt_hs256`, or `jwt_jwks`. | | `CORECRUXD_ROUTE_AUTH` | derived | `enforce` when auth is enabled or the listener is non-loopback; otherwise `shadow`. Shipped packaging pins `enforce`. | | `CORECRUXD_DATA_DIR` | `../CoreCruxData/v1` | Data directory. | | `CORECRUXD_HTTP_PORT` | `14800` | HTTP API port. | | `CORECRUXD_GRPC_PORT` | `4007` | gRPC API port. | | `CORECRUXD_MCP_PORT` | `14801` | MCP server port. | | `CORECRUXD_MCP_ENABLED` | `true` | Enable the built-in MCP server. | | `CORECRUXD_BUILD_CCXI` | `0` | Build `.ccxi` indexes at seal time. | | `CORECRUXD_CREDIT_METER` | `false` | Enable the default-off comped-wallet credit meter and `/v1/credits/spend` test rail. | | `CORECRUXD_EMBEDDING_URL` | unset | Enables dense fact retrieval. | | `CORECRUXD_EMBEDDING_MODEL` | `nomic-embed-text` | Embedding model name. | | `CORECRUXD_CORECRUX_BASE_URL` | unset | CoreCrux admin base URL for `/console` lane-weight controls. | | `CORECRUXD_CORECRUX_ADMIN_TOKEN` | unset | Optional bearer token forwarded to CoreCrux admin endpoints. | | `CORECRUXD_CORECRUX_PASSPORT_ID` | unset | Optional passport id forwarded to CoreCrux admin endpoints. | Security defaults: loopback binds are safe for local development; non-loopback HTTP binds require a real auth mode; non-loopback MCP binds should set `CRUX_AGENT_TOKEN(S)`; set `CRUX_MCP_HANDOFF_SECRET` if handoff packages must survive restarts. The daemon refuses to start unless `CORECRUXD_AUTH_MODE` is explicit (`off` | `dev_scopes` | `jwt_hs256` | `jwt_jwks`). Usage receipts (adoption signal) are **opt-in and off by default** — the daemon sends no outbound signal unless you set an `https://` collector endpoint *and* record consent. See [`docs/usage-receipts.md`](docs/usage-receipts.md) (`CORECRUXD_USAGE_RECEIPTS_SUBMIT` / `_ENDPOINT` / `_CONSENT_AT`). Credit Meter is also **opt-in and off by default**. `CORECRUXD_CREDIT_METER=1` enables only the seeded comped-wallet spend rail for pinned quotes and signed spend receipts; fiat minting, Paddle, and production billing remain separately gated. The same flag meters successful RCX-verified `/v1/gpu1/rerank` compute: callers carry the pinned quote in `options.credit_quote`, and the result adds the spend receipt id, credits spent, and post-spend wallet balance.
First five minutes (HTTP API) Store a fact (Docker default is `dev_scopes`, hence the scope header): ```bash curl -s -X PUT http://localhost:14800/v1/facts \ -H "Content-Type: application/json" \ -H "X-Corecrux-Scopes: facts:write,facts:read,admin:read" \ -d '{"entity":"project","key":"status","value":"Crux Daemon is running locally","confidence":0.95}' | jq . ``` Query facts (note the mandatory-by-convention token budget): ```bash curl -s "http://localhost:14800/v1/facts?query=Crux+Daemon&token_budget=500" \ -H "X-Corecrux-Scopes: facts:read,admin:read" | jq . ``` Verify store integrity: ```bash docker exec crux-crux-1 corecruxctl verify-store --data-dir /data --scope recent docker exec crux-crux-1 corecruxctl verify-store --data-dir /data --scope all --mode full --strict ``` Common endpoints: | Method | Path | Purpose | |---|---|---| | `GET` | `/healthz` / `/readyz` / `/metrics` | health, readiness, Prometheus | | `GET` | `/v1/version` | build, features, sync, update state | | `PUT` / `GET` | `/v1/facts` | store / query facts | | `POST` | `/v1/admin/append` | append events | | `POST` | `/v1/query/text-search` | BM25 retrieval | | `GET` | `/v1/receipts/{id}` / `…/verification` | fetch / verify a receipt | Full route notes: [`docs/api-reference.md`](docs/api-reference.md).
Embeddings (bring your own) Crux ships no embedding model. Point it at an endpoint you control: ```bash CORECRUXD_EMBEDDING_URL=http://localhost:11434 CORECRUXD_EMBEDDING_MODEL=nomic-embed-text ``` Supported patterns: Ollama, vLLM, TEI, llama.cpp, LiteLLM. When unset, fact queries use keyword matching and confidence ranking only.
CLI, backups, upgrades, troubleshooting `corecruxctl` subcommands: `verify-store` (integrity), `replay` (drift classification), `receipts` (inspect/export), `ccxi` (companion indexes), `projections` (projection state). Before upgrading: stop cleanly → snapshot `CORECRUXD_DATA_DIR` → `corecruxctl verify-store --scope recent` plus `corecruxctl verify-store --scope all --mode full --strict` when the window allows → keep the previous binary until the new one passes `/readyz`. Rollback = restore the snapshot, restart the previous binary. Never delete live shard data by hand. | Symptom | Check | |---|---| | Daemon exits at startup | Set `CORECRUXD_AUTH_MODE`. | | HTTP works but MCP doesn't | `CORECRUXD_MCP_ENABLED`, port `14801`. | | Non-loopback bind refused | Use `jwt_hs256` / `jwt_jwks`, or stay loopback. | | Text search empty | Enable `.ccxi` build; ensure sealed/indexed data exists. | | Embeddings inactive | Set `CORECRUXD_EMBEDDING_URL` + model. | | Store verification fails | Stop daemon, snapshot data, inspect with `corecruxctl`. | More: [`docs/troubleshooting.md`](docs/troubleshooting.md) · [`docs/ops-guide.md`](docs/ops-guide.md) · [`docs/release-packaging.md`](docs/release-packaging.md)
Naming & repository scope | Name | Meaning | |---|---| | `Crux` | Product and repository name. | | `Crux Daemon` | The local daemon distribution documented here. | | `corecruxd` | Canonical daemon binary built by Cargo. | | `crux` | User-facing release alias for `corecruxd`. | | `corecruxctl` | CLI for verification, replay, receipts, and operations. | | `crux-hook` | Agent lifecycle hooks, including encrypted compaction snapshot sync. | | `CORECRUXD_*` | Environment-variable prefix for daemon config. | **This repo contains** the local-first daemon, CLI, MCP server, append-only storage, BM25 retrieval, CROWN receipt verification, capability-token signing/verification, the outbox-push sync client, and the Claude Code lifecycle hooks. Every claim about *local* trust can be verified by reading this tree. **This repo does not contain** the hosted backend (operated by CueCrux Ltd) or GPU/CUDA acceleration (a separate distribution; this repo is CPU-only). Self-hosting without the hosted backend keeps the daemon fully functional in local-only mode (`crux-router` returns `DegradedLocal` for hosted-tier decisions). This boundary is intentional — audit the local half here, the hosted half via the published Trust Contract and the receipts your daemon emits.
## Community Built in the open, verified in the open. If Crux is useful to you, a star helps others find it. [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md) · [Code of conduct](CODE_OF_CONDUCT.md) · [Changelog](CHANGELOG.md) · [Trust Contract](TRUST-CONTRACT.md) - Before running a release artifact, verify its signature, SBOM and provenance with [`docs/verify-release.md`](docs/verify-release.md). - Report vulnerabilities privately through [`SECURITY.md`](SECURITY.md); do not open a public issue for security bugs. - For non-security bugs or feature requests, use the GitHub issue templates: [bug report](.github/ISSUE_TEMPLATE/bug_report.yml) or [feature request](.github/ISSUE_TEMPLATE/feature_request.yml). ## Licence Crux Daemon is open source under the [Apache License, Version 2.0](LICENSE). - Commercial use, modification, distribution, and private use are permitted. - Running Crux as a managed, hosted, or cloud service is permitted. - Apache-2.0 includes an express patent grant (section 3). - Redistribution must retain the copyright, licence, and `NOTICE` attribution (sections 4(a)–4(d)); it grants no trademark rights (section 6). - Curated content is covered separately by [`content/LICENCE-CONTENT.md`](content/LICENCE-CONTENT.md). - Plain-English answers: [`docs/LICENCE-FAQ.md`](docs/LICENCE-FAQ.md). Copyright (c) 2026 CueCrux Ltd. # ============================================================ # BEGIN CHANGELOG.md # ============================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **Cadence:** weekly rolling builds are cut from `main`; versioned releases > ship every 4–8 weeks. Every versioned release gets human-readable notes > here — if you tag a release, you write its entry. ## [Unreleased] ### Added - **Jev setup is checkable and configurable.** `crux-jev doctor` checks a Jev integration end to end without spending a Jev call: `crux-jev`'s configuration, the two daemon flags, the token's read scopes (naming the `dev_scopes` literal-token trap when they fail), the published signing key and the pinned keyring, each failure with its fix. The daemon reads `daemon.stream_receipts` and `daemon.context_surface` from `config.yaml` (the environment still wins), and `GET /v1/version` reports `capabilities.stream_receipts`. - **`crux-jev verify --tenant` / `--actor`.** The signed body must name the tenant (default `local`) and, when asked, the minting passport; the result reports `tenant_id` and `actor_passport`. - **Crux Claude Code mod in setup.** `corecruxctl login` / `start` and `crux-config-wizard init` now install `crux-desktop`, a Claude Code function-hook mod that owns session continuity: a context block on the first prompt, `save_session` (as `hook:session:`) after every turn, before compaction and at session end, a status-line entry and a `/crux` pane. The installer writes the mod to `~/.claude/mods/crux-desktop`, adds it to `CLAUDE_CODE_PLUGIN_DIRS` and `pluginConfigs` in `~/.claude/settings.json`, and registers the daemon as a user-scope `crux` MCP server in `~/.claude.json` (an existing server of that name is left alone). With the mod installed, `hooks install` no longer wires the SessionStart banner or the PreCompact save. New `corecruxctl mods install|uninstall|status` (and the same under `crux-config-wizard mods`); opt out with `--no-mod`. - **`corecruxctl receipts verify-stream-receipt`** verifies a daemon stream receipt (`model_invocation`, `context_injected`, `stream_completed`, `stream_aborted`) offline from its observation record against a pinned public keyring, checking the daemon's own signature over the canonical body. It rejects other receipt classes signed by the same key (e.g. `usage_ping`) on the signed `kind`, takes optional `--expect-receipt-id` / `--kind` checks, and on success prints only fields decoded from the signed body — the record's `payload.output_hash` and similar copies are unsigned. Library entry point: `corecrux_receipts::verify_stream_receipt_payload_v1`. - **`GET /v1/receipts/signing-keys`** serves the daemon's receipt-signing public key, unauthenticated, as an `Ed25519KeyRingV1` that `verify-stream-receipt --keyring` reads unchanged (`keyId` is the passport fingerprint receipts carry; also `publicKeyHex`, `alg`, `use`). Verifiers no longer need `admin:read` on `/v1/admin/version`. Key discovery, not trust: pin the key at enrolment. - `GET /v1/observations/aggregate` takes `chains=returned|all|none`. - Session metrics `vaultcrux_session_capability_graph_edges{origin,tier}` and `vaultcrux_session_excluded_count{origin,reason}` for issued session plans. (#813) ### Changed - **`crux-jev` fixes.** - `pin-key` reads the `publicKeyHex` the signing-keys route actually serves (it looked for `public_key_hex`), needs no token, and writes the keyring atomically. - `verify` checks every decision fact naming a receipt, so a later edited version can no longer pass on the strength of an earlier good one. - A request with `store_state: "false"` (a string, which is truthy) or an unknown `state_layout` is refused with exit 2 instead of being coerced or failing as exit 1. - A Jev reply with no answers raises a named error rather than `KeyError`. - **Jev docs match the code.** The cookbook no longer claims a receipt proves *when* a decision was made offline (the body's timestamps are caller-supplied), documents the `dev_scopes` token and the `admin:*` scopes in the default agent-token set, the `verify`/`pin-key` exit codes, and drops the stale "receipt ids are not unique" and "no offline verifier" text. - **Claude Code setup no longer reaches for private or hosted endpoints.** The `crux-coord` hook and the SessionStart banner defaulted to CueCrux's tailnet daemon (`100.70.12.73`) and to `crux.cuecrux.com` when no endpoint was configured; both now default to the loopback daemon, as the hook launcher does, and `crux-coord` reads the endpoint `login` saved. The banner no longer reports "degraded" for a loopback daemon with auth off, and sends no `Authorization` header without a token. - **`workspace-cuecrux` leaves the default profile set.** `init --profiles=all` wrote CueCrux's internal endpoints into every user's CLAUDE.md; enable it with `crux-config-wizard add workspace-cuecrux`. - **Hook install works from native Windows.** Home and Claude config paths fall back to `USERPROFILE` and honour `CLAUDE_CONFIG_DIR`; hook command paths are shell-quoted and forward-slashed for Claude Code's hook shell; the `crux-hook` and `jq` probes accept `.exe`. The Python banner stack stays a Unix feature. - **`corecruxctl start`'s summary names the registered MCP endpoint** rather than the HTTP URL relabelled as MCP. - **Passport-bound callers see and verify their own stream receipts.** With `CORECRUXD_AGENT_PASSPORTS=1` an agent token is confined to its own tenant, while the stream receipts it mints are signed under `local` and logged in unscoped `mediation::` sessions. `GET /v1/observations/aggregate` now lists a daemon-signed receipt kind to the passport that minted it (record `principal`), and `GET /v1/receipts/{id}/verification` lets a `receipts:read` caller verify a stream receipt whose signed `actor_passport` is its own passport, whatever `tenant_id` it asks under. Other kinds keep session scoping; every other caller gets the same answer as for a missing receipt. - **Agent tokens can verify receipts.** The default `CORECRUXD_AGENT_TOKEN_HTTP_SCOPES` set gains `receipts:read`, so a token that can mint a receipt (`sessions:write`) can also call `GET /v1/receipts/{id}/verification`. - **Aggregate `chains` is bounded.** `GET /v1/observations/aggregate` now reports chain status only for the sessions behind the returned records (it covered every session log: ~7.3 MB and ~31 s for a 10-record answer on a busy node). `?chains=all` restores the full map. - **Pro claim placements are derived from named implementation sites.** A claim now earns `daemon` or `hosted_control_plane` only by naming where it is implemented. **Behaviour change in `/v1/version`:** a claim with no site reports `unimplemented` (previously `contracted_external`), and `sync:managed_backup`, `audit:central_retention` and `control_plane:hosted` no longer report as `hosted_control_plane`. (#811) ### Fixed - **`context_custody_audit` no longer reports a false all-green.** REMEMBER is scored for the calling agent's passport (missing, revoked, or category-less → `partial`); PROVE is `strong` only once `CRUX_EXPORT_VERIFY_PUBLIC_KEY_HEX` pins the export signer; `standing_gap` names what remains. Expect PROVE to read `partial` on nodes without a pinned signer. (#807) - **MCP `get_bootstrap` honours `token_budget`.** The schema lacked it and the handler passed none, so a cold-start call with `token_budget=500` could return up to 100 full bootstrap facts. (#810) - **Concurrent observe step opens no longer share a `seq`.** The seq was minted under a read lock and stored under a later write lock, so two racing opens could both take `max + 1` and drop a step from the audit chain. (#814) ### Security - **Stream receipt ids are unique.** `POST /v1/mediation/receipts` refuses a stream-receipt draft whose `receipt_id` is already used (409 `RECEIPT_ID_CONFLICT`), and `/v1/receipts/{id}/verification` answers 409 `RECEIPT_ID_AMBIGUOUS` instead of the first match when a log holds more than one body for an id, so a later mint cannot shadow an earlier receipt. - **Receipt kinds are reserved.** `POST /v1/sessions/{sid}/observations` (and the batch route) refuse `model_invocation`, `context_injected`, `stream_completed`, `stream_aborted` and `usage_ping` with 422 `RESERVED_OBSERVATION_KIND`; only the signing path writes them, so `/v1/observations/aggregate?kind=model_invocation` no longer lists unsigned imitations. - **Promotion confirm refuses records for another tenant.** `POST /v1/sync/tenants/{tenantId}/promotions/confirm` authorized only the path tenant, then applied the caller-supplied records as sent, so a caller allowed to write tenant A could plant facts in tenant B. The whole batch is now refused (403) before anything applies when a record's entity is outside the path tenant's namespace, its tenant stamp names another tenant, or the caller's passport may not write the entity. **Behaviour change:** such batches get 403 instead of a partial apply. (#808) - **The observation aggregate feed no longer shows other passports' sessions.** `GET /v1/observations/aggregate` without `session_id` returned every passport's prompts, responses and mediation receipts to any `query:read` caller. A passport-bound caller now sees only its own sessions; an unbound operator with raw `admin:read` keeps the full view. **Behaviour change** for passport-bound callers. (#814) - **A revoked passport is refused at the sync and operator-tier gates even with `CRUX_PASSPORT_REVOCATION=0`.** `require_passport_tier` checked only the reputation tier, so with the dispatch-wide gate off a revoked passport could still `sync_push` / `sync_pull` and call operator-tier identity tools. (#806) - **`coord_announce` can no longer attribute an intent to someone else.** On an unbound session, `by_passport` was trusted as given. It must now name the caller's own passport (403 otherwise); an admin acts for another passport through the existing passport-header override. **Behaviour change:** an unbound announce naming a passport the caller doesn't hold is refused. (#812) ## [0.5.65] - 2026-09-22 ### Added - **Memory parity.** Recall applies a match floor and reports honest misses instead of padding weak hits (#791); a curated engram tier plus a memory digest is rendered into `CLAUDE.md` and `AGENTS.md`, with engrams on by default (#792); read-only native memory ingest as a projection (#787). - **Prompt-cache awareness.** A cache-invalidation ledger (#788), a monotone MCP tool surface per session so the tool list stops churning the cache (#789), and cold-context awareness (#790). The boot banner's Python renderer gained the cold-cache line and a token fallback (#793). - **Proof-carrying adaptive packs, first steps.** The `pack.conformance.v1` manifest format (#767) and shadow-corpus staging with a replay harness (#768, #769). - **Extension lifecycle seams.** Staged activation (#763), a conformance hook (#764), per-pack outcome events (#765), and atomic rollback to a pinned prior build (#766). - **Delegated embedding has one payer per daemon** (#760). - An `escrow_recovery_code` fuzz target (#758) and an enforced-by lint for load-bearing auth claims (#748). ### Changed - Four Pro claims that had no gate in the daemon were withdrawn from sale rather than advertised (#761). ### Security - Unwedged the security gates: yanked crates replaced and two live advisories resolved (#785). A failing scheduled security audit now notifies someone instead of failing silently (#786). - Dependency bumps: blake3, syn, wat, crc32fast, uuid, and CI actions (#749, #756, #771, #772, #773, #775, #776, #780, #781, #784). ### Fixed - **One tenant erasure could wedge every write on the shard.** `forget-tenants` with `reclaim: true` retires a segment together with the L0 directory run whose `run_id` equals its `segment_seq`. That pairing is not reliable: `publish_dir_run_v1` resolves a run-id collision by incrementing `run_id`, so a run describing segment *N* can be published as `run_id N+1` — and a stale `directory/` file is enough to trigger it (host `crux` carries 1083 dirrun files against 1061 segments). The displaced run survives the reclaim still naming a segment that is gone. `rebuild_directory_from_runs` treated that extent as corruption and returned `dirrun references missing segment_seq N`, which fails `ShardStorage::open` — and open is on the path of every write, while reads scan `segments/` directly and stay green. The result is the silent-500 signature this store has now hit twice, once for 44 hours: all ingest returning 500 with `/readyz` healthy, and `corecruxctl storage repair-manifest` reporting the shard *healthy* because the segment is already out of the manifest and only the directory still names it. An extent naming a retired segment is now a **miss**, not an error: the manifest is append-only and published runs are never rewritten, so a run outliving its segment is the designed state after erasure. The extent is skipped and counted, and each affected run logs `dirrun-extents-skipped` once. `corecruxctl storage repair-manifest` learned the same class, so a store already wedged by an older daemon can be recovered offline: it now reports `dangling_dir_runs` alongside `dangling_segments` and retires the runs whose named segments are *all* gone. A run that also names live segments is reported and left alone — its live extents are how those streams resolve, so dropping it would trade a wedged shard for a lossy one. (#740) - **One dense query could OOM the daemon on a large corpus.** The process-wide cache of parsed `.ccxe` companions was bounded only by corpus size — its own doc comment said "stays bounded by the corpus", which is precisely the problem — and the query path then *copied* every vector out of it to build the provider, so the whole corpus was resident twice for the length of every query. On host `crux` (~900 dense segments) a single `/v1/query/text-search` took the daemon from 2.8 GB to an OOM kill at 5.74 GB against a 5.5 GiB memcg limit, with `/readyz` green until the moment it died. The cache now carries a byte budget with LRU eviction, default 512 MiB and settable with `CORECRUXD_DENSE_CACHE_BUDGET_BYTES` (`0` restores the old unbounded behaviour). Candidate vectors are shared with the provider rather than copied, which removes the second resident copy outright; a segment larger than the whole budget is still cached rather than re-read on every access. **This is a memory bound, not a recall cap.** An evicted companion is re-read on the next query, so the candidate set, the scores and the ranking are bit-identical — the CE dense lane stays uncapped and exact per ExecPlan `dense-lane-and-extraction-upsell-2026-06-26`. A regression test asserts exactly that: a query touching more segments than the budget can hold scores identically to one with an unbounded cache. (#740) ## [0.5.64] - 2026-08-23 ### Fixed - **One truncated posting list could hang every query touching a segment.** `decode_block` signals truncation by returning `data.len()` and pushing no values; `pfordelta_decode` looped `while deltas.len() < count` without checking the call had made progress, so once the data ran out the loop could not terminate — wedging the decoding thread and, with it, every query over that segment. A `block_len` byte of zero reached the same state by another route. The declared count had the same origin problem: the first four bytes of whatever was handed in went straight into `Vec::with_capacity`, so an eleven-byte input claiming `u32::MAX` reserved about 17 GB. It is now clamped to what the remaining bytes could possibly encode, and the loop breaks when a block decodes nothing, returning what decoded cleanly. (#744) - **The console could not resolve a work gate even where the daemon declared it available.** Behind Caddy + oauth2-proxy the client `Authorization` is stripped, the proxy's verified identity headers are forwarded, and one shared bridge JWT is injected — `sub: console-bridge`, admin scopes, `tenant_id: *`, **no `passport_id`**. The identity rail was enabled and mapped the operator's login to a canonical passport, and `/v1/version` declared the `work_gate_resolution` rung `identity_header` available — yet every console Approve/Reject failed with *"a canonical passport_id claim is required for gate resolution"*, because the rail was consulted only by its own mint endpoints and never on the decision routes. That is precisely the anti-lying property the gate-oversight work asserts: the declaration said "available" while the real attempt failed. On the human-decision routes — `POST /v1/work/gate/{id}/approve|reject` and `POST /v1/passport/mint-requests/{id}/approve|reject` — a bearer carrying no canonical passport claim now consults the identity rail under **exactly** the trust rules its mint endpoint applies: rail enabled, peer trusted (loopback or a listed CIDR), identity header naming an allowlisted login, and that entry carrying `#passport_id`. The passport is then bound to the context and the rail recorded on the receipt envelope as `approver_source`. (#738) - **Signed exports could be re-signed by anyone.** Every verifier checked the artifact against the public key the artifact itself carried — a self-attestation, not a custody proof: edit an export, re-sign it with a key generated a second ago, and both `verify_bundle_v1` and `run_context_verify` returned a green `ok`. The write side had the mirror hole: `resolve_audit_export_signing_key` minted a throwaway Ed25519 key whenever it had neither a configured signing key nor a data dir, so the daemon could hand out an artifact no verifier could ever pin. The write side now **refuses**: a one-shot signer cannot be pinned and cannot tie two exports to one issuer, so it is a configuration error rather than a degraded mode. `build_bundle_v1` independently rejects `key_class = ephemeral` so a caller cannot hand-assemble one either. The enum variant survives, so bundles already in the wild still parse and verify. (#735) ## [0.5.63] - 2026-08-21 ### Fixed - **The ephemeral-fact GC no longer clones the whole fact store every hour.** `run_sweep_once` copied every fact into a `Vec` once an hour to pick sweep candidates; on a daemon whose fact journal had grown to 2.4 GB that was a ~1.5 GB allocation burst on top of a ~2.7 GB baseline, which tripped a 4 GiB container limit and OOM-killed the production daemon every ~60 minutes for more than twelve hours (restart policy hid it). The candidate scan now streams `all_facts()` under the read guard and keeps only candidate ids; selection semantics are unchanged. (#739) - **Receipt verification is bound to its tenant and chain position** (D3) so a receipt cannot verify under another tenant's chain. (#736) - **`/v1/coord/active` is scoped to the caller's tenant** (D4 residual). (#731) - **HTTP fact writes are attributed to the verified passport** rather than the asserted one. (#719) - Span capture no longer races the interest cache (#733); ExecPlan age is taken from git commit time, not checkout mtime (#717); the execplan-discipline gate names a runnable command (#718). ### Added - `text-search` expand hits hydrate to `stream_id` + content (#732). - Code-intel liveness carries an outcome dimension (#730); code-health projects debt findings into `debt:/` entities (#729); every pack mutation is attributed to the pack build behind it (#728). - The per-subject CEK registry lands on main (erasure, Audit II Tier 2 M5) (#725); the pre-deploy checklist moves behind a config-wizard skill (#727); observation instrumentation at the three sites where "empty" is a bug (#724), with curated `crux.outcome` guidance (#734). - Test: observation subject isolation is pinned (#737). ### Changed - Dependency refresh: tree-sitter 0.26.12, rcgen 0.14.9, thiserror 2.0.20, http-body-util 0.1.5, wat 1.256.0, plus GitHub Actions bumps. ## [0.5.62] - 2026-08-19 ### Fixed - **The macOS release build compiles again.** Provenance retention is deliberately disabled off Linux — it needs descriptor-anchored filesystem operations — and the two `cfg(not(target_os = "linux"))` stubs return early to do that. The early return left the rest of each function unreachable, which `-D unreachable-code` rejected, so the aarch64-apple-darwin job failed and v0.5.61 shipped without macOS binaries or provenance attestation. Nothing functional changed on any platform; the lint allowances are scoped to non-Linux, so the shipping Linux build keeps full strictness. macOS is only compiled on `v*` tags, which is why this reached a release before anyone saw it. (#722) ## [0.5.61] - 2026-08-19 ### Security - **Lending an approver identity is now refused wherever the device's own human can be named.** `bind_passport` on a device grant binds the approving admin's passport — which is correct for your own second machine, and impersonation if an admin ticks it while approving a colleague's. No mechanism can tell those apart, so rather than policing the intent the need is removed: where the identity rail can name the device's human, lending is refused and that human must arrive as themselves. A stronger rung that is configured but *unusable* refuses too, matching the ladder's no-silent-downgrade rule — a misconfigured identity rail must surface, not become a licence to lend. Where no stronger rung exists, lending remains the only way to name anyone and stays available. (#705, accepted risk R3) - **`/activate` no longer exists on daemons that cannot use it.** The device-approval page was served unconditionally, so a daemon with `CORECRUXD_DEVICE_GRANT_ENABLED` off still returned a fully-branded "Approve device login" form that could never approve anything. That is pure phishing surface with zero function: an attacker who obtains a `user_code` gets a genuine, correctly-branded, TLS-served page on the real domain to send a console admin to. Phishing is the first threat this rail's own module docs name, and their stated mitigation — the page shows the requesting client name — is weakest precisely when the page is legitimate, because it is. It now 404s with the flag named, matching the rest of the rail's disabled-⇒-404 idiom. This does not make the page non-public where the grant *is* enabled; that is a front-proxy allowlist decision, since approval itself is already `admin:write`-gated server-side. (#705) ### Added - **The board can now digest itself.** The daemon already derived the expensive parts of ExecPlan-board analysis — dependency depth, cycles, and the orchestrator edges that point the wrong way — but nothing ever read them on a cadence, so with 200+ open plans the analysis only existed when a human thought to ask. A new background pass (`CORECRUXD_CRUCIBLE=1`, default off, daily) runs that analysis and appends one `__board_digest__::` fact. It surfaces only: it never edits a plan, changes a work state, or resolves a finding. It is also fully deterministic and offline — no model call, no egress, no credentials — so the same binary runs the same pass on a laptop and on a server. When `CRUX_EXECPLANS_ROOT` is unset it declines to write rather than report an empty board, because a digest asserting nothing is open is indistinguishable from a digest of a board the daemon cannot see. (#715) - **Daemon-side enrichment can now use Claude, OpenAI, or a local model.** A completion provider with four modes (`none`, `anthropic`, `openai`, `local`). `none` is the default and builds no request at all, so the local-first path never depends on a model being configured, reachable, or paid for; `local` targets an OpenAI-compatible server on the machine and needs no credential. Anthropic and OpenAI get separate adapters rather than one shim, because they are separate protocols — different path, different auth header, a mandatory `max_tokens`, a top-level system field, and reply text in a content array rather than a choices array. Routing one through the other silently returns empty text instead of failing. `local` reuses the OpenAI adapter because Ollama, llama.cpp and vLLM all serve that shape natively. (#715) - **The number of distinct passports a work gate requires is now the operator's to set.** `CORECRUXD_WORK_GATE_APPROVERS` takes `1` or `2`, default `2` — the shipped behaviour, where a requester may not resolve their own gate. A hard ban does not produce separation of duties on a deployment with one human in it. It produces a second passport minted solely to approve one's own work — which passes the check while recording `distinct_passports` for what is really one person. That is a *worse* audit trail than admitting single-party approval, so the count is configurable and the receipt records the truth either way. The separation facts ride the **signed** `action_summary`, not the envelope (envelope fields are documented as non-authoritative, and "was this really two-party?" is exactly the question that needs tamper-evident material): ``` work::::approvers=:parties=<1|2>:sep= ``` `sep` is `distinct_passports` or `none:self_approved`. **`distinct_passports` means exactly that** — the daemon verifies two passports differ and cannot establish they belong to two people; the receipt never claims otherwise, and nothing in it says "four eyes". The `work_gate_resolution` capability also reports the policy, so a single-party deployment is visibly single-party. Anything unset, malformed, or out of range resolves to `2`: a typo must never silently relax a human-oversight boundary. (#705) ### Security - **A device-grant approver could grant scopes it did not itself hold.** `POST /v1/auth/device/approve` required `admin:write` and then issued whatever scopes the request named. The module contract already promised the issued scopes were "a concrete subset of the authenticated approver's verified grants", but nothing enforced it — and scope matching is exact, with no hierarchy, so `admin:write` does **not** imply `facts:write`. An approver holding only `admin:write` could therefore mint a device credential carrying authority it never had. The passport binding added in this release widened the impact: such a credential also carries a canonical passport, so it could resolve work gates — the Art.14 human-approval boundary itself. Approve now refuses any scope the approver does not hold, naming the offending scopes. Found by the adversarial pass; see `docs/design/gate-oversight-adversarial-2026-08-18.md`, which also records four accepted risks that are **not** refused. (#705) ### Added - **The daemon now declares whether a human can resolve a work gate on it, and by which evidence rung.** `work_gate_resolution` joins the runtime-capability descriptor on `/v1/version`, selected by a ladder over what the deployment is actually configured for — proxy-asserted identity header, device-grant vouching, or an unverified local daemon — strongest configured rung first. **A configured rung that cannot issue blocks rather than downgrading.** An identity rail with no passport-bound allowlist entry does not quietly fall through to the device grant: it reports `degraded` and names the fix. Without that rule a misconfiguration silently converts strong oversight into weak oversight while the receipt looks identical. The Gates page derives its Approve/Reject affordance from this declaration instead of hardcoding a flow, renders the daemon's `reason` verbatim when resolution is unavailable, and fails closed when the descriptor is absent — so a control that renders as actionable where the daemon would refuse is now structurally impossible rather than merely absent today. The console acquires no bearer token to do it: the client's standing invariant (same-origin credentials only) is preserved. (#705) - **The login rails can now name the human, so a paired device can resolve a work gate.** `resolve_gate_http` requires a canonical `passport_id` claim, but every issuance rail minted `passport_id: None` — so no credential obtained through `crux login` could satisfy the Art.14 human-approval boundary, and the console's Approve button was visible but inert. The rails shipped 2026-06-16; the canonical-passport requirement arrived six weeks later, and the two were never joined. - **Identity rail** — `CORECRUXD_TS_IDENTITY_ALLOWLIST` entries take an optional `#` suffix (`alice@example.com=acme:facts:write#p_alice`). `#` rather than a third colon-delimited field because scopes already contain colons. The passport is operator-controlled authority, exactly like the tenant and scopes; it is never taken from the request. - **Device grant** — `POST /v1/auth/device/approve` accepts `bind_passport`, binding **the approving admin's own** verified passport to the issued token and carrying it across refresh. Deliberately a flag, not a passport string: letting an approver name an arbitrary passport would be impersonation-by-admin and would hollow out non-self-review. `/activate` exposes it with that warning in the label. An entry or approval without a passport still issues an unbound token, and an unbound token still cannot resolve a gate — asserted in both directions, so binding on the rails cannot become a way for an unbound token to slip through. Both surfaces now report `passport_id` and a plain-language `gate_resolution` line, so an operator learns the token cannot approve at issuance rather than at the click. - **The identity rail works behind any authenticating proxy, not just Tailscale.** It was already proxy-agnostic apart from one hardcoded header name. `CORECRUXD_IDENTITY_HEADER` now names the header, defaulting to the `tailscale serve` preset so existing deployments are byte-identical; presets for oauth2-proxy, Cloudflare Access, Authelia and Authentik are documented. Every trust check is unchanged — loopback-or-listed-CIDR, fail-closed on absent peer info. **The proxy must strip-then-set the header:** a trusted CIDR proves the proxy sent the request, not that it authored the header. Where that cannot be guaranteed, use the device grant. (#705) ### Fixed - **A local-only daemon's Gates page was still empty for any tenant but `default`.** The previous entry widened the pending-gate read for *verified* credentials and deliberately kept auth-off confined to `default`, preserving the audit property that an unauthenticated reader must name a tenant rather than enumerate the queue. That keyed the decision on **auth mode**, when the property it actually protects is **reachability** — so the one posture with no online auth by design, a loopback daemon with a single operator at the keyboard, was left with exactly the symptom the original report described: an empty queue, and (because the console builds its tenant picker from this same response) no tenant to switch to. The unverified case now widens to every tenant only when the daemon's own listener is loopback-only **and** the request arrived on a direct loopback socket carrying no `Forwarded` / `X-Forwarded-For` assertion. Both halves are required: a reverse proxy on the same host makes every remote client's peer address loopback, so peer-is-loopback alone would hand a remote caller local authority. Anything reachable — proxied, or a non-loopback listener — keeps the narrow `default` view, and naming a tenant works exactly as before. `direct_local` only ever widens the unverified case: it cannot grant a tenant a verified token lacks, and it cannot rescue a missing tenant claim. (#706) - **The console Gates page rendered "queue is clear" while approvals were waiting in another tenant.** `GET /v1/work/gate/pending` resolved the caller to exactly one tenant via `resolve_authorized_tenant`, which collapses a wildcard/admin credential to `default`. Pending gates held in any other tenant — the reported case had two under `work` — were filtered out, and `/console#/trust/cx-gates`, which asked with no tenant context at all, showed the rich empty state. That hides the passport-attributed Art.14 human-oversight path exactly when it is owed. The read now answers for **every tenant the credential is authorized for** and declares that scope on the wire as `tenant_scope` (`["*"]` = all). A verified wildcard token spans every tenant; a multi-tenant token lists the tenants it owns (where the single-tenant write resolver refuses to guess); a single-tenant token is unchanged; a token with no tenant claim is still refused. Auth-off/DevScopes stay confined to `default` — those wildcards are a local-development convenience, not a proven principal, so an unauthenticated reader still has to name a tenant rather than enumerate the queue. Naming `?tenant_id=` keeps the previous, narrower behaviour and every existing denial. The Gates page carries the scope verbatim, offers an authorized-tenant selector, names each row's tenant, refuses to reuse the "queue is clear" state when the view is narrowed, and renders a failed or unauthorized read as an explicit failure instead of an empty queue. The same fix reaches the Overwatch "needs you" lane and "Withhold all", which read the same endpoint. Approve/reject are untouched: still operator-gated, canonical-passport bound, and still mint a CROWN `ad_ga_*` receipt. (#703) ## [0.5.59] - 2026-08-08 ### Fixed - **Erasing a tenant's corpus could take every subsequent write down with it.** `POST /v1/admin/forget-tenants` with `"reclaim": true` deleted each whole-tenant segment's file group but never updated the shard MANIFEST. `ShardStorage::open` opens every segment the manifest references, and local ingest opens the shard once per request, so from the first reclaim onward *every* `POST /v1/local/ingest` returned 500 — while reads, which scan the `segments/` directory rather than the manifest, carried on normally. On the affected host that combination went unnoticed for 38 hours: 17 dangling entries, all ingest dead, `/readyz` green throughout. - Reclaim now appends a `RemoveSegment` tombstone to the manifest and fsyncs it **before** unlinking anything. A crash in that window leaves files on disk under a manifest that has already forgotten them — inert, and reclaimable again — instead of files deleted with the manifest still pointing at them. - If the manifest cannot be updated, nothing is unlinked and the response says so (`reclaimed: false`, plus a `manifest_error`) rather than reporting a reclaim that did not happen. - `RemoveSegment` is a new manifest record type. Removal is expressed as an **append**, so no existing record or checksum is rewritten. Older daemons skip the unknown record and simply see the pre-repair state, which makes a downgrade safe but means an affected shard is only repaired once running 0.5.59 or later. - **`corecruxctl storage repair-manifest`** reports MANIFEST entries whose segment file is missing, and with `--apply` retires them. It works on the manifest alone rather than through `ShardStorage`, because opening the shard is precisely what a shard in this state can no longer do. - **Live `.ccxp` embedding-profile sidecars were being quarantined on every shard open.** The companion allowlist named `.ccxi` and `.ccxv` but not `.ccxp`, so each open swept every profile sidecar out of `segments/`. This failed silently in the worst direction: a segment with no `.ccxp` reads as legacy and is scored *without* the embedding-fingerprint check the sidecar exists to enforce. Recovering the files is a matter of moving them back out of `quarantine/`. ### Added - **The write path now has a health signal.** Local-ingest seal failures increment `corecrux_local_ingest_seal_failed_total`, and after 3 consecutive failures `/readyz` reports `local_ingest_seal` unhealthy with the underlying error. Consecutive rather than cumulative, so a single malformed document cannot unready a node. Tune or disable with `CORECRUXD_SEAL_FAILED_READYZ_THRESHOLD` (`0` disables); it defaults to on, because a write path that fails silently is what made the outage above last as long as it did. ## [0.5.58] - 2026-08-07 ### Fixed - **Ingest can no longer seal a corpus with no vectors and call it healthy.** `POST /v1/local/ingest` returned an ordinary `202` with `sealed: true`, a full `frame_count` and `dense_vectors: 0` whenever the embed step failed. BM25 still indexes, so the tenant looked fine while semantic retrieval was simply gone — a corpus that reads as healthy and answers worse, with nothing in the response to check. The cause was **ureq's default 10 MiB response-body cap**, hit while *reading* the embedder's reply: a 1024-dim vector is ~12.6 KB of JSON, so a batch past roughly 830 chunks overflowed it. The ceiling therefore moved with the embedding dimension, not the chunk count, which is why 749 chunks passed and 776 failed. - `EmbeddingClient::embed_batch` now issues sub-batches of at most 128 texts and concatenates in order, so how many chunks a caller sends no longer decides whether embedding succeeds. A failing sub-batch fails the whole call — never a short or partially embedded result. - The `202` carries `dense_status` (`ok` | `partial` | `skipped` | `not_configured` | `not_applicable`) and `dense_expected` beside the existing `dense_vectors`, and the daemon logs `local-ingest-dense-gap-sealed` at WARN with the segment sequence. **Assert `dense_status == "ok"`** after any ingest that expects semantic recall. `crux-ingest` asserts it for you and warns when a batch seals lexical-only. - Both fields are additive: a client that ignores them sees the response shape it saw before. - This matters beyond the response cap — *any* embedder error took the same silent path. An audit of one host found 41 of 641 segments sealed lexical-only, most of them from ordinary `502`s. - **`/v1/local/ingest` honours the request-body limit its own error advertises.** The route inherited axum's 2 MiB `DefaultBodyLimit` while its `413` named the 16 MiB daemon limit, so every client that sized batches from that number sized them eight times too large. ### Added - **`segment_seq` on query results.** Each `/v1/query/text-search` result and `/expand` chunk now carries the sealed segment's own sequence — the value the ingest receipt returned — so a consumer can join a result back to the ingest that produced it. `segment_index`, which sits beside it, is a position in the daemon-wide loaded-reader list: it is **not** convertible to `segment_seq`, and the difference between them was measured at 1, then 18, then 17 on one host within hours as unrelated segments came and went. Deriving one from the other unmaps every hit at once and scores a plausible, uniform 0% instead of erroring. `segment_index` is unchanged for existing consumers. ## [0.5.57] - 2026-08-07 ### Fixed - **A CPU-only daemon can verify the governance receipts it mints.** Erasure, `compact_facts` erasure, `memory_forget` and held hard-erasure overrides all persist their receipt as the observation envelope itself — Ed25519, passport signed, on disk. Until now none of the three documented surfaces could attest to one: `GET /v1/receipts/{id}` is 501 without a dataplane (by design), the verification endpoint's local fallback only understood *stream* receipts, and `corecruxctl inspect-receipt` searches sealed segments, where a governance receipt never appears. The daemon produced an audit artefact nothing could check. - `GET /v1/receipts/{id}/verification` gains a second local fallback for the observation-envelope shape, reporting `crux.governance_receipt_verification.v1`. A caller lacking scope for the receipt's *own* tenant gets **404**, byte for byte identical to a missing receipt, so the endpoint cannot be used as a cross-tenant existence oracle. - `corecruxctl inspect-receipt` resolves and verifies them too, printing kind, signer, body hash and signature status. - Both scans are bounded to `__governance__*` logs. This is a correctness constraint, not a tuning choice: a production node carries tens of thousands of per-session observation logs, and walking them all on an audit lookup would make verification a denial-of-service surface. - **`serde_json/preserve_order` is now declared where the wire format is defined.** `canonical_body_bytes` signs `serde_json::to_value(record)`, so map ordering *is* the signed format. `corecruxd` only had the feature because `crux-session` happens to enable it and Cargo unifies features across the graph — an accidental dependency that left the audit trail one dependency change away from every existing receipt failing to verify. The feature is now declared by `corecrux-receipts` and pinned by a golden-vector test. ### Changed - Observation record types, `canonical_body_bytes` and `verify_observation_envelope` move from `corecruxd` into `corecrux-receipts`. `corecruxd` is a bin-only crate, so `corecruxctl` could not otherwise reach the verifier; sharing beats a second copy of a signature check that can drift. ### Added - `corecruxctl start --agent `. - LlamaIndex and CrewAI adapters under the unchanged conformance suite. - Console Patchbay no longer freezes the tab when a system has more than 28 plans; drift reporting distinguishes a rejected credential from an unreachable daemon. ## [0.5.56] - 2026-08-07 ### Added - **A tenant's retrieval corpus can finally be erased.** Until now a corpus could be created but never retired, so a mis-ingested or superseded tenant was permanent — and there was no GDPR Art.17 story for the daemon. Two layers, deliberately separated by reversibility: - *Layer 1, logical erasure.* A persisted set of forgotten `(tenant_hash, watermark_segment_seq)` pairs, enforced in the BM25 scorers as a **required** parameter rather than an optional wrapper — so the compiler forces every present and future serving path to state its intent instead of one being silently missed. Reversible via `DELETE /v1/admin/forget-tenants/{tenantId}`. - *Layer 2, physical reclaim.* Opt-in per request (`"reclaim": true`), never automatic. Whole-tenant segments are evicted from the `IndexManager` and only then unlinked; mixed-tenant segments are retained, still masked, and reported as `mixed_segments_retained`. Irreversible — recovery is restore-from-backup only. - `POST /v1/admin/forget-tenants` (+ singular alias) requires `admin:write` **per named tenant**, with the whole batch rejected if any tenant fails and nothing masked. Reserved `__`-prefixed namespaces are refused by convention, so a future reserved namespace is protected the day it is introduced. - `GET /v1/admin/tenants/{tenantId}/footprint` reports segments, docs and bytes, so the blast radius is inspectable *before* anything is erased. - The whole surface is gated on `CORECRUXD_TENANT_ERASURE=1` and the routes 404 without it. Fact-store erasure remains out of scope: the response key is `corpus_erased`, never `tenant_forgotten`. - Key escrow M0–M3b, device-identity and CRL transport auth for the relay, and a span outcome dimension. - Console Patchbay — a spatial projection of the open work board (`GET /v1/work/graph`). ### Changed - Tenant scoping tightened across the stack: derived from the token, applied by type, exact-matched, and pushed down into gRPC and the lower layers. - Login verification is strict, and M1 auth fails closed. ## [0.5.55] - 2026-08-02 ### Added - **Account entitlement is now a verified property of a signed token, not an environment variable.** Three milestones of `crux-pro-capabilities-rcx-entitled-2026-07-27` land together, all **dark** — nothing enforces on them yet, `OperatingMode` still comes from `CORECRUXD_OPERATING_MODE`, and the cutover is a later milestone. - `corecruxd::entitlement` — an on-disk entitlement store on the reserved `__entitlement__::rcx` entity (born private, no freshness horizon), plus `resolve_entitlement`: revocation → presence → parse → tenant scope → signature → expiry → tier. Forgery and revocation always resolve `FreeLocal`; an expired token follows its own `FallbackPolicy`, so a paying user who is merely offline keeps working. - `corecruxd::pairing` — the daemon half of account pairing, reusing the RFC 8628 device grant already shipped at `/v1/auth/device/*`. No licence key and no typed secret: the `user_code` is a short-lived, single-use public correlator, useless without an authenticated browser session, and never written to configuration. - `OperatingMode::GovernanceHosted`. `MaxPrivate` remains a composite — Governance entitlement plus a private deployment shape — so no issuer can hand it out. ### Changed - **`RcxTier` cut over to the published ladder: `Free | Pro | Governance`.** The previous `Free | Pro | Team | Enterprise` matched no product CueCrux sells. `Team` is retired outright and `Enterprise` becomes `Governance`; `crux-enterprise-shim` re-gates accordingly (`not_enterprise_tier` → `not_governance_tier`). Retired values now fail *deserialisation*, which is what makes a stale token fail closed to `FreeLocal` rather than resolve to some default. `team_scope` and `enterprise_scope` stay on the token: removing either would change the signed bytes for every tier, not just the retired one. - **New spec version `rcx-ct/1.2`, accepted alongside `rcx-ct/1.1` rather than replacing it.** The spec-version check is not a version ladder but a match with a rejecting catch-all, so repointing the 1.1 constant would have failed every delegation token already minted. `1.2` is a *tier-vocabulary* version and is orthogonal to delegation: 1.0 forbids a delegation policy, 1.1 requires one, 1.2 makes it optional — a plain entitlement token carries none. A cross-language byte-parity vector pins the Rust and TypeScript encoders together on a `1.2` token with `tier: governance`. - **`corecruxd` split into sibling crates** — `corecrux-workspace-scan`, `corecrux-billing`, `corecrux-providers` and `corecrux-secrets` — with the unwrap ratchet re-baselined across the new boundary and `AGENTS.md` added for the extracted crates. - Desktop shell gains isolated Registry and WikiCrux tabs. - Workspace test coverage raised from 88.49% to 90.11% across the nine highest-debt files. ### Fixed - **Seat identity is taken from the credential, not the request body**, closing a bypass of the M8 per-seat rate ceiling on enriched verdicts. - **`dossier` and `storybook` now authorise against the tenant they answer for** (code-intel M3b). - `ledger:history` is no longer advertised as a Pro claim. The route is fully implemented but nothing produces a record, and selling a capability with no producer is a truth-in-selling problem; the route and its handlers stay. - `run_git` gained a real wall-clock deadline. `GIT_TIMEOUT_SECS` fed only `GIT_HTTP_LOW_SPEED_TIME`, an HTTP-transport setting that neither a local `git blame` nor an SSH remote ever consults, so the call could block unbounded on a stale `index.lock`. - `corecruxctl --version` now exists. It had no version surface at all, unlike `crux-hook` and `corecruxd`. - The console `.mcpb` is rebuilt so it ships Apache-2.0 rather than the retired CCL-1.0. - The config wizard parses profile frontmatter on a CRLF checkout. - The release path and a hang guard that was failing tests are unbroken. ### Security - **wasmtime 47.0.2 → 47.0.3**, clearing [RUSTSEC-2026-0222] (stores can mix up type indices between engines) and [RUSTSEC-2026-0223] (preemption and traps during bulk operations break internal VM state). Lockfile only; `wasmtime` is optional behind the default-off `wasm-extensions` feature, so default builds never compiled it. [RUSTSEC-2026-0222]: https://rustsec.org/advisories/RUSTSEC-2026-0222 [RUSTSEC-2026-0223]: https://rustsec.org/advisories/RUSTSEC-2026-0223 ## [0.5.54] - 2026-07-30 ### Changed - **Relicensed to Apache-2.0 — Crux Daemon is now open source.** The CueCrux Community Licence (CCL v1.0), a source-available BSL-style licence, is replaced by the **Apache License, Version 2.0** across the repository. The CCL already named Apache 2.0 as its Change Licence, so this brings that conversion forward for every version instead of waiting out the per-release three-year clock. Redistribution in competing products and offering Crux as a hosted service to third parties — the two rights the CCL withheld — are now granted, along with Apache-2.0's express patent grant (section 3). - `LICENCE.md` → `LICENSE`, containing the unmodified upstream Apache-2.0 text, so GitHub and SBOM scanners detect `Apache-2.0` instead of reporting an unrecognised custom licence. - New `NOTICE` file carrying the attribution required by section 4(d), plus the trademark and `content/` scope notes that must not be edited into the verbatim licence text. - Per-file headers on all 539 crate `.rs` files (and every script, workflow, proto, SDK source, and console asset) now read `SPDX-License-Identifier: Apache-2.0`. The contradictory "All rights reserved." line is dropped. - `license = "Apache-2.0"` in `[workspace.package]` and in the desktop shell, Python/TypeScript SDK, deb, Homebrew, and MCPB manifests; `LicenseRef-CCL-1.0` removed from the `cargo-deny` allowlist. - `scripts/check-licence-headers.sh` now enforces the Apache-2.0 header and SPDX line. Contribution terms in `CONTRIBUTING.md` are inbound=outbound under section 5 — no CLA. `docs/LICENCE-FAQ.md` rewritten; `docs/design/licence-recommendation.md` (the BUSL 1.1 proposal) marked superseded. - Curated content under `content/` keeps its separate licence (`content/LICENCE-CONTENT.md`) and is unaffected; that directory currently ships a placeholder manifest with no covered assets. - **Licence file layout deduplicated to a single GitHub licence tab.** `LICENCE-CODE.md` (a three-line stub pointing at `LICENCE.md`) is removed, and the content licence moved from the root to `content/LICENCE-CONTENT.md` — the directory it governs. GitHub's licence detector scans only the repository root, so it now surfaces one top-level licence (`LICENCE.md`, the code licence) instead of two tabs. `LICENCE.md` links to the content licence, and release bundles ship it under `content/` alongside the assets it covers. ### Added - **The token-burn cost lens gains a model/effort axis.** `crux-cost` already opened every transcript line by line and discarded what it found: `gitBranch` was parsed but used only as an internal ranking tie-breaker, and `model`, `effort` and `cwd` were not parsed at all. All four are now parsed onto the event, promoted onto `CostReport`, and rolled up into a per-model burn breakdown with per-effort burn *within* each model, rendered by both `corecruxctl session cost` and the console `cx-cost` page. - **It reconciles.** Per-model context, plus the `` pseudo-model, plus records carrying usage but no model id, sum exactly to `headline.measured_context_total`. - **Coverage travels with every effort number.** `effort` is absent from 61% of the measured corpus and its absence correlates with model — 100% / 100% / 22.5% / 9.4% across 46,239 assistant records — so a cross-model effort comparison is confounded at source and no sample size fixes it. `effort_coverage_pct` lives on the type, so no surface can render an effort figure without the denominator that qualifies it. - `` is reported separately, never ranked as a model and never dropped. Model-id normalisation folds only stable presentation variants (`[1m]` context suffix, `us.anthropic.`/`bedrock/`/`vertex/` route prefixes, `-v1:0`); floating aliases such as `opus` and `sonnet` are deliberately left unresolved, since resolving them would silently merge two models the day the alias moves. - The five new `CostReport` fields are additive, serde-default and omitted when absent, so an older daemon ignores a newer report and a legacy report is unchanged on the wire. A daemon must be upgraded before the console can render the axis. - Adds the cost lane's first integration coverage: a post-then-get test through both `/v1/cost/report` handlers. - **`CITATION.cff`.** Machine-readable citation metadata (GitHub "Cite this repository" button). Under Apache-2.0 citation is appreciated but not a licence condition. - **A Windows GUI smoke lane for the desktop shell.** `desktop-shell.yml` gains `desktop GUI smoke (windows, interactive session)`, running on a self-hosted runner that executes inside a real logged-on desktop (`[self-hosted, windows-gui]`). It bundles the MSI, installs it per-machine, launches the installed app, and asserts what only a desktop can show: a window appears, an `msedgewebview2` host actually starts, the bundled `corecruxd` sidecar is spawned from the `externalBin` slot, and a graceful close reaps that sidecar. A desktop screenshot uploads on every run, including failures. Non-blocking — it depends on a single operator-owned box, so an offline runner must not gate the merge queue. This closes the gap the native-Windows build fix left open: that change noted there was "no Windows job to catch the next one" of ~49 `#[cfg(unix)]` sites. Compiling was never the hard part — a Tauri/WebView2 window cannot be created in a headless or Session 0 context at all, so app launch was unobservable on every existing runner. `scripts/provision-windows-gui-runner.ps1` provisions such a box and refuses to run where a GUI cannot exist; `docs/self-hosted-runner.md` records why Server Core and Session 0 are each disqualifying, and what the lane deliberately does not cover (reboot re-attach, the Defender first-bind prompt, WSL2 parity with a developer box). ### Fixed - **The SessionEnd cost hook now records whether it worked.** The generated `cost)` launcher branch failed silently on three paths — `corecruxctl` absent, no configured endpoint, post rejected — each swallowed by `|| true`. Quiet is the correct posture for a SessionEnd hook, which must never block session end; undiagnosable is not. There was no way to distinguish "no sessions ran" from "every session silently failed to post", and the branch had no test coverage of any kind, which is why it survived. - Every attempt writes a one-line outcome record (result, reason, endpoint, endpoint source, launcher version) to `~/.claude/hooks/crux-cost.state.json`; failures additionally append to `crux-cost.errors.log`. `exit 0` remains unconditional on every path. - `CRUX_HTTP_URL` can never actually be empty — `${CRUX_HTTP_URL:-…}` substitutes for empty as well as unset — so the real misconfiguration is the *unconfigured loopback default*, which fails with a connection refused the operator cannot place. The launcher now records whether the endpoint came from config or from the built-in default, and names the remedy. - `corecruxctl hooks status` reports the last outcome and flags a stale launcher by version marker. The boot self-check warns when the installed launcher predates the running build, or when capture last failed or was never attempted — nothing warns when no outcome has been recorded yet, since a fresh install has simply not ended a session. - Six shell-level tests run the *embedded* launcher template under `bash`, so they cannot drift from what `hooks install` writes. **This takes effect only after `corecruxctl hooks install` is re-run.** An un-re-run wizard keeps the old silent script, and that is the most likely way this fix appears not to work. - **Three tests that asserted things the machine does not guarantee.** Each failed CI on an unrelated PR and passed on a re-run of the identical commit; two of them gate required checks, so they cost merges rather than just noise. - `envelope_build_latency_under_2ms_for_10_facts` (`crux-mcp`) asserted a wall-clock bound — the only one in the workspace. On a shared runner that measures how busy the machine is, not how fast the code is. The figure is still measured and printed; the assertion is gone, and the test is renamed `envelope_build_covers_all_ten_facts` after the check that carries content (`memories_used.len() == 10`). Raising the constant was rejected: it trades a frequent flake for a rarer one and keeps a load-dependent test. - `allowed_origins_reads_the_env_var` (`corecruxd`) round-tripped through `set_var`/`getenv` and read back the built-in defaults instead of the value it had just set — *while holding the module's `env_lock()`*. The mutex was never the problem: `setenv` can reallocate the environment block, and a concurrent `getenv` anywhere in this 2000-plus-test binary may then read a stale pointer, which no Rust-level lock can fence (hence `set_var` being `unsafe` from the 2024 edition). It now parses via `resolve_allowed_origins` directly, matching its five neighbours, and removes the last `set_var` in the file. - `pick_free_port_is_bindable` (`crux-shell-lifecycle`) asserted that a port picked by binding `:0` and dropping the listener is immediately re-bindable. Nothing promises that — sibling tests binding `:0` in parallel can take it in the gap. It now retries a bounded number of times, so only a genuinely broken helper fails. - **The desktop shell no longer opens stray console windows on Windows.** Two spawn sites, both console-subsystem binaries launched from a GUI-subsystem app (`windows_subsystem = "windows"`) that has no console to inherit — so Windows allocated a fresh console *window* for each: - `spawn_sidecar` (`crux-shell-lifecycle`) launched the bundled `corecruxd` with a visible console that sat beside the app for its entire lifetime. Redirecting stdout/stderr into the sidecar log did not suppress it: the streams and the window are independent. - the Windows credential lookup (`crux-shell-connection`) shells out to `powershell.exe` for the `PasswordVault` read, flashing a console on *every* attach-profile activation and retry. `-NonInteractive` governs the prompt, not the window. Both now set `CREATE_NO_WINDOW` (`0x08000000`) via `CommandExt::creation_flags`. `rundll32.exe`, used to open external links, is GUI-subsystem (verified from its PE header) and needed no change. Found by the new Windows GUI smoke lane on its first green run — the assertions passed while the uploaded screenshot showed the console box. That lane now fails if either window returns. - **A cold `cargo build` now completes on native Windows.** Three unrelated stops, none of which CI can see (every runner is Linux, and the release matrix is Linux + macOS — both unix): - `aws-lc-sys` assembles its x86_64 Windows routines with NASM, absent from a default Windows toolchain, so the build script aborted. The crate ships pre-assembled objects for this case; `AWS_LC_SYS_PREBUILT_NASM = "1"` now lives in `.cargo/config.toml`. Inert on Linux and macOS, where the prebuilt path is gated off by target. - `fsync_dir` (`corecrux-projections`) and `write_control_atomic` (`corecruxd`) bound a variable consumed only inside `#[cfg(unix)]`. Against the workspace-wide `unused_variables = "deny"`, that is a hard error off unix. Both bind the value under `#[cfg(not(unix))]` rather than renaming to `_path`/`_parent`, which would have suppressed the lint on unix where the variable is load-bearing. - **`cargo clippy --workspace -- -D warnings` now passes on native Windows.** Seven `clippy::unnecessary_wraps` errors across `corecrux-receipts`, `corecrux-storage`, and `crux-claude-hooks`: each is a directory-fsync or file-permission routine whose body is unix-only, so off-unix it collapses to `Ok(())` and the `Result` looks redundant. The signature is shared with a genuinely fallible unix implementation, so these are suppressed at the site with a note, not "fixed" by dropping the return type. Native Windows remains post-v1 per `docs/getting-started.md`; WSL2 is still the supported path. This only stops the gap widening — there are ~49 `#[cfg(unix)]` sites and no Windows job to catch the next one. - **`config.example.env` no longer claims a `.env` file is read.** The daemon has no dotenv support, so copying the file and starting `corecruxd` failed with "CORECRUXD_AUTH_MODE must be set explicitly" — indistinguishable from a genuine config error. The header now shows how to export the values. ### Security - **JWT HTTP fact storage is tenant-isolated by default.** The daemon freezes `CORECRUXD_TENANT_WRITE_STAMP` at startup, defaults it to `on` for JWT auth, rejects invalid configuration and ambiguous/missing tenant authority, and keeps `off`/`shadow` only as explicit shared-`default` migration modes. Generic and console facts, context recall, engram overlays, memory candidates, result envelopes, replay, and paired audit reads now preserve one authorized tenant end to end. MCP and stores with independent tenant contracts are explicitly outside this flag. - **`.mcp.json` is gitignored.** MCP clients write the daemon's agent bearer token into that file at the repository root, where it was previously committable. ## [0.5.38] - 2026-07-10 ### Added - **AST-derived code-structure scanner.** Behind `CORECRUXD_AST_SCAN`, the workspace scanner produces the `WorkspaceScan` shape from a `syn` AST pass instead of the regex scanner (flag-off byte-identical). ~17× faster on the Crux tree (p95 ~0.9 s vs ~15 s) and more accurate: call-edges resolved module-qualified, dead-code by AST identifier-reachability rather than the O(n²) substring pass. Context-graph edges fold in as `Extracted` confidence. - **Watched repositories.** `POST/GET/DELETE /v1/repos` register a repo the daemon should know about (tenant-scoped; `corecruxctl repo add|list|remove`; MCP `register_repo` / `list_repos`). Registering a local path runs a one-shot scan. An active file-watch loop (`CORECRUXD_REPO_WATCH`, default off) keeps a repo's graph current via incremental re-index — a single-file edit re-parses only that file — using `notify` with a WSL `/mnt` polling fallback. - **Polyglot extraction.** TypeScript/TSX, Vue (`