fix(mission_runtime): seed per-mission gateway with shared pairing state
Fresh mission runtime containers had no ZEROCLAW pairing token so the topology_worker got 401 Unauthorized on WS connect. Mount the shared runtimes /root/clawmates-runtime/data as /zeroclaw-data so the gateway boots pre-paired and accepts the servers ZEROCLAW_TOKEN. Seed dir overridable via CLAWMATES_RUNTIME_SEED_DIR. Known caveat: sqlite sessions dir is shared across concurrent mission runtimes. Fine while topology_worker runs sequentially per mission; next iteration should copy-on-write per-mission.
This commit is contained in:
@@ -53,6 +53,20 @@ const EDGE_NETWORK: &str = "clawmates_edge";
|
|||||||
/// by `clawmates-runtime.service`.
|
/// by `clawmates-runtime.service`.
|
||||||
const MISSIONS_HOST_ROOT: &str = "/var/lib/clawmates-missions";
|
const MISSIONS_HOST_ROOT: &str = "/var/lib/clawmates-missions";
|
||||||
|
|
||||||
|
/// Host path holding the pre-paired ZeroClaw state (config.toml,
|
||||||
|
/// pairing key, brains, agent definitions) that the shared
|
||||||
|
/// `clawmates-runtime.service` uses. Per-mission runtime containers
|
||||||
|
/// mount this same dir so their gateway accepts the server's already-
|
||||||
|
/// provisioned ZEROCLAW_TOKEN instead of demanding a fresh /pair
|
||||||
|
/// handshake. Overridable via `CLAWMATES_RUNTIME_SEED_DIR` for
|
||||||
|
/// dev / test.
|
||||||
|
///
|
||||||
|
/// Known caveat: the sqlite sessions dir under here is currently
|
||||||
|
/// shared across all concurrent mission runtimes. In practice missions
|
||||||
|
/// don't collide often (topology_worker runs them one at a time per
|
||||||
|
/// mission) but this is an obvious next-iteration split point.
|
||||||
|
const DEFAULT_SEED_DIR: &str = "/root/clawmates-runtime/data";
|
||||||
|
|
||||||
/// Deterministic docker container name for a mission's runtime.
|
/// Deterministic docker container name for a mission's runtime.
|
||||||
/// Uses the full UUID hex — UUIDv7 encodes time in the leading bytes,
|
/// Uses the full UUID hex — UUIDv7 encodes time in the leading bytes,
|
||||||
/// so a short prefix isn't guaranteed unique across missions minted
|
/// so a short prefix isn't guaranteed unique across missions minted
|
||||||
@@ -129,6 +143,8 @@ impl MissionRuntimeProvisioner {
|
|||||||
// present or docker start fails with EACCES/ENOENT.
|
// present or docker start fails with EACCES/ENOENT.
|
||||||
let mission_dir = format!("{MISSIONS_HOST_ROOT}/{mission_id}");
|
let mission_dir = format!("{MISSIONS_HOST_ROOT}/{mission_id}");
|
||||||
let _ = tokio::fs::create_dir_all(&mission_dir).await;
|
let _ = tokio::fs::create_dir_all(&mission_dir).await;
|
||||||
|
let seed_dir =
|
||||||
|
std::env::var("CLAWMATES_RUNTIME_SEED_DIR").unwrap_or_else(|_| DEFAULT_SEED_DIR.to_string());
|
||||||
let mounts = vec![
|
let mounts = vec![
|
||||||
// Mount just this mission's directory. Agents can navigate
|
// Mount just this mission's directory. Agents can navigate
|
||||||
// its `/repo` subdir but never see other missions'.
|
// its `/repo` subdir but never see other missions'.
|
||||||
@@ -139,6 +155,16 @@ impl MissionRuntimeProvisioner {
|
|||||||
read_only: Some(false),
|
read_only: Some(false),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
// Seed /zeroclaw-data from the shared runtime's paired
|
||||||
|
// data dir so the fresh gateway accepts our ZEROCLAW_TOKEN
|
||||||
|
// without a /pair handshake.
|
||||||
|
Mount {
|
||||||
|
target: Some("/zeroclaw-data".to_string()),
|
||||||
|
source: Some(seed_dir),
|
||||||
|
typ: Some(MountTypeEnum::BIND),
|
||||||
|
read_only: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let host_config = HostConfig {
|
let host_config = HostConfig {
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ export function MissionCanvas({
|
|||||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||||
const [runs, setRuns] = useState<MissionRunSummary[]>([]);
|
const [runs, setRuns] = useState<MissionRunSummary[]>([]);
|
||||||
const [lastLoadedAt, setLastLoadedAt] = useState<Date | null>(null);
|
const [lastLoadedAt, setLastLoadedAt] = useState<Date | null>(null);
|
||||||
const [refreshClicks, setRefreshClicks] = useState(0);
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!selectedId) {
|
if (!selectedId) {
|
||||||
|
|||||||
@@ -181,25 +181,13 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
const method = init?.method ?? "GET";
|
const method = init?.method ?? "GET";
|
||||||
const busted =
|
const busted =
|
||||||
method === "GET" ? `${path}${path.includes("?") ? "&" : "?"}_t=${Date.now()}` : path;
|
method === "GET" ? `${path}${path.includes("?") ? "&" : "?"}_t=${Date.now()}` : path;
|
||||||
const t0 = Date.now();
|
const r = await fetch(busted, {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(`[api] → ${method} ${busted}`);
|
|
||||||
let r: Response;
|
|
||||||
try {
|
|
||||||
r = await fetch(busted, {
|
|
||||||
...init,
|
...init,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(init?.headers ?? {}),
|
...(init?.headers ?? {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(`[api] ✗ ${method} ${busted} (${Date.now() - t0}ms):`, e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(`[api] ← ${method} ${busted} (${Date.now() - t0}ms) status=${r.status}`);
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const text = await r.text().catch(() => "");
|
const text = await r.text().catch(() => "");
|
||||||
throw new Error(`${init?.method ?? "GET"} ${path} → ${r.status} ${text}`);
|
throw new Error(`${init?.method ?? "GET"} ${path} → ${r.status} ${text}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user