mod compound;
mod knowledge;
mod reads;
mod session;
#[cfg(test)]
mod tests;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use uuid::Uuid;
use crate::graph::Graph;
use crate::hooks::policy_match::PolicyMatcherSet;
use crate::mcp::metadata::PeerContext;
use crate::mcp::metrics;
use crate::mcp::protocol::{self, AuditEntry, Command, ErrorCode, Request, Response};
use crate::store::session as sess;
pub(crate) struct RequestContext {
pub peer: PeerContext,
pub daemon_session: Uuid,
pub repo_root: PathBuf,
pub policy_matcher: Arc<tokio::sync::RwLock<PolicyMatcherSet>>,
}
pub async fn load_policy_matcher(store: &crate::store::Store) -> PolicyMatcherSet {
match store.scan_prefix("policy:").await {
Ok(records) => PolicyMatcherSet::from_records_lenient(&records),
Err(error) => {
tracing::warn!(error = %error, "daemon: policy matcher boot scan failed");
PolicyMatcherSet::empty()
}
}
}
pub(crate) async fn dispatch_v2(
graph: &Arc<tokio::sync::RwLock<Graph>>,
ctx: &RequestContext,
req: Request,
) -> Response {
let command_kind = req.cmd.kind();
let start = Instant::now();
let resp: Response = 'dispatch: {
if req.v != protocol::PROTOCOL_VERSION {
let resp = Response::err(
req.id,
ErrorCode::VersionMismatch,
format!(
"protocol version mismatch: client={} server={}",
req.v,
protocol::PROTOCOL_VERSION
),
);
if req.cmd.is_mutation() {
best_effort_audit(graph, ctx, &req, false, Some(ErrorCode::VersionMismatch)).await;
}
break 'dispatch resp;
}
if req.session != ctx.daemon_session {
let resp = Response::err(
req.id,
ErrorCode::SessionMismatch,
format!(
"session mismatch: request={} daemon={}; re-read daemon metadata and retry",
req.session, ctx.daemon_session
),
);
if req.cmd.is_mutation() {
best_effort_audit(graph, ctx, &req, false, Some(ErrorCode::SessionMismatch)).await;
}
break 'dispatch resp;
}
if is_side_effecting_read(&req.cmd) {
dispatch_side_effecting_read(graph, ctx, &req).await
} else if matches!(&req.cmd, Command::MemQuery(_)) {
dispatch_mem_query(graph, &req).await
} else if matches!(&req.cmd, Command::PolicyEvaluate(_)) {
dispatch_policy_evaluate(graph, ctx, &req).await
} else if is_session_side(&req.cmd) {
dispatch_session_side(graph, ctx, &req).await
} else if is_knowledge_mutation(&req.cmd) {
if matches!(&req.cmd, Command::PolicyWrite(_)) {
dispatch_policy_write(graph, ctx, &req).await
} else {
dispatch_knowledge_mutation(graph, ctx, &req).await
}
} else if is_compound(&req.cmd) {
dispatch_file_edit_hook(graph, ctx, &req).await
} else if is_config_command(&req.cmd) {
dispatch_config(graph, &req).await
} else {
dispatch_via_v1(graph, ctx, &req).await
}
};
let elapsed_us = start.elapsed().as_micros().min(u128::from(u32::MAX)) as u32;
let is_error = matches!(resp, Response::Err { .. });
metrics::record(command_kind, elapsed_us, is_error);
resp
}
fn is_side_effecting_read(cmd: &Command) -> bool {
matches!(cmd, Command::MemGet(_) | Command::MemBootstrap(_))
}
fn is_session_side(cmd: &Command) -> bool {
matches!(
cmd,
Command::SessionLog(_)
| Command::InstructionsLoaded(_)
| Command::ConsultationHit(_)
| Command::PolicyShadowObserve(_)
| Command::SessionFlush
| Command::SessionHarvest
| Command::SessionClearConsults
| Command::SubagentHarvest(_)
| Command::SubagentSpawned(_)
| Command::SubagentEdge(_)
)
}
fn is_knowledge_mutation(cmd: &Command) -> bool {
matches!(
cmd,
Command::GotchaUpsert(_)
| Command::GotchaConfirm(_)
| Command::GotchaTombstone(_)
| Command::PolicyWrite(_)
| Command::FileEnrich(_)
| Command::FileReparse(_)
| Command::DocCapture(_)
| Command::DecisionUpsert(_)
| Command::DevNoteUpsert(_)
| Command::RecordImport(_)
)
}
fn is_compound(cmd: &Command) -> bool {
matches!(cmd, Command::FileEditHook(_))
}
fn is_config_command(cmd: &Command) -> bool {
matches!(
cmd,
Command::ConfigGet(_) | Command::ConfigSet(_) | Command::SandboxAudit(_)
)
}
async fn dispatch_config(graph: &Arc<tokio::sync::RwLock<Graph>>, req: &Request) -> Response {
use crate::store::enforcement::{
get_enforcement_mode, get_policy_mode, get_retention_days, set_enforcement_mode,
set_policy_mode, set_retention_days, EnforcementMode,
};
let request_id = req.id;
let g = graph.read().await;
let store = g.store();
match &req.cmd {
Command::ConfigGet(input) => {
let value = match input.key.as_str() {
"audit.write_durability" => {
let mode = get_enforcement_mode(store).await;
match mode {
EnforcementMode::Advisory => "best_effort".to_string(),
EnforcementMode::Strict => "strict".to_string(),
}
}
"enforcement.retention" => get_retention_days(store).await.to_string(),
"policy.mode" => match get_policy_mode(store).await {
EnforcementMode::Advisory => "advisory".to_string(),
EnforcementMode::Strict => "strict".to_string(),
},
other => {
return Response::err(
request_id,
ErrorCode::ValidationFailed,
format!(
"unknown config key: {other}; valid keys: audit.write_durability, enforcement.retention, policy.mode"
),
);
}
};
Response::ok(request_id, serde_json::Value::String(value))
}
Command::ConfigSet(input) => match input.key.as_str() {
"audit.write_durability" => {
let mode = match input.value.as_str() {
"best_effort" => EnforcementMode::Advisory,
"strict" => EnforcementMode::Strict,
other => {
return Response::err(
request_id,
ErrorCode::ValidationFailed,
format!(
"invalid audit.write_durability: {other}; valid values: best_effort, strict"
),
);
}
};
match set_enforcement_mode(store, mode).await {
Ok(old) => {
let old_label = match old {
EnforcementMode::Advisory => "best_effort",
EnforcementMode::Strict => "strict",
};
Response::ok(request_id, serde_json::json!({ "old": old_label }))
}
Err(e) => Response::err(request_id, ErrorCode::StoreError, e.to_string()),
}
}
"enforcement.retention" => {
let days: u64 = match input.value.parse() {
Ok(d) if d > 0 => d,
Ok(_) => {
return Response::err(
request_id,
ErrorCode::ValidationFailed,
"retention must be at least 1 day".to_string(),
);
}
Err(_) => {
return Response::err(
request_id,
ErrorCode::ValidationFailed,
format!(
"invalid retention value: {} (expected integer days)",
input.value
),
);
}
};
match set_retention_days(store, days).await {
Ok(()) => Response::ok(request_id, serde_json::Value::Null),
Err(e) => Response::err(request_id, ErrorCode::StoreError, e.to_string()),
}
}
"policy.mode" => {
let mode = match input.value.as_str() {
"advisory" => EnforcementMode::Advisory,
"strict" => EnforcementMode::Strict,
other => {
return Response::err(
request_id,
ErrorCode::ValidationFailed,
format!(
"invalid policy.mode: {other}; valid values: advisory, strict"
),
);
}
};
match set_policy_mode(store, mode).await {
Ok(old) => {
let old_label = match old {
EnforcementMode::Advisory => "advisory",
EnforcementMode::Strict => "strict",
};
Response::ok(request_id, serde_json::json!({ "old": old_label }))
}
Err(e) => Response::err(request_id, ErrorCode::StoreError, e.to_string()),
}
}
other => Response::err(
request_id,
ErrorCode::ValidationFailed,
format!(
"unknown config key: {other}; valid keys: audit.write_durability, enforcement.retention, policy.mode"
),
),
},
Command::SandboxAudit(input) => {
let _ = crate::store::enforcement::record_event(
store,
crate::store::enforcement::EnforcementEventType::EnforcementConfigChanged {
setting: input.setting.clone(),
old_value: input.old_value.clone(),
new_value: input.new_value.clone(),
},
crate::store::enforcement::SubjectKind::Config,
input.setting.clone(),
"cli".to_string(),
None,
input.reason.clone(),
None,
)
.await;
Response::ok(request_id, serde_json::Value::Null)
}
_ => unreachable!("is_config_command guard"),
}
}
fn build_audit_entry(
ctx: &RequestContext,
request_id: Uuid,
command_kind: &str,
target_key: &str,
accepted: bool,
error_code: Option<ErrorCode>,
) -> AuditEntry {
AuditEntry {
ts: now_secs(),
peer_uid: ctx.peer.uid,
peer_pid: ctx.peer.pid,
daemon_session: ctx.daemon_session,
request_id,
command_kind: command_kind.to_string(),
target_key: target_key.to_string(),
accepted,
error_code,
}
}
fn serialize_audit(entry: &AuditEntry) -> Option<Vec<u8>> {
match rmp_serde::to_vec_named(entry) {
Ok(b) => Some(b),
Err(e) => {
tracing::warn!("audit: serialize failed: {e}");
None
}
}
}
fn audit_nanos_key(prefix: &str) -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!("{prefix}{nanos}")
}
async fn write_session_audit(store: &crate::store::Store, entry: &AuditEntry) {
let Some(bytes) = serialize_audit(entry) else {
return;
};
let key = audit_nanos_key("audit:session:");
if let Err(e) = store.put_raw(&key, &bytes).await {
tracing::warn!("audit: session write failed for {key}: {e}");
}
}
async fn best_effort_audit(
graph: &Arc<tokio::sync::RwLock<Graph>>,
ctx: &RequestContext,
req: &Request,
accepted: bool,
error_code: Option<ErrorCode>,
) {
let entry = build_audit_entry(
ctx,
req.id,
req.cmd.kind(),
req.cmd.target_key(),
accepted,
error_code,
);
let g = graph.read().await;
write_session_audit(g.store(), &entry).await;
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
use compound::dispatch_file_edit_hook;
use knowledge::{dispatch_knowledge_mutation, dispatch_policy_evaluate, dispatch_policy_write};
#[cfg(test)]
use reads::command_to_v1;
use reads::{dispatch_mem_query, dispatch_side_effecting_read, dispatch_via_v1};
use session::dispatch_session_side;