| 1 | // codewhale-cu remote agent — runs on an ssh-registered computer. |
| 2 | // One-shot: `node agent.mjs <base64(json)>` prints exactly one JSON receipt |
| 3 | // line. The request is {"tool": "...", "args": {...}}; the shared handler |
| 4 | // enforces the tool allow-list, so the transport can never become a shell. |
| 5 | import { handle } from "./src/app-handler.mjs"; |
| 6 | |
| 7 | function reply(obj) { |
| 8 | process.stdout.write(JSON.stringify(obj) + "\n"); |
| 9 | process.exit(0); |
| 10 | } |
| 11 | |
| 12 | const argv = process.argv.slice(2); |
| 13 | if (!argv[0]) reply({ ok: false, error: { code: "missing_payload", message: "usage: node agent.mjs <base64 payload> | node agent.mjs --serve" } }); |
| 14 | |
| 15 | if (argv[0] === "--serve") { |
| 16 | // Persistent mode: each stdin line is base64 {id, tool, args}; each stdout |
| 17 | // line is the JSON receipt {id, ok, ...}. The process stays alive, so its |
| 18 | // backend retains the open_application binding and owned input between |
| 19 | // calls. The allow-list still applies — the channel cannot become a shell. |
| 20 | let buf = ""; |
| 21 | process.stdin.setEncoding("utf8"); |
| 22 | process.stdin.on("data", (chunk) => { |
| 23 | buf += chunk; |
| 24 | let i; |
| 25 | while ((i = buf.indexOf("\n")) !== -1) { |
| 26 | const line = buf.slice(0, i).trim(); |
| 27 | buf = buf.slice(i + 1); |
| 28 | if (line) void serve(line); |
| 29 | } |
| 30 | }); |
| 31 | process.stdin.on("end", () => process.exit(0)); |
| 32 | } else { |
| 33 | let req; |
| 34 | try { |
| 35 | req = JSON.parse(Buffer.from(argv[0], "base64").toString("utf8")); |
| 36 | } catch { |
| 37 | reply({ ok: false, error: { code: "bad_payload", message: "payload is not base64 JSON" } }); |
| 38 | } |
| 39 | |
| 40 | if (["left_mouse_down", "recordingStart"].includes(req?.tool)) { |
| 41 | reply({ ok: false, error: { code: "persistent_session_required", message: "This operation needs a persistent computer session; the one-shot SSH agent exits after each request. Upgrade the plugin so it serves the agent over one connection, or use a complete drag gesture." } }); |
| 42 | } |
| 43 | |
| 44 | reply(await handle(req, { computerId: "remote" })); |
| 45 | } |
| 46 | |
| 47 | async function serve(line) { |
| 48 | let req; |
| 49 | try { |
| 50 | req = JSON.parse(Buffer.from(line, "base64").toString("utf8")); |
| 51 | } catch { |
| 52 | process.stdout.write(JSON.stringify({ id: null, ok: false, error: { code: "bad_payload", message: "line is not base64 JSON" } }) + "\n"); |
| 53 | return; |
| 54 | } |
| 55 | const receipt = await handle(req, { computerId: "remote", sessionId: "ssh-serve", persistentInputOwner: true }); |
| 56 | process.stdout.write(JSON.stringify({ id: req?.id ?? null, ...receipt }) + "\n"); |
| 57 | } |
| 58 |