research: spawn per-topic ZeroClaw team container on start (commit 1/3)
Commit 1 of the path-B (real per-topic isolation) plan. The
container spawns and its coordinates persist — nothing talks to
it yet; commit 2 wires ZeroClawDriveExecutor to prefer the topic's
URL when populated. This split keeps each landing verifiable.
Backend
- Migration 0038: research_topics gets zeroclaw_container_name +
zeroclaw_gateway_url columns. Both nullable so a topic can exist
before a spawn and teardown just NULLs them out.
- cm-db: ResearchTopic struct extended; get/list SELECTs updated;
new set_zeroclaw_container(id, workspace_id, name, url) helper
used both for spawn (Some/Some) and teardown (None/None).
- cm-api: bollard added as a workspace dep (matches cm-sandbox's
version). New research_container module:
· connect() → uses DOCKER_HOST when set (prod's socket-proxy
at tcp://socket-proxy:2375) else the local socket. Same
pattern cm-sandbox already uses.
· container_name_for(topic_id) → "research-<uuid>-team"
(deterministic so a re-start reattaches to the same
container instead of orphaning it).
· inherited_env() → propagates ZEROCLAW_*, OPENAI_*,
ANTHROPIC_*, GEMINI_*, GROQ_* from the parent server env
(provider config + tokens), stripping the server's own
ZEROCLAW_GATEWAY_URL/WORKSPACE so the team runtime doesn't
loop back on itself. Appends ZEROCLAW_GATEWAY_PORT=42617
and ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace for the
team's own listener.
· spawn(docker, topic_id, repo_host_path, state_host_path):
- inspect: if the container already exists, start it if
stopped and return its coordinates (idempotent restart).
- else create with:
image = CLAWMATES_RESEARCH_TEAM_IMAGE or
clawmates-runtime:latest
cmd = [daemon, --host, 0.0.0.0]
env = inherited_env()
mounts = repo_host_path → /workspace/repo (rw)
state_host_path → /zeroclaw-data (rw)
network = CLAWMATES_RESEARCH_TEAM_NETWORK or
clawmates_core
labels = clawmates.role=research-team,
clawmates.research.topic_id=<uuid>
- creates state_host_path first so bind doesn't ENOENT.
· stop(docker, name) → stop + remove. Idempotent on 404/304.
- start_topic wires spawn after the clone completes:
· state root = CLAWMATES_RESEARCH_WORKSPACE_ROOT / <topic> /
state
· on success, persists (name, url) on the topic row so commit
2 can look them up when constructing the executor
· every failure (docker connect, docker create/start, DB
persist) is best-effort: logs and continues. A missing team
container leaves the topic pointing at the workspace-wide
gateway URL (env), preserving prior behavior.
Deploy prerequisites (not in this commit)
- The compose stack's clawmates_server service needs bind-mounts
of CLAWMATES_RESEARCH_WORKSPACE_ROOT (e.g.
/var/lib/clawmates-research:/var/lib/clawmates-research) so
paths the server writes to are visible on the host and the
spawned team container mounts the same underlying data.
- socket-proxy's ACL must allow POST + DELETE on /containers
(already the case in prod per the audited compose file).
This commit is contained in:
@@ -36,6 +36,15 @@ pub struct ResearchTopic {
|
||||
/// repo. Written once on the first successful clone; subsequent starts
|
||||
/// reuse it. Null until then.
|
||||
pub repo_workspace_path: Option<String>,
|
||||
/// Docker container name of the per-topic ZeroClaw team runtime, e.g.
|
||||
/// "research-<topic_id>-team". Set by `research_container::spawn`;
|
||||
/// cleared by teardown. Also used to look up the container for stop.
|
||||
pub zeroclaw_container_name: Option<String>,
|
||||
/// Reachable URL of the per-topic team's gateway, e.g.
|
||||
/// `http://research-<topic_id>-team:42617`. Persisted so
|
||||
/// topology_worker can point ZeroClawDriveExecutor at the isolated
|
||||
/// endpoint for THIS topic's runs instead of the global env one.
|
||||
pub zeroclaw_gateway_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -79,6 +88,32 @@ pub async fn create(pool: &PgPool, input: NewTopic<'_>) -> Result<Uuid, DbError>
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Persist the per-topic ZeroClaw container coordinates. Called from
|
||||
/// `research_container::spawn` after `docker start` succeeds. Pass `None`
|
||||
/// on both to clear the fields during teardown.
|
||||
pub async fn set_zeroclaw_container(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: Uuid,
|
||||
container_name: Option<&str>,
|
||||
gateway_url: Option<&str>,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"UPDATE research_topics
|
||||
SET zeroclaw_container_name = $3,
|
||||
zeroclaw_gateway_url = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
workspace_id,
|
||||
container_name,
|
||||
gateway_url,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist the clone path for a topic's bound repo. Set once, on the first
|
||||
/// successful clone; a re-start reads it back and skips re-cloning.
|
||||
pub async fn set_repo_workspace_path(
|
||||
@@ -106,7 +141,8 @@ pub async fn list(pool: &PgPool, workspace_id: Uuid) -> Result<Vec<ResearchTopic
|
||||
ResearchTopic,
|
||||
"SELECT id, workspace_id, title, description, outcome_kind, status,
|
||||
created_by, created_at, updated_at, published_at, topology_kind,
|
||||
repo_id, repo_workspace_path
|
||||
repo_id, repo_workspace_path,
|
||||
zeroclaw_container_name, zeroclaw_gateway_url
|
||||
FROM research_topics
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY updated_at DESC",
|
||||
@@ -126,7 +162,8 @@ pub async fn get(
|
||||
ResearchTopic,
|
||||
"SELECT id, workspace_id, title, description, outcome_kind, status,
|
||||
created_by, created_at, updated_at, published_at, topology_kind,
|
||||
repo_id, repo_workspace_path
|
||||
repo_id, repo_workspace_path,
|
||||
zeroclaw_container_name, zeroclaw_gateway_url
|
||||
FROM research_topics
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
|
||||
Reference in New Issue
Block a user