use super::*;
pub(crate) async fn run_fileless_policy(
input: &serde_json::Value,
variant: HookVariant,
action: &decide::Action,
agent_id: Option<&str>,
) -> Result<()> {
let Ok(cwd) = std::env::current_dir() else {
return Ok(());
};
let repo_root = discover_repo_root(&cwd);
let root_for_slug = repo_root.as_deref().unwrap_or(&cwd);
let mati_root = match mati_root_for(root_for_slug) {
Ok(root) => root,
Err(_) => {
log_fail_open("<fileless-bash>", "cannot determine mati root");
emit_allow(variant);
return Ok(());
}
};
let worktree = mati_core::store::session::worktree_scope_tag(&cwd);
let actor_scope =
mati_core::store::session::combined_actor_scope(worktree.as_deref(), agent_id);
let Some((decision, events)) = evaluate_governed_policy(
Some(&mati_root),
action,
"db_client",
actor_scope.as_deref(),
"<fileless-bash>",
input
.pointer("/tool_input/command")
.and_then(|value| value.as_str()),
PolicyEvaluationOptions {
introspection_exemption: policy_block_exempt(variant, input),
codex_agent: matches!(
variant,
HookVariant::CodexPreBash | HookVariant::CodexPreApplyPatch
),
},
)
.await
else {
emit_allow(variant);
return Ok(());
};
let (stdout, stderr, exit_code) = format_decision(variant, &decision, "<policy>");
if !stdout.is_empty() {
println!("{stdout}");
}
if !stderr.is_empty() {
eprintln!("{stderr}");
}
let session_id = input.get("session_id").and_then(|v| v.as_str());
fire_events(
&mati_root,
&events,
session_id,
agent_id,
actor_scope.as_deref(),
None,
)
.await;
if exit_code != 0 {
let _ = std::io::Write::flush(&mut std::io::stderr());
std::process::exit(exit_code);
}
Ok(())
}
#[derive(Clone, Copy)]
pub(crate) struct PolicyEvaluationOptions {
pub(super) introspection_exemption: bool,
pub(super) codex_agent: bool,
}
pub(crate) async fn evaluate_governed_policy(
mati_root: Option<&Path>,
action: &decide::Action,
governed_tool: &str,
actor_scope: Option<&str>,
failure_path: &str,
raw_command: Option<&str>,
options: PolicyEvaluationOptions,
) -> Option<(Decision, Vec<HookEvent>)> {
if action.tool != governed_tool && action.tool != "unknown" {
return None;
}
let Some(mati_root) = mati_root else {
log_fail_open(failure_path, "cannot determine mati root");
return None;
};
if !ensure_daemon(mati_root).await {
log_fail_open(failure_path, "daemon not running after auto-start");
return None;
}
let evaluation =
policy_verdicts_for_action(mati_root, action, actor_scope, failure_path, raw_command)
.await?;
let strict = evaluation.verdicts.iter().any(|verdict| verdict.strict);
let core_verdicts = policy_core_verdicts(
&evaluation.verdicts,
options.introspection_exemption,
options.codex_agent,
);
let result = decide::evaluate_policy_verdicts(&core_verdicts, strict);
let mut events: Vec<HookEvent> = result
.events
.into_iter()
.map(|event| match event {
HookEvent::PolicyShadowObserved { key, would, .. } => HookEvent::PolicyShadowObserved {
key,
would,
action: Some(action.clone()),
},
event => event,
})
.collect();
if let Some(key) = evaluation.bypass_key {
events.push(HookEvent::UnclassifiedPolicyLiteralBypass { key });
}
Some((result.decision, events))
}
pub(crate) fn policy_core_verdicts(
verdicts: &[mati_core::mcp::protocol::PolicyVerdict],
introspection_exemption: bool,
codex_agent: bool,
) -> Vec<decide::PolicyVerdict> {
verdicts
.iter()
.filter_map(|verdict| {
if introspection_exemption
&& matches!(verdict.stage, mati_core::store::PolicyStage::Shadow)
&& matches!(verdict.mode, mati_core::store::PolicyMode::Block)
{
return None;
}
let no_accepted_sources = verdict.via.is_empty();
let codex_unfulfillable = codex_agent
&& !verdict
.via
.iter()
.any(|source| mati_core::store::CODEX_PRODUCIBLE_SOURCES.contains(source));
let unfulfillable = matches!(verdict.stage, mati_core::store::PolicyStage::Enforce)
&& matches!(verdict.mode, mati_core::store::PolicyMode::Block)
&& (no_accepted_sources || codex_unfulfillable);
Some(decide::PolicyVerdict {
key: verdict.key.clone(),
rule: if unfulfillable {
let accepted = if no_accepted_sources {
"no sources".to_string()
} else {
verdict
.via
.iter()
.map(|source| format!("{source:?}"))
.collect::<Vec<_>>()
.join(", ")
};
if no_accepted_sources {
format!(
concat!(
"mati: policy {} cannot be satisfied because requires.via accepts {}; ",
"this policy is steering only here"
),
verdict.key, accepted
)
} else {
format!(
concat!(
"mati: policy {} cannot be satisfied in Codex because it accepts only {}; ",
"Codex can mint mem_get and hook_context receipts but cannot attest ",
"to those sources, so this policy is steering only here"
),
verdict.key, accepted
)
}
} else {
verdict.rule.clone()
},
requires_key: verdict.requires_key.clone(),
block: matches!(verdict.mode, mati_core::store::PolicyMode::Block)
&& !unfulfillable
&& (!introspection_exemption || verdict.satisfied),
satisfied: verdict.satisfied,
stage: verdict.stage,
})
})
.collect()
}
pub(crate) async fn policy_verdicts_for_action(
mati_root: &Path,
action: &decide::Action,
actor_scope: Option<&str>,
failure_path: &str,
raw_command: Option<&str>,
) -> Option<mati_core::mcp::protocol::PolicyEvaluateResult> {
let response = daemon_v2(
mati_root,
mati_core::mcp::protocol::Command::PolicyEvaluate(
mati_core::mcp::protocol::PolicyEvaluateInput {
action: action.clone(),
actor: actor_scope.map(str::to_string),
raw_command: raw_command.map(str::to_string),
},
),
)
.await;
let data = match response {
DaemonResult::Ok(resp) => match daemon_data(&resp) {
Some(data) => data,
None => {
log_fail_open(failure_path, "policy_evaluate returned error");
return None;
}
},
_ => {
log_fail_open(failure_path, "policy_evaluate failed");
return None;
}
};
match serde_json::from_value(data) {
Ok(verdicts) => Some(verdicts),
Err(_) => {
log_fail_open(failure_path, "policy_evaluate returned invalid data");
None
}
}
}
pub(crate) fn policy_block_exempt(variant: HookVariant, input: &serde_json::Value) -> bool {
matches!(
variant,
HookVariant::ClaudePreBash | HookVariant::CodexPreBash
) && input
.pointer("/tool_input/command")
.and_then(|value| value.as_str())
.is_some_and(decide::is_schema_introspection)
}
pub(crate) fn merge_policy_result(
adapter: &mut AdapterResult,
variant: HookVariant,
policy_decision: Decision,
policy_events: Vec<HookEvent>,
) {
let merged_decision = match policy_decision {
Decision::Deny { .. } => policy_decision,
Decision::Advisory {
context: policy_context,
} => match &adapter.decision {
Decision::Deny { .. } => adapter.decision.clone(),
Decision::Advisory { context } => Decision::Advisory {
context: format!("{context}\n{policy_context}"),
},
Decision::AlreadyConsulted { .. } if variant == HookVariant::ClaudePreEdit => {
Decision::Advisory {
context: policy_context,
}
}
Decision::AlreadyConsulted { context } => Decision::AlreadyConsulted {
context: format!("{context}\n{policy_context}"),
},
Decision::Liability { staleness, context } => Decision::Liability {
staleness: *staleness,
context: format!("{context}\n{policy_context}"),
},
_ => Decision::Advisory {
context: policy_context,
},
},
Decision::Allow => adapter.decision.clone(),
_ => adapter.decision.clone(),
};
adapter.decision = merged_decision;
adapter.events.extend(policy_events);
let (stdout, stderr, exit_code) = format_decision(variant, &adapter.decision, "<policy>");
adapter.stdout = stdout;
adapter.stderr = stderr;
adapter.exit_code = exit_code;
}