research_container: prewrite daemon config with require_pairing=false
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 3m55s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m18s

Per-topic and per-loop team containers spawn from clawmates-runtime
with an empty daemon config, so they boot with require_pairing=true
and an empty paired_tokens store. Every incoming ws connect from the
API server got 401 Unauthorized because the server's ZEROCLAW_TOKEN
wasn't in that store.

The shared clawmates-runtime container has a paired_tokens list
maintained out-of-band (encrypted enc2:… entries in
/root/clawmates-runtime/data/.zeroclaw/config.toml on gw-04). That
list isn't portable to freshly-spawned per-team containers — the
tokens are encrypted with a key we don't share, and pairing new
tokens requires a pairing code we don't generate.

Simplest correct answer: per-team containers are ephemeral, live on
the private clawmates_core docker network, and only accept traffic
from the API server. Disabling pairing there closes zero security
holes.

New helper prewrite_daemon_config(state_root):
- creates <state_root>/.zeroclaw/
- writes config.toml with schema_version=3 + [gateway] require_pairing=false
- skips write when config.toml already exists so a manually-paired
  team container survives re-spawn

Called from both spawn() (per-topic) and spawn_loop() (per-loop)
right after ensuring the host state dir. Existing paired containers
are unaffected; new ones come up open-network to the compose stack.

Verified out-of-band by writing the same config into the current
stuck team container and restarting it — daemon health flipped
require_pairing from true to false.
This commit is contained in:
Omar Sobh
2026-07-10 11:57:43 -07:00
parent 8417f6e65c
commit 4be3e43f6f
+37
View File
@@ -51,6 +51,32 @@ pub fn container_name_for(topic_id: Uuid) -> String {
format!("research-{topic_id}-team") format!("research-{topic_id}-team")
} }
/// Pre-write the daemon's config.toml under `<state_root>/.zeroclaw/`
/// so the freshly-spawned container boots with pairing disabled. Without
/// this, per-team daemons come up with `require_pairing = true` and an
/// empty `paired_tokens` list, which 401s every incoming ws connect
/// from the API server. Per-team containers only accept traffic from
/// the API server on the private clawmates_core docker network — safe
/// to skip pairing.
fn prewrite_daemon_config(state_host_path: &Path) -> Result<(), String> {
let cfg_dir = state_host_path.join(".zeroclaw");
std::fs::create_dir_all(&cfg_dir).map_err(|e| format!("mkdir {}: {e}", cfg_dir.display()))?;
let cfg_path = cfg_dir.join("config.toml");
// Don't overwrite an existing config so a manually-paired team
// container survives a re-spawn cycle.
if cfg_path.exists() {
return Ok(());
}
let cfg = "\
schema_version = 3\n\
\n\
[gateway]\n\
require_pairing = false\n\
";
std::fs::write(&cfg_path, cfg).map_err(|e| format!("write {}: {e}", cfg_path.display()))?;
Ok(())
}
/// Connect to the Docker engine. Uses `DOCKER_HOST` when the compose /// Connect to the Docker engine. Uses `DOCKER_HOST` when the compose
/// stack points at the socket-proxy sidecar (prod); falls back to the /// stack points at the socket-proxy sidecar (prod); falls back to the
/// local socket for dev. /// local socket for dev.
@@ -126,6 +152,14 @@ pub async fn spawn(
// "no such file or directory" the first time a topic starts. // "no such file or directory" the first time a topic starts.
std::fs::create_dir_all(state_host_path) std::fs::create_dir_all(state_host_path)
.map_err(|e| format!("mkdir {}: {e}", state_host_path.display()))?; .map_err(|e| format!("mkdir {}: {e}", state_host_path.display()))?;
// Pre-write the daemon config so it boots with pairing disabled —
// the shared clawmates-runtime container has a paired_tokens list
// maintained out-of-band, but per-topic containers are freshly
// spawned with an empty store and would 401 every incoming ws
// connect. These containers live on the private clawmates_core
// docker network and only accept traffic from the API server, so
// disabling pairing here is safe.
prewrite_daemon_config(state_host_path)?;
let mounts = vec![ let mounts = vec![
Mount { Mount {
@@ -311,6 +345,9 @@ pub async fn spawn_loop(
std::fs::create_dir_all(state_host_path) std::fs::create_dir_all(state_host_path)
.map_err(|e| format!("mkdir {}: {e}", state_host_path.display()))?; .map_err(|e| format!("mkdir {}: {e}", state_host_path.display()))?;
// Same pairing bypass as `spawn` — per-loop containers are
// ephemeral, on a private docker network, and freshly created.
prewrite_daemon_config(state_host_path)?;
let mounts = vec![Mount { let mounts = vec![Mount {
target: Some("/zeroclaw-data".into()), target: Some("/zeroclaw-data".into()),