Skip to main content

trusty_memory/web/
mod.rs

1//! HTTP API + embedded SPA shell for the trusty-memory admin UI.
2//!
3//! Why: The web admin panel is the primary GUI for non-MCP clients. Bundling
4//! the Svelte build via `rust-embed` keeps deployment to "drop the binary on
5//! a host"; the JSON API surface mirrors the MCP tool set so anything
6//! trusty-memory can do via Claude Code can also be done via curl or browser.
7//! What: All `/api/v1/*` handlers (status, palaces, drawers, recall, KG,
8//! config, chat) plus an embedded-asset fallback that serves `ui/dist/`.
9//! Test: `cargo test -p trusty-memory web::tests` covers the asset
10//! fallback and JSON shape of every read endpoint against an in-memory
11//! palace built on a `tempdir`.
12
13use axum::{
14    routing::{delete, get, post},
15    Router,
16};
17
18use crate::AppState;
19
20pub(crate) mod activity;
21pub(crate) mod admin;
22pub(crate) mod error;
23pub(crate) mod health;
24pub(crate) mod kg_routes;
25pub(crate) mod palace_routes;
26pub(crate) mod prompt_context;
27pub(crate) mod recall_routes;
28pub(crate) mod rpc;
29pub(crate) mod static_assets;
30
31// Re-export the pub(crate) items that other modules (primarily `chat.rs`)
32// reference via `crate::web::`. Only items that are actually imported from
33// outside the `web/` module are listed here; submodule-internal items are
34// accessed directly from their source module within the `web/` hierarchy.
35pub(crate) use error::{open_handle, ApiError};
36pub(crate) use kg_routes::DreamStatusPayload;
37pub(crate) use palace_routes::{load_user_config, palace_info_from};
38pub(crate) use rpc::creator_info_from_http;
39
40/// Dedicated palace id used by the `/health` round-trip probe (issue #185).
41///
42/// Why: Earlier revisions of `run_health_round_trip` picked whichever palace
43/// happened to be first on disk (APFS creation order on macOS), which meant
44/// the probe always wrote — and, if recall failed, *leaked* — a drawer in a
45/// real user-facing palace. Routing the probe to a dedicated palace whose id
46/// starts with the reserved `__` prefix means leaked drawers are confined to a
47/// palace the user never sees (filtered by `MemoryService::list_palaces`) and
48/// real palaces stay clean.
49/// What: A constant `&str` reused by the probe and tests. The leading double
50/// underscore is the project-wide convention for "system" palaces hidden from
51/// user listings.
52/// Test: `health_probe_palace_is_invisible`, `health_probe_cleans_up_on_success`,
53/// `health_probe_cleans_up_on_recall_miss`.
54pub(crate) const HEALTH_PROBE_PALACE: &str = "__health_probe__";
55
56/// Build the public router with API routes + SPA asset fallback.
57///
58/// Why: `run_http` calls this so the same router shape is used in tests.
59/// What: All API routes under `/api/v1`, fallback to the SPA shell.
60/// Test: `serves_index_html_fallback` and `status_endpoint_returns_payload`.
61pub fn router() -> Router<AppState> {
62    // axum 0.8 path syntax uses `{param}` instead of `:param`. The shared
63    // `trusty_common::server::with_standard_middleware` layer brings in CORS,
64    // tracing, and gzip (with SSE excluded) so we don't drift from sibling
65    // trusty-* daemons.
66    let router = Router::new()
67        .route("/api/v1/status", get(palace_routes::status))
68        .route("/api/v1/config", get(palace_routes::config))
69        .route(
70            "/api/v1/palaces",
71            get(palace_routes::list_palaces).post(palace_routes::create_palace),
72        )
73        .route(
74            "/api/v1/palaces/{id}",
75            get(palace_routes::get_palace_handler)
76                .delete(palace_routes::delete_palace_handler)
77                .patch(palace_routes::update_palace_handler),
78        )
79        .route(
80            "/api/v1/palaces/{id}/drawers",
81            get(palace_routes::list_drawers).post(palace_routes::create_drawer),
82        )
83        .route(
84            "/api/v1/palaces/{id}/drawers/{drawer_id}",
85            delete(palace_routes::delete_drawer),
86        )
87        // Issue #70 — `/memories` is a backward-compatible alias for `/drawers`.
88        // Some clients (and earlier docs) POST/GET against `…/memories`, which
89        // 404'd because only `/drawers` was registered. Aliasing here keeps
90        // both vocabularies working against the same handlers without breaking
91        // existing `/drawers` callers.
92        .route(
93            "/api/v1/palaces/{id}/memories",
94            get(palace_routes::list_drawers).post(palace_routes::create_drawer),
95        )
96        .route(
97            "/api/v1/palaces/{id}/memories/{drawer_id}",
98            delete(palace_routes::delete_drawer),
99        )
100        .route(
101            "/api/v1/palaces/{id}/recall",
102            get(recall_routes::recall_handler),
103        )
104        .route("/api/v1/recall", get(recall_routes::recall_all_handler))
105        .route(
106            "/api/v1/palaces/{id}/kg",
107            get(kg_routes::kg_query).post(kg_routes::kg_assert),
108        )
109        .route(
110            "/api/v1/palaces/{id}/kg/subjects",
111            get(kg_routes::kg_list_subjects),
112        )
113        .route(
114            "/api/v1/palaces/{id}/kg/subjects_with_counts",
115            get(kg_routes::kg_list_subjects_with_counts),
116        )
117        .route("/api/v1/palaces/{id}/kg/all", get(kg_routes::kg_list_all))
118        .route("/api/v1/palaces/{id}/kg/graph", get(kg_routes::kg_graph))
119        .route("/api/v1/palaces/{id}/kg/count", get(kg_routes::kg_count))
120        .route(
121            "/api/v1/palaces/{id}/kg/triples/{triple_id}",
122            delete(kg_routes::kg_delete_triple),
123        )
124        .route(
125            "/api/v1/palaces/{id}/dream/status",
126            get(kg_routes::palace_dream_status),
127        )
128        .route("/api/v1/dream/status", get(kg_routes::dream_status))
129        .route("/api/v1/dream/run", post(kg_routes::dream_run))
130        .route("/api/v1/kg/gaps", get(prompt_context::kg_gaps_handler))
131        .route(
132            "/api/v1/kg/prompt-context",
133            get(prompt_context::prompt_context_handler),
134        )
135        .route(
136            "/api/v1/kg/aliases",
137            post(prompt_context::add_alias_handler),
138        )
139        .route(
140            "/api/v1/kg/prompt-facts",
141            get(prompt_context::list_prompt_facts_handler)
142                .delete(prompt_context::remove_prompt_fact_handler),
143        )
144        .route("/api/v1/chat", post(crate::chat::chat_handler))
145        .route("/api/v1/chat/providers", get(crate::chat::list_providers))
146        .route(
147            "/api/v1/palaces/{id}/chat/sessions",
148            get(crate::chat::list_chat_sessions).post(crate::chat::create_chat_session),
149        )
150        .route(
151            "/api/v1/palaces/{id}/chat/sessions/{session_id}",
152            get(crate::chat::get_chat_session).delete(crate::chat::delete_chat_session),
153        )
154        // Issue #99: inter-project messaging.
155        .route(
156            "/api/v1/messages",
157            get(crate::chat::list_messages_handler).post(crate::chat::send_message_handler),
158        )
159        .route(
160            "/api/v1/messages/mark_read",
161            post(crate::chat::mark_message_read_handler),
162        )
163        .route("/health", get(health::health))
164        .route("/api/v1/logs/tail", get(admin::logs_tail))
165        .route("/api/v1/activity", get(activity::activity_handler))
166        .route(
167            "/api/v1/activity/hook",
168            post(activity::hook_activity_handler),
169        )
170        .route("/api/v1/admin/stop", post(admin::admin_stop))
171        // Issue: fire-and-forget memory save for callers that cannot speak
172        // MCP. Sub-agents spawned via Claude Code's Agent tool inherit no
173        // MCP connections, so `memory_remember` is unreachable to them.
174        // This endpoint lets the agent shell out to `trusty-memory note`
175        // (which in turn POSTs here) and the request returns 202 the moment
176        // the body is parsed — the actual `memory_remember` dispatch runs
177        // on a detached `tokio::spawn`. Failures are logged at warn but
178        // never surface to the caller because the contract is one-way.
179        .route("/api/v1/remember", post(admin::remember_async))
180        // Multi-transport refactor: a single JSON-RPC 2.0 endpoint that
181        // accepts the same envelopes the UDS transport speaks. Lets
182        // browser clients, curl, and the stdio bridge fallback hit the
183        // tool surface without learning the REST routes. The REST
184        // routes above remain for backwards compatibility.
185        .route("/rpc", post(rpc::rpc_handler))
186        .fallback(static_assets::static_handler);
187
188    trusty_common::server::with_standard_middleware(router)
189}
190
191#[cfg(test)]
192mod tests;