Compare commits
2
Commits
bf32da949f
...
0a647c0bfa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a647c0bfa | ||
|
|
06ae608d0c |
@@ -11,7 +11,13 @@
|
|||||||
//! against the world) is layered on top by Slices 5–8.
|
//! against the world) is layered on top by Slices 5–8.
|
||||||
//!
|
//!
|
||||||
//! Design notes:
|
//! Design notes:
|
||||||
//! - Runtime provisioning is opt-in via `RuntimeProvisioner::from_env`.
|
//! - Runtime provisioning is opt-in. Claws are provisioned against the
|
||||||
|
//! mission's OWN daemon (`RuntimeProvisioner::for_gateway` with the
|
||||||
|
//! per-mission endpoint), falling back to the global gateway only when
|
||||||
|
//! there is no per-mission runtime. Provisioning into the global gateway
|
||||||
|
//! while the run executes on a per-mission daemon leaves that daemon
|
||||||
|
//! without the `claw_*` agents — it falls back to the default `scout`
|
||||||
|
//! agent, which cannot see `/mission/repo`.
|
||||||
//! Missing runtime = "insert DB rows only, no live claw" — the
|
//! Missing runtime = "insert DB rows only, no live claw" — the
|
||||||
//! mission still boots; live claws land the moment the runtime
|
//! mission still boots; live claws land the moment the runtime
|
||||||
//! env is configured + the mission re-launches.
|
//! env is configured + the mission re-launches.
|
||||||
@@ -70,9 +76,13 @@ pub async fn on_launch(
|
|||||||
// running. Falls back silently when docker is unreachable so
|
// running. Falls back silently when docker is unreachable so
|
||||||
// dev-mode + tests still work — the topology_worker will use the
|
// dev-mode + tests still work — the topology_worker will use the
|
||||||
// shared runtime endpoint in that case.
|
// shared runtime endpoint in that case.
|
||||||
|
// The mission's own runtime endpoint. Claws MUST be provisioned against
|
||||||
|
// THIS gateway, not the global one — see RuntimeProvisioner::for_gateway.
|
||||||
|
let mut mission_gateway: Option<String> = None;
|
||||||
if let Some(prov) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
|
if let Some(prov) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
|
||||||
match prov.ensure_container(mission_id).await {
|
match prov.ensure_container(mission_id).await {
|
||||||
Ok(ec) => {
|
Ok(ec) => {
|
||||||
|
mission_gateway = Some(ec.endpoint.clone());
|
||||||
let container_name = crate::mission_runtime::container_name(mission_id);
|
let container_name = crate::mission_runtime::container_name(mission_id);
|
||||||
if let Err(e) = cm_db::repo::missions::set_runtime_binding(
|
if let Err(e) = cm_db::repo::missions::set_runtime_binding(
|
||||||
pool,
|
pool,
|
||||||
@@ -157,7 +167,13 @@ pub async fn on_launch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let provisioner = RuntimeProvisioner::from_env();
|
// Provision into the mission's own daemon when we have one (so the daemon
|
||||||
|
// that actually runs the turns knows these claws); fall back to the global
|
||||||
|
// gateway only for dev/no-docker setups where the run uses it too.
|
||||||
|
let provisioner = match mission_gateway.clone() {
|
||||||
|
Some(url) => RuntimeProvisioner::for_gateway(url),
|
||||||
|
None => RuntimeProvisioner::from_env(),
|
||||||
|
};
|
||||||
let mut first_team_id: Option<Uuid> = None;
|
let mut first_team_id: Option<Uuid> = None;
|
||||||
let mut provisioned_claws: Vec<cm_domain::AgentId> = Vec::new();
|
let mut provisioned_claws: Vec<cm_domain::AgentId> = Vec::new();
|
||||||
for (purpose, template_id) in &picks {
|
for (purpose, template_id) in &picks {
|
||||||
@@ -213,17 +229,29 @@ pub async fn on_launch(
|
|||||||
// the freshly-provisioned claws for the run. Non-fatal: without the
|
// the freshly-provisioned claws for the run. Non-fatal: without the
|
||||||
// pin, agents still write (to the sandbox) but the committer can't
|
// pin, agents still write (to the sandbox) but the committer can't
|
||||||
// find the changes in /mission/repo.
|
// find the changes in /mission/repo.
|
||||||
if !provisioned_claws.is_empty() {
|
if !provisioned_claws.is_empty() && mission_gateway.is_some() {
|
||||||
if let Some(mp) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
|
if let Some(mp) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
|
||||||
if let Err(e) = mp
|
match mp
|
||||||
.pin_agent_workspaces(mission_id, &provisioned_claws, "/mission/repo")
|
.pin_agent_workspaces(mission_id, &provisioned_claws, "/mission/repo")
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
Ok(()) => {
|
||||||
|
// The daemon reads config ONCE at boot and never re-reads
|
||||||
|
// the file, so the pin is invisible until it restarts. Its
|
||||||
|
// agents were created through its own config API, so they
|
||||||
|
// are already persisted to the file and survive the
|
||||||
|
// restart; the pairing code is re-minted on every launch.
|
||||||
|
if let Err(e) = mp.restart_container(mission_id).await {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"mission_orchestrator: pin workspaces for mission {mission_id} failed (continuing): {e}"
|
"mission_orchestrator: restart runtime for {mission_id} failed (continuing, workspace pin will not apply): {e}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => eprintln!(
|
||||||
|
"mission_orchestrator: pin workspaces for mission {mission_id} failed (continuing): {e}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Herdr second-runtime: if runtime_kind='local_herdr', spawn a
|
// Herdr second-runtime: if runtime_kind='local_herdr', spawn a
|
||||||
|
|||||||
@@ -482,6 +482,46 @@ fn extract_pairing_code_from_json(body: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MissionRuntimeProvisioner {
|
impl MissionRuntimeProvisioner {
|
||||||
|
/// Restart the mission's runtime container and wait for its gateway to
|
||||||
|
/// answer again. Needed after `pin_agent_workspaces`: the daemon reads
|
||||||
|
/// config once at boot and never re-reads the file, so a file-only setting
|
||||||
|
/// (`workspace.path`, which the config prop API cannot set) only takes
|
||||||
|
/// effect across a restart. Agents provisioned through the daemon's own
|
||||||
|
/// config API are already persisted to that file, so they survive.
|
||||||
|
pub async fn restart_container(&self, mission_id: Uuid) -> Result<(), String> {
|
||||||
|
let name = container_name(mission_id);
|
||||||
|
self.docker
|
||||||
|
.restart_container(&name, None::<bollard::query_parameters::RestartContainerOptions>)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("restart mission runtime container: {e}"))?;
|
||||||
|
// Wait for the gateway to serve again so the caller can launch a run
|
||||||
|
// immediately after. ~20s ceiling; the daemon normally boots in ~2s.
|
||||||
|
for _ in 0..40 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
// NOTE: exec_capture only fails on docker errors — a curl that
|
||||||
|
// can't connect still "succeeds" (it prints `curl: (7) …`), so we
|
||||||
|
// must inspect the BODY. /health answers `{"paired":…}`.
|
||||||
|
let body = self
|
||||||
|
.exec_capture(
|
||||||
|
&name,
|
||||||
|
vec![
|
||||||
|
"curl".into(),
|
||||||
|
"-fsS".into(),
|
||||||
|
"-m".into(),
|
||||||
|
"2".into(),
|
||||||
|
format!("http://127.0.0.1:{GATEWAY_PORT}/health"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
if body.contains("\"paired\"") {
|
||||||
|
eprintln!("mission_runtime: restarted {name}, gateway healthy");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(format!("{name} gateway did not come back after restart"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Force-remove the mission's runtime container AND its host workspace
|
/// Force-remove the mission's runtime container AND its host workspace
|
||||||
/// dir (the `/mission/repo` checkout bind source). Idempotent: a missing
|
/// dir (the `/mission/repo` checkout bind source). Idempotent: a missing
|
||||||
/// container or dir is not an error — this is called both by the terminal
|
/// container or dir is not an error — this is called both by the terminal
|
||||||
|
|||||||
@@ -66,12 +66,26 @@ impl RuntimeProvisioner {
|
|||||||
let gateway_url = std::env::var("ZEROCLAW_GATEWAY_URL")
|
let gateway_url = std::env::var("ZEROCLAW_GATEWAY_URL")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|u| !u.is_empty())?;
|
.filter(|u| !u.is_empty())?;
|
||||||
|
Self::for_gateway(gateway_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a provisioner aimed at a SPECIFIC gateway, reusing the durable
|
||||||
|
/// `ZEROCLAW_TOKEN`. Mirrors `ZeroClawDriveExecutor::from_env_for_gateway`.
|
||||||
|
///
|
||||||
|
/// Missions MUST use this with their own per-mission runtime endpoint:
|
||||||
|
/// each mission runs its turns against its own daemon, and that daemon
|
||||||
|
/// loads config once at boot and never re-reads the file. Provisioning a
|
||||||
|
/// mission's claws against the global gateway therefore leaves the
|
||||||
|
/// per-mission daemon with no `claw_*` agents at all — it silently falls
|
||||||
|
/// back to the default agent (`scout`), which is jailed to the global
|
||||||
|
/// workspace and cannot see `/mission/repo`.
|
||||||
|
pub fn for_gateway(gateway_url: String) -> Option<RuntimeProvisioner> {
|
||||||
let token = std::env::var("ZEROCLAW_TOKEN")
|
let token = std::env::var("ZEROCLAW_TOKEN")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|t| !t.is_empty())?;
|
.filter(|t| !t.is_empty())?;
|
||||||
Some(RuntimeProvisioner {
|
Some(RuntimeProvisioner {
|
||||||
http: reqwest::Client::new(),
|
http: reqwest::Client::new(),
|
||||||
gateway_url,
|
gateway_url: gateway_url.trim_end_matches('/').to_string(),
|
||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ if [ -z "${IMAGES_ONLY:-}" ]; then
|
|||||||
ssh "$BUILD_HOST" 'set -e; cd ~/clawmates
|
ssh "$BUILD_HOST" 'set -e; cd ~/clawmates
|
||||||
export PATH=$HOME/.cargo/bin:$PATH CARGO_NET_GIT_FETCH_WITH_CLI=true SQLX_OFFLINE=true
|
export PATH=$HOME/.cargo/bin:$PATH CARGO_NET_GIT_FETCH_WITH_CLI=true SQLX_OFFLINE=true
|
||||||
cargo build --release -p clawmates-node
|
cargo build --release -p clawmates-node
|
||||||
|
# rsync excludes frontend/public/dl/ (build artifacts), so the dir may not
|
||||||
|
# exist on the build host — create it before staging the daemon binary.
|
||||||
|
mkdir -p frontend/public/dl
|
||||||
cp target/release/clawmates-node frontend/public/dl/clawmates-node-linux-amd64
|
cp target/release/clawmates-node frontend/public/dl/clawmates-node-linux-amd64
|
||||||
for svc in server frontend; do
|
for svc in server frontend; do
|
||||||
docker build -f images/$svc.Dockerfile -t '"$REGISTRY"'/clawmates/$svc:'"$TAG"' \
|
docker build -f images/$svc.Dockerfile -t '"$REGISTRY"'/clawmates/$svc:'"$TAG"' \
|
||||||
|
|||||||
Reference in New Issue
Block a user