Files
clawmates/tools/runtime-spike/drive.mjs
T
Omar SobhandClaude Opus 4.8 bd982943b8
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
chore: commit ZeroClaw per-tenant runtime spike + architecture doc
Saves earlier-phase artifacts that were sitting untracked:
- docs/agent-engine-architecture.md — per-tenant containerized ZeroClaw runtime
  decision doc (clawmates = §15 control plane; zeroclaw = per-tenant runtime).
- deploy/clawmates-runtime/ — slim runtime Dockerfile, dev compose, example
  agent config, README (the proven Phase-1 drive recipe).
- tools/runtime-spike/drive.mjs — Node WS drive client for the spike.

No secrets (only env-var names / commented placeholders).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-15 21:29:16 -07:00

66 lines
2.9 KiB
JavaScript

// Phase-1 drive client: pair with a ZeroClaw gateway, open /ws/chat as an
// agent, send one message, and print the streamed turn events. This is the
// Clawmates-side seam that (in prod) the control plane uses to run a claw's
// turn and journal its events. Node >=22 (global WebSocket + fetch).
//
// node tools/runtime-spike/drive.mjs <base_url> <agent_alias> "<message>"
// e.g. node tools/runtime-spike/drive.mjs http://100.108.129.81:42617 scout "Say hi in 5 words"
const [base, agent, message] = process.argv.slice(2);
if (!base || !agent || !message) {
console.error('usage: drive.mjs <base_url> <agent_alias> "<message>"');
process.exit(2);
}
// 1) Pair: read the one-time code from env (printed in the daemon's startup log).
const code = process.env.PAIR_CODE;
if (!code) {
console.error("set PAIR_CODE=<code from daemon startup log>");
process.exit(2);
}
const pairRes = await fetch(`${base}/pair`, {
method: "POST",
headers: { "X-Pairing-Code": code, "Content-Type": "application/json" },
body: "{}",
});
if (!pairRes.ok) {
console.error(`pair failed: ${pairRes.status} ${await pairRes.text()}`);
process.exit(1);
}
const pair = await pairRes.json();
const token = pair.token ?? pair.bearer ?? pair.access_token;
console.log(`✓ paired, token acquired (${String(token).slice(0, 6)}…)`);
// 2) Open the agent chat socket and run one turn.
// Node's global WebSocket can't set headers, so pass the token as ?token=
// (gateway accepts header > subprotocol > query).
const wsUrl =
base.replace(/^http/, "ws") +
`/ws/chat?agent=${encodeURIComponent(agent)}&name=${encodeURIComponent("clawmates-spike")}` +
`&token=${encodeURIComponent(token)}`;
const ws = new WebSocket(wsUrl);
const t0 = Date.now();
ws.addEventListener("open", () => {
console.log("✓ ws open → sending message");
ws.send(JSON.stringify({ type: "message", content: message }));
});
ws.addEventListener("message", (ev) => {
let m;
try { m = JSON.parse(ev.data); } catch { return console.log("raw:", ev.data); }
switch (m.type) {
case "session_start": console.log(` session_start id=${m.session_id} resumed=${m.resumed}`); break;
case "chunk": process.stdout.write(m.content ?? ""); break;
case "tool_call": console.log(`\n [tool_call] ${m.name} ${JSON.stringify(m.args)}`); break;
case "tool_result": console.log(` [tool_result] ${m.name}: ${String(m.output).slice(0, 120)}`); break;
case "approval_request": console.log(`\n [APPROVAL REQUIRED] ${JSON.stringify(m)} ← §15 gate hook`); break;
case "done":
console.log(`\n✓ done in ${Date.now() - t0}ms`);
ws.close(); process.exit(0);
case "error": console.error("\n✗ error:", JSON.stringify(m)); ws.close(); process.exit(1);
default: console.log(" evt:", JSON.stringify(m));
}
});
ws.addEventListener("error", (e) => { console.error("ws error:", e.message ?? e); process.exit(1); });
setTimeout(() => { console.error("timeout (90s)"); process.exit(1); }, 90_000);