Skip to main content

ai_memory/
lib.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4#![recursion_limit = "512"]
5// The library target was added by the proptest infra (Agent G) to expose
6// production modules to the integration test crate. The bin target's
7// clippy run already gates CI — re-running pedantic against the same
8// modules through the lib target would re-flag the same pre-existing
9// lint backlog the bin target already passes. Allow at the lib level;
10// the bin target is the authoritative gate for production-code linting.
11#![allow(clippy::pedantic, clippy::all)]
12
13// Library interface for ai-memory. Exposes public modules for testing and external use.
14
15// ---------------------------------------------------------------------------
16// v0.7.x (issue #1174 PR3 — pm-v3.1 time-secs sweep) — common time-unit
17// conversions to seconds. Replaces ~50 inline literals (`3600`,
18// `86_400`, `604_800`) across the codebase. The substrate is large
19// enough that magic numeric literals are a debt accelerator; named
20// constants make time-unit math grep-able and refactor-safe.
21//
22// `i64` matches the column type the values feed into (TTL seconds,
23// `chrono::Duration::seconds`, lifecycle thresholds). `u64` callers
24// (`std::time::Duration::from_secs`, prometheus counters) cast at the
25// use site via `SECS_PER_HOUR as u64` etc. — the byte-equal value is
26// preserved either way.
27// ---------------------------------------------------------------------------
28
29pub const SECS_PER_MINUTE: i64 = 60;
30pub const SECS_PER_HOUR: i64 = 3_600;
31pub const SECS_PER_DAY: i64 = 86_400;
32pub const SECS_PER_WEEK: i64 = 604_800;
33
34/// Milliseconds per second — for secs→ms conversions feeding wire/SQL
35/// surfaces that take milliseconds (e.g. postgres `statement_timeout`).
36/// `u64` matches the `*_timeout_secs` config field type; `i64`/`u128`
37/// callers cast at the use site like the `SECS_PER_*` family above.
38pub const MILLIS_PER_SEC: u64 = 1_000;
39
40/// Rounding factor for similarity/score values surfaced on wire
41/// responses (HTTP handlers + MCP tools) — `1000.0` keeps three
42/// decimal places via `(score * FACTOR).round() / FACTOR`.
43pub const SCORE_DISPLAY_ROUND_FACTOR: f64 = 1000.0;
44
45// ---------------------------------------------------------------------------
46// v0.7.0 multi-agent literal-sweep (scanner B finding F-B7) — byte-unit
47// consts so substrate-wide size math is grep-able and refactor-safe.
48// Pre-sweep ~10 production sites used bare `1024` / `1024 * 1024`
49// arithmetic; named consts make memory-cap intents explicit at the
50// callsite.
51// ---------------------------------------------------------------------------
52
53pub const KIB: usize = 1024;
54pub const MIB: usize = KIB * KIB;
55pub const GIB: usize = MIB * KIB;
56
57// ---------------------------------------------------------------------------
58// v0.7.0 multi-agent literal-sweep (scanner B findings F-B6/F-B9/F-B10) —
59// named consts for the genuinely-duplicated, load-bearing values so they are
60// grep-able and refactor-safe. Each equals the literal it replaces
61// byte-for-byte; this is a naming refactor, never a value change.
62//
63// Deliberately NOT promoted (per-site semantic audit, F-B1..F-B5/F-B8):
64//   * `Duration::from_secs(30 | 60 | 5)` and `from_millis(500)` are NOT one
65//     semantic — the 30s sites alone span `GENERATE_TIMEOUT`,
66//     `CIRCUIT_BREAKER_COOLDOWN`, `QuorumPolicy` total-timeout, an embeddings
67//     retry sleep, and several HTTP-client `.timeout(..)` calls, and most
68//     already carry a proper local name. Folding them into one
69//     `DEFAULT_NETWORK_TIMEOUT` would couple unrelated knobs and erase those
70//     local names — a false SSOT. These small-int timeouts are exactly the
71//     legitimate-literal class the vendor-literal gate carves out
72//     (CLAUDE.md §"Lint gates"), so they are left in place.
73//   * F-B5 `from_millis(10)` poll ticks — almost entirely test-region noise.
74//   * F-B7 KIB/MIB/GIB landed above.
75// ---------------------------------------------------------------------------
76
77/// F-B6 — Axum production request-body cap (2 MiB).
78pub const HTTP_BODY_LIMIT_BYTES: usize = 2 * MIB;
79/// F-B6 — test-side body read cap; pinned equal to the production limit so
80/// tests exercise the full `0..=2 MiB` envelope production accepts (was an
81/// asymmetric 1 MiB across 90+ `to_bytes(.., 1024 * 1024)` call sites).
82pub const TEST_BODY_READ_CAP: usize = HTTP_BODY_LIMIT_BYTES;
83/// F-B9 — recall primary-context semantic blend weight. Named to disambiguate
84/// from `ConfidenceTier::LIKELY_MIN` (also 0.7, a different concept).
85pub const RECALL_PRIMARY_CTX_BLEND: f32 = 0.7;
86/// F-B10 — recall cosine-similarity gate (relaxed 0.3 → 0.2 in v0.6.2 Patch 2,
87/// scenario-18; load-bearing per CLAUDE.md §"Recall Pipeline").
88pub const RECALL_COSINE_GATE: f64 = 0.2;
89
90/// #1558 batch 5 wave 3 — canonical secret-redaction placeholder
91/// rendered by every `Debug` impl that masks credential material
92/// (`AppConfig.api_key`, `[llm].api_key`, `ResolvedLlm.api_key`,
93/// `HooksSubscriptionConfig.hmac_secret`, x25519 `Keypair.secret`,
94/// `RuntimeContext.hooks_hmac_secret`). One spelling, hoist-only;
95/// `src/llm.rs` keeps its own site per the vendor carve-out.
96pub const REDACTED_PLACEHOLDER: &str = "<redacted>";
97
98// ---------------------------------------------------------------------------
99// v0.7.0 multi-agent literal-sweep (scanner F finding F-F-ROUTE-1) —
100// canonical HTTP route-path consts. The substrate's HTTP router
101// (`build_router_with_timeout` in `src/lib.rs`) registers ~87
102// `.route(...)` calls at `/api/v1/`; pre-sweep, callers / tests built
103// these path strings via `format!("/api/v1/...")` or inline `"/api/v1/..."`
104// literals at thousands of sites. The consts below carve out the
105// stable surfaces so a future rename (or a hypothetical `/api/v2/`
106// transition) is a const edit instead of a substrate-wide grep.
107//
108// Naming: `ROUTE_<DOMAIN>_<VERB?>` for static paths; templates with
109// `{id}`-style axum placeholders kept as separate `_TEMPLATE` const
110// to make the template-vs-concrete distinction explicit.
111//
112// Operator directive 2026-05-31 (FIX IT NOW — no AI NHI defers) per
113// memory `f57da43e`: F-F-ROUTE-1 was previously deferred for design
114// review; this commit lands the minimum viable subset (the 19
115// highest-traffic surfaces from the audit) leaving the
116// `format!(handlers::routes::MEMORIES_ID, ...)` consumer-side helper as a
117// follow-up surface refactor.
118// ---------------------------------------------------------------------------
119
120pub const ROUTE_HEALTH: &str = handlers::routes::HEALTH;
121pub const ROUTE_METRICS: &str = handlers::routes::METRICS_BARE;
122pub const ROUTE_METRICS_V1: &str = handlers::routes::METRICS;
123pub const ROUTE_CAPABILITIES: &str = handlers::routes::CAPABILITIES;
124pub const ROUTE_MEMORIES: &str = handlers::routes::MEMORIES;
125pub const ROUTE_MEMORIES_BULK: &str = handlers::routes::MEMORIES_BULK;
126pub const ROUTE_MEMORY_BY_ID_TEMPLATE: &str = handlers::routes::MEMORIES_ID;
127pub const ROUTE_RECALL: &str = handlers::routes::RECALL;
128pub const ROUTE_SEARCH: &str = handlers::routes::SEARCH;
129pub const ROUTE_SESSION_START: &str = handlers::routes::SESSION_START;
130pub const ROUTE_SYNC_PUSH: &str = handlers::routes::SYNC_PUSH;
131pub const ROUTE_SYNC_SINCE: &str = handlers::routes::SYNC_SINCE;
132pub const ROUTE_NOTIFY: &str = handlers::routes::NOTIFY;
133pub const ROUTE_INBOX: &str = handlers::routes::INBOX;
134pub const ROUTE_SUBSCRIPTIONS: &str = handlers::routes::SUBSCRIPTIONS;
135pub const ROUTE_NAMESPACES: &str = handlers::routes::NAMESPACES;
136pub const ROUTE_ARCHIVE: &str = handlers::routes::ARCHIVE;
137pub const ROUTE_PROMOTE_TEMPLATE: &str = handlers::routes::MEMORIES_ID_PROMOTE;
138pub const ROUTE_LINKS: &str = handlers::routes::LINKS;
139
140// ---------------------------------------------------------------------------
141// v0.7.0 multi-agent literal-sweep (scanner F finding F-F-METHOD-1) —
142// canonical MCP JSON-RPC method names. Pre-sweep, 13+ sites in
143// `src/mcp/mod.rs` hardcoded the method names; a future MCP spec bump
144// would touch every match arm. Centralised here so the dispatch
145// loop's match arms point at named consts.
146// ---------------------------------------------------------------------------
147
148// #1558 batch 3 — these crate-root aliases now point at the
149// domain-canonical SSOT in `crate::mcp::jsonrpc` (version tag,
150// reserved error codes, method names, protocol revision all live
151// there); kept so existing consumers/tests keep compiling.
152pub const METHOD_INITIALIZE: &str = mcp::jsonrpc::METHOD_INITIALIZE;
153pub const METHOD_TOOLS_LIST: &str = mcp::jsonrpc::METHOD_TOOLS_LIST;
154pub const METHOD_TOOLS_CALL: &str = mcp::jsonrpc::METHOD_TOOLS_CALL;
155pub const METHOD_PROMPTS_LIST: &str = mcp::jsonrpc::METHOD_PROMPTS_LIST;
156pub const METHOD_PROMPTS_GET: &str = mcp::jsonrpc::METHOD_PROMPTS_GET;
157pub const METHOD_RESOURCES_LIST: &str = mcp::jsonrpc::METHOD_RESOURCES_LIST;
158pub const METHOD_RESOURCES_READ: &str = mcp::jsonrpc::METHOD_RESOURCES_READ;
159
160// ---------------------------------------------------------------------------
161// v0.7.x (issue #1174 PR2 — pm-v3.1 HTTP const sweep) — canonical
162// constants for the most-used HTTP header / MIME literals. Replaces
163// ~210 inline string literals across handler tests, federation
164// requests, subscription dispatch, and the HTTP daemon bootstrap.
165//
166// Naming follows the conventional Rust HTTP-crate style:
167// SCREAMING_SNAKE_CASE, separated by the field they represent.
168//
169// Byte-equal preservation: the wire still emits exactly
170// `"content-type"` / `"application/json"`. The consts merely
171// centralise the literals so a rename or typed-header migration is
172// a one-line edit rather than a 210-site grep.
173//
174// Out of scope for these consts: `hyper::header::CONTENT_TYPE` /
175// `axum::http::header::CONTENT_TYPE` typed-header sites stay on the
176// typed constant; `#[serde(rename = "...")]` attributes stay as
177// compile-time literals.
178// ---------------------------------------------------------------------------
179
180pub const HEADER_CONTENT_TYPE: &str = "content-type";
181pub const MIME_JSON: &str = "application/json";
182
183// ---------------------------------------------------------------------------
184// v0.7.0 multi-agent literal-sweep (operator directive `4f1f258b`,
185// scanners C+F) — canonical HTTP header constants for the most-
186// trafficked custom header that previously had NO centralised
187// declaration. `X-Agent-Id` is the substrate's identity-resolution
188// header per CLAUDE.md §"Agent Identity"; pre-sweep it appeared
189// hardcoded in 140+ production + test sites with a case-mismatch
190// (`X-Agent-Id` vs `x-agent-id`) already in tree. axum lowercases
191// header names server-side, so the canonical wire form is lowercase
192// — this matches the existing `HEADER_CONTENT_TYPE` (`"content-type"`),
193// `SIGNATURE_HEADER` (`"x-memory-sig"` in `federation/signing.rs`),
194// `NONCE_HEADER` (`"x-memory-nonce"`), and `PEER_ID_HEADER`
195// (`"x-peer-id"` in `federation/peer_attestation.rs`).
196// ---------------------------------------------------------------------------
197
198pub const HEADER_AGENT_ID: &str = "x-agent-id";
199
200/// API-key auth header consumed by the HTTP daemon's auth middleware
201/// (`handlers/transport.rs`) and SENT by every internal client
202/// (federation push/receive, CLI remote commands). Client and server
203/// must agree byte-for-byte — a drifted copy is a silent auth break
204/// (#1558 batch 4).
205pub const HEADER_API_KEY: &str = "x-api-key";
206
207/// HMAC signature header on signed webhook/approval callbacks
208/// (`subscriptions.rs` dispatch ⇄ `handlers/approvals.rs` verify).
209pub const HEADER_AI_MEMORY_SIGNATURE: &str = "x-ai-memory-signature";
210
211/// Timestamp header paired with [`HEADER_AI_MEMORY_SIGNATURE`] for
212/// HMAC replay-window checks.
213pub const HEADER_AI_MEMORY_TIMESTAMP: &str = "x-ai-memory-timestamp";
214
215// ---------------------------------------------------------------------------
216// v0.7.0 multi-agent literal-sweep (scanner B finding F-B7.x) —
217// canonical metadata-JSON-key consts.
218//
219// `Memory::metadata` is a free-form `serde_json::Value` blob; the
220// substrate INTERPRETS specific keys to enforce identity, visibility,
221// and provenance. Pre-sweep, these load-bearing key names appeared as
222// scattered string literals across handlers / MCP tools / federation /
223// CLI / storage — 100+ sites for `"agent_id"` alone. The consts below
224// centralise ONLY the keys that carry substrate semantics (NHI
225// attribution, visibility scope, governance policy, provenance edge
226// labels). Bare JSON field-name literals used for wire-protocol
227// shaping (e.g. `"name"`, `"description"`, `"properties"` in MCP
228// tool-schema JSON) are intentionally left as inline string literals
229// — those are protocol-driven, not substrate semantics, and changing
230// the key would be a wire break.
231//
232// A rename of any const below is a single-line edit + a multi-call-
233// site `grep` + replace; pre-sweep it was a substrate-wide search.
234// ---------------------------------------------------------------------------
235
236/// `metadata.agent_id` — the NHI identity stamp written on every
237/// substrate row per CLAUDE.md §"Agent Identity". Read by visibility
238/// predicates, governance rule evaluator, federation peer attestation,
239/// audit chain. Immutable post-write (preserved across update / dedup
240/// / import / sync / consolidate per `identity::preserve_agent_id`).
241pub const META_KEY_AGENT_ID: &str = "agent_id";
242
243/// `metadata.scope` — visibility marker (one of [`MemoryScope::all_strs`]
244/// at `crate::models::namespace::MemoryScope`). Controls which agents
245/// can see a memory via hierarchical namespace matching per Task 1.5.
246/// Memories without this key are treated as `"private"` by the query
247/// layer (see `crate::models::namespace::MemoryScope::default()`).
248pub const META_KEY_SCOPE: &str = "scope";
249
250/// `metadata.governance` — embedded governance policy blob
251/// (`GovernancePolicy::from_metadata`). Read by the substrate
252/// governance engine (`db::enforce_governance`) to evaluate rules
253/// before the canonical write path; honoured by Allow / Deny / Pending
254/// decision tree.
255pub const META_KEY_GOVERNANCE: &str = "governance";
256
257/// `metadata.imported_from_agent_id` — original NHI claim preserved
258/// when `ai-memory import` restamps `agent_id` with the importing
259/// caller's id (absent when `--trust-source` is passed). Documented at
260/// CLAUDE.md §"Agent Identity (NHI)" → "Special metadata keys".
261pub const META_KEY_IMPORTED_FROM_AGENT_ID: &str = "imported_from_agent_id";
262
263/// `metadata.consolidated_from_agents` — array of source authors,
264/// preserved on `memory_consolidate` (the consolidator's id becomes
265/// `agent_id`; the original authors stay readable from this array).
266/// Documented at CLAUDE.md §"Agent Identity (NHI)" → "Special metadata
267/// keys".
268pub const META_KEY_CONSOLIDATED_FROM_AGENTS: &str = "consolidated_from_agents";
269
270/// `metadata.mined_from` — source-format tag (`claude` / `chatgpt` /
271/// `slack`) stamped by `ai-memory mine` alongside the caller's
272/// `agent_id`. Documented at CLAUDE.md §"Agent Identity (NHI)" →
273/// "Special metadata keys".
274pub const META_KEY_MINED_FROM: &str = "mined_from";
275
276/// `metadata.target_agent_id` — recipient NHI for memories that
277/// represent agent-to-agent shares / notifications. Read by the
278/// canonical visibility predicate `is_visible_to_caller` to permit
279/// the named target to see otherwise-private rows alongside the owner.
280pub const META_KEY_TARGET_AGENT_ID: &str = "target_agent_id";
281
282// ---------------------------------------------------------------------------
283// ARCH-14 (FX-C4-batch2, 2026-05-26) — canonical route-count constant.
284//
285// The daemon's `build_router_with_timeout` registers exactly this
286// many production `.route(...)` calls at `/api/v1/`. The constant is
287// load-bearing for the docs (CLAUDE.md §"Architecture") and is
288// mechanically pinned by `tests/route_count_invariant.rs` so any
289// addition / removal of a route surface requires bumping this
290// constant in lockstep with the test failing.
291//
292// The 90th `.route(` at the bottom of `build_router_with_timeout` is
293// the `/slow` slowloris-test route gated by `#[cfg(test)]` — that is
294// counted by `EXPECTED_TEST_ROUTES_COUNT` below.
295// ---------------------------------------------------------------------------
296
297pub const EXPECTED_PRODUCTION_ROUTES_COUNT: usize = 89;
298pub const EXPECTED_TEST_ROUTES_COUNT: usize = 1;
299
300/// Number of distinct URL paths (multi-line-aware) registered by the
301/// production router. Derived via
302/// `awk '/\.route\(/{in=1}in&&/"\/[^"]*"/{match($0,/"\/[^"]*"/);print substr($0,RSTART,RLENGTH);in=0}' src/lib.rs | sort -u | wc -l`
303/// excluding the `#[cfg(test)]`-gated `/slow` slowloris route. Pinned
304/// by `tests/route_count_invariant.rs` so the docs surface count
305/// cannot drift silently. v0.7.0 multi-agent literal-sweep (scanner
306/// A, finding F-A4.1) — previously the `73 unique URL paths` count
307/// was cited in 30+ doc sites with no const.
308pub const EXPECTED_PRODUCTION_UNIQUE_PATHS_COUNT: usize = 75;
309
310// ---------------------------------------------------------------------------
311// v0.7.0 multi-agent literal-sweep (scanner A, finding F-A3.1) —
312// canonical CLI subcommand counts. The source `pub enum Command` in
313// `src/daemon_runtime.rs` declares 82 variants; two (`Migrate`,
314// `SchemaInit`) are `#[cfg(feature = "sal")]`-gated, so the default
315// build compiles 80 and `--features sal` OR `--features sal-postgres`
316// unlocks the full 82. Pre-sweep, the count was cited in 24+ doc
317// surfaces with zero machine-checkable anchor — CLAUDE.md alone had 7
318// different historical counts (40, 57, 58, 63, 79, 80, 82). Pinned by
319// `tests/cli_subcommand_count_invariant.rs`.
320// ---------------------------------------------------------------------------
321
322/// Variants in `pub enum Command` (src/daemon_runtime.rs) that
323/// COMPILE under the default build. The source file declares 82
324/// variants; two (`Migrate`, `SchemaInit`) are `#[cfg(feature =
325/// "sal")]`-gated and excluded from default builds, leaving 80.
326/// (v0.7.0 #1443 added `Expand` for the `ai-memory expand` CLI parity
327/// surface, bumping 78 → 79; #1598 added `Reembed` for the
328/// `ai-memory reembed` vector-space migration, bumping 79 → 80.)
329pub const EXPECTED_CLI_SUBCOMMANDS_DEFAULT: usize = 80;
330
331/// Variants in `pub enum Command` that COMPILE under `--features sal`
332/// (or `sal-postgres`, which implies sal in `Cargo.toml`). Equals the
333/// awk-canonical source-file count: every variant declared in the
334/// enum body (including `Migrate` + `SchemaInit`). v0.7.0 #1443 added
335/// `Expand`, bumping 80 → 81; #1598 added `Reembed`, bumping 81 → 82.
336pub const EXPECTED_CLI_SUBCOMMANDS_SAL: usize = 82;
337
338// ---------------------------------------------------------------------------
339// ARCH-10 (FX-C4-batch2, 2026-05-26) — minimal FFI self-identification
340// symbol.
341//
342// `cbindgen.toml` at v0.7.0 advertises a `staticlib`/`cdylib` build
343// surface for the iOS / Android cross-compile lanes (`mobile-cross-
344// compile` CI workflow + `mobile-ios` / `mobile-android` release
345// jobs) that previously produced artifacts with ZERO callable
346// `extern "C"` symbols. Operators linking the artifact via Xcode /
347// AGP would find nothing to call and have no way to confirm the
348// linker actually pulled in the substrate.
349//
350// This symbol gives the artifact a self-identification entry point
351// so consumers can at minimum link-and-validate the symbol table
352// before the full C ABI surface lands in a v0.7.x follow-up
353// (issue #1068 Layer 2 / #1069 wrapper SDK). The function returns
354// the substrate's Cargo.toml `version` field as a NUL-terminated
355// C string pointer with `'static` lifetime.
356//
357// Naming convention: `ai_memory_<verb>` matches the
358// `cbindgen.toml` namespace contract; the function name will be the
359// stable ABI handle for downstream consumers.
360// ---------------------------------------------------------------------------
361
362/// FFI: returns the substrate's Cargo.toml `version` field as a
363/// NUL-terminated UTF-8 C string with `'static` lifetime.
364///
365/// # Safety
366///
367/// The returned pointer is valid for the lifetime of the program;
368/// callers MUST NOT free it. The pointed-to bytes are immutable.
369///
370/// Stable since v0.7.0 (ARCH-10).
371#[unsafe(no_mangle)]
372pub extern "C" fn ai_memory_version() -> *const std::os::raw::c_char {
373    // `concat!` with a trailing nul byte gives a `&'static [u8]` of
374    // exactly the right shape; CStr::from_bytes_with_nul produces
375    // the pointer without an allocation.
376    const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
377    VERSION.as_ptr().cast::<std::os::raw::c_char>()
378}
379
380/// The crate version (compile-time `CARGO_PKG_VERSION`) as one named
381/// const — wire surfaces (capabilities, serverInfo, backup manifests,
382/// boot banners, webhook user-agent) all report it from here instead
383/// of nine scattered `env!` calls (#1558 batch 5).
384pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
385
386// ---------------------------------------------------------------------------
387// v0.7.x (issue #1174 PR5 — pm-v3.1 namespace-sentinel sweep) — the
388// default namespace for AI-NHI memory writes when the caller omits
389// the `namespace` parameter. Bare value: `"global"`.
390//
391// Distinguished from [`GLOBAL_NAMESPACE`] (underscored `"_global"`),
392// which is the system-reserved namespace for substrate-internal
393// rows (governance, quotas, audit). NEVER conflate these — they
394// are different namespaces with different semantics. The
395// underscore prefix is the reserved-namespace convention.
396//
397// Replaces ~25 inline literal `"global"` sites across config,
398// storage, handlers, MCP tools, and models. The wire value is
399// preserved byte-for-byte (`"global"` stays `"global"` on every
400// JSON-RPC and HTTP response); only the literal's source location
401// changes.
402// ---------------------------------------------------------------------------
403
404/// v0.7.x (issue #1174 PR5 — pm-v3.1 namespace-sentinel sweep) — the
405/// default namespace for AI-NHI memory writes when the caller omits
406/// the `namespace` parameter. Bare value: `"global"`.
407///
408/// Distinguished from [`GLOBAL_NAMESPACE`] (underscored `"_global"`),
409/// which is the system-reserved namespace for substrate-internal
410/// rows (governance, quotas, audit). NEVER conflate these — they
411/// are different namespaces with different semantics. The
412/// underscore prefix is the reserved-namespace convention.
413pub const DEFAULT_NAMESPACE: &str = "global";
414
415/// Per-user ai-memory data directory name (`~/.ai-memory`) — home of
416/// reflection exports + persona artefacts (#1558 batch 6).
417pub const AI_MEMORY_HOME_DIR_NAME: &str = ".ai-memory";
418
419/// v0.7.x (issue #1174 PR5) — re-export of the system-reserved
420/// namespace constant defined originally at `src/quotas.rs:70`.
421/// Centralised here so other modules don't independently re-define
422/// the literal. SEPARATE from [`DEFAULT_NAMESPACE`] — see that
423/// doc-comment for the disambiguation.
424pub use crate::quotas::GLOBAL_NAMESPACE;
425
426/// `_inbox/` namespace prefix for agent-to-agent notification routing.
427/// Reserved-namespace convention; the recipient's `target_agent` id
428/// is appended to form the canonical inbox namespace
429/// (`_inbox/<target>`).
430///
431/// v0.7.0 multi-agent literal-sweep (scanner E finding F-E5 / #1436):
432/// pre-fix 4 production sites hand-formatted the string
433/// (`format!("_inbox/{target}")`); see [`inbox_namespace`] for the
434/// canonical helper.
435pub const INBOX_NAMESPACE_PREFIX: &str = "_inbox/";
436
437/// Build the canonical inbox namespace for a target agent id.
438/// Returns `"_inbox/<target>"` formatted via the
439/// [`INBOX_NAMESPACE_PREFIX`] const. Use this in place of inline
440/// `format!("_inbox/{target}")` so a future rename of the prefix
441/// (or addition of validation, normalization, etc.) touches one
442/// place. Closes scanner E finding F-E5 (#1436).
443#[must_use]
444pub fn inbox_namespace(target_agent: &str) -> String {
445    format!("{INBOX_NAMESPACE_PREFIX}{target_agent}")
446}
447
448pub mod approvals;
449// v0.7.0 WT-1-B — substrate-level atomisation engine. Decomposes
450// long-form memories into atomic propositions with full provenance
451// (atom_of FK, derives_from edge, signed_events trail). The first
452// downstream consumer landing on the WT-1-A schema v36 foundation.
453pub mod atomisation;
454pub mod audit;
455pub mod autonomy;
456pub mod bench;
457// v0.7.0 QW-3 — daemon-side background tasks. Carries the TTL sweep
458// loop for `offloaded_blobs`; future v0.8.0 substrate tasks land
459// here without churning `daemon_runtime`.
460pub mod background;
461pub mod cli;
462pub mod color;
463/// v0.7.0 Form 5 (issue #758) — auto-confidence + shadow-mode +
464/// freshness-decay + calibration tooling. Closes the FORM 5 PARTIAL
465/// audit finding by adding deterministic auto-derivation, opt-in
466/// shadow-mode telemetry, half-life-driven freshness decay, and a
467/// per-source baseline calibration sweep on top of the legacy
468/// caller-provided `confidence` field.
469pub mod confidence;
470pub mod config;
471pub mod curator;
472pub mod daemon_runtime;
473// v0.7.0 L0.5-3 — module renamed from `db` → `storage` as part of
474// the flat-to-modular refactor. The `pub use storage as db;` shim
475// below preserves every `crate::db::*` path across the codebase
476// (handlers, mcp, cli, autonomy, bench, store, curator, transcripts,
477// tests) so the rename is a pure refactor with zero callsite churn.
478pub mod storage;
479
480// Backward-compat shim from L0.5-3 rename — preserves
481// `crate::db::*` paths used elsewhere in the codebase. To be
482// removed in v0.8.0 once all callsites migrate to
483// `crate::storage::*` AND external consumers migrate to the
484// `crate::store::MemoryStore` SAL trait surface.
485//
486// ARCH-13 (FX-C4-batch2, 2026-05-26): marked `#[deprecated]` on the
487// public re-export so any out-of-tree consumer pinning
488// `ai_memory::db::*` gets a compile-time deprecation warning. The
489// integration-test crate under `tests/` uses `ai_memory::db::*`
490// extensively (open / insert / set_namespace_standard / etc.) so a
491// hard downgrade to `pub(crate) use` would break those tests; the
492// deprecation attribute is the load-bearing signal for the v0.8.0
493// migration. External consumers should reach for the
494// `crate::store::MemoryStore` SAL trait instead (the canonical
495// public surface), and the in-tree handlers continue to use the
496// short `db::*` path until the ARCH-2 SAL boundary cleanup migrates
497// the remaining 40+ handler sites.
498#[allow(dead_code)]
499#[deprecated(
500    since = "0.7.0",
501    note = "use `ai_memory::store::MemoryStore` (the SAL trait surface) instead; the sqlite-only legacy `db` alias is slated for removal in v0.8.0"
502)]
503pub use storage as db;
504pub mod embeddings;
505// v0.7.0 (issue #228) — E2E memory content encryption at rest.
506// Per-agent X25519 keypair + ChaCha20-Poly1305 AEAD. Gated behind
507// `[encryption].at_rest = true` in config OR
508// `AI_MEMORY_ENCRYPT_AT_REST=1`. See `src/encryption/mod.rs`.
509pub mod encryption;
510pub mod errors;
511pub mod federation;
512// v0.7.0 L2-5 (issue #670) — forensic evidence bundle assembly +
513// verification. OSS surface for the AgenticMem Attest tier.
514pub mod forensic;
515pub mod handlers;
516// v0.7 Track B — harness detection. B4 reads the MCP `clientInfo.name`
517// captured at the JSON-RPC `initialize` handshake and maps it to a
518// `Harness` enum so downstream paths (capabilities-v3, B1's
519// `memory_load_family`, B2's `memory_smart_load`) can shape responses
520// based on whether the harness supports deferred-tool registration.
521pub mod harness;
522pub mod hnsw;
523// v0.7 Track G — programmable lifecycle hook pipeline. G1 lands
524// the config schema + SIGHUP hot-reload plumbing; the executor
525// (G3) and the actual fire points (G7+) layer on top of this
526// module without touching call sites in `handlers.rs` etc.
527pub mod hooks;
528pub mod identity;
529// v0.7.0 L1-2 — knowledge-graph substrate helpers (anti-cycle check).
530pub mod kg;
531// v0.7.0 (issue #651) — pluggable inference backend trait pulled
532// forward from v0.8 RFC per operator directive
533// `28860423-d12c-4959-bc8b-8fa9a94a33d9`. Unifies the
534// `embeddings::Embed` + `llm::OllamaClient` surface behind one trait
535// so a future GPU/MTP backend (v0.8 Phase 1) drops in transparently.
536pub mod inference;
537pub mod llm;
538// v0.7.x (#1183, split out of #1174 PR4) — per-CLI-binary WrapStrategy
539// table for `ai-memory wrap <agent>`. Sibling to `llm.rs` so the
540// per-vendor substrate has one home per concern (HTTP wire shape in
541// `llm.rs`, CLI ABI in `llm_cli_wrap.rs`). The CLI-binary-name
542// detection logic that PICKS a WrapStrategy stays in `cli/wrap.rs`.
543pub mod llm_cli_wrap;
544pub mod log_paths;
545pub mod logging;
546pub mod mcp;
547pub mod metrics;
548pub mod mine;
549pub mod models;
550// v0.7.0 Form 3 (issue #756) — multi-step ingest orchestrator. Batman
551// closeout: deterministic helpers run first (Jaccard, cosine, FTS
552// classifier), then LLM stages prepend a SHARED PREFIX and consume
553// helper outputs through explicit-trust slots. Stages within a run
554// share the prompt-cache key so reasoning-class LLMs hit the cache.
555pub mod multistep_ingest;
556// v0.7.0 L2-3 (issue #668) — reflection invalidation propagation.
557// Notification (not cascade) when a Reflection→Reflection supersedes
558// edge lands: walks `reflects_on` edges from dependents and writes
559// notification memories into `<namespace>/_invalidations`.
560pub mod notification;
561// v0.7.0 Gap 3 (#886) — recall-consumption observation tier. Writes
562// one row per returned candidate at recall time and flips the
563// `consumed` flag when a subsequent store/link request cites the
564// candidate. Backed by the `recall_observations` table (schema v47).
565pub mod observations;
566// v0.7.0 QW-3 — context-offload substrate primitive. Offload+deref
567// store with Ed25519-signed audit events; v0.8.0 short-term-context-
568// compression (Mermaid canvas + auto-cadence + node_id integration)
569// builds on this plumbing.
570pub mod offload;
571// v0.7.0 QW-2 — Persona-as-artifact substrate primitive. Curator-
572// generated Markdown profile of an entity, derived from a cluster
573// of Reflection-kind memories. First-class MemoryKind variant +
574// MCP tools + namespace-policy cadence + optional filesystem export.
575pub mod persona;
576// v0.7.0 L1-5 — SKILL.md parser and structured-document ingestion pipelines.
577pub mod parsing;
578// v0.7.0 K9 — unified permission system. Composes declarative
579// `[permissions.rules]` matchers, the K3 `[permissions].mode`
580// knob, and G1-G11 hook decisions into a single `Decision`.
581// Wired into the five op paths (store, link, delete, archive,
582// consolidate) so callers consult one evaluator regardless of
583// which source produced the outcome.
584//
585// v0.7.0 L0.5-4 — module renamed from `permissions` → `governance`
586// as part of the flat-to-modular refactor. The `pub use governance
587// as permissions;` shim below preserves every `crate::permissions::*`
588// path across the codebase (handlers, mcp, config, cli, tests) so the
589// rename is a pure refactor with zero callsite churn.
590pub mod governance;
591
592// Backward-compat shim from L0.5-4 rename — preserves
593// `crate::permissions::*` paths used elsewhere in the codebase.
594// To be removed in a future cleanup once all callsites migrate
595// to `crate::governance::*`.
596#[allow(dead_code)]
597pub use governance as permissions;
598pub mod profile;
599// v0.7 Track K, Task K8 — per-agent rate limits + storage caps.
600// `agent_quotas` table backs three counters (memories/day, storage
601// bytes, links/day) consulted by the store_memory + memory_link write
602// paths; daily counters reset at UTC midnight via a sweep loop.
603pub mod quotas;
604// v0.7.0 (issue #1389) — fail-safe recovery of agent context from
605// host-written transcript files (Claude Code JSONL, Codex CLI,
606// Gemini CLI). Closes the #1388 substrate failure mode where an
607// AI agent session terminated by SIGKILL between conversation
608// turns loses every decision / agreed plan it didn't volunteer-
609// `memory_store`. SessionStart-hook calls the CLI verb; in-session
610// agents call the MCP tool; both route through the canonical
611// `recover_from_transcript` handler in this module.
612pub mod recover;
613pub mod replication;
614pub mod reranker;
615// v0.7.x (issue #1174 follow-up #1192 / #1196) — cross-surface
616// substrate state (HMAC override, decompression cap, audit chain,
617// session-recall tracker, keypair cache). Held as `Arc<RuntimeContext>`
618// by every long-lived runtime so the HTTP daemon, MCP stdio binary,
619// and CLI all share one source of truth. The legacy free-fn surface
620// (`config::active_hooks_hmac_secret`, `audit::emit`,
621// `reranker::global_session_recall_tracker`, …) delegates here so the
622// wire / chain / cache semantics stay byte-equivalent.
623pub mod runtime_context;
624pub mod signed_events;
625pub mod sizes;
626pub mod subscriptions;
627pub mod synthesis;
628pub mod tls;
629pub mod toon;
630pub mod transcripts;
631pub mod validate;
632/// #951 (Track A QC sweep, 2026-05-20) — canonical
633/// `is_visible_to_caller` helper, non-sal-gated so both feature
634/// flag profiles share the same predicate. See module docstring
635/// for the drift history that motivated the consolidation.
636pub mod visibility;
637
638#[cfg(feature = "sal")]
639pub mod migrate;
640
641#[cfg(feature = "sal")]
642pub mod store;
643
644// ---------------------------------------------------------------------------
645// Router construction
646// ---------------------------------------------------------------------------
647
648/// Build the daemon's HTTP `axum::Router` from the API-key middleware
649/// state and the composite app state.
650///
651/// This is the single source of truth for the daemon's HTTP route
652/// table (88 production routes / 74 unique URL paths at v0.7.0). It is
653/// exposed through the lib crate so the integration test suite can
654/// construct an in-process `axum::Router` and exercise endpoints via
655/// `Router::oneshot()` instead of spawning a subprocess + curl, which:
656///
657/// 1. eliminates the OS-level daemon-spawn overhead per test
658///    (~200-500ms),
659/// 2. exposes the routes' line coverage to `cargo llvm-cov` (subprocess
660///    coverage attribution requires extra `LLVM_PROFILE_FILE` plumbing
661///    that the test harness doesn't provide), and
662/// 3. lets test failures surface assertion-level diagnostics instead
663///    of "curl returned 000" black holes.
664///
665/// The function takes the same two state values that `serve()`
666/// constructs inline so the production binary and the test harness
667/// share a single route map.
668///
669/// DOC-5 (med/low review batch) — promoted from the pre-existing `//`
670/// banner so the doc-comment attaches to the symbol (cargo-doc + IDE
671/// surfaces) and is symmetric with the sibling
672/// [`build_router_with_timeout`].
673pub fn build_router(
674    api_key_state: handlers::ApiKeyState,
675    app_state: handlers::AppState,
676) -> axum::Router {
677    build_router_with_timeout(
678        api_key_state,
679        app_state,
680        std::time::Duration::from_secs(config::DEFAULT_REQUEST_TIMEOUT_SECS),
681    )
682}
683
684/// v0.7.0 H7 (round-2) — variant of [`build_router`] that takes an
685/// explicit per-request wall-clock timeout. Composes a per-request
686/// timeout middleware so a slow-POST (slowloris-style) attacker
687/// cannot keep a handler scope alive indefinitely. Requests that
688/// exceed the timeout get a `504 Gateway Timeout` response with a
689/// `{"error":"request timed out"}` body. The production daemon
690/// calls this with the value resolved from
691/// `AppConfig::effective_request_timeout_secs` (default 60 s); tests
692/// pass a short timeout to drive the timeout edge directly.
693///
694/// Implementation: a custom axum middleware wraps every request in
695/// `tokio::time::timeout`, returning the structured timeout response
696/// when the future does not resolve in time. This avoids enabling
697/// tower-http's `timeout` feature (which would require a
698/// `Cargo.toml` change). The behaviour matches what
699/// `tower::timeout::TimeoutLayer` would provide modulo the status
700/// code (we return 504 to stay distinguishable from request-shape
701/// 400s).
702pub fn build_router_with_timeout(
703    api_key_state: handlers::ApiKeyState,
704    app_state: handlers::AppState,
705    request_timeout: std::time::Duration,
706) -> axum::Router {
707    use axum::{
708        extract::DefaultBodyLimit,
709        routing::{delete, get, post, put},
710    };
711    use tower_http::{cors::CorsLayer, trace::TraceLayer};
712
713    // Timeout middleware: wraps each downstream future in
714    // `tokio::time::timeout`. The closure captures the `Duration` by
715    // value so it lives for the router's lifetime.
716    let timeout = request_timeout;
717    let timeout_layer = axum::middleware::from_fn(
718        move |req: axum::extract::Request, next: axum::middleware::Next| async move {
719            use axum::response::IntoResponse;
720            match tokio::time::timeout(timeout, next.run(req)).await {
721                Ok(resp) => resp,
722                Err(_) => {
723                    tracing::warn!(
724                        timeout_secs = timeout.as_secs(),
725                        "H7: request exceeded per-request wall-clock timeout — returning 504"
726                    );
727                    (
728                        axum::http::StatusCode::GATEWAY_TIMEOUT,
729                        axum::Json(serde_json::json!({"error": "request timed out"})),
730                    )
731                        .into_response()
732                }
733            }
734        },
735    );
736
737    axum::Router::new()
738        .route(handlers::routes::HEALTH, get(handlers::health))
739        // v0.6.0.0: Prometheus scrape endpoint. Exposed at both /metrics
740        // (the community convention) and /api/v1/metrics (consistent with
741        // the rest of the REST surface).
742        .route(
743            handlers::routes::METRICS_BARE,
744            get(handlers::prometheus_metrics),
745        )
746        .route(handlers::routes::METRICS, get(handlers::prometheus_metrics))
747        .route(handlers::routes::MEMORIES, get(handlers::list_memories))
748        .route(handlers::routes::MEMORIES, post(handlers::create_memory))
749        .route(handlers::routes::MEMORIES_BULK, post(handlers::bulk_create))
750        .route(handlers::routes::MEMORIES_ID, get(handlers::get_memory))
751        .route(handlers::routes::MEMORIES_ID, put(handlers::update_memory))
752        .route(
753            handlers::routes::MEMORIES_ID,
754            delete(handlers::delete_memory),
755        )
756        .route(
757            handlers::routes::MEMORIES_ID_PROMOTE,
758            post(handlers::promote_memory),
759        )
760        // v0.7.0 #1416 — L4 layered-capture HTTP surface. Mirrors the
761        // MCP `memory_capture_turn` tool so postgres-backed daemons gain
762        // a callable L4 turn-capture path (the MCP tool only ever runs
763        // against a local sqlite connection). Routes through the SAL
764        // `MemoryStore::capture_turn_idempotent` trait method.
765        .route(handlers::routes::CAPTURE_TURN, post(handlers::capture_turn))
766        .route(handlers::routes::SEARCH, get(handlers::search_memories))
767        .route(handlers::routes::RECALL, get(handlers::recall_memories_get))
768        .route(
769            handlers::routes::RECALL,
770            post(handlers::recall_memories_post),
771        )
772        .route(handlers::routes::FORGET, post(handlers::forget_memories))
773        .route(
774            handlers::routes::CONSOLIDATE,
775            post(handlers::consolidate_memories),
776        )
777        .route(
778            handlers::routes::CONTRADICTIONS,
779            get(handlers::detect_contradictions),
780        )
781        // v0.7.0 L6 — S51 autonomous-tier surface. `auto_tag` and
782        // `expand_query` are the two REST mirrors of the corresponding
783        // MCP tools; they were never wired before L6 (S51 expected
784        // them and got 404). Both 503 when no LLM is configured.
785        .route(handlers::routes::AUTO_TAG, post(handlers::auto_tag_handler))
786        .route(
787            handlers::routes::EXPAND_QUERY,
788            post(handlers::expand_query_handler),
789        )
790        // v0.7.0 L9 — HTTP parity for the MCP `tools/list` JSON-RPC
791        // method. Surfaces the canonical tool catalog under the
792        // daemon's resolved Profile. Backend-agnostic — pure config
793        // enumeration, no DB access — so postgres and sqlite return
794        // identical bodies (NHI-D-501-postgres-traits).
795        .route(handlers::routes::TOOLS_LIST, get(handlers::tools_list))
796        // v0.7.0 L10 — HTTP parity for the MCP `memory_load_family`
797        // tool. Returns top-K memories tagged with the requested
798        // family on both sqlite and postgres backends
799        // (NHI-D-501-postgres-loadfamily).
800        .route(
801            handlers::routes::MEMORY_LOAD_FAMILY,
802            post(handlers::load_family_handler),
803        )
804        .route(handlers::routes::LINKS, post(handlers::create_link))
805        .route(handlers::routes::LINKS, delete(handlers::delete_link))
806        .route(handlers::routes::LINKS_ID, get(handlers::get_links))
807        // HTTP parity for MCP-only tools. The `/api/v1/namespaces` surface
808        // serves three verbs: GET lists namespaces OR (when ?namespace=…)
809        // fetches the namespace standard, POST sets a standard, DELETE
810        // clears one. S34/S35 use the query-string form; the path form
811        // (`/api/v1/namespaces/{ns}/standard`) is kept for MCP-tool parity.
812        .route(
813            handlers::routes::NAMESPACES,
814            get(handlers::get_namespace_standard_qs),
815        )
816        .route(
817            handlers::routes::NAMESPACES,
818            post(handlers::set_namespace_standard_qs),
819        )
820        .route(
821            handlers::routes::NAMESPACES,
822            delete(handlers::clear_namespace_standard_qs),
823        )
824        .route(
825            handlers::routes::NAMESPACES_NS_STANDARD,
826            post(handlers::set_namespace_standard),
827        )
828        .route(
829            handlers::routes::NAMESPACES_NS_STANDARD,
830            get(handlers::get_namespace_standard),
831        )
832        .route(
833            handlers::routes::NAMESPACES_NS_STANDARD,
834            delete(handlers::clear_namespace_standard),
835        )
836        // Pillar 1 / Stream A — hierarchical namespace taxonomy.
837        .route(handlers::routes::TAXONOMY, get(handlers::get_taxonomy))
838        // Pillar 2 / Stream D — pre-write near-duplicate check.
839        .route(
840            handlers::routes::CHECK_DUPLICATE,
841            post(handlers::check_duplicate),
842        )
843        // Pillar 2 / Stream B — entity registry.
844        .route(handlers::routes::ENTITIES, post(handlers::entity_register))
845        .route(
846            handlers::routes::ENTITIES_BY_ALIAS,
847            get(handlers::entity_get_by_alias),
848        )
849        // Pillar 2 / Stream C — KG timeline.
850        .route(handlers::routes::KG_TIMELINE, get(handlers::kg_timeline))
851        // Pillar 2 / Stream C — KG link supersession.
852        .route(
853            handlers::routes::KG_INVALIDATE,
854            post(handlers::kg_invalidate),
855        )
856        // Pillar 2 / Stream C — KG outbound traversal.
857        .route(handlers::routes::KG_QUERY, post(handlers::kg_query))
858        // v0.7.0 Continuation 6 — KG path enumeration (S65).
859        .route(
860            handlers::routes::KG_FIND_PATHS,
861            post(handlers::kg_find_paths),
862        )
863        // #934 (Track C, 2026-05-20) — alias for legacy callers that
864        // hit the bare `/api/v1/find_paths` route (advertised under
865        // the MCP `memory_find_paths` shape + pre-v0.7.0 docs). Pre-
866        // fix the bare path was intercepted by the postgres-gate
867        // fallback and returned a misleading 501 "not yet
868        // implemented" — actually the route just lived under `/kg/`.
869        // Mounting both paths to the same handler closes the drift
870        // for all callers without a redirect.
871        .route(handlers::routes::FIND_PATHS, post(handlers::kg_find_paths))
872        // v0.7.0 Continuation 6 — link signature verification (S52).
873        .route(
874            handlers::routes::LINKS_VERIFY,
875            post(handlers::verify_link_handler),
876        )
877        // v0.7.0 Continuation 6 — per-agent quota status (S61).
878        .route(
879            handlers::routes::QUOTA_STATUS,
880            post(handlers::quota_status_handler),
881        )
882        .route(handlers::routes::STATS, get(handlers::get_stats))
883        .route(handlers::routes::GC, post(handlers::run_gc))
884        .route(handlers::routes::EXPORT, get(handlers::export_memories))
885        .route(handlers::routes::IMPORT, post(handlers::import_memories))
886        .route(handlers::routes::ARCHIVE, get(handlers::list_archive))
887        .route(handlers::routes::ARCHIVE, post(handlers::archive_by_ids))
888        .route(handlers::routes::ARCHIVE, delete(handlers::purge_archive))
889        .route(
890            handlers::routes::ARCHIVE_ID_RESTORE,
891            post(handlers::restore_archive),
892        )
893        .route(
894            handlers::routes::ARCHIVE_STATS,
895            get(handlers::archive_stats),
896        )
897        .route(handlers::routes::AGENTS, get(handlers::list_agents))
898        .route(handlers::routes::AGENTS, post(handlers::register_agent))
899        .route(
900            handlers::routes::AGENTS_ID_PUBKEY,
901            axum::routing::put(handlers::bind_agent_pubkey),
902        )
903        .route(handlers::routes::PENDING, get(handlers::list_pending))
904        .route(
905            handlers::routes::PENDING_ID_APPROVE,
906            post(handlers::approve_pending),
907        )
908        .route(
909            handlers::routes::PENDING_ID_REJECT,
910            post(handlers::reject_pending),
911        )
912        // v0.7.0 K10 — Approval API. POST is HMAC-gated; SSE rides on
913        // top of the existing api_key_auth middleware (no extra gate).
914        .route(
915            handlers::routes::APPROVALS_PENDING_ID,
916            post(handlers::approval_decide),
917        )
918        .route(
919            handlers::routes::APPROVALS_STREAM,
920            get(handlers::approvals_sse),
921        )
922        // Phase 3 foundation (issue #224) — peer-to-peer sync endpoints.
923        .route(handlers::routes::SYNC_PUSH, post(handlers::sync_push))
924        .route(handlers::routes::SYNC_SINCE, get(handlers::sync_since))
925        // HTTP parity for MCP-only tools.
926        .route(
927            handlers::routes::CAPABILITIES,
928            get(handlers::get_capabilities),
929        )
930        .route(handlers::routes::NOTIFY, post(handlers::notify))
931        .route(handlers::routes::INBOX, get(handlers::get_inbox))
932        .route(handlers::routes::SUBSCRIPTIONS, post(handlers::subscribe))
933        .route(
934            handlers::routes::SUBSCRIPTIONS,
935            delete(handlers::unsubscribe),
936        )
937        .route(
938            handlers::routes::SUBSCRIPTIONS,
939            get(handlers::list_subscriptions),
940        )
941        .route(
942            handlers::routes::SESSION_START,
943            post(handlers::session_start),
944        )
945        // v0.7.0 Cluster E API-2 (issue #767) — Agent Skills HTTP parity.
946        // Seven routes mirroring the seven L1-5 `memory_skill_*` MCP
947        // tools so HTTP-daemon operators can drive skills without
948        // dropping back to stdio JSON-RPC. No new MCP tools land here —
949        // the MCP surface stays at whatever `Profile::full().
950        // expected_tool_count()` reports (canonical SSOT in
951        // `src/profile.rs`; pinned by `profile_full_matches_registry_all`).
952        .route(
953            handlers::routes::SKILL_REGISTER,
954            post(handlers::skill_register_route),
955        )
956        .route(
957            handlers::routes::SKILL_LIST,
958            get(handlers::skill_list_route),
959        )
960        .route(handlers::routes::SKILL_ID, get(handlers::skill_get_route))
961        .route(
962            handlers::routes::SKILL_ID_RESOURCE,
963            get(handlers::skill_resource_route),
964        )
965        .route(
966            handlers::routes::SKILL_ID_EXPORT,
967            post(handlers::skill_export_route),
968        )
969        .route(
970            handlers::routes::SKILL_ID_PROMOTE,
971            post(handlers::skill_promote_route),
972        )
973        .route(
974            handlers::routes::SKILL_ID_COMPOSE,
975            post(handlers::skill_compose_route),
976        )
977        // v0.7.0 #1095 — `POST /api/v1/share` HTTP parity for the
978        // MCP-only `memory_share` tool. Closes the SR-4 three-surface
979        // parity audit gap (#1095). Mirrors the MCP wire shape
980        // (`source_memory_id` + `target_agent_id`) and wraps the same
981        // substrate primitive (`crate::mcp::tools::share::handle_share`)
982        // so MCP / HTTP behave byte-equally.
983        .route(handlers::routes::SHARE, post(handlers::share_memory))
984        // v0.7.0 #1111 — 14 HTTP routes for the MCP-only tools the
985        // SR-4 three-surface-parity audit flagged. Each route is a thin
986        // wrapper around the existing `crate::mcp::handle_<name>`
987        // substrate primitive; wire envelopes are byte-equal across
988        // the MCP and HTTP surfaces. See
989        // [`crate::handlers::route_1111`] for the per-handler module.
990        .route(
991            handlers::routes::MEMORY_SMART_LOAD,
992            post(handlers::route_1111::handle_smart_load_http),
993        )
994        .route(
995            handlers::routes::MEMORY_REFLECT,
996            post(handlers::route_1111::handle_reflect_http),
997        )
998        .route(
999            handlers::routes::MEMORY_RECALL_OBSERVATIONS,
1000            post(handlers::route_1111::handle_recall_observations_http),
1001        )
1002        .route(
1003            handlers::routes::MEMORY_REFLECTION_ORIGIN,
1004            post(handlers::route_1111::handle_reflection_origin_http),
1005        )
1006        .route(
1007            handlers::routes::MEMORY_DEPENDENTS_OF_INVALIDATED,
1008            post(handlers::route_1111::handle_dependents_of_invalidated_http),
1009        )
1010        .route(
1011            handlers::routes::MEMORY_EXPORT_REFLECTION,
1012            post(handlers::route_1111::handle_export_reflection_http),
1013        )
1014        .route(
1015            handlers::routes::MEMORY_ATOMISE,
1016            post(handlers::route_1111::handle_atomise_http),
1017        )
1018        .route(
1019            handlers::routes::MEMORY_CALIBRATE_CONFIDENCE,
1020            post(handlers::route_1111::handle_calibrate_confidence_http),
1021        )
1022        .route(
1023            handlers::routes::MEMORY_VERIFY,
1024            post(handlers::route_1111::handle_verify_http),
1025        )
1026        .route(
1027            handlers::routes::MEMORY_REPLAY,
1028            post(handlers::route_1111::handle_replay_http),
1029        )
1030        .route(
1031            handlers::routes::MEMORY_SUBSCRIPTION_REPLAY,
1032            post(handlers::route_1111::handle_subscription_replay_http),
1033        )
1034        .route(
1035            handlers::routes::MEMORY_SUBSCRIPTION_DLQ_LIST,
1036            post(handlers::route_1111::handle_subscription_dlq_list_http),
1037        )
1038        .route(
1039            handlers::routes::MEMORY_RULE_LIST,
1040            post(handlers::route_1111::handle_rule_list_http),
1041        )
1042        .route(
1043            handlers::routes::MEMORY_CHECK_AGENT_ACTION,
1044            post(handlers::route_1111::handle_check_agent_action_http),
1045        )
1046        .layer(axum::middleware::from_fn_with_state(
1047            api_key_state,
1048            handlers::api_key_auth,
1049        ))
1050        // v0.7.0 Wave-3 Continuation — postgres route gate. On sqlite
1051        // deployments this is a pure pass-through. On postgres-backed
1052        // daemons it short-circuits any un-migrated endpoint with a
1053        // structured 501 envelope so operators never see silent data
1054        // corruption from the unused `app.db` scratch connection.
1055        // See `handlers::postgres_endpoint_supported` for the allow-list.
1056        .layer(axum::middleware::from_fn_with_state(
1057            app_state.clone(),
1058            postgres_route_gate_layer,
1059        ))
1060        .layer(TraceLayer::new_for_http())
1061        .layer(DefaultBodyLimit::max(HTTP_BODY_LIMIT_BYTES))
1062        // #1579 B4 — gzip response compression (4.6× measured
1063        // response-size win on recall payloads in the perf audit).
1064        // Honors the request's `Accept-Encoding` header; requests
1065        // without `gzip` in the accept list pass through
1066        // identity-coded. The layer's DEFAULT predicate
1067        // (`SizeAbove(32) AND NotForContentType`) already exempts
1068        // `text/event-stream`, so the SSE `/approvals/stream`
1069        // surface is never wrapped in a gzip stream that would
1070        // buffer events — pinned by the
1071        // `issue_1579_b4_*` router tests.
1072        .layer(tower_http::compression::CompressionLayer::new())
1073        .layer(CorsLayer::new())
1074        // H7 (v0.7.0 round-2) — per-request wall-clock timeout.
1075        // Applied outermost (last in the layer stack) so it bounds
1076        // every other middleware: the API-key auth, the postgres
1077        // gate, and the body decoder all run inside the timeout
1078        // window. Default 60 s; configurable via
1079        // `AppConfig::request_timeout_secs`.
1080        .layer(timeout_layer)
1081        .with_state(app_state)
1082}
1083
1084/// v0.7.0 Wave-3 Continuation — adapter that picks up the appropriate
1085/// gate function depending on whether the binary was built with the
1086/// `sal` feature flag. Standard builds compile this to a no-op pass-
1087/// through closure so the wire shape stays identical to pre-Wave-3.
1088#[cfg(feature = "sal")]
1089async fn postgres_route_gate_layer(
1090    state: axum::extract::State<handlers::AppState>,
1091    req: axum::extract::Request,
1092    next: axum::middleware::Next,
1093) -> axum::response::Response {
1094    handlers::postgres_route_gate(state, req, next).await
1095}
1096
1097#[cfg(not(feature = "sal"))]
1098async fn postgres_route_gate_layer(
1099    _state: axum::extract::State<handlers::AppState>,
1100    req: axum::extract::Request,
1101    next: axum::middleware::Next,
1102) -> axum::response::Response {
1103    next.run(req).await
1104}
1105
1106// ---------------------------------------------------------------------------
1107// H7 (v0.7.0 round-2) — per-request HTTP timeout tests.
1108// ---------------------------------------------------------------------------
1109
1110#[cfg(test)]
1111mod h7_timeout_tests {
1112    use std::time::Duration;
1113
1114    use axum::{Router, body::Body, http::Request, response::IntoResponse, routing::post};
1115    use tower::ServiceExt as _;
1116
1117    /// The timeout middleware sandwich: a thin Router with a single
1118    /// slow handler that always sleeps past the configured timeout.
1119    /// Exercises the same `axum::middleware::from_fn` closure shape
1120    /// `build_router_with_timeout` builds, without standing up the
1121    /// full AppState graph.
1122    fn timeout_router(timeout: Duration, handler_sleep: Duration) -> Router {
1123        async fn slow_handler(_body: axum::body::Bytes) -> impl IntoResponse {
1124            // Sleep duration is captured below via a small wrapper to
1125            // keep the closure shape inferrable.
1126            axum::http::StatusCode::OK
1127        }
1128        let timeout_layer = axum::middleware::from_fn(
1129            move |req: axum::extract::Request, next: axum::middleware::Next| async move {
1130                match tokio::time::timeout(timeout, next.run(req)).await {
1131                    Ok(resp) => resp,
1132                    Err(_) => (
1133                        axum::http::StatusCode::GATEWAY_TIMEOUT,
1134                        axum::Json(serde_json::json!({"error": "request timed out"})),
1135                    )
1136                        .into_response(),
1137                }
1138            },
1139        );
1140        // The actual slow handler — sleeps `handler_sleep` then 200.
1141        Router::new()
1142            .route(
1143                "/slow",
1144                post(move |_b: axum::body::Bytes| async move {
1145                    tokio::time::sleep(handler_sleep).await;
1146                    slow_handler(axum::body::Bytes::new()).await
1147                }),
1148            )
1149            .layer(timeout_layer)
1150    }
1151
1152    #[tokio::test]
1153    async fn slow_handler_returns_504_when_timeout_fires() {
1154        // Wire: middleware timeout=50ms, handler sleeps 500ms → 504.
1155        // Mirrors the production contract: a client that pumps a body
1156        // slow-loris-style past the configured ceiling sees a
1157        // structured timeout response instead of the daemon holding
1158        // the scope open forever.
1159        let router = timeout_router(Duration::from_millis(50), Duration::from_millis(500));
1160        let resp = router
1161            .oneshot(
1162                Request::builder()
1163                    .method("POST")
1164                    .uri("/slow")
1165                    .header(crate::HEADER_CONTENT_TYPE, crate::MIME_JSON)
1166                    .body(Body::from("{}"))
1167                    .unwrap(),
1168            )
1169            .await
1170            .unwrap();
1171        // tower::timeout-style middleware returns 504 Gateway Timeout
1172        // when the inner future times out. axum's `INTERNAL_SERVER_ERROR`
1173        // shape would also be acceptable per the round-2 contract
1174        // ("408 or 500 — whatever the timeout produces"); we picked 504
1175        // deliberately because it stays distinguishable from
1176        // request-shape 400s and never collides with the inner
1177        // handler's own status codes.
1178        assert!(
1179            resp.status() == axum::http::StatusCode::GATEWAY_TIMEOUT
1180                || resp.status() == axum::http::StatusCode::REQUEST_TIMEOUT
1181                || resp.status() == axum::http::StatusCode::INTERNAL_SERVER_ERROR,
1182            "expected a timeout-style response code, got {}",
1183            resp.status()
1184        );
1185    }
1186
1187    #[tokio::test]
1188    async fn fast_handler_passes_through_when_timeout_does_not_fire() {
1189        // Wire: middleware timeout=1s, handler sleeps 10ms → 200.
1190        let router = timeout_router(Duration::from_secs(1), Duration::from_millis(10));
1191        let resp = router
1192            .oneshot(
1193                Request::builder()
1194                    .method("POST")
1195                    .uri("/slow")
1196                    .body(Body::from("{}"))
1197                    .unwrap(),
1198            )
1199            .await
1200            .unwrap();
1201        assert_eq!(resp.status(), axum::http::StatusCode::OK);
1202    }
1203}