sec(gate): every denial names its rule; write tools are judged by path; the container tier records denials
Rule ids on the gate's rules, recorded on every denial as
{"rule":"<id>","payload":<hook event>} and read back by
vm_tool_gate::denial_detail into gate.denied events, so 'which rule fires
and how often' is a query on detail->>'rule'. Older bare-payload lines
still parse, with no rule.
A write-path policy: Write/Edit/MultiEdit/NotebookEdit are refused when
aimed at the hooks, their records, or the settings that install them, on
either tier (/root/toolgate, /root/toolhooks, /root/tap, the guest and
.claude settings, and .git/hooks/ — a hook in the checkout runs with the
agent's authority unseen). An agent could Write over denied.jsonl and erase
its own denials; now it cannot, and the same paths are refused to Bash
whatever the tool in front of them (sed -i is a text tool that writes).
The extractor prints file_path as a third line; the predicate and the
generated shell are tested against the same payloads, and the shell test
now reads the record back.
The container tier drained the tap and the inert marker and never the
denials: gate.denied existed only for microVM phases. drain_denied fixes
that. Harness: a gatepolicy scenario with two negative controls on the
container tier, asserting the two rule ids in the record.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
co-authored by
Claude Opus 5
parent
13f7fb3aff
commit
3909fa14ca
@@ -651,6 +651,22 @@ async fn drain_finished_container_phases(pool: &PgPool) -> Result<(), String> {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// What the gate refused. Until 2026-09-20 this tier drained the inert
|
||||
// marker and the tap and never the denials, so `gate.denied` existed
|
||||
// only for microVM phases — a container-tier agent's blocked curl
|
||||
// left a line in the guest and nothing in the record.
|
||||
for line in crate::container_tool_hooks::drain_denied(&docker, &container).await {
|
||||
crate::mission_events::record(
|
||||
pool,
|
||||
crate::mission_events::MissionEvent::new(
|
||||
mission_id,
|
||||
crate::container_tool_hooks::GATE_DENIED,
|
||||
)
|
||||
.phase(phase_id)
|
||||
.detail(crate::vm_tool_gate::denial_detail(&line)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let tools = crate::container_tool_hooks::drain(&docker, &container).await;
|
||||
if tools.is_empty() {
|
||||
continue;
|
||||
@@ -1826,11 +1842,13 @@ async fn launch_microvm_phase(
|
||||
.await;
|
||||
}
|
||||
for line in &g.denied {
|
||||
let detail = serde_json::from_str::<serde_json::Value>(line)
|
||||
.unwrap_or_else(|_| serde_json::json!({ "raw": line }));
|
||||
let detail = crate::vm_tool_gate::denial_detail(line);
|
||||
crate::mission_events::record(
|
||||
&pool2,
|
||||
crate::mission_events::MissionEvent::new(mission_id, "gate.denied")
|
||||
crate::mission_events::MissionEvent::new(
|
||||
mission_id,
|
||||
crate::container_tool_hooks::GATE_DENIED,
|
||||
)
|
||||
.phase(phase_id)
|
||||
.run(run_id)
|
||||
.detail(detail),
|
||||
|
||||
Reference in New Issue
Block a user