CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# 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.
- `envelope_build_latency_under_2ms_for_10_facts` (`crux-mcp`) asserted a
wall-clock bound — the only one in the workspace. On a shared runner that
measures how busy the machine is, not how fast the code is. The figure is
still measured and printed; the assertion is gone, and the test is renamed
`envelope_build_covers_all_ten_facts` after the check that carries content
(`memories_used.len() == 10`). Raising the constant was rejected: it trades a
frequent flake for a rarer one and keeps a load-dependent test.
- `allowed_origins_reads_the_env_var` (`corecruxd`) round-tripped through
`set_var`/`getenv` and read back the built-in defaults instead of the value
it had just set — *while holding the module's `env_lock()`*. The mutex was
never the problem: `setenv` can reallocate the environment block, and a
concurrent `getenv` anywhere in this 2000-plus-test binary may then read a
stale pointer, which no Rust-level lock can fence (hence `set_var` being
`unsafe` from the 2024 edition). It now parses via `resolve_allowed_origins`
directly, matching its five neighbours, and removes the last `set_var` in
the file.
- `pick_free_port_is_bindable` (`crux-shell-lifecycle`) asserted that a port
picked by binding `:0` and dropping the listener is immediately re-bindable.
Nothing promises that — sibling tests binding `:0` in parallel can take it
in the gap. It now retries a bounded number of times, so only a genuinely
broken helper fails.
- **The desktop shell no longer opens stray console windows on Windows.** Two
spawn sites, both console-subsystem binaries launched from a GUI-subsystem app
(`windows_subsystem = "windows"`) that has no console to inherit — so Windows
allocated a fresh console *window* for each:
- `spawn_sidecar` (`crux-shell-lifecycle`) launched the bundled `corecruxd`
with a visible console that sat beside the app for its entire lifetime.
Redirecting stdout/stderr into the sidecar log did not suppress it: the
streams and the window are independent.
- the Windows credential lookup (`crux-shell-connection`) shells out to
`powershell.exe` for the `PasswordVault` read, flashing a console on *every*
attach-profile activation and retry. `-NonInteractive` governs the prompt,
not the window.
Both now set `CREATE_NO_WINDOW` (`0x08000000`) via `CommandExt::creation_flags`.
`rundll32.exe`, used to open external links, is GUI-subsystem (verified from
its PE header) and needed no change.
Found by the new Windows GUI smoke lane on its first green run — the
assertions passed while the uploaded screenshot showed the console box. That
lane now fails if either window returns.
- **A cold `cargo build` now completes on native Windows.** Three unrelated
stops, none of which CI can see (every runner is Linux, and the release matrix
is Linux + macOS — both unix):
- `aws-lc-sys` assembles its x86_64 Windows routines with NASM, absent from a
default Windows toolchain, so the build script aborted. The crate ships
pre-assembled objects for this case; `AWS_LC_SYS_PREBUILT_NASM = "1"` now
lives in `.cargo/config.toml`. Inert on Linux and macOS, where the prebuilt
path is gated off by target.
- `fsync_dir` (`corecrux-projections`) and `write_control_atomic`
(`corecruxd`) bound a variable consumed only inside `#[cfg(unix)]`. Against
the workspace-wide `unused_variables = "deny"`, that is a hard error off
unix. Both bind the value under `#[cfg(not(unix))]` rather than renaming to
`_path`/`_parent`, which would have suppressed the lint on unix where the
variable is load-bearing.
- **`cargo clippy --workspace -- -D warnings` now passes on native Windows.**
Seven `clippy::unnecessary_wraps` errors across `corecrux-receipts`,
`corecrux-storage`, and `crux-claude-hooks`: each is a directory-fsync or
file-permission routine whose body is unix-only, so off-unix it collapses to
`Ok(())` and the `Result` looks redundant. The signature is shared with a
genuinely fallible unix implementation, so these are suppressed at the site
with a note, not "fixed" by dropping the return type.
Native Windows remains post-v1 per `docs/getting-started.md`; WSL2 is still
the supported path. This only stops the gap widening — there are ~49
`#[cfg(unix)]` sites and no Windows job to catch the next one.
- **`config.example.env` no longer claims a `.env` file is read.** The daemon
has no dotenv support, so copying the file and starting `corecruxd` failed
with "CORECRUXD_AUTH_MODE must be set explicitly" — indistinguishable from a
genuine config error. The header now shows how to export the values.
### Security
- **JWT HTTP fact storage is tenant-isolated by default.** The daemon freezes
`CORECRUXD_TENANT_WRITE_STAMP` at startup, defaults it to `on` for JWT auth,
rejects invalid configuration and ambiguous/missing tenant authority, and
keeps `off`/`shadow` only as explicit shared-`default` migration modes.
Generic and console facts, context recall, engram overlays, memory
candidates, result envelopes, replay, and paired audit reads now preserve
one authorized tenant end to end. MCP and stores with
independent tenant contracts are explicitly outside this flag.
- **`.mcp.json` is gitignored.** MCP clients write the daemon's agent bearer
token into that file at the repository root, where it was previously
committable.
## [0.5.38] - 2026-07-10
### Added
- **AST-derived code-structure scanner.** Behind `CORECRUXD_AST_SCAN`, the
workspace scanner produces the `WorkspaceScan` shape from a `syn` AST pass
instead of the regex scanner (flag-off byte-identical). ~17× faster on the
Crux tree (p95 ~0.9 s vs ~15 s) and more accurate: call-edges resolved
module-qualified, dead-code by AST identifier-reachability rather than the
O(n²) substring pass. Context-graph edges fold in as `Extracted` confidence.
- **Watched repositories.** `POST/GET/DELETE /v1/repos` register a repo the
daemon should know about (tenant-scoped; `corecruxctl repo add|list|remove`;
MCP `register_repo` / `list_repos`). Registering a local path runs a one-shot
scan. An active file-watch loop (`CORECRUXD_REPO_WATCH`, default off) keeps a
repo's graph current via incremental re-index — a single-file edit re-parses
only that file — using `notify` with a WSL `/mnt` polling fallback.
- **Polyglot extraction.** TypeScript/TSX, Vue (`<script>` blocks) and Python
via `tree-sitter`, alongside Rust via `syn`; a language-agnostic repo walk
scans repositories that are not Cargo workspaces.
- **Typed code edges in the relation graph.** `RelationTypeV1` gains
`Calls` / `Imports` / `Defines` / `DependsOn` (append-only); behind
`CORECRUXD_CODEGRAPH_EDGES` a repo's code graph is emitted as tenant-scoped,
temporal relation edges and traversable via `POST /v1/relations/expand`.
- **Code-graph retrieval boost (spike).** A code-graph adjacency closure for
`fused_retrieve`'s graph lane, behind `CORECRUXD_CODEGRAPH_FUSION`
(default off; no recall study yet — see the ExecPlan).
- **Console code-graph view.** `/console/codegraph` renders the typed code+claim
graph with node/edge/confidence visual language, focus + inspector, and
`file:line` deep-links.
_ExecPlan: `ast-polyglot-code-graph-and-repo-watch-2026-07-08` (M0–M8);
supersedes `workspace-scan-storyline-improvements-2026-05-03` and
`crux-code-intelligence-2026-06-12`._
- **Code map serving.** `GET /v1/repos/{repoId}/codemap` (`format=summary|full`)
serves the AST scan persisted at registration/re-index — the read side of
`POST /v1/repos`. Tenant-scoped `admin:read`; distinct 404s for unregistered
vs never-scanned repos. Downstream: the WikiCrux code-maps surface
(wiki.cuecrux.com/code, `wiki_codemap` MCP tool) consumes this endpoint.
_ExecPlan: `codemap-endpoint-and-agent-docs-hardening-2026-07-10`._
- **Credit Meter spend rail (default-off).** `CORECRUXD_CREDIT_METER=1` enables
the comped-wallet `POST /v1/credits/spend` path: pinned quotes → signed
`crux.credit_spend_receipt.v1`, idempotent on retry (no double-debit).
- **Vendor observations.** Handoff/vendor observation capture with provider
breakdowns (`list_observations` / `get_observation` / `verify_observation`);
MCP handoffs are observed.
- **Usage receipts (opt-in).** Signed, metadata-only `usage_ping` receipts with
a consent-gated submitter; `/v1/version` gains update/version-notify state.
- **Agent-docs hardening.** Nested `AGENTS.md` in all 28 crates
(symbol-anchored, ≤50 lines); `check-agent-docs.sh` v2 gates llms.txt link
parity, nested-file presence, `llms-full.txt` freshness and (in CI) executes
the cheap documented commands; deterministic `llms-full.txt` generator;
CLAIMS 10–15 and INVARIANTS I5 (witness anchoring) / I6 (custody-proof
export); README redesigned around the 60-second agent-first quickstart.
### Fixed
- **Merged scan routing.** A cargo workspace containing any non-Rust supported
files no longer flattens to a single polyglot package with zero routes:
`run_repo_scan_at` merges the native Rust workspace scan with a
rust-excluded tree-sitter pass (self-scan: 29 packages / 319 routes /
14,290 symbols), and the watch loop re-indexes through the same path.
- Stale agent docs: MCP tool `token_usage` → `session_token_usage`; CODEMAP's
nonexistent `ShardStorage::append` → `append_batch`/`append_batch_with_stats`.
- **Passport key create race.** `write_new_passport_seed` losers of the
`create_new` race could read the winner's key file before its bytes landed
("key file is empty"); the key is now written in one buffer and the
AlreadyExists path retries briefly. Test temp dirs additionally salt
nanos with pid + a counter (coarse VM clocks collided parallel tests into
one dir — the CI flake behind this).
## [0.5.36] - 2026-07-03
### Added
- **Usage receipts self-populate.** The daemon now auto-emits one `usage_ping`
(`event_class=daemon_start`, keyed to the root passport) on startup — but only
when the operator has opted into submission (the three-way consent gate).
Default installs still dial nothing (`assert-no-phone-home` stays green); once
opted in, the adoption signal registers on every boot with no manual mint (#322).
- **Version-notify.** The usage-receipt collector's response advertises the
latest Crux release; the daemon compares it to its own version and, when
behind, logs a warning and surfaces `update.latest_release` / `update.behind`
on `/v1/version` (#322).
## [0.5.35] - 2026-07-03
### Added
- **Opt-in signed usage receipts (Phase T).** A local, signed, metadata-only
`usage_ping` CROWN receipt (default-OFF), plus a consent-gated, verifiable
opt-in submitter — the daemon's only sanctioned outbound signal, gated behind
`CORECRUXD_USAGE_RECEIPTS_{SUBMIT,ENDPOINT,CONSENT_AT}`; inert under default
config so `assert-no-phone-home` stays green (#315, #317, #318). See
`docs/usage-receipts.md`.
- **Side-by-side demo** — `/console/receipts-vs-console`: the CROWN
receipts-as-debugging timeline next to a vendor free-console mock (#316).
- OpenAPI: receipts routes covered in `/v1/openapi.json` plus a route-level
contract test (#168).
- Upgrade-aware `501` responses on platform-only endpoints (HTTP + MCP) that
signpost the hosted platform instead of a bare not-implemented (#169).
- Workspace wizard: live-session coordination protocol (`workspace-cuecrux` v2 → v3).
### Changed
- **Launch defaults ON** — coordination plane (`CORECRUXD_COORD`),
passport-revocation enforcement (`CRUX_PASSPORT_REVOCATION`), agent-card
discovery (`CRUX_AGENT_CARD`), and scoped-forget default to ON for fresh
installs; typed action traces + activity signing remain documented opt-in (#314).
- Trust surface: `assert-no-phone-home.sh` + the CROWN receipt-tamper demo are
now release-blocking gates in `release.yml` (#313).
- CI: `paths-ignore` replaced with a skip-but-report change-scope gate.
## [0.4.6] - 2026-06-11
### Added
- Console: Live board panel (`#/coord`) — coordination plane viewer (#166).
## [0.4.5] - 2026-06-11
### Fixed
- Coord board per-session recency gate (#165).
## [0.4.4] - 2026-06-11
### Fixed
- Coordination follow-up: announce overlap warnings, plus presence touch at
bind/announce — board liveness fix (#164).
## [0.4.3] - 2026-06-11
### Added
- Coordination plane: live-session board for concurrent agent sessions —
`/v1/coord`, `coord_status` / `coord_announce` MCP tools, boot digest (#163).
- Console: receipts view + CROWN verify panel with `#/receipts` deep link
(#162).
## [0.4.2] - 2026-06-11
### Added
- Agent→passport resolution + mediation receipts for external mediators
(B0–B4) (#161).
## [0.4.1] - 2026-06-10
### Added
- MCP tool-surface floor additions: `get_passport`, `receipt_verify`,
`sync_status` (#160).
### Changed
- CRC-v1 default-on, with a legacy opt-out (#159).
## [0.4.0] - 2026-06-10
### Added
- Graph-driven dynamic MCP tool surface + capability-graph edges (#158).
- CRC-v1 pointer-first response contract: spec + daemon search tools (M0+M2)
(#156).
- `corecrux.lane.*` registry with free→paid minting and usage-report ingest
(#154).
### Changed
- Hardened daemon auth and agent helpers (#153).
- Dependency bumps (opentelemetry_sdk, wasmtime, rcgen, sha2, chrono, uuid,
and others).
### Fixed
- Flaky gRPC replication-auth env-test race serialized (#157).
## [0.3.1] - 2026-06-05
### Fixed
- `corecruxd --version` and the boot banner now report the real short git sha in
container builds instead of `(unknown)`. The Docker builder has no `.git`, so
`build.rs` now honours a `CORECRUX_GIT_SHA` env (set from a `GIT_SHA` build-arg
the Docker workflow passes as `github.sha`), falling back to `git` then
`unknown`. Makes deploy audits able to confirm the running commit.
## [0.3.0] - 2026-06-05
### Fixed
- **New-tool probe fixes** (memory / freshness / coordination surface). A probe
of the freshness/memory + orchestrator/punchcard/work tools surfaced 12 issues;
all are fixed (ExecPlan `crux-new-tool-probe-fixes-2026-06-05`):
- **Latest-version-wins recall.** `FactStore::store`/`try_store` now retire the
prior `(entity, key)` version (`superseded_by`) so `query_facts` returns the
current value instead of every historical version. Re-stores and `memory_edit`
were leaking stale values into recall; `include_superseded` / `memory_view` /
`memory_history` still expose the full chain.
- **`memory_edit`** now stamps the editor's passport `actor` (was `null`),
preserves the prior `horizon_class` (was reset to the entity default), and
carries the user pin to the new version (was silently dropped — losing decay
and scoped-forget protection).
- **Scoped-forget honours pins.** Pinned facts survive `memory_forget` by
default (documented #9 contract); `include_pinned: true` overrides for a
GDPR Art.17 erasure. `__memory_pin::` added to the forget reserved prefixes.
- **`update_work_state` 401.** The MCP `loopback_patch` helper was the only
loopback verb not attaching the bearer token; it now does.
- **Anonymous coordination writes.** MCP loopback writes forward
`X-Corecrux-Passport-Id` from the session, and the punchcard
acquire/release bodies accept `holder_passport` (preferred over the header
actor), so orchestrator/punchcard/work writes are attributed to a real
passport instead of `anonymous`.
- **Orchestrator passport members.** `attach_to_orchestrator` accepts a
`passport` member (validated against the passport store by id or
principal_id) instead of returning an opaque 400; the error now names all
accepted types.
- **`memory_forget_dry_run`** returns `facts_that_would_be_affected` under
`structuredContent` so MCP clients actually receive it.
- **`create_work`** documents that `project_id` must be an existing project
(no implicit `default`).
- **Loopback error surfacing.** The MCP→daemon loopback helpers now disable
`ureq`'s `http_status_as_error`, read the response body on 4xx/5xx, and
surface the daemon's problem+json `detail` (e.g. `daemon returned 404:
project not found` / `passport 'x' not found`) instead of a bare
`status 404`. All four verbs (get/post/patch/delete) share one agent +
status-error path; transport failures are reported distinctly.
### Changed
- **New `update_orchestrator` MCP tool** wraps `PATCH /v1/orchestrators/{id}`
(name / assignee / state incl. `archived`) so an orchestrator can be closed
out via MCP.
- **`store_fact`** advertises `horizon_class` + `freshness_horizon` in its
schema (the handler already read them) so a freshness horizon is settable in
one call.
- **Envelope `memories_used`** carries `age_hours` alongside `age_days` for
unit consistency with the freshness/query rows.
### Added
- **GitHub shared memory** — selected GitHub repos become a searchable corpus
any agent attached to the daemon can read:
- PAT-based connection; the token is encrypted at rest with XChaCha20-
Poly1305 using a key derived from the daemon-root passport via BLAKE3
KDF (`LocalPassportKey::derive_subkey`). Endpoints:
`GET /v1/integrations/github/status`,
`POST /v1/integrations/github/connect` (verifies via api.github.com),
`POST /v1/integrations/github/disconnect`.
- Repo selection: `GET /v1/integrations/github/repos[/accessible]`,
`POST/DELETE /v1/integrations/github/repos/{owner}/{repo}/select`.
- Background sync worker pulls commits + PRs + issues + comments into
facts under `github::owner/repo::{commit,pr,issue,comment}/{id}`.
Polling cadence configurable via
`CORECRUXD_GITHUB_SYNC_INTERVAL_SECS` (default 900s);
`POST /v1/integrations/github/sync` triggers immediately.
- Mention parser: `[work:<id>]` markers in PR/issue bodies link back to
Plan A work items.
- Five new MCP tools surface the indexed corpus to coding agents:
`github_search`, `github_recent_commits`, `github_open_prs`,
`github_open_issues`, `github_comments_since`.
- Console UI: GitHub section in Settings with PAT connect form, repo
picker, sync button, and per-repo selection. Project drawer surfaces
open issues inline when `planning_target = github://owner/repo`.
- **Coordination** — multi-passport, projects, and a 6-state work kanban for
cross-agent coordination on the same daemon:
- `GET/POST/PATCH/DELETE /v1/passports` + `GET /v1/passports/{id}` — multi-
passport store; auto-seeds `personal-default` / `work-default` /
`public-default` on first boot. Per-passport `agent_work_gate` toggle
queues agent state changes for human approval when set.
- `POST /session` accepts new optional `project_id` / `tenant_id` /
`passport_id` and returns the resolved binding via `X-CueCrux-*` response
headers. `GET /v1/sessions/active` lists recent bindings.
- `GET/POST/DELETE /v1/projects` + `GET /v1/projects/{id}` + member/tenant
sub-routes. Auto-seeds a `default` project on first boot. `planning_target`
supports `tenant://` or `github://` URLs (the latter activates once Plan B
GitHub indexing ships).
- `GET/POST /v1/work` + `GET/PATCH /v1/work/{id}` + comments + transitions +
`POST /v1/work/gate/{id}/approve|reject`. Six work states: Planned ·
In Progress · Blocked · Archive · Complete · Deployed. PATCH returns 200
(applied) or 202 (queued behind a gate).
- Six new MCP tools: `list_projects`, `get_project_context`, `list_work`,
`create_work`, `update_work_state`, `comment_on_work`. Both Claude Code
sessions and other agents can read/write the same kanban from inside
their own session.
- Console UI: new `Projects` and `Work` panels, rebuilt `Passport` panel
with the six layers of agent self-knowledge (Identity + Rules filled;
Operator/Directive/Playbook/Continuity as "Available in CueCrux Cloud"
placeholders). Active-project picker in the rail.
- In-process relation graph wired into the open Crux Daemon: new
`POST /v1/relations` (write edge — `facts:write` scope),
`GET /v1/relations?tenant_id=&from_id=` (list outgoing — `admin:read`),
`POST /v1/relations/expand` (multi-hop graph traversal — `admin:read`).
Edges persist as JSONL at `data_dir/relations.jsonl` and are replayed into
in-memory `ProjectionState` on startup. The `corecrux-projections::query::graph_expand`
algorithm is now usable from the open daemon without a dataplane stub.
- Console settings page (cog icon, top-right of rail): persists chosen auth
mode, embedding endpoint URL, and model; surfaces `restart_required` when
changes need a daemon bounce. New endpoints: `GET/PUT /v1/console/settings`.
- Storage-breakdown chart relabelled to Text Search / Projections / Embedding /
Graph, each with a hover tooltip explaining what populates it. Graph bar now
reads real edge counts from the new relation surface.
- Overview hero condensed: daemon-posture and boundary-check facts are now
inline chips with custom CSS tooltips inside the hero band, replacing the
two stacked cards beneath.
- Embedded Crux Console redesigned for non-technical users: first-run
onboarding flow with live healthz/readyz/version tiles and a 3-card auth
picker (off / dev_scopes / jwt_hs256), nav reordered (Passport before
Integrations), Add Fact form, fact search box, tenant Personal/Work/Public
tabs, and a hand-rolled SVG storage-breakdown bar chart with Chunks/Bytes
toggle. Aligned to the cuecrux palette. Single-file `playground/index.html`
stays under 100 KB with no external dependencies.
- New console endpoints: `GET /v1/console/onboarding`,
`POST /v1/console/onboarding/complete`, `POST /v1/console/onboarding/restart`,
`GET /v1/console/storage-breakdown`, `POST /v1/console/facts/add`. Existing
`GET /v1/console/facts` accepts `q=` and `top_k=`; `GET /v1/console/tenants`
accepts `category=personal|work|public|all` and emits a `category` field per
tenant (prefix-based; `personal` is the default).
- `CORECRUXD_CONSOLE_DEV_PATH` env: when set, the daemon serves the console
HTML from disk instead of the embedded `include_str!` copy. Bind-mount via
the new `docker-compose.dev.yml` overlay for instant browser-refresh
iteration without rebuilding the image.
- Persistent console settings file at `data_dir/console/settings.json`
(atomic tmp+rename writes, schema-versioned).
- JSONL persistence for fact store and session store — facts survive daemon restarts
- Paginated fact export endpoint (`GET /v1/facts/export`) with cursor pagination
- Bidirectional sync client — pull enriched facts from remote CoreCrux, push local facts back
- Background sync task with configurable interval (`CORECRUXD_SYNC_INTERVAL_SECS`)
- Privacy controls for sync: `private` flag on facts, 14 default entity-prefix blocklist, preview-before-push
- 3 new MCP tools: `sync_pull`, `sync_push`, `sync_status` (21 tools total)
- Architecture Decision Records (`docs/adr/`): append-only segments, CROWN receipts, CPU-only edition
- Benchmark documentation (`docs/benchmarks.md`)
- gRPC integration tests covering all data-plane RPCs
- Runnable Rust example (`examples/rust/append_and_query.rs`)
### Changed
- Split `corecrux-storage/src/lib.rs` (13k LOC) into 9 domain modules
- Split `corecruxd/src/http.rs` (10k LOC) into 10 handler sub-modules
- Migrated CI to self-hosted Hetzner runners
- Stripped all GPU/CUDA data-plane code (~10k LOC removed) — Crux Daemon is CPU-only
- Built-in MCP support is now part of the supported `corecruxd` runtime path, quickstarts/examples/docs, and CI smoke checks
- Standalone integration-test runs now have a dedicated helper script that builds `corecruxd`, exports `CORECRUXD_BINARY`, and runs the integration crate consistently
### Fixed
- Flaky `crux-observe` config test (env-var race condition)
- `SessionStore::put` TTL parameter across all callers
- `arduino/setup-protoc` GitHub API rate limit (added `repo-token`)
- Handoff import/export privacy and authenticity handling for MCP agent transfers
- MCP agent/session scoping across fact queries, entity listing, and session operations
- HTTP `private=true` fact writes are now rejected instead of implying unsupported caller scoping
- Runtime/docs/example drift around append compatibility, text-search request shapes, and local-daemon feature surfaces
- Integration harness startup and readiness behavior across HTTP, gRPC, and MCP listeners
### Security
- Fact sync privacy: sensitive entity prefixes (`finance:`, `health:`, `personal:`, etc.) are never pushed upstream
- Sync push requires explicit `confirm: true` via MCP tool — preview mode by default
- MCP bearer-token enforcement now returns `401` instead of silently accepting anonymous POSTs when agent tokens are configured
## [0.1.0] - 2026-04-03
### Added
- Append-only event store with sealed segments, BLAKE3 integrity, and crash recovery
- CPU BM25 retrieval via `.ccxi` companion indexes with PForDelta compression
- Graph signal fusion for relation-aware retrieval
- CROWN receipt generation with Ed25519 signatures and BLAKE3 chain
- Token-budgeted retrieval (fill results until token budget exhausted)
- Relevance floor (minimum BM25 score threshold with `below_floor` count)
- Progressive retrieval (scan/expand two-pass pattern)
- Coverage and gap reporting on every query response
- Fact store (receipted key-value entity memory with BM25 search)
- Session store (scoped state per session with token counting)
- CLI tools: `corecruxctl verify-store`, `replay`, `inspect-receipt`, `explain`, `gaps`
- Contribution manifest (`crux-contrib`) with BLAKE3 content-addressed envelopes
- Sync client (`crux-sync`) with outbox-based offline-first VaultCrux sync
- HTTP API on port 14800 (`/healthz`, `/readyz`, `/metrics`, `/v1/append`, `/v1/query/*`, `/v1/facts/*`, `/v1/sessions/*`)
- gRPC data plane (append, read, replay, export)
- Prometheus metrics endpoint (100+ operational metrics)
- Docker image (Debian bookworm-slim) with docker-compose.yml
- GitHub Actions CI (lint, test, 82%+ coverage, release binaries, Docker push)
- Strict linting (`unsafe_code` forbidden, pedantic clippy, 81% coverage floor)
### Security
- CueCrux Community Licence (CCL v1.0) with 3-year Apache 2.0 conversion
- No telemetry, no phone-home, no tracking in standalone mode
- `cargo-deny` supply chain and licence audit in CI
- `cargo-audit` CVE scanning in CI
[unreleased]: https://github.com/CueCrux/Crux/compare/v0.5.65...HEAD
[0.5.65]: https://github.com/CueCrux/Crux/compare/v0.5.64...v0.5.65
[0.5.64]: https://github.com/CueCrux/Crux/compare/v0.5.63...v0.5.64
[0.5.63]: https://github.com/CueCrux/Crux/compare/v0.5.62...v0.5.63
[0.5.62]: https://github.com/CueCrux/Crux/compare/v0.5.61...v0.5.62
[0.5.61]: https://github.com/CueCrux/Crux/compare/v0.5.59...v0.5.61
[0.5.59]: https://github.com/CueCrux/Crux/compare/v0.5.58...v0.5.59
[0.5.58]: https://github.com/CueCrux/Crux/compare/v0.5.57...v0.5.58
[0.5.57]: https://github.com/CueCrux/Crux/compare/v0.5.56...v0.5.57
[0.5.56]: https://github.com/CueCrux/Crux/compare/v0.5.55...v0.5.56
[0.5.55]: https://github.com/CueCrux/Crux/compare/v0.5.54...v0.5.55
[0.5.54]: https://github.com/CueCrux/Crux/compare/v0.5.53...v0.5.54
[0.4.6]: https://github.com/CueCrux/Crux/compare/v0.4.5...v0.4.6
[0.4.5]: https://github.com/CueCrux/Crux/compare/v0.4.4...v0.4.5
[0.4.4]: https://github.com/CueCrux/Crux/compare/v0.4.3...v0.4.4
[0.4.3]: https://github.com/CueCrux/Crux/compare/v0.4.2...v0.4.3
[0.4.2]: https://github.com/CueCrux/Crux/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/CueCrux/Crux/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/CueCrux/Crux/compare/v0.3.1...v0.4.0
[0.1.0]: https://github.com/CueCrux/Crux/releases/tag/v0.1.0