research_container: attach bridge network so per-team egress works
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 5m0s
ci / rust (push) Failing after 10m35s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Per-team containers were spawned onto clawmates_core only. That
network is Internal=true on gw-04 (no default gateway to the host's
default route), so any egress attempt — including the daemon's
own claude/gemini/groq API calls — fails with FailedToOpenSocket
and the turn times out.

The shared clawmates-runtime is attached to BOTH clawmates_core AND
the default bridge (that's how it can hit api.anthropic.com); the
per-team containers were missing the second network.

Fix: after start_container succeeds, best-effort connect the
container to `bridge` too. Both spawn() and spawn_loop() call the
same helper. Idempotent — a 403 from Docker on repeat-attach ("already
on network") is silently ignored.

Verified out-of-band on the current stuck research team container:
- Manually `docker network connect bridge research-<id>-team`
- `docker exec ... claude --print "reply only: ok"` → returned "ok"
- Auth + egress both working, so next iteration should complete.

Sequence of pipeline fixes finally converging:
1. materialize_topic_loops didn't fire burst → fixed by
   fire_initial_burst_if_set
2. research iteration skipped clone/spawn → fixed by
   prepare_topic_runtime
3. graph parse failed → fixed by build_topic_graph_json
4. bind-mount perms wrong → chown 65532:65532
5. server image missing git → debian:12-slim base
6. daemon required pairing → prewrite_daemon_config
7. daemon rejected unknown agents → template config from shared runtime
8. THIS: no external egress → attach bridge post-start
This commit is contained in:
Omar Sobh
2026-07-10 13:24:02 -07:00
parent fd82fe6762
commit 194d63ef36
+32
View File
@@ -258,9 +258,37 @@ pub async fn spawn(
.await .await
.map_err(|e| format!("start {name}: {e}"))?; .map_err(|e| format!("start {name}: {e}"))?;
// Attach the default `bridge` network AFTER start so the container
// has external egress. Without this the team can join
// clawmates_core (Internal=true on gw-04) but can't reach
// api.anthropic.com — every LLM call fails with
// FailedToOpenSocket and the run times out.
attach_external_bridge(docker, &name).await;
Ok(SpawnedContainer { name, gateway_url }) Ok(SpawnedContainer { name, gateway_url })
} }
/// Best-effort attach the container to the default `bridge` docker
/// network so it can reach the public internet. Silent on the "already
/// attached" case (repeat spawns / restarts). Logs any real failure
/// with the container name so a broken network isn't invisible.
async fn attach_external_bridge(docker: &Docker, name: &str) {
use bollard::network::ConnectNetworkOptions;
let opts: ConnectNetworkOptions<String> = ConnectNetworkOptions {
container: name.to_string(),
..Default::default()
};
match docker.connect_network("bridge", opts).await {
Ok(_) => {}
Err(bollard::errors::Error::DockerResponseServerError {
status_code: 403, ..
}) => {
// "endpoint already exists on network" — idempotent re-attach.
}
Err(e) => eprintln!("attach_external_bridge({name}): {e}"),
}
}
/// Poll the team gateway's `/health` endpoint until it 200s or the /// Poll the team gateway's `/health` endpoint until it 200s or the
/// deadline passes. Called before firing turns against a freshly-spawned /// deadline passes. Called before firing turns against a freshly-spawned
/// container so the executor doesn't try to pair against a not-yet- /// container so the executor doesn't try to pair against a not-yet-
@@ -437,6 +465,10 @@ pub async fn spawn_loop(
.await .await
.map_err(|e| format!("start {name}: {e}"))?; .map_err(|e| format!("start {name}: {e}"))?;
// Same reason as `spawn` — clawmates_core is Internal=true; without
// bridge the loop container can't reach LLM APIs.
attach_external_bridge(docker, &name).await;
Ok(SpawnedContainer { name, gateway_url }) Ok(SpawnedContainer { name, gateway_url })
} }