cortex_mcp/tools/
admit_axiom.rs1use std::sync::{Arc, Mutex};
12
13use chrono::Utc;
14use cortex_core::{
15 accepted_axiom_source_commits, is_axiom_source_commit_fresh, parse_authority_feedback_loop,
16 parse_axiom_execution_trust, parse_cortex_context_trust, MemoryId,
17 AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT,
18};
19use cortex_memory::{
20 AdmissionLifecycle, AxiomTrustExchangeAdmissionRequest, TrustExchangeAdmission,
21};
22use cortex_store::repo::{MemoryCandidate, MemoryRepo};
23use cortex_store::Pool;
24use serde_json::{json, Value};
25
26use crate::tool_handler::{GateId, ToolError, ToolHandler};
27
28#[derive(Debug)]
48pub struct CortexAdmitAxiomTool {
49 pool: Arc<Mutex<Pool>>,
50}
51
52impl CortexAdmitAxiomTool {
53 #[must_use]
55 pub fn new(pool: Arc<Mutex<Pool>>) -> Self {
56 Self { pool }
57 }
58}
59
60impl ToolHandler for CortexAdmitAxiomTool {
61 fn name(&self) -> &'static str {
62 "cortex_admit_axiom"
63 }
64
65 fn gate_set(&self) -> &'static [GateId] {
66 &[GateId::SessionWrite]
67 }
68
69 fn call(&self, params: Value) -> Result<Value, ToolError> {
70 let exec_json = params["axiom_execution_trust_json"]
72 .as_str()
73 .filter(|s| !s.is_empty())
74 .ok_or_else(|| {
75 ToolError::InvalidParams("axiom_execution_trust_json is required".into())
76 })?;
77
78 let ctx_json = params["cortex_context_trust_json"]
79 .as_str()
80 .filter(|s| !s.is_empty())
81 .ok_or_else(|| {
82 ToolError::InvalidParams("cortex_context_trust_json is required".into())
83 })?;
84
85 let loop_json = params["authority_feedback_loop_json"]
86 .as_str()
87 .filter(|s| !s.is_empty())
88 .ok_or_else(|| {
89 ToolError::InvalidParams("authority_feedback_loop_json is required".into())
90 })?;
91
92 let persist = params["persist"].as_bool().unwrap_or(false);
93
94 let exec = parse_axiom_execution_trust(exec_json).map_err(|err| {
96 ToolError::InvalidParams(format!(
97 "axiom_execution_trust_json schema error: {}",
98 err.reason
99 ))
100 })?;
101
102 let ctx = parse_cortex_context_trust(ctx_json).map_err(|err| {
103 ToolError::InvalidParams(format!(
104 "cortex_context_trust_json schema error: {}",
105 err.reason
106 ))
107 })?;
108
109 let loop_rec = parse_authority_feedback_loop(loop_json).map_err(|err| {
110 ToolError::InvalidParams(format!(
111 "authority_feedback_loop_json schema error: {}",
112 err.reason
113 ))
114 })?;
115
116 let accepted = accepted_axiom_source_commits();
118 if !is_axiom_source_commit_fresh(&exec.tool_provenance.source_commit, &accepted) {
119 return Err(ToolError::PolicyRejected(format!(
120 "{AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT}: \
121 tool_provenance.source_commit `{}` is not on the Cortex-side acceptance list",
122 exec.tool_provenance.source_commit,
123 )));
124 }
125
126 let request = AxiomTrustExchangeAdmissionRequest::new(
128 exec.clone(),
129 AdmissionLifecycle::CandidateOnly,
130 )
131 .with_cortex_context_trust(ctx)
132 .with_authority_feedback_loop(loop_rec);
133
134 let decision = request.decide();
135
136 let policy = decision.policy_decision();
138 let policy_outcome = format!("{:?}", policy.final_outcome);
139 let failing_edges: Vec<&str> = decision.invariants();
140 let forbidden_uses: Vec<String> = decision
141 .forbidden_uses()
142 .map(|uses| {
143 uses.iter()
144 .map(|u| {
145 serde_json::to_value(u)
146 .ok()
147 .and_then(|v| v.as_str().map(ToOwned::to_owned))
148 .unwrap_or_else(|| "unknown".to_string())
149 })
150 .collect()
151 })
152 .unwrap_or_default();
153
154 let decision_name = decision.decision_name();
155
156 if persist {
158 if let TrustExchangeAdmission::AdmitCandidate { .. } = &decision {
159 let now = Utc::now();
160 let candidate_id = MemoryId::new();
161
162 let claim = format!(
163 "{}: {}",
164 exec.action_id,
165 exec.source_anchors
166 .first()
167 .map(|a| a.r#ref.as_str())
168 .unwrap_or("")
169 );
170
171 let source_anchors_json: Vec<Value> = exec
172 .source_anchors
173 .iter()
174 .map(|a| {
175 json!({
176 "source_id": a.source_id,
177 "source_type": serde_json::to_value(a.source_type)
178 .unwrap_or(Value::Null),
179 "ref": a.r#ref,
180 "hash": a.hash,
181 })
182 })
183 .collect();
184
185 let candidate = MemoryCandidate {
186 id: candidate_id,
187 memory_type: "semantic".to_string(),
188 claim,
189 source_episodes_json: json!([]),
190 source_events_json: json!(exec.action_id),
191 domains_json: json!([]),
192 salience_json: json!({ "score": 0.5 }),
193 confidence: 0.5,
194 authority: "axiom_candidate".to_string(),
195 applies_when_json: json!([]),
196 does_not_apply_when_json: json!([]),
197 created_at: now,
198 updated_at: now,
199 };
200
201 let pool = self
202 .pool
203 .lock()
204 .map_err(|err| ToolError::Internal(format!("pool lock poisoned: {err}")))?;
205 let repo = MemoryRepo::new(&pool);
206
207 let _ = source_anchors_json;
210
211 repo.insert_candidate(&candidate).map_err(|err| {
212 ToolError::Internal(format!("insert_candidate failed: {err}"))
213 })?;
214
215 repo.set_pending_mcp_commit(&candidate.id, now)
216 .map_err(|err| {
217 ToolError::Internal(format!("set_pending_mcp_commit failed: {err}"))
218 })?;
219
220 tracing::info!(
221 "cortex_admit_axiom: persisted candidate {} as pending_mcp_commit",
222 candidate.id
223 );
224
225 return Ok(json!({
226 "decision": decision_name,
227 "policy_outcome": policy_outcome,
228 "failing_edges": failing_edges,
229 "forbidden_uses": forbidden_uses,
230 "persisted": true,
231 "memory_id": candidate.id.to_string(),
232 }));
233 }
234 }
235
236 Ok(json!({
238 "decision": decision_name,
239 "policy_outcome": policy_outcome,
240 "failing_edges": failing_edges,
241 "forbidden_uses": forbidden_uses,
242 "persisted": false,
243 }))
244 }
245}