research_container: prewrite daemon config with require_pairing=false
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:
@@ -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()),
|
||||||
|
|||||||
Reference in New Issue
Block a user