fix(context): keep daemon-internal namespaces out of undirected recall - #650
Merged
Merged
Conversation
On a fresh node, `GET /v1/context` answered a user's question with the
daemon's own manual. Measured against a node holding exactly ONE user
fact, authenticated with real scopes:
no arguments 20 facts, 19 __bootstrap__::, 1 the user's
entity= 1 fact, 0 1
query= 50 facts, 49 __bootstrap__::, 1 the user's
So "what database do we use" returned 50 results, 49 of them Crux
documentation. This is the injection surface an agent gets at session
start, so it is the first thing a new user sees.
Not an auth artifact -- it was first spotted with auth off, and the
numbers above are with CORECRUXD_AUTH_MODE=dev_scopes and a scoped
caller. `__bootstrap__::` IS forced private at ingest by enforce_global,
but the seeder writes actor: None, and the bundle maps
`private && actor.is_some()`, so the assembler's owner check never
engages. The real gate is fetch-time visibility, which admits these to
any authenticated caller.
Fix: undirected recall (keyword, and the zero-hint default bundle) skips
daemon-internal namespaces. Addressed recall does not, so nothing becomes
unreachable -- `entity=__bootstrap__::doc:architecture` still returns it,
and `get_bootstrap` (a separate path, untouched) remains the intended door
to that content.
The predicate lives in corecrux-memory::fact_privacy next to the prefix
list that defines the convention, rather than being invented locally in
one HTTP handler. It keys on the `__` marker: 37 of the 39
DEFAULT_PRIVATE_PREFIXES use it, and the two that do not (decisions::,
github::) are private USER content rather than daemon bookkeeping -- which
is exactly the line being drawn. Storage, export and audit paths must not
use it; they need the complete set, and the doc comment says so.
After: no-args 20 -> 1, query= 50 -> 1, addressed still 1.
Tests: 2 regression tests on the context surface (undirected excludes,
addressed still returns) + 2 on the predicate (covers every __ prefix;
excludes user content). cargo test --workspace 7750 passed, 0 failed.
fmt, clippy --workspace -D warnings, licence headers, unwrap ratchet clean.
Found while building the M6 framework adapters: the adapters deliberately
do NOT filter this client-side, because trimming results in an adapter is
what their conformance suite forbids. It had to be fixed here or not at all.
agent:claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On a fresh node,
GET /v1/contextanswers a user's question with the daemon's own manual.Measured against a node holding exactly one user fact, authenticated with real scopes (
CORECRUXD_AUTH_MODE=dev_scopes):__bootstrap__::entity=project:atlasquery="what database does atlas use"So "what database do we use" returns 50 results, 49 of them Crux documentation.
/v1/contextis the injection surface an agent gets at session start — this is the first thing a new user sees.Not an auth artifact
It was first spotted with auth off, so I checked with auth on before claiming anything; the table above is the auth-on run. The mechanism:
__bootstrap__::is forced private at ingest byenforce_global. But the seeder writesactor: None, and the bundle mapsprivate && actor.is_some()— so the assembler's private-owner check never engages. That check is explicitly documented as defence-in-depth, with fetch-time visibility as the real gate, and fetch-time admits these to any authenticated caller.The fix
Undirected recall — keyword, and the zero-hint default bundle — skips daemon-internal namespaces. Addressed recall does not, so nothing becomes unreachable:
entity=__bootstrap__::doc:architecturestill returns it (test).get_bootstrapis a separate path, untouched, and remains the intended door to that content.After: no-args 20 → 1,
query=50 → 1, addressed still 1.The predicate lives in
corecrux-memory::fact_privacy, next to the prefix list that defines the convention, rather than being invented locally in one HTTP handler — otherwise the next recall surface invents its own. It keys on the__marker: 37 of the 39DEFAULT_PRIVATE_PREFIXESuse it, and the two that don't (decisions::,github::) are private user content rather than daemon bookkeeping, which is exactly the line being drawn. The doc comment says plainly that storage, export and audit paths must not use it — they need the complete set.Checks
2 regression tests on the context surface (undirected excludes; addressed still returns) + 2 on the predicate (covers every
__prefix; excludes user content).cargo test --workspace7750 passed, 0 failed. fmt,clippy --workspace -D warnings, licence headers and the unwrap ratchet all clean.Why it surfaced now
Found while building the M6 framework adapters (#641, #643). The adapters deliberately do not filter this client-side — trimming results in an adapter is precisely what their conformance suite forbids. So it had to be fixed here or not at all.
🤖 Generated with Claude Code