llms-full.txt
Rama main · réplica de lectura del repositorio original.
# 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/<name>/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 <target>`.
# ============================================================
# 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<String>` 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<lane>` 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 `<stem>.ccxe@<key>`, 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::<owner>::` 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 <BASE64_ED25519_PUBLIC_KEY>
```
`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 <receipt id> --kind model_invocation
```
`keyring.json` is `{"v":1,"keys":[{"keyId":"<passport fingerprint>","pubKeyBase64":"<ed25519 public key>"}]}`.
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 <body.cbor>` | Recomputes RFC6962 inclusion path against the stored leaf hash, log index, tree size, and root hash. |
| `corecruxctl receipts verify-rfc3161-timestamp --body <body.cbor> [--expected-imprint-hash <sha256>]` | Recomputes the stored timestamp-token SHA-256 and optionally checks the expected message imprint hash. |
| `corecruxctl receipts verify-rfc3161-timestamp --body <body.cbor> --tsa-root-cert <root.pem> [--expected-policy-oid <oid>] [--expected-nonce-hex <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 <url>] [--tsa-enabled --tsa-url <url> --tsa-root-cert <root.pem>]` | 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 <body.cbor>` | 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<i32>` 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<DataPlanePool>` 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 **<http://127.0.0.1:14800>** — 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 <receipt-id> --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:<id>`. 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:<token-name>` 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
`<data_dir>/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://<host>: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 <token>`.
- `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 <token>` 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 <token>` 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 <static-named-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 <daemon> # 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 <agent-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:<token-name>`; 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 `#<passport_id>` resolves the decision *as that passport*, and the
receipt envelope records `approver_source` so the provenance is auditable.
Without the `#<passport_id>` 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 <id> --file state.json
corecruxctl session pull <id>
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("<your-agent-token>")` (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:<topic>` 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::<agent>::<slug>`), 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:<claude-session-id>"`. 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": "<sha>",
"branch": "<name>", // 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:<id>")` 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://<dir>` or `file://<path>`) 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:<id>`; `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:<repo>"` (keys `dead:`/`unused-dep:`/`stub:`/`todo:`/`dark:<…>`, value `{class,file,line,message,tool,commit_sha}`; one `run:<date>` 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:<repo>/<area>"`, value `{ceiling, upgrade_trigger, file_line, commit_sha}`. Same findings as the `debt:` keys under `codehealth:<repo>`, 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:<repo>:<path>` 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<br/>MCP Server<br/>28 token-filtered tools]
observe[crux-observe<br/>Self-Observation]
sync[crux-sync<br/>Outbox Sync]
contrib[crux-contrib<br/>Contributions]
corecruxd[corecruxd<br/>HTTP + gRPC Daemon]
corecruxctl[corecruxctl<br/>CLI Tool]
retrieval[corecrux-retrieval<br/>BM25 + Graph Fusion]
memory[corecrux-memory<br/>Fact + Session Store]
projections[corecrux-projections<br/>Entity State]
receipts[corecrux-receipts<br/>CROWN Receipts]
storage[corecrux-storage<br/>Shard Store]
index[corecrux-index<br/>.ccxi Indexes]
segment[corecrux-segment<br/>Sealed Segments]
frame[corecrux-frame<br/>Frame Encoding]
types[corecrux-types<br/>Core Types]
proto[corecrux-proto<br/>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-<target>.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-<target>.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-<target>`,
`corecruxctl-<target>`, `crux-hook-<target>`, 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 <file> -p <crate> --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
# ============================================================
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/Images/readme/CueCrux-Arc-Loop-White.png">
<img src="docs/Images/readme/CueCrux-Arc-Loop.png" alt="CueCrux arc-loop mark" width="96">
</picture>
<br /><br />
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/Images/readme/crux-dark.svg">
<img src="docs/Images/readme/crux-light.svg" alt="CRUX" width="216">
</picture>
### 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)
[](https://github.com/CueCrux/Crux/actions/workflows/ci.yml)
[](https://github.com/CueCrux/Crux/actions/workflows/coverage-attestation.yml)
[](#100-mcp-tools-memory-first)
[](LICENSE)
<img src="docs/Images/readme/console-overwatch.png" alt="Crux console Overwatch view: a gate queue of decisions that need you, a live fleet of agent sessions, and a rolling receipted activity ticker" width="88%">
<sub><b>Overwatch</b> — the gates that need you, the live fleet, every action receipted.
Ships inside the daemon at <code>localhost:14800</code> — no account, no cloud dashboard.</sub>
</div>
## 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
```
<details>
<summary>From a release binary, or from source</summary>
**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.
</details>
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.
<table>
<tr>
<td width="50%"><img src="docs/Images/readme/console-canvas.png" alt="Crux console Canvas board: a size-adaptive tile board of sessions, plans and gates that can be dragged, panned and expanded"></td>
<td width="50%"><img src="docs/Images/readme/console-graph.png" alt="Crux console relation graph: sessions, work items, gates, projects and passports as draggable nodes with flow-traced edges"></td>
</tr>
<tr>
<td align="center"><sub><b>Canvas</b> — your whole operation as a size-adaptive board</sub></td>
<td align="center"><sub><b>the relation graph</b> — sessions, work, gates and passports, flow-traced</sub></td>
</tr>
<tr>
<td width="50%"><img src="docs/Images/readme/console-execplans.png" alt="Crux console ExecPlans kanban: planned, in-progress, blocked and shipped plan cards with risk class, current milestone and progress"></td>
<td width="50%"><img src="docs/Images/readme/console-sessions.png" alt="Crux console Sessions view: saved sessions for resume and audit, each with its attached plan, passport, token usage and progress"></td>
</tr>
<tr>
<td align="center"><sub><b>plans as living documents</b> — milestones, gates, risk, progress</sub></td>
<td align="center"><sub><b>sessions that survive restarts</b> — resume, audit, attribute token spend</sub></td>
</tr>
</table>
## 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 <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).
<div align="center">
<img src="docs/Images/readme/console-receipts-3d.png" alt="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" width="80%">
<sub><b>receipts as a literal chain</b> — click any block in the console, inspect the proof</sub>
</div>
**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.
<img src="docs/Images/readme/benchmarks.svg" width="1160"
alt="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¹.
<sub>¹ 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.</sub>
## 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.
<img src="docs/Images/readme/how-it-works.svg" width="1160"
alt="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.
<img src="docs/Images/readme/platform.svg" width="1160"
alt="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
<details>
<summary><b>Requirements & configuration</b></summary>
| 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.
</details>
<details>
<summary><b>First five minutes (HTTP API)</b></summary>
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).
</details>
<details>
<summary><b>Embeddings (bring your own)</b></summary>
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.
</details>
<details>
<summary><b>CLI, backups, upgrades, troubleshooting</b></summary>
`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)
</details>
<details>
<summary><b>Naming & repository scope</b></summary>
| 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.
</details>
## 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:<id>`) 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::<group>` 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<Fact>` 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:<repo>/<area>` 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__::<run_id>` 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:<id>:<action>:<state>:approvers=<policy>:parties=<1|2>:sep=<evidence>
```
`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 `#<passport>` 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 <claude|codex|cursor>`.
- 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 `<synthetic>` 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.
- `<synthetic>` 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.
- `en