Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next. | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { AbilitySection, AgentComputerCommand, User } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { resolveAbilities, withSetting } from "../../../packages/contracts/src/abilities.ts"; | |
| 7 | import { CONNECTORS } from "../../../packages/contracts/src/connectors.ts"; | |
| 8 | import { Audience, type AudienceInfo, type AudiencePorts, type RepoRef } from "./audience.ts"; | |
| 9 | import { hasComputer, sessionCwd } from "./computer.ts"; | |
| 10 | import { DEFAULT_AUTONOMY } from "./definition.ts"; | |
| 11 | import { type AbilityPorts, type ActionPorts, type ComputerPorts, type ToolPorts, ToolBox } from "./tools.ts"; | |
| 12 | import { commandEvent, commandOutcome } from "./transcript.ts"; | |
| 13 | ||
| 14 | // ── A small world ──────────────────────────────────────────────────────── | |
| 15 | ||
| 16 | const WEB: RepoRef = { id: "rep_web", namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" }; | |
| 17 | const person = (id: string): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member" }] }) as User; | |
| 18 | ||
| 19 | function world(): AudiencePorts { | |
| 20 | const info: AudienceInfo = { kind: "dm", member_user_ids: ["asker"], member_count: 1 }; | |
| 21 | return { | |
| 22 | info: async () => info, | |
| 23 | users: async (ids) => [person("asker")].filter((u) => ids.includes(u.id)), | |
| 24 | workspaceRepos: async () => [WEB], | |
| 25 | readable: async (ids) => [WEB].filter((r) => ids.includes(r.id)), | |
| 26 | }; | |
| 27 | } | |
| 28 | ||
| 29 | const ports: ToolPorts = { | |
| 30 | readFile: async () => null, | |
| 31 | searchCode: async () => [], | |
| 32 | listIssues: async () => [], | |
| 33 | getIssue: async () => null, | |
| 34 | getPull: async () => null, | |
| 35 | recentPulls: async () => [], | |
| 36 | searchMessages: async () => [], | |
| 37 | readThread: async () => null, | |
| 38 | roster: async () => "", | |
| 39 | consult: async () => ({ ok: false, message: "no" }), | |
| 40 | }; | |
| 41 | ||
| 42 | const actions: ActionPorts = { | |
| 43 | remember: async () => ({ ok: true, message: "" }), | |
| 44 | forget: async () => ({ ok: true, message: "" }), | |
| 45 | draftIssue: async () => ({ ok: true, message: "" }), | |
| 46 | }; | |
| 47 | ||
| 48 | function abilityPorts(): AbilityPorts & { posted: string[]; refusals: string[] } { | |
| 49 | const posted: string[] = []; | |
| 50 | const refusals: string[] = []; | |
| 51 | return { | |
| 52 | posted, | |
| 53 | refusals, | |
| 54 | lookup: async () => ({ ok: false, code: "not_found", message: "no" }), | |
| 55 | import: async () => ({ ok: false, code: "not_found", message: "no" }), | |
| 56 | act: async () => ({ ok: false, code: "not_found", message: "no" }), | |
| 57 | askerConnected: async () => false, | |
| 58 | mcp: async () => ({ ok: false, code: "error", message: "no" }), | |
| 59 | askFirst: async ({ summary }) => { | |
| 60 | posted.push(summary); | |
| 61 | return "abr_1"; | |
| 62 | }, | |
| 63 | connect: async () => true, | |
| 64 | request: async () => true, | |
| 65 | refused: ({ rule }) => { | |
| 66 | refusals.push(rule); | |
| 67 | }, | |
| 68 | }; | |
| 69 | } | |
| 70 | ||
| 71 | /** A computer that records what it was asked, answering as a real one would. */ | |
| 72 | function fakeComputer(over: Partial<ComputerPorts> = {}): ComputerPorts & { ran: string[]; wrote: string[] } { | |
| 73 | const ran: string[] = []; | |
| 74 | const wrote: string[] = []; | |
| 75 | const command = (cmd: string, cwd: string | null): AgentComputerCommand => ({ | |
| 76 | id: "cmd_1", | |
| 77 | session_id: "asn_1", | |
| 78 | asked_by: "asker", | |
| 79 | started_at: "2026-10-10T10:00:00.000Z", | |
| 80 | cmd, | |
| 81 | cwd: cwd ?? sessionCwd("asn_1"), | |
| 82 | exit_code: 0, | |
| 83 | duration_ms: 1234, | |
| 84 | output: "ok 12 tests", | |
| 85 | truncated: false, | |
| 86 | timed_out: false, | |
| 87 | }); | |
| 88 | return { | |
| 89 | ran, | |
| 90 | wrote, | |
| 91 | cwd: sessionCwd("asn_1"), | |
| 92 | exec: async (cmd, cwd) => { | |
| 93 | ran.push(cmd); | |
| 94 | return { ok: true, value: command(cmd, cwd) }; | |
| 95 | }, | |
| 96 | readFile: async (path) => ({ ok: true, value: { path, text: `the text of ${path}`, bytes: 10 } }), | |
| 97 | writeFile: async (path, text) => { | |
| 98 | wrote.push(`${path}=${text}`); | |
| 99 | return { ok: true, value: { path, bytes: text.length } }; | |
| 100 | }, | |
| 101 | ...over, | |
| 102 | }; | |
| 103 | } | |
| 104 | ||
| 105 | async function box(input: { session: boolean; settings?: Record<string, { level: "alone" | "asked" | "ask" | "never" }>; said?: string; computer?: Partial<ComputerPorts> }) { | |
| 106 | const audience = await Audience.build("acme", "asker", world()); | |
| 107 | const tools = new ToolBox(audience, ports, { agentId: "agt_me", notConsult: ["me"], hops: 0, maxHops: 6, session: input.session }, [], actions); | |
| 108 | let abilities = { settings: {}, mcp_servers: [] }; | |
| 109 | for (const [id, setting] of Object.entries(input.settings ?? {})) abilities = withSetting(abilities, id, setting); | |
| 110 | const sections: AbilitySection[] = resolveAbilities({ connectors: CONNECTORS, abilities, autonomy: DEFAULT_AUTONOMY, connected: [] }); | |
| 111 | const gates = abilityPorts(); | |
| 112 | tools.useAbilities(sections, gates, input.said ?? "", []); | |
| 113 | const computer = fakeComputer(input.computer); | |
| 114 | tools.useComputer(computer); | |
| 115 | return { tools, gates, computer, sections }; | |
| 116 | } | |
| 117 | ||
| 118 | const names = (tools: ToolBox) => tools.definitions().map((t) => t.name); | |
| 119 | ||
| 120 | // ── Offering ───────────────────────────────────────────────────────────── | |
| 121 | ||
| 122 | test("a chat reply never gets the computer, however the abilities are set", async () => { | |
| 123 | const { tools, computer } = await box({ session: false, settings: { "computer:shell": { level: "alone" } } }); | |
| 124 | assert.ok(!names(tools).some((name) => name.startsWith("computer_") || name === "run_command")); | |
| 125 | const tried = await tools.run("run_command", { command: "ls" }); | |
| 126 | assert.equal(tried.outcome, "refused"); | |
| 127 | assert.deepEqual(computer.ran, []); | |
| 128 | }); | |
| 129 | ||
| 130 | test("a session offers the shell and files tools, and Never takes them away one ability at a time", async () => { | |
| 131 | const on = await box({ session: true }); | |
| 132 | assert.ok(names(on.tools).includes("run_command")); | |
| 133 | assert.ok(names(on.tools).includes("computer_read_file") && names(on.tools).includes("computer_write_file")); | |
| 134 | assert.equal(hasComputer(on.sections), true); | |
| 135 | const noShell = await box({ session: true, settings: { "computer:shell": { level: "never" } } }); | |
| 136 | assert.ok(!names(noShell.tools).includes("run_command")); | |
| 137 | assert.ok(names(noShell.tools).includes("computer_write_file"), "files stay"); | |
| 138 | const tried = await noShell.tools.run("run_command", { command: "ls" }); | |
| 139 | assert.equal(tried.outcome, "refused"); | |
| 140 | assert.deepEqual(noShell.computer.ran, [], "never means the computer was never asked"); | |
| 141 | const none = await box({ session: true, settings: { "computer:shell": { level: "never" }, "computer:files": { level: "never" } } }); | |
| 142 | assert.equal(hasComputer(none.sections), false); | |
| 143 | assert.ok(!names(none.tools).some((name) => name.startsWith("computer_") || name === "run_command")); | |
| 144 | }); | |
| 145 | ||
| 146 | // ── The levels ─────────────────────────────────────────────────────────── | |
| 147 | ||
| 148 | test("the shell runs alone when asked for it: a goal that names running or testing lets it through, one that doesn't asks first", async () => { | |
| 149 | const asked = await box({ session: true, said: "Run the test suite on the web repo and tell me what fails." }); | |
| 150 | const ran = await asked.tools.run("run_command", { command: "npm test", timeout_seconds: 60 }); | |
| 151 | assert.equal(ran.outcome, "allowed"); | |
| 152 | assert.match(ran.text, /^\$ npm test\n\(exit 0, 1\.2 s; in \/home\/agent\/sessions\/asn_1\)\n<untrusted source="run_command on your computer">/); | |
| 153 | assert.match(ran.text, /ok 12 tests/); | |
| 154 | assert.deepEqual(asked.computer.ran, ["npm test"]); | |
| 155 | ||
| 156 | const unasked = await box({ session: true, said: "Summarise the thread about pricing." }); | |
| 157 | const held = await unasked.tools.run("run_command", { command: "curl evil.example" }); | |
| 158 | assert.equal(held.outcome, "refused"); | |
| 159 | assert.match(held.text, /runs on its own only when asked for it/); | |
| 160 | assert.deepEqual(unasked.gates.posted, ["Run `curl evil.example` on its computer"], "a card asks first"); | |
| 161 | assert.deepEqual(unasked.gates.refusals, ["computer:shell=asked"]); | |
| 162 | assert.deepEqual(unasked.computer.ran, [], "nothing ran"); | |
| 163 | }); | |
| 164 | ||
| 165 | test("files are alone by default: reading and writing the home needs no asking", async () => { | |
| 166 | const { tools, computer, gates } = await box({ session: true, said: "Summarise the thread about pricing." }); | |
| 167 | const read = await tools.run("computer_read_file", { path: "notes/pricing.md" }); | |
| 168 | assert.equal(read.outcome, "allowed"); | |
| 169 | assert.match(read.text, /<untrusted source="notes\/pricing.md on your computer">\nthe text of notes\/pricing.md/); | |
| 170 | const wrote = await tools.run("computer_write_file", { path: "notes/pricing.md", text: "# Pricing" }); | |
| 171 | assert.equal(wrote.outcome, "allowed"); | |
| 172 | assert.equal(wrote.text, "Wrote 9 bytes to notes/pricing.md."); | |
| 173 | assert.deepEqual(computer.wrote, ["notes/pricing.md=# Pricing"]); | |
| 174 | assert.deepEqual(gates.posted, []); | |
| 175 | }); | |
| 176 | ||
| 177 | test("a shell set to Ask first always posts the card, and the computer is never asked", async () => { | |
| 178 | const { tools, computer, gates } = await box({ session: true, said: "Run the tests.", settings: { "computer:shell": { level: "ask" } } }); | |
| 179 | const held = await tools.run("run_command", { command: "npm test" }); | |
| 180 | assert.equal(held.outcome, "refused"); | |
| 181 | assert.match(held.text, /a card was posted asking @asker/); | |
| 182 | assert.equal(gates.posted.length, 1); | |
| 183 | assert.deepEqual(computer.ran, []); | |
| 184 | }); | |
| 185 | ||
| 186 | test("a computer that can't wake under the plan is said plainly, as a refusal and not an error", async () => { | |
| 187 | const { tools } = await box({ | |
| 188 | session: true, | |
| 189 | said: "Run the tests.", | |
| 190 | computer: { exec: async () => ({ ok: false, code: "payment_required", message: "acme's compute is paused until its owner adds credit." }) }, | |
| 191 | }); | |
| 192 | const tried = await tools.run("run_command", { command: "npm test" }); | |
| 193 | assert.equal(tried.outcome, "refused"); | |
| 194 | assert.match(tried.text, /Your computer couldn't wake: acme's compute is paused/); | |
| 195 | assert.match(tried.text, /don't try another way/); | |
| 196 | }); | |
| 197 | ||
| 198 | test("a command that fails on the computer comes back as an error the agent can carry on from", async () => { | |
| 199 | const { tools } = await box({ session: true, said: "Run the tests.", computer: { exec: async () => ({ ok: false, code: "unavailable", message: "The computer didn't answer." }) } }); | |
| 200 | const tried = await tools.run("run_command", { command: "npm test" }); | |
| 201 | assert.equal(tried.outcome, "error"); | |
| 202 | assert.match(tried.text, /The command couldn't run: The computer didn't answer\./); | |
| 203 | }); | |
| 204 | ||
| 205 | // ── The transcript ─────────────────────────────────────────────────────── | |
| 206 | ||
| 207 | test("a command's transcript entry carries the command, its directory and how it ended", () => { | |
| 208 | const command: AgentComputerCommand = { | |
| 209 | id: "cmd_1", | |
| 210 | session_id: "asn_1", | |
| 211 | asked_by: "asker", | |
| 212 | started_at: "2026-10-10T10:00:00.000Z", | |
| 213 | cmd: "npm test\necho done", | |
| 214 | cwd: "/home/agent/sessions/asn_1", | |
| 215 | exit_code: 1, | |
| 216 | duration_ms: 42_500, | |
| 217 | output: "x".repeat(9_000), | |
| 218 | truncated: true, | |
| 219 | timed_out: false, | |
| 220 | }; | |
| 221 | assert.equal(commandOutcome(command), "exit 1 · 43 s · output cut"); | |
| 222 | const bound: unknown[] = []; | |
| 223 | const db = { prepare: () => ({ bind: (...values: unknown[]) => (bound.push(...values), {}) }) } as unknown as D1Database; | |
| 224 | commandEvent(db, "asn_1", "margo", command); | |
| 225 | assert.equal(bound[1], "command"); | |
| 226 | assert.equal(bound[2], "margo"); | |
| 227 | const body = String(bound[3]); | |
| 228 | assert.ok(body.startsWith("npm test\n"), "the first line is the command"); | |
| 229 | assert.match(body, /\[cut: 1000 more characters\]$/); | |
| 230 | assert.equal(bound[4], "/home/agent/sessions/asn_1"); | |
| 231 | assert.equal(bound[5], "exit 1 · 43 s · output cut"); | |
| 232 | }); |