research_container: template daemon config from shared runtime
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 39s
ci / rust (push) Successful in 3m11s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 3m50s

Per-team daemon boots with require_pairing=false but no
`[agents.*]` sections. Server's ws connect authenticates fine and
then trips over "Unknown agent `coordinator` — no [agents.coordinator]
entry configured." (400).

Fix: prewrite_daemon_config now reads a template config from
CLAWMATES_RUNTIME_TEMPLATE_CONFIG (default
/var/lib/clawmates-runtime-template/config.toml) which mirrors the
shared clawmates-runtime container's config with all agent + provider
sections. We strip the template's [gateway] block (its paired_tokens
list is encrypted with the shared runtime's key and un-decryptable
per-team) and prepend a fresh [gateway] require_pairing = false.

Falls back to a minimal pairing-off config with a loud eprintln when
the template isn't readable — the log line makes the misconfig
visible instead of silently 400-ing.

Verified out-of-band on the current stuck team container:
- Restarted with the shared config + rewritten [gateway] section
- Daemon boots cleanly, logs "Pairing: DISABLED (all requests accepted)"
- ws /ws/chat handshake returns 101 Switching Protocols + session_start
  message (auth working end-to-end)

gw-04 deploy step (already applied):
1. sudo mkdir -p /var/lib/clawmates-runtime-template
2. sudo cp /root/clawmates-runtime/data/.zeroclaw/config.toml \
       /var/lib/clawmates-runtime-template/config.toml
3. sudo chown -R 65532:65532 /var/lib/clawmates-runtime-template
4. Compose: added
   /var/lib/clawmates-runtime-template:/var/lib/clawmates-runtime-template:ro
   + CLAWMATES_RUNTIME_TEMPLATE_CONFIG env
   to /opt/clawmates/docker-compose.yml
   (backup: docker-compose.yml.bak-template)

Follow-up: expose a "reload template" endpoint or re-copy the shared
config on each server boot so we don't drift when the shared runtime
adds a new agent.
This commit is contained in:
Omar Sobh
2026-07-10 12:42:22 -07:00
parent 4be3e43f6f
commit fd82fe6762
+54 -8
View File
@@ -62,21 +62,67 @@ fn prewrite_daemon_config(state_host_path: &Path) -> Result<(), String> {
let cfg_dir = state_host_path.join(".zeroclaw"); let cfg_dir = state_host_path.join(".zeroclaw");
std::fs::create_dir_all(&cfg_dir).map_err(|e| format!("mkdir {}: {e}", cfg_dir.display()))?; std::fs::create_dir_all(&cfg_dir).map_err(|e| format!("mkdir {}: {e}", cfg_dir.display()))?;
let cfg_path = cfg_dir.join("config.toml"); 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() { if cfg_path.exists() {
return Ok(()); return Ok(());
} }
let cfg = "\ // Prefer the shared runtime's config as a template so per-team
schema_version = 3\n\ // daemons come up with the full `[agents.*]` + `[providers.*]`
\n\ // sections. Without them the daemon rejects ws connects like
[gateway]\n\ // `?agent=coordinator` with a 400 "Unknown agent". Template path
require_pairing = false\n\ // set on gw-04 via CLAWMATES_RUNTIME_TEMPLATE_CONFIG (bind-mounted
"; // from the shared runtime container's config).
//
// We strip the template's `[gateway]` block — its `paired_tokens`
// list is encrypted with the shared runtime's key and won't
// decrypt on a fresh per-team daemon — and replace it with a
// clean `[gateway] require_pairing = false`. Per-team containers
// live on the private clawmates_core network and only accept
// traffic from the API server, so disabling pairing there closes
// no security holes.
let template_path = std::env::var("CLAWMATES_RUNTIME_TEMPLATE_CONFIG")
.unwrap_or_else(|_| "/var/lib/clawmates-runtime-template/config.toml".to_string());
let cfg = match std::fs::read_to_string(&template_path) {
Ok(t) => rewrite_gateway_section(&t),
Err(e) => {
eprintln!(
"prewrite_daemon_config: template {template_path} not readable ({e}) — \
falling back to minimal config, per-team ws connects will 400 on Unknown agent"
);
"schema_version = 3\n\n[gateway]\nrequire_pairing = false\n".to_string()
}
};
std::fs::write(&cfg_path, cfg).map_err(|e| format!("write {}: {e}", cfg_path.display()))?; std::fs::write(&cfg_path, cfg).map_err(|e| format!("write {}: {e}", cfg_path.display()))?;
Ok(()) Ok(())
} }
/// Replace the `[gateway]` section of a TOML string with a clean one
/// that disables pairing. Preserves everything else (agents, providers,
/// etc.) verbatim. The stripped section stops at the next `[header]`.
/// Also drops the template's `schema_version` since we prepend our own.
fn rewrite_gateway_section(src: &str) -> String {
let mut out = String::from("schema_version = 3\n\n[gateway]\nrequire_pairing = false\n");
let mut in_gateway = false;
for line in src.lines() {
if line.starts_with("schema_version") {
continue;
}
if line.starts_with("[gateway]") {
in_gateway = true;
continue;
}
if in_gateway {
if line.starts_with('[') {
in_gateway = false;
} else {
continue;
}
}
out.push('\n');
out.push_str(line);
}
out
}
/// 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.