fix(missions): the container tier now records its tool calls — verified live
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:
co-authored by
Claude Opus 5
parent
cd59e4798d
commit
e84413d437
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
return Ok(());
|
// 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(());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user