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]>
66 lines
2.9 KiB
JavaScript
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);
|