feat(gate): task permission — the work surface is allowed, the platform is not
deploy / test (push) Successful in 5m40s
deploy / build (push) Successful in 5m47s

ActGov's second layer (arXiv 2609.24446), in the honest form our evidence
supports. The paper binds each task to its minimum tools; 171 recorded
tool calls cannot justify a per-task minimum, but they do justify the line
this draws: files, commands, search, web, delegation and skills are the
work surface and pass; ListAgents, ScheduleWakeup, CronCreate,
SendMessage and the rest reach the platform itself and do not.

That line is not theoretical. ListAgents and ScheduleWakeup were both
called by microVM missions whose --allowedTools is Read Edit Write Bash
Agent. Neither is on that list; both ran, because the flag governs
permission prompting and not availability. Our gate is the only place
this can be enforced.

TaskPolicy is rendered into the same guest script as the floor and the
role policies. A phase names its own set with "agent_tools" — NOT
"tools", which security_scan already owns for its scanner list; both are
now in phase_config::KNOWN_KEYS, adjacent, each saying what the other is.

SHADOW BY DEFAULT. The gate records what it would have refused to
would-deny.jsonl and allows the call; the host drains it into
gate.would_deny on both tiers. CLAWMATES_TASK_PERMISSION=enforce flips
it. A policy tightened on a guess and enforced on day one is how an agent
learns to work around the gate, and a shadow mode nobody can read is an
off switch with extra steps.

The VM probe needed a sentinel: a refusal and a call that merely would
have been refused are both JSON objects with the same keys, and telling
them apart by content would confuse the one distinction shadow mode
exists to make.

Asymmetry, stated rather than hidden: a VM is per-phase and honours the
phase's own agent_tools; a container serves every phase of its mission
and gets the mission-wide default. Narrowing per phase there needs a
re-install between phases and is not done.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-09-22 10:07:58 -05:00
co-authored by Claude Opus 5
parent 2d1f3954e8
commit 79a6119f4e
7 changed files with 411 additions and 18 deletions
+24 -2
View File
@@ -649,7 +649,18 @@ impl MissionRuntimeProvisioner {
// Re-install on reuse: the container outlives the server
// process, and a hook that exists only on first creation is a
// hook that quietly disappears after a redeploy.
let hooks = crate::container_tool_hooks::install(&self.docker, &name).await;
let hooks = crate::container_tool_hooks::install_with(
&self.docker,
&name,
// A container serves every phase of the mission, so the
// policy installed here is the mission-wide default. A
// phase's own `agent_tools` is honoured on the microVM
// tier, where the VM is per-phase; narrowing per phase
// here would need a re-install between phases and is not
// done.
Some(&crate::vm_tool_gate::TaskPolicy::default_shadow()),
)
.await;
let pairing_code = self.mint_pairing_code(&name).await;
return Ok(EnsuredContainer {
endpoint: endpoint_url(&name),
@@ -865,7 +876,18 @@ impl MissionRuntimeProvisioner {
// Gate and observe the tools claude runs inside its own subprocess.
// Best-effort by design: a phase that runs unhooked still delivers, and
// failing the launch to protect telemetry would be the wrong trade.
let hooks = crate::container_tool_hooks::install(&self.docker, &name).await;
let hooks = crate::container_tool_hooks::install_with(
&self.docker,
&name,
// A container serves every phase of the mission, so the
// policy installed here is the mission-wide default. A
// phase's own `agent_tools` is honoured on the microVM
// tier, where the VM is per-phase; narrowing per phase
// here would need a re-install between phases and is not
// done.
Some(&crate::vm_tool_gate::TaskPolicy::default_shadow()),
)
.await;
Ok(EnsuredContainer {
endpoint: endpoint_url(&name),