Skip to main content

systemprompt_identifiers/
context.rs

1//! Execution-context identifier — UUID v4 only.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use crate::error::IdValidationError;
7use crate::{
8    ClientSessionId, GatewayConversationId, McpServerId, ServiceName, SessionId, TaskId, UserId,
9};
10
11crate::define_id!(ContextId, validated, schema, validate_uuid_v4);
12
13fn validate_uuid_v4(s: &str) -> Result<(), IdValidationError> {
14    uuid::Uuid::parse_str(s).map_err(|e| IdValidationError::uuid("ContextId", e))?;
15    Ok(())
16}
17
18const GATEWAY_CONVERSATION_NAMESPACE: uuid::Uuid =
19    uuid::Uuid::from_u128(0x993f_3f2c_f4d9_463b_853a_d3f0_3e19_0898);
20
21const MESSAGING_NAMESPACE: uuid::Uuid =
22    uuid::Uuid::from_u128(0x6b1d_2a7e_9c84_4f31_b5e0_71a2_4d8c_3f06);
23
24const SESSION_NAMESPACE: uuid::Uuid =
25    uuid::Uuid::from_u128(0x4c1e_8b02_7a63_4d51_9f2c_0e58_a7d4_31bb);
26
27const CLI_PROBE_NAMESPACE: uuid::Uuid =
28    uuid::Uuid::from_u128(0x2d84_9f60_1c3b_4a72_8e15_b7d0_63f9_a541);
29
30const MCP_VALIDATION_NAMESPACE: uuid::Uuid =
31    uuid::Uuid::from_u128(0x91b6_04ce_7d2f_4380_b8a9_5e16_c74d_2f08);
32
33const TASK_NAMESPACE: uuid::Uuid = uuid::Uuid::from_u128(0x5ae0_37b9_8f14_4c26_9d7b_e842_06c1_fd35);
34
35const LEGACY_CONTEXT_UUID: &str = "00000000-0000-0000-0000-4c4547414359";
36
37impl ContextId {
38    pub fn generate() -> Self {
39        Self(uuid::Uuid::new_v4().to_string())
40    }
41
42    #[must_use]
43    pub fn from_uuid(uuid: uuid::Uuid) -> Self {
44        Self(uuid.to_string())
45    }
46
47    #[must_use]
48    pub fn derived_from_gateway_conversation(user_id: &UserId, gw: &GatewayConversationId) -> Self {
49        let owner =
50            uuid::Uuid::new_v5(&GATEWAY_CONVERSATION_NAMESPACE, user_id.as_str().as_bytes());
51        Self(uuid::Uuid::new_v5(&owner, gw.as_str().as_bytes()).to_string())
52    }
53
54    #[must_use]
55    pub fn derived_from_messaging(platform: &str, org: &str, channel: &str) -> Self {
56        let key = format!("{platform}:{org}:{channel}");
57        Self(uuid::Uuid::new_v5(&MESSAGING_NAMESPACE, key.as_bytes()).to_string())
58    }
59
60    #[must_use]
61    pub fn derived_from_session(session_id: &SessionId) -> Self {
62        Self(uuid::Uuid::new_v5(&SESSION_NAMESPACE, session_id.as_str().as_bytes()).to_string())
63    }
64
65    // Why: same namespace and bytes as `derived_from_session` over the bare
66    // UUID, so a gateway request and the hook events of one Claude Code run
67    // resolve to the same context row.
68    #[must_use]
69    pub fn derived_from_client_session(session: &ClientSessionId) -> Self {
70        Self(uuid::Uuid::new_v5(&SESSION_NAMESPACE, session.as_str().as_bytes()).to_string())
71    }
72
73    #[must_use]
74    pub fn derived_from_cli_probe(server_name: &McpServerId) -> Self {
75        Self(uuid::Uuid::new_v5(&CLI_PROBE_NAMESPACE, server_name.as_str().as_bytes()).to_string())
76    }
77
78    #[must_use]
79    pub fn derived_from_mcp_validation(service_name: &ServiceName) -> Self {
80        let name = service_name.as_str().as_bytes();
81        Self(uuid::Uuid::new_v5(&MCP_VALIDATION_NAMESPACE, name).to_string())
82    }
83
84    #[must_use]
85    pub fn derived_from_task(task_id: &TaskId) -> Self {
86        Self(uuid::Uuid::new_v5(&TASK_NAMESPACE, task_id.as_str().as_bytes()).to_string())
87    }
88
89    // Why: migrations 012 (security) and 014 (ai) backfilled NULL
90    // `context_id` columns with this fixed UUID, and `user_contexts` carries
91    // the matching row so those FKs resolve. It names that one row; it is
92    // not a fallback for an absent or unparseable context id.
93    #[must_use]
94    pub fn legacy_context_row() -> Self {
95        Self(LEGACY_CONTEXT_UUID.to_owned())
96    }
97}