research_container: template daemon config from shared runtime
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:
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user