Skip to content
232 linesCodeBlameRaw

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.1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { AbilitySection, AgentComputerCommand, User } from "@g1t/contracts";
5
6import { resolveAbilities, withSetting } from "../../../packages/contracts/src/abilities.ts";
7import { CONNECTORS } from "../../../packages/contracts/src/connectors.ts";
8import { Audience, type AudienceInfo, type AudiencePorts, type RepoRef } from "./audience.ts";
9import { hasComputer, sessionCwd } from "./computer.ts";
10import { DEFAULT_AUTONOMY } from "./definition.ts";
11import { type AbilityPorts, type ActionPorts, type ComputerPorts, type ToolPorts, ToolBox } from "./tools.ts";
12import { commandEvent, commandOutcome } from "./transcript.ts";
13
14// ── A small world ────────────────────────────────────────────────────────
15
16const WEB: RepoRef = { id: "rep_web", namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" };
17const person = (id: string): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member" }] }) as User;
18
19function 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
29const 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
42const actions: ActionPorts = {
43 remember: async () => ({ ok: true, message: "" }),
44 forget: async () => ({ ok: true, message: "" }),
45 draftIssue: async () => ({ ok: true, message: "" }),
46};
47
48function 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. */
72function 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
105async 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
118const names = (tools: ToolBox) => tools.definitions().map((t) => t.name);
119
120// ── Offering ─────────────────────────────────────────────────────────────
121
122test("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
130test("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
148test("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
165test("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
177test("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
186test("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
198test("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
207test("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});