Skip to main content

ai_memory/identity/
sentinels.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4//! Reserved caller-identity sentinels — the SSOT for every internal /
5//! system principal string the substrate compares against, stamps on
6//! rows, or carves out of cross-tenant ownership gates (#1558
7//! identity-sentinel remediation).
8//!
9//! Scattering these as inline literals is a security hazard, not a
10//! style nit: the cross-tenant ownership gates exempt callers whose
11//! principal EQUALS one of these strings, so a one-character drift
12//! between the site that CONSTRUCTS a privileged
13//! [`crate::store::CallerContext`] and the gate that COMPARES against
14//! it silently changes an authz decision. The wire-side rejection list
15//! [`crate::validate::RESERVED_AGENT_IDS`] is built from these consts,
16//! keeping the "reject wire callers claiming internal names" property
17//! mechanically in sync with the construction sites.
18//!
19//! Note the deliberate distinction from
20//! [`crate::identity::keypair::DAEMON_KEYPAIR_LABEL`]: that const is a
21//! key-file LABEL (which file on disk holds the daemon's Ed25519
22//! signing key); [`DAEMON_PRINCIPAL`] here is a CALLER IDENTITY. They
23//! share the string `"daemon"` today but govern different mechanisms,
24//! so each is named for its own semantic.
25
26/// Privileged internal daemon principal. Internal admin/system paths
27/// construct `CallerContext::for_admin(DAEMON_PRINCIPAL)` directly;
28/// the cross-tenant ownership gates (`handlers/{parity,links,kg,
29/// hook_subscribers}.rs`, `mcp/tools/namespace.rs`) exempt callers
30/// with this exact principal.
31pub const DAEMON_PRINCIPAL: &str = "daemon";
32
33/// Resolve-failure sentinel: stamped as the caller when HTTP/MCP
34/// agent-id resolution ERRORS (invalid header/param shape). Ownership
35/// gates treat it as "not an owner of anything"; it must never match a
36/// stored `metadata.agent_id`.
37pub const ANONYMOUS_INVALID: &str = "anonymous:invalid";
38
39/// Legacy unowned-marker / system principal stamped on legacy-rewrite
40/// rows by `handlers/hook_subscribers.rs`; also matched as the
41/// unowned-marker sentinel in cross-tenant gates.
42pub const SYSTEM_PRINCIPAL: &str = "system";
43
44/// Internal federation catch-up principal (`src/federation/receive.rs`).
45pub const FEDERATION_CATCHUP: &str = "federation-catchup";
46
47/// Internal subscription approval-dispatch principal
48/// (`src/handlers/subscriptions.rs::dispatch_approval_requested`).
49pub const SUBSCRIPTION_DISPATCH: &str = "subscription-dispatch";
50
51/// Internal HTTP-daemon maintenance principal used by
52/// `CallerContext::for_admin` paths in `handlers/{http,power,
53/// hook_subscribers}.rs`.
54pub const AI_HTTP_INTERNAL: &str = "ai:http-internal";
55
56/// Internal store-migration principal (`src/migrate.rs`).
57pub const AI_MIGRATE: &str = "ai:migrate";
58
59/// Internal export-path principal (`src/store/postgres.rs::export_*`).
60pub const EXPORT_INTERNAL: &str = "export-internal";
61
62/// Internal governance-maintenance principal
63/// (`src/store/postgres.rs::governance_*`).
64pub const GOVERNANCE_INTERNAL: &str = "governance-internal";
65
66/// Internal embedding-backfill sweep principal (#1579 A4 —
67/// `src/daemon_runtime.rs` serve-boot sweep over
68/// [`crate::store::MemoryStore::list_unembedded`]). The sweep is an
69/// operator-level maintenance path: it must see and re-embed EVERY
70/// row regardless of `metadata.scope`, so it runs under
71/// `CallerContext::for_admin(EMBEDDING_BACKFILL)`.
72pub const EMBEDDING_BACKFILL: &str = "embedding-backfill";
73
74/// Default agent id stamped by the HTTP daemon surface when acting as
75/// itself (NOT a privileged carve-out — unlike [`AI_HTTP_INTERNAL`]).
76pub const AI_HTTP: &str = "ai:http";
77
78/// Agent id the curator daemon writes with.
79pub const AI_CURATOR: &str = "ai:curator";
80
81/// Prefix for per-request synthesized anonymous HTTP principals
82/// (`anonymous:req-<uuid8>` — see
83/// [`crate::identity::anonymous_request_id`], the one synthesis
84/// helper every fallback path must route through; #1560).
85pub const ANONYMOUS_REQ_PREFIX: &str = "anonymous:req-";
86
87/// Prefix marking an NHI (AI) agent identity (`ai:<client>@<host>:…`,
88/// `ai:claude-code@…`, the operator-set `AI_MEMORY_AGENT_ID=ai:…`
89/// forms). The id-shape ladder in [`crate::identity::resolve_agent_id`]
90/// synthesizes ids under this prefix from `initialize.clientInfo.name`;
91/// #1600 derives the default `memory_update` `edit_source` from it
92/// ([`crate::models::EditSource::default_for_agent_id`]).
93pub const AI_AGENT_ID_PREFIX: &str = "ai:";
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    /// The reserved-name rejection list must carry every privileged
100    /// principal const above — a const added here without a matching
101    /// `RESERVED_AGENT_IDS` entry would let a wire caller claim it.
102    #[test]
103    fn reserved_agent_ids_cover_all_privileged_sentinels() {
104        for s in [
105            DAEMON_PRINCIPAL,
106            SYSTEM_PRINCIPAL,
107            FEDERATION_CATCHUP,
108            SUBSCRIPTION_DISPATCH,
109            AI_HTTP_INTERNAL,
110            AI_MIGRATE,
111            EXPORT_INTERNAL,
112            GOVERNANCE_INTERNAL,
113            EMBEDDING_BACKFILL,
114        ] {
115            assert!(
116                crate::validate::RESERVED_AGENT_IDS.contains(&s),
117                "privileged sentinel `{s}` missing from RESERVED_AGENT_IDS"
118            );
119        }
120    }
121}