fix(missions): the container tier now records its tool calls — verified live
deploy / test (push) Successful in 4m53s
deploy / build (push) Failing after 5m30s

Ran it end to end on a real mission. First time the container tier has ever
been observable:

  tool.call   10    Bash 6, Read 3, Write 1
  file.touch   4    research/tapproof.md
  reasoning    5
  prompt.composed 5

Three defects found by running it, each of which left every other link
looking correct:

1. The settings document pointed PostToolUse at {TAP_DIR}/tap.sh while the
   installer wrote {HOOK_DIR}/tap.sh. Claude Code does not complain about a
   hook command that does not exist — it records nothing. Asserting the
   script "mentions tap.sh" had passed; the PATHS have to be compared, and
   a test now does that for every hook the document names.

2. The mission container runs CLAWMATES_RUNTIME_IMAGE, not the shared
   runtime container I had swapped. It was still on an image whose daemon
   schema has no `settings` field, so set_claude_cli_settings returned
   404 path_not_found — which the error message said plainly, and which is
   the only reason this was quick to spot.

3. The sweep used connect_with_local_defaults(). The server reaches Docker
   through a socket proxy (DOCKER_HOST), so that connector fails there — and
   my code returned Ok(()) on the error, silently. The tap filled up, the
   query matched rows, and nothing ran. Now uses container_exec::connect and
   logs the failure; a test pins the choice.

All three are the same shape as the bug they were chasing: installed,
inert, indistinguishable from working. The tests added for each compare the
two ends rather than asserting a string appears somewhere.

Full workspace suite green: 107 binaries, 412 lib tests.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-21 06:29:59 -07:00
co-authored by Claude Opus 5
parent cd59e4798d
commit e84413d437
2 changed files with 73 additions and 4 deletions
+62 -2
View File
@@ -77,8 +77,8 @@ fn build_install_script() -> String {
mkdir -p {hooks} {tap}\n\ mkdir -p {hooks} {tap}\n\
cat > {hooks}/tool-gate.sh <<'CM_GATE_EOF'\n{gate}\nCM_GATE_EOF\n\ cat > {hooks}/tool-gate.sh <<'CM_GATE_EOF'\n{gate}\nCM_GATE_EOF\n\
chmod +x {hooks}/tool-gate.sh\n\ chmod +x {hooks}/tool-gate.sh\n\
cat > {hooks}/tap.sh <<'CM_TAP_EOF'\n{tap_script}\nCM_TAP_EOF\n\ cat > {tap}/tap.sh <<'CM_TAP_EOF'\n{tap_script}\nCM_TAP_EOF\n\
chmod +x {hooks}/tap.sh\n\ chmod +x {tap}/tap.sh\n\
cat > {settings_path} <<'CM_SETTINGS_EOF'\n{settings}\nCM_SETTINGS_EOF\n", cat > {settings_path} <<'CM_SETTINGS_EOF'\n{settings}\nCM_SETTINGS_EOF\n",
hooks = HOOK_DIR, hooks = HOOK_DIR,
tap = TAP_DIR, tap = TAP_DIR,
@@ -126,6 +126,40 @@ pub async fn drain(docker: &Docker, container: &str) -> Vec<crate::vm_tool_tap::
mod tests { mod tests {
use super::*; use super::*;
/// Every command the settings document names must be a file the installer
/// actually writes.
///
/// This caught a real one: the document pointed PostToolUse at
/// `{TAP_DIR}/tap.sh` while the installer wrote `{HOOK_DIR}/tap.sh`, so
/// the hook referenced a file that did not exist. Claude Code does not
/// complain about a missing hook command — it simply records nothing, and
/// a mission ran with the tap installed, pointed at nothing, and silent.
///
/// Asserting that the script "mentions tap.sh" did not catch it. The paths
/// have to be compared.
#[test]
fn every_hook_command_is_a_file_the_installer_writes() {
let settings = crate::vm_tool_tap::guest_settings(None, Some(TAP_DIR), Some(HOOK_DIR));
let script = build_install_script();
let hooks = settings["hooks"].as_object().expect("hooks");
assert!(!hooks.is_empty(), "no hooks at all");
for (event, entries) in hooks {
let cmd = entries[0]["hooks"][0]["command"]
.as_str()
.unwrap_or_else(|| panic!("{event} has no command"));
assert!(
script.contains(&format!("cat > {cmd} <<")),
"{event} points at {cmd}, which the installer never writes — \
the hook is registered and inert"
);
assert!(
script.contains(&format!("chmod +x {cmd}")),
"{event} points at {cmd}, which is never made executable"
);
}
}
#[test] #[test]
fn the_script_writes_both_hooks_and_the_settings_document() { fn the_script_writes_both_hooks_and_the_settings_document() {
let s = build_install_script(); let s = build_install_script();
@@ -209,6 +243,32 @@ mod tests {
); );
} }
/// The drain must use the connector that honours DOCKER_HOST.
///
/// The server reaches Docker through a socket proxy, so
/// `connect_with_local_defaults` fails there — and it failed SILENTLY,
/// which meant the sweep did nothing while the tap filled up and every
/// other link in the chain looked correct. Cost a full diagnostic cycle.
#[test]
fn the_sweep_connects_the_way_the_rest_of_the_server_does() {
let runner = include_str!("phase_runner.rs");
let body = runner
.split("async fn drain_finished_container_phases(")
.nth(1)
.and_then(|s| s.split("\nasync fn ").next())
.expect("sweep body");
assert!(
body.contains("container_exec::connect()"),
"the sweep must use the DOCKER_HOST-aware connector"
);
assert!(
// The CALL, not the word: the comment above it names the
// connector it is warning against.
!body.contains("connect_with_local_defaults()"),
"the local-socket connector fails behind the socket proxy"
);
}
/// The generated installer must be valid shell — a here-doc or quoting slip /// The generated installer must be valid shell — a here-doc or quoting slip
/// makes it fail in the container, where the only symptom is a mission that /// makes it fail in the container, where the only symptom is a mission that
/// silently runs unhooked. /// silently runs unhooked.
+10 -1
View File
@@ -541,8 +541,17 @@ async fn drain_finished_container_phases(pool: &PgPool) -> Result<(), String> {
let mission_id: Uuid = row.get("mission_id"); let mission_id: Uuid = row.get("mission_id");
let container: String = row.get("runtime_container_name"); let container: String = row.get("runtime_container_name");
let Ok(docker) = bollard::Docker::connect_with_local_defaults() else { // `container_exec::connect`, NOT connect_with_local_defaults: the
// server reaches Docker through a socket proxy (DOCKER_HOST), so the
// local-socket connector fails — and it failed SILENTLY here, so the
// sweep did nothing while the tap filled up and every other link in
// the chain looked correct.
let docker = match crate::container_exec::connect() {
Ok(d) => d,
Err(e) => {
eprintln!("phase_runner: cannot reach docker to drain tool taps: {e}");
return Ok(()); return Ok(());
}
}; };
let tools = crate::container_tool_hooks::drain(&docker, &container).await; let tools = crate::container_tool_hooks::drain(&docker, &container).await;
if tools.is_empty() { if tools.is_empty() {