Ground truth for the hardening work tracked in issue #565. Describes the threat Remnic's memory surface faces from adaptive data-extraction attacks, what we protect today, and what we have not yet measured.
This document is the ground truth for PRs 2–5 (attack harness, baseline measurement, query-budget mitigation, anomaly-detection mitigation). It deliberately does not propose implementations.
- Adaptive extraction attacks that reach Remnic through the MCP surface
(
packages/remnic-core/src/access-mcp.ts), the HTTP surface (packages/remnic-core/src/access-http.ts), or the CLI surface (packages/remnic-core/src/access-cli.ts). - An attacker who has a valid bearer token — or who shares a host process with a connector that has one — and is attempting to read memory they should not read, including memory in another namespace or memory about topics they did not contribute.
- Read-path extraction in particular:
remnic.recall,remnic.memory_search,remnic.lcm_search,remnic.memory_get,remnic.memory_timeline,remnic.memory_entities_list,remnic.entity_get,remnic.memory_profile,remnic.briefing,remnic.review_queue_list. - Passive leakage through debug / introspection tools:
remnic.memory_last_recall,remnic.memory_qmd_debug,remnic.memory_graph_explain,remnic.memory_intent_debug.
- An attacker with filesystem access to
memoryDir. If they can readnamespaces/<ns>/facts/*.md, they have already won; encryption-at-rest is a separate initiative and does not belong to this threat model. - An attacker with shell access to the gateway process (can read env vars, attach ptrace, etc.).
- Supply-chain attacks against
@remnic/coreitself, its transitive npm deps, or the QMD binary. - Prompt-injection that flows into memory via the
observepath with the goal of influencing future behavior (this is a separate concern tracked as memory poisoning; seetrust-zones.ts). - Cryptographic attacks against the bearer-token format.
Listed in rough order of sensitivity.
| Asset | Location | Why it matters |
|---|---|---|
| Raw memory content (facts, corrections, decisions, preferences) | memoryDir/namespaces/<ns>/facts/**/*.md, corrections/, decisions/ |
Contains names, emails, preferences, private facts about real people and projects. Primary target. |
| Entity graph + relationships | memoryDir/namespaces/<ns>/entities/*.md |
Discloses who the user talks to, works with, relates to. Graph-shape leaks useful even without content. |
| LCM conversation archive | memoryDir/namespaces/<ns>/lcm/** |
Near-verbatim conversation turns. Richer than extracted facts. |
IDENTITY.md + profile.md |
workspace/IDENTITY.md, profile.md |
Behavioral profile; personal by design. |
| Trust-zone records | memoryDir/namespaces/<ns>/trust-zones/ |
Discloses what the system believes about provenance. Signal for a poisoning follow-up. |
| Recall-audit trail | <pluginStateDir>/transcripts/<YYYY-MM-DD>/<sessionKey>.jsonl |
Past queries, injected content sizes, candidate memory IDs. Disclosure reveals what else the user has been asked. |
| Extraction-judge cache | in-memory in extraction-judge.ts |
Borderline; leaks which candidate facts were judged un-worthy. |
| Work-layer tasks/projects | memoryDir/namespaces/<ns>/work/** |
User's task list, deadlines, collaborators. |
| Bearer tokens | ~/.remnic/tokens.json (new) / ~/.engram/tokens.json (legacy fallback) — see defaultTokensPath / legacyTokensPath in packages/remnic-core/src/tokens.ts |
A leaked token gives T2 capability to an attacker. |
| Shared context / feedback inbox | memoryDir/shared-context/** |
Cross-agent coordination notes. |
Referenced throughout the rest of this document.
No valid token; reaches the HTTP listener or MCP stdio surface from outside.
Every HTTP route is gated by isAuthorized() in access-http.ts
(Authorization: Bearer …, constant-time compared). MCP-over-stdio is only
reachable by a process the host already trusts to spawn, so T1 against stdio
requires T4 first. T1 against HTTP is blocked by the token check.
The common case. A legitimate connector (Codex, Replit, Openclaw host) holds a valid token and uses it for the namespace it is entitled to.
A T2 attacker in this sense is a compromised or adversarial agent on the user's machine: malware running in a user shell, a malicious VS Code extension, an MCP connector that was installed from an untrusted source. It holds a real token, talks to its real namespace, and does not try to cross namespaces — instead it tries to extract everything in the namespace it legitimately has access to via adaptive querying.
This is the primary tier ADAM-style attacks target, and the primary target of PR 4's query-rate budget and PR 5's anomaly detection.
A connector that has a token but attempts to read memory belonging to a different principal / namespace, either by:
- passing a forged
namespaceparameter in a tool call, - passing a forged
X-Engram-Principalheader whentrustPrincipalHeaderis enabled, - crafting a
sessionKeywhose prefix rule maps to another principal.
Namespace ACLs (canReadNamespace / canWriteNamespace in
packages/remnic-core/src/namespaces/principal.ts) are the primary defense,
augmented by resolveReadableNamespace in access-service.ts which refuses
unauthenticated reads when namespacesEnabled. T3 is largely addressed by
existing code; the work in #565 is to measure the residual leakage through
shared-namespace promotion and recall snapshots.
Can read memoryDir, tokens file, or attach to the gateway process. Out of
scope. Note: on today's default install, memoryDir is a plain directory with
user-readable permissions; there is no cryptographic protection and this is
intentional given the out-of-scope statement in the issue.
Enumerated in access-mcp.ts:98-817. Every read-path tool that touches memory
is reachable by any client that successfully completes the MCP initialize
handshake and passes tools/call. The surface is broad — 47 tool names
including legacy-alias pairs — so attackers have many phrasings to try.
Read-path tools that return memory content:
recall,recall_explain,recall_tier_explainmemory_get,memory_timeline,memory_search,lcm_searchmemory_entities_list,entity_getmemory_profile,memory_identity,identity_anchor_getmemory_questions,memory_last_recallmemory_intent_debug,memory_qmd_debug,memory_graph_explainreview_queue_list,review_listday_summary,briefingcontinuity_incident_list,continuity_audit_generate
Write-path tools are not the extraction attack surface but are listed for
completeness: memory_store, suggestion_submit, observe,
context_checkpoint, memory_promote, memory_feedback,
memory_governance_run, procedure_mining_run, plus the continuity/work/shared
families.
access-http.ts exposes REST routes at /engram/v1/* and a single MCP
transport at POST /mcp. Authentication is a bearer token checked with
timingSafeEqual. Principal is resolved in this order (see
docs/namespaces.md):
X-Engram-Principalheader, iftrustPrincipalHeaderis enabled.- Adapter-derived identity from adapter headers (
adapter-id, etc.). - Server default principal (
--principal). - Session-key prefix rules.
"default".
A global write rate-limit exists (WRITE_RATE_LIMIT_WINDOW_MS = 60_000,
WRITE_RATE_LIMIT_MAX_REQUESTS = 30 at access-http.ts:59-60). It applies
only to write routes and is global (not per-principal). Read routes —
including /engram/v1/recall, GET /engram/v1/memories (list/browse),
GET /engram/v1/entities, POST /engram/v1/lcm/search, and
GET /engram/v1/review-queue — have no rate limit. Note: there is no
/engram/v1/memory/search HTTP route; the memory_search capability is only
exposed via the MCP tool surface (access-mcp.ts).
remnic recall, remnic memory search, remnic memory get, and related
commands run in-process with the same permissions as the invoking user. Out
of the MCP threat model but in scope for the harness because the same
orchestrator code paths are exercised — PR 2's in-process fixture will drive
the orchestrator directly rather than going through transports.
From the ADAM paper (Apr 2026, arXiv 2604.09747): the attacker issues a sequence of queries to a memory-backed agent, uses entropy of the response distribution to pick the next query, and iterates. Published results show up to 100% ASR (attack success rate) on the systems tested.
Against Remnic, the concrete shape is:
- Attacker completes
initializeon MCP and getstools/list. - Attacker issues a seed
remnic.recallquery ("what do you know about X?") orremnic.memory_searchquery. - Response contains summaries and memory IDs. Attacker feeds these back into the loop to pick the next query with highest expected information gain.
- Parallel channels —
remnic.memory_entities_listto enumerate the entity graph,remnic.memory_qmd_debugto read index statistics,remnic.memory_last_recallto read the previous recall snapshot — give the attacker side-information the paper's systems did not have.
Within T2 (own namespace), none of these calls today fail a policy check. The
attacker gets a full response every time, constrained only by topK defaults
and whatever the direct-answer tier decides to inline. ASR is not measured
today; PR 3 will produce the baseline number.
For T3 (cross-namespace), canReadNamespace rejects the request early when
the requested namespace does not include the principal in its readPrincipals
list, so direct cross-namespace queries fail. Residual T3 leakage paths are:
sharednamespace: by design, all principals can read. Auto-promotion (autoPromoteToSharedEnabled) copies facts matchingautoPromoteToSharedCategoriesintoshared. An attacker with T2 access to any namespace can querysharedand get facts promoted from other principals. The harness in PR 2 must include this path.- Debug snapshots (
memory_last_recall,memory_graph_explain) currently return the last snapshot without a namespace ACL check in every code path —access-service.tsdoes check viaresolveReadableNamespacewhen the snapshot has a namespace, but the "no snapshot namespace" branch deserves audit under PR 2's harness.
canReadNamespace / canWriteNamespace in namespaces/principal.ts check the
requested namespace against namespacePolicies[*].readPrincipals /
writePrincipals. Wildcards ("*") are explicit. resolveReadableNamespace
in access-service.ts adds a hard gate: when namespacesEnabled is true and
the principal is absent, the call is rejected with
authentication required: namespaces are enabled and no principal was supplied.
Status: Effective for direct T3 namespace forgery. Not a defense against T2-within-own-namespace extraction.
access-http.ts:907-924 enforces Authorization: Bearer …, with a dynamic
loader (authTokensGetter) so rotation takes effect without restart. Constant-
time comparison via timingSafeStringEqual (access-http.ts:916).
Status: Effective for T1. Does not bound T2 behavior once the token is valid.
Global 30-writes-per-60s on HTTP writes (access-http.ts:59-60). Protects
against write-path abuse and memory poisoning; does not apply to reads.
Status: Not a read-path defense. Noted here so PR 4 does not re-litigate it.
packages/remnic-core/src/recall-audit.ts defines appendRecallAuditEntry,
and the Openclaw host plugin (src/index.ts:1690,1860,1902) writes one
JSONL entry per before_prompt_build recall when
cfg.recallTranscriptsEnabled is true. Pruned daily by
pruneRecallAuditEntries (src/index.ts:793).
Gap discovered while writing this document: the MCP / HTTP / CLI access
layers (access-mcp.ts, access-http.ts, access-service.recall()) do not
call appendRecallAuditEntry. Recall invocations made directly through those
surfaces are not audited. That means an ADAM attacker driving the MCP surface
leaves no entry in transcripts/…/*.jsonl, only whatever coarse logging the
transport itself emits. PR 5 must wire the audit into the access layer before
it can do useful anomaly detection, or the data it operates on will be a
subset of the traffic that actually matters.
Status: Partial — host-only. Logged here as a concrete gap for PR 5.
trust-zones.ts tags memory by provenance class (user_input, tool_output,
web_content, subagent_trace, system_memory, manual) and placement
zone (quarantine, working, trusted). Primarily a poisoning defense, not
an extraction defense. Listed because the hardening surface overlaps and we
should not re-invent a parallel tagging scheme in PR 5.
MECE (see taxonomy/) is a categorization/deduplication mechanism. It does
not reduce the information available through recall. Evaluated and rejected
as an extraction-mitigation. Mentioned here only because the issue explicitly
asked us to evaluate it.
access-schema.ts validates inbound requests, and access-idempotency.ts
dedups repeated writes. Neither affects read-path extraction.
Each gap is stated as something the harness should probe so PR 4/5 have a concrete target.
- No per-connector, per-namespace read-query budget. An MCP client can
issue arbitrary numbers of
recall/memory_search/lcm_search/memory_entities_listcalls with no quota enforcement. PR 4 owns this. - No entropy-pattern anomaly detection. Nothing flags an attacker who spreads probes across many semantically similar queries to avoid QMD caches. PR 5 owns this. Requires fix for §6.4 first.
- No cumulative-disclosure cap per session. Today a session can retrieve every memory it has read access to if it keeps asking. The threshold for "this session has now seen N% of the namespace" is un-enforced.
- Debug / introspection tools are not rate-limited and not ACL-gated by
default.
memory_qmd_debug,memory_graph_explain, andmemory_last_recallcan be called freely by any authenticated client. These are side-channels for an ADAM attacker. - Recall-audit is not written from the access layer. See §6.4.
sharednamespace auto-promotion is an unbudgeted data-copy mechanism. Any fact the attacker's own principal is allowed to produce in any namespace can, if eligible, be copied intosharedand then read by a principal that should not have seen it. Threat model for this interaction needs explicit ASR measurement in PR 3.- Response size is not capped.
topKdefaults exist but can be overridden by the caller inremnic.recall. The harness should probe whethertopK: 10000is honored and produce the current observed cap.
Proposed thresholds for the hardening milestones. Baseline (§PR 3) replaces "unknown" in the left column once measured.
| Scenario | Baseline ASR | Target ASR after PR 4 | Target ASR after PR 5 |
|---|---|---|---|
| T2 same-namespace, 1000-query budget | unknown | < 50% | < 20% |
T3 cross-namespace via forged namespace |
expected ~0% (ACL blocks) | ~0% | ~0% |
T3 cross-namespace via shared auto-promotion |
unknown | unknown | < 10% |
| T1 unauthenticated | ~0% | ~0% | ~0% |
"ASR" = fraction of ground-truth facts in the fixture that the attacker correctly recovers within the query budget. Exact definition finalized in PR 2.
Secondary metrics:
- Audit coverage: fraction of access-layer recall calls that produce a
recall-auditJSONL entry. Target: 100% by end of PR 5. - False-positive rate for anomaly detection: flags raised on a benign
workload (the existing eval harness fixtures in
@remnic/bench). Target: < 1% per-session flag rate. - Query-budget configurability: the budget introduced in PR 4 must be settable per-namespace and per-adapter, with a documented way to disable it for a principal marked as trusted.
- Encryption-at-rest of
memoryDir. Separate initiative. - Differential-privacy noise injection into recall responses. Out of scope for #565; potentially a future PR if PR 5's anomaly detection proves insufficient.
- Defending against a T2 attacker who exfiltrates memory by writing it back
out via
observeto a namespace they do control. This is an adjacent concern and will be tracked separately if/when it materializes in the harness.
PRs #638 and #639 introduced the CrossNamespaceBudget and
AccessAuditAdapter classes, but they were not wired into the actual
recall paths. PRs #649–#652 close this gap:
| PR | Slice | Change | Status |
|---|---|---|---|
| #649 | 6 | Wire CrossNamespaceBudget into EngramAccessService.recall() |
Open |
| #650 | 7 | Wire AccessAuditAdapter into EngramAccessService.recall() |
Merged |
| #651 | 8 | Add security_mitigations check to remnic doctor |
Merged |
| #652 | 9 | Mitigation-aware ADAM target + mitigated baseline | Open |
As of this writing, slices 7 and 8 (PRs #650, #651) are merged to
main. Slices 6 and 9 (PRs #649, #652) are still open. Once #649
lands, CrossNamespaceBudget will be invoked inside
EngramAccessService.recall() alongside the already-wired
AccessAuditAdapter.
Both mitigations ship disabled by default (rule 48):
recallCrossNamespaceBudgetEnabled: falserecallAuditAnomalyDetectionEnabled: false
Operators enable them explicitly in config. The remnic doctor command
(wired in PR #651 via summarizeSecurityMitigations in
operator-toolkit.ts) warns when both are disabled.
The mitigated baseline (PR #652, still open as of this writing) re-runs the T3 scenario with a cross-namespace budget of 30 queries per 60-second window:
| Scenario | Queries | Budget limit | ASR |
|---|---|---|---|
| T3 unmitigated (baseline) | 200 | none | 0.0% (ACL enforced) |
| T3 mitigated (budget=30/60s) | 200 | 30/60s | 0.0% (ACL + budget) |
The T3 ASR was already 0.0% in the baseline because the synthetic target enforces namespace ACLs. The budget mitigation provides defense in depth — it would throttle a regression that accidentally disabled the ACL check.
- Issue #565 (this work).
- ADAM — Adaptive Data Extraction Attack, arXiv:2604.09747 (Apr 2026).
docs/namespaces.md— principal / namespace resolution model.packages/remnic-core/src/namespaces/principal.ts— ACL implementation.packages/remnic-core/src/access-mcp.ts— MCP surface enumeration.packages/remnic-core/src/access-http.ts— HTTP surface, token check, write rate-limit.packages/remnic-core/src/access-service.ts— read-path namespace gating (resolveReadableNamespace,resolveRecallNamespace).packages/remnic-core/src/recall-audit.ts— recall audit (host-wired only; see §6.4).packages/remnic-core/src/trust-zones.ts— provenance tagging (poisoning defense; not an extraction defense).