@frockbot/plugin-fly-sprite 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frockbot.json +16 -0
- package/package.json +42 -6
- package/src/agent.ts +6 -0
- package/src/computer.test.ts +538 -0
- package/src/computer.ts +1391 -0
- package/src/doctor.test.ts +154 -0
- package/src/host-client.test.ts +618 -0
- package/src/host-client.ts +794 -0
- package/src/host-double.ts +268 -0
- package/src/host.ts +331 -0
- package/src/index.ts +7 -0
- package/src/manifest.ts +3 -0
- package/src/processes.test.ts +176 -0
- package/src/provider.ts +591 -0
- package/src/screenshot.test.ts +145 -0
- package/src/sync.test.ts +1060 -0
- package/src/sync.ts +1275 -0
- package/src/workspace.test.ts +928 -0
- package/src/workspace.ts +782 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
package/frockbot.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 3,
|
|
3
|
+
"id": "fly-sprite",
|
|
4
|
+
"displayName": "Fly Sprite Computer",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"compatibility": { "frockbot": ">=0.0.1" },
|
|
7
|
+
"contributions": {
|
|
8
|
+
"runtime": { "entry": "./provider" },
|
|
9
|
+
"desktop": {
|
|
10
|
+
"entry": "./host",
|
|
11
|
+
"execution": "trusted-main",
|
|
12
|
+
"commands": []
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"permissions": ["network:sprites"]
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-fly-sprite",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./agent": "./src/agent.ts",
|
|
9
|
+
"./computer": "./src/computer.ts",
|
|
10
|
+
"./frockbot.json": "./frockbot.json",
|
|
11
|
+
"./host": "./src/host.ts",
|
|
12
|
+
"./host-client": "./src/host-client.ts",
|
|
13
|
+
"./manifest": "./src/manifest.ts",
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./provider": "./src/provider.ts",
|
|
16
|
+
"./sync": "./src/sync.ts",
|
|
17
|
+
"./workspace": "./src/workspace.ts"
|
|
18
|
+
},
|
|
19
|
+
"frockbot": {
|
|
20
|
+
"manifest": "./frockbot.json"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "bun test src",
|
|
24
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@cordisjs/plugin-webui": "0.8.2",
|
|
28
|
+
"@frockbot/computer-core": "0.1.0",
|
|
29
|
+
"@frockbot/computer-host-protocol": "0.1.0",
|
|
30
|
+
"@frockbot/computer-host-runtime": "0.1.0",
|
|
31
|
+
"@frockbot/kernel-contracts": "0.1.0",
|
|
32
|
+
"@frockbot/plugin-computer": "0.1.0",
|
|
33
|
+
"cordis": "4.0.0-rc.8"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@frockbot/plugin-testkit": "0.1.0",
|
|
37
|
+
"@frockbot/workspace-store": "0.1.0",
|
|
38
|
+
"@types/bun": "1.4.0",
|
|
39
|
+
"@types/node": "26.2.0",
|
|
40
|
+
"typescript": "5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
6
45
|
"repository": {
|
|
7
46
|
"type": "git",
|
|
8
47
|
"url": "git+https://github.com/timoconnellaus/frockbot.git",
|
|
9
48
|
"directory": "packages/plugin-fly-sprite"
|
|
10
|
-
},
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
49
|
}
|
|
14
50
|
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/// <reference types="bun" />
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import { verifyPluginPackage } from "@frockbot/plugin-testkit";
|
|
5
|
+
import { DESKTOP_GUI_LEASE_KEY } from "@frockbot/computer-host-runtime";
|
|
6
|
+
import manifest from "../frockbot.json" with { type: "json" };
|
|
7
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
8
|
+
import {
|
|
9
|
+
computerBotKey,
|
|
10
|
+
FlySpriteComputer,
|
|
11
|
+
flySpriteNameForBot,
|
|
12
|
+
type ComputerHostFactoryV1,
|
|
13
|
+
} from "./computer.ts";
|
|
14
|
+
import { FakeComputerHost, type FakeComputerRunV1 } from "./host-double.ts";
|
|
15
|
+
import { configuredFlyBotId } from "./host.ts";
|
|
16
|
+
import {
|
|
17
|
+
FlySpriteComputerProvider,
|
|
18
|
+
flySpriteNameForComputer,
|
|
19
|
+
} from "./provider.ts";
|
|
20
|
+
|
|
21
|
+
// The Computer's own shell scripts — the provisioning document, `control.sh`'s
|
|
22
|
+
// `flock` lease, and `ensure-agent.sh`'s slot reclaim — are run for real in
|
|
23
|
+
// `@frockbot/computer-host-runtime`'s suite. They moved there with the scripts
|
|
24
|
+
// themselves (ADR 0004): a Bot Durable Object no longer installs them, so a
|
|
25
|
+
// test that stood up a provider to read one out of a provisioning command was
|
|
26
|
+
// testing the wrong module. What is left here is this Package's own subject:
|
|
27
|
+
// what a Bot tenant means on a Computer, and what the provider-neutral
|
|
28
|
+
// Computer interface answers.
|
|
29
|
+
|
|
30
|
+
/** The interpreter every suite fixture shares, over the script it was sent. */
|
|
31
|
+
function computerRunner(script: string): FakeComputerRunV1 {
|
|
32
|
+
if (script.includes("__FROCKBOT_EXIT__")) {
|
|
33
|
+
return {
|
|
34
|
+
stdout: script.includes("exit 7")
|
|
35
|
+
? "boom\n__FROCKBOT_EXIT__7\n"
|
|
36
|
+
: "\n__FROCKBOT_EXIT__0\n",
|
|
37
|
+
stderr: "warning noise\n",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (script.includes("echo tool-output")) return { stdout: "tool-output\n" };
|
|
41
|
+
if (script.includes("missing.md")) return { stdout: "__MISSING__\n" };
|
|
42
|
+
if (script.includes('rm -f "$TARGET"')) {
|
|
43
|
+
return { stdout: "__DELETED__\n", stderr: "bash: /etc/profile: noise\n" };
|
|
44
|
+
}
|
|
45
|
+
if (script.includes('find "$ROOT"')) {
|
|
46
|
+
const offset = Number(/OFFSET=(\d+)/.exec(script)?.[1] ?? 0);
|
|
47
|
+
const limit = Number(/LIMIT=(\d+)/.exec(script)?.[1] ?? 100);
|
|
48
|
+
const lines = Array.from({ length: 205 }, (_, index) =>
|
|
49
|
+
[
|
|
50
|
+
Buffer.from(`memory-${String(index).padStart(3, "0")}.md`).toString(
|
|
51
|
+
"base64",
|
|
52
|
+
),
|
|
53
|
+
`version-${index}`,
|
|
54
|
+
"7",
|
|
55
|
+
"1700000000",
|
|
56
|
+
].join("\t"),
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
stdout: `${lines.slice(offset, offset + limit + 1).join("\n")}\n`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (script.includes('mv "$TMP" "$TARGET"'))
|
|
63
|
+
return { stdout: "__WRITTEN__\n" };
|
|
64
|
+
if (script.includes('base64 -w0 "$TARGET"')) {
|
|
65
|
+
// meta, content hash, size, mtime, bytes — the Workspace file shape.
|
|
66
|
+
return {
|
|
67
|
+
stdout: [
|
|
68
|
+
"",
|
|
69
|
+
"0".repeat(64),
|
|
70
|
+
"8",
|
|
71
|
+
"1700000000",
|
|
72
|
+
Buffer.from("remember").toString("base64"),
|
|
73
|
+
"",
|
|
74
|
+
].join("\n"),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (script.includes("browser.mjs")) {
|
|
78
|
+
return {
|
|
79
|
+
stdout: JSON.stringify({
|
|
80
|
+
url: "https://example.com",
|
|
81
|
+
snapshot: "- heading",
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fakeHost(): FakeComputerHost {
|
|
89
|
+
return new FakeComputerHost(computerRunner);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function attach(
|
|
93
|
+
host: FakeComputerHost,
|
|
94
|
+
agentControlOwnerId?: string,
|
|
95
|
+
): FlySpriteComputer {
|
|
96
|
+
return new FlySpriteComputer({
|
|
97
|
+
identity: { userId: "owner" },
|
|
98
|
+
host: host.factory,
|
|
99
|
+
spriteName: "frockbot-test",
|
|
100
|
+
...(agentControlOwnerId ? { agentControlOwnerId } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function signal(): AbortSignal {
|
|
105
|
+
return new AbortController().signal;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A host that refuses to provision anything, to prove nothing asked it to. */
|
|
109
|
+
function storageOnlyHost(host: FakeComputerHost): ComputerHostFactoryV1 {
|
|
110
|
+
return (identity, tenant) => {
|
|
111
|
+
const surface = host.surface(tenant.botId);
|
|
112
|
+
void identity;
|
|
113
|
+
return {
|
|
114
|
+
...surface,
|
|
115
|
+
open: () =>
|
|
116
|
+
Promise.reject(new Error("Workspace access must not open a desktop")),
|
|
117
|
+
viewer: () =>
|
|
118
|
+
Promise.reject(new Error("Workspace access must not publish a viewer")),
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe("Fly Sprite computer", () => {
|
|
124
|
+
test("derives stable traversal-safe keys for Bot directories and Sprites", () => {
|
|
125
|
+
expect(computerBotKey("General")).toMatch(/^general-[a-f0-9]{12}$/);
|
|
126
|
+
expect(computerBotKey("../../Health 🩺")).toMatch(/^health-[a-f0-9]{12}$/);
|
|
127
|
+
expect(computerBotKey("General")).not.toBe(computerBotKey("general"));
|
|
128
|
+
expect(() => computerBotKey(" ")).toThrow("1-200 characters");
|
|
129
|
+
expect(flySpriteNameForBot("general", "frockbot")).toMatch(
|
|
130
|
+
/^frockbot-[a-f0-9]{12}$/,
|
|
131
|
+
);
|
|
132
|
+
expect(flySpriteNameForBot("general", "frockbot")).not.toBe(
|
|
133
|
+
flySpriteNameForBot("health", "frockbot"),
|
|
134
|
+
);
|
|
135
|
+
expect(
|
|
136
|
+
flySpriteNameForBot("general", `f${"x".repeat(62)}`).length,
|
|
137
|
+
).toBeLessThanOrEqual(63);
|
|
138
|
+
// ADR 0012: the Sprite name is derived from the User and nothing else, so
|
|
139
|
+
// every Bot the User owns lands on the same Computer.
|
|
140
|
+
expect(flySpriteNameForComputer({ userId: "owner" })).toBe(
|
|
141
|
+
flySpriteNameForComputer({ userId: " owner " }),
|
|
142
|
+
);
|
|
143
|
+
expect(flySpriteNameForComputer({ userId: "owner" })).not.toBe(
|
|
144
|
+
flySpriteNameForComputer({ userId: "owner-2" }),
|
|
145
|
+
);
|
|
146
|
+
expect(configuredFlyBotId({ FROCKBOT_BOT_ID: " bot-7 " })).toBe("bot-7");
|
|
147
|
+
expect(configuredFlyBotId({})).toBe("barebones");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("attaches each Bot as its own tenant with its own viewer", async () => {
|
|
151
|
+
const host = fakeHost();
|
|
152
|
+
const computer = attach(host);
|
|
153
|
+
|
|
154
|
+
const generalComputer = computer.bot({ id: "general", name: "General" });
|
|
155
|
+
const general = await generalComputer.ensure();
|
|
156
|
+
const health = await computer.bot("health").ensure();
|
|
157
|
+
|
|
158
|
+
expect(general.botKey).not.toBe(health.botKey);
|
|
159
|
+
expect(general.spriteName).toBe("frockbot-test");
|
|
160
|
+
expect(general.directory).toBe(
|
|
161
|
+
`agent-data/agents/${computerBotKey("general")}`,
|
|
162
|
+
);
|
|
163
|
+
expect(general.display).toBe(":100");
|
|
164
|
+
expect(general.viewerUrl).toContain("/vnc.html#");
|
|
165
|
+
expect(general.viewerUrl).toContain("password=secret-pass");
|
|
166
|
+
// A viewer per tenant, opened on the host — the token and the VNC password
|
|
167
|
+
// never leave the Computer except inside this URL.
|
|
168
|
+
expect(host.viewerSessions.map(({ action }) => action)).toEqual([
|
|
169
|
+
"open",
|
|
170
|
+
"open",
|
|
171
|
+
]);
|
|
172
|
+
expect(computer.displayForTenant(computerBotKey("general"))).toBe(":100");
|
|
173
|
+
await generalComputer.refreshViewer(general.viewerSessionId);
|
|
174
|
+
expect(host.viewerSessions.map(({ action }) => action)).toEqual([
|
|
175
|
+
"open",
|
|
176
|
+
"open",
|
|
177
|
+
"renew",
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("describes an in-place runtime update in the provider message", async () => {
|
|
182
|
+
const host = fakeHost();
|
|
183
|
+
host.provisioning = {
|
|
184
|
+
kind: "update",
|
|
185
|
+
phase: "runtime",
|
|
186
|
+
label: "Updating the Computer runtime",
|
|
187
|
+
index: 1,
|
|
188
|
+
total: 2,
|
|
189
|
+
status: "complete",
|
|
190
|
+
resumed: false,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const provider = new FlySpriteComputerProvider(attach(host));
|
|
194
|
+
const computer = await provider.open(
|
|
195
|
+
{ userId: "owner" },
|
|
196
|
+
{ botId: "general" },
|
|
197
|
+
{ providerId: "fly-sprite", generation: 1 },
|
|
198
|
+
);
|
|
199
|
+
const connected = await computer.presence?.connect();
|
|
200
|
+
|
|
201
|
+
expect(connected?.message).toBe(
|
|
202
|
+
"Updating the Computer: Updating the Computer runtime",
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("an unconfigured Computer refuses rather than pretending", async () => {
|
|
207
|
+
const computer = new FlySpriteComputer({ spriteName: "frockbot-test" });
|
|
208
|
+
expect(computer.configured).toBe(false);
|
|
209
|
+
await expect(computer.bot("general").ensure()).rejects.toThrow(
|
|
210
|
+
"Set SPRITES_TOKEN",
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("runs shells from a Bot-private workspace with explicit identity", async () => {
|
|
215
|
+
const host = fakeHost();
|
|
216
|
+
const agent = attach(host).bot("General");
|
|
217
|
+
|
|
218
|
+
expect(await agent.run("echo tool-output", signal())).toContain(
|
|
219
|
+
"tool-output",
|
|
220
|
+
);
|
|
221
|
+
const script = host.scripts.find((candidate) =>
|
|
222
|
+
candidate.includes("echo tool-output"),
|
|
223
|
+
);
|
|
224
|
+
expect(script).toContain("export HOME=/home/box");
|
|
225
|
+
expect(script).toContain("export FROCKBOT_BOT_ID='General'");
|
|
226
|
+
expect(script).toContain(`cd '/workspaces/${computerBotKey("General")}'`);
|
|
227
|
+
// The script is the request body, not an argv. That is the whole point of
|
|
228
|
+
// the host seam: a ~2 KB argv is what answered HTTP 431 (ADR 0004).
|
|
229
|
+
expect(host.commands.at(-1)?.script).toBe(script);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a human session holds the User-wide desktop lease and blocks every Bot", async () => {
|
|
233
|
+
// One Computer, two clients of it — a human's and a Bot's — exactly as one
|
|
234
|
+
// Sprite's `flock` is shared across everything that reaches it.
|
|
235
|
+
const host = fakeHost();
|
|
236
|
+
const hostComputer = attach(host);
|
|
237
|
+
const generalHost = hostComputer.bot("general");
|
|
238
|
+
await generalHost.ensure();
|
|
239
|
+
await generalHost.takeControl();
|
|
240
|
+
expect(host.leases.get(DESKTOP_GUI_LEASE_KEY)?.owner).toBe(
|
|
241
|
+
generalHost.controlOwnerId,
|
|
242
|
+
);
|
|
243
|
+
expect(generalHost.controlOwnerId).toStartWith("human:");
|
|
244
|
+
expect(host.leases.has(computerBotKey("general"))).toBe(false);
|
|
245
|
+
|
|
246
|
+
const agentComputer = attach(host);
|
|
247
|
+
const generalAgent = agentComputer.bot("general");
|
|
248
|
+
await expect(
|
|
249
|
+
generalAgent.run("echo tool-output", signal()),
|
|
250
|
+
).rejects.toThrow("user is controlling");
|
|
251
|
+
const providerComputer = await new FlySpriteComputerProvider(
|
|
252
|
+
agentComputer,
|
|
253
|
+
).open(
|
|
254
|
+
{ userId: "owner" },
|
|
255
|
+
{ botId: "general" },
|
|
256
|
+
{ providerId: "fly-sprite", generation: 1 },
|
|
257
|
+
);
|
|
258
|
+
await expect(
|
|
259
|
+
providerComputer.workspace?.write({
|
|
260
|
+
path: {
|
|
261
|
+
root: {
|
|
262
|
+
kind: "bot-instructions",
|
|
263
|
+
userId: "owner",
|
|
264
|
+
botId: "general",
|
|
265
|
+
},
|
|
266
|
+
path: "profile.md",
|
|
267
|
+
},
|
|
268
|
+
bytes: new TextEncoder().encode("remember"),
|
|
269
|
+
// The handle is open for a Bot, so the Bot is the only writer it may
|
|
270
|
+
// record.
|
|
271
|
+
writer: {
|
|
272
|
+
kind: "bot",
|
|
273
|
+
botId: "general",
|
|
274
|
+
sessionId: "session-1",
|
|
275
|
+
turnId: "turn-1",
|
|
276
|
+
runId: "run-1",
|
|
277
|
+
},
|
|
278
|
+
expectedGenerationId: null,
|
|
279
|
+
}),
|
|
280
|
+
).resolves.toMatchObject({ status: "ok" });
|
|
281
|
+
await expect(
|
|
282
|
+
agentComputer.bot("health").run("echo tool-output", signal()),
|
|
283
|
+
).rejects.toThrow(generalHost.controlOwnerId);
|
|
284
|
+
|
|
285
|
+
await generalHost.releaseControl();
|
|
286
|
+
expect(host.leases.has(DESKTOP_GUI_LEASE_KEY)).toBe(false);
|
|
287
|
+
expect(await generalAgent.run("echo tool-output", signal())).toContain(
|
|
288
|
+
"tool-output",
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("the guarded command checks both the tenant and User-wide lease keys", async () => {
|
|
293
|
+
const host = fakeHost();
|
|
294
|
+
const agent = attach(host).bot("general");
|
|
295
|
+
const botKey = computerBotKey("general");
|
|
296
|
+
|
|
297
|
+
host.leases.set(botKey, { owner: "bot-holder", fresh: true });
|
|
298
|
+
await expect(agent.run("echo tool-output", signal())).rejects.toThrow(
|
|
299
|
+
"bot-holder",
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
host.leases.set(botKey, { owner: "expired-holder", fresh: false });
|
|
303
|
+
expect(await agent.run("echo tool-output", signal())).toContain(
|
|
304
|
+
"tool-output",
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
host.leases.set(DESKTOP_GUI_LEASE_KEY, {
|
|
308
|
+
owner: "desktop-holder",
|
|
309
|
+
fresh: true,
|
|
310
|
+
});
|
|
311
|
+
await expect(agent.run("echo tool-output", signal())).rejects.toThrow(
|
|
312
|
+
"desktop-holder",
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("a computerUse caller passes the User-wide lease it already holds", async () => {
|
|
317
|
+
const host = fakeHost();
|
|
318
|
+
const taskOwner = "task-general-tk-gui";
|
|
319
|
+
const agent = attach(host, taskOwner).bot("general");
|
|
320
|
+
host.leases.set(DESKTOP_GUI_LEASE_KEY, {
|
|
321
|
+
owner: taskOwner,
|
|
322
|
+
fresh: true,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(await agent.run("echo tool-output", signal())).toContain(
|
|
326
|
+
"tool-output",
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("opens durable files without provisioning a browser or public viewer", async () => {
|
|
331
|
+
const host = fakeHost();
|
|
332
|
+
const provider = new FlySpriteComputerProvider(
|
|
333
|
+
new FlySpriteComputer({
|
|
334
|
+
identity: { userId: "owner" },
|
|
335
|
+
host: storageOnlyHost(host),
|
|
336
|
+
spriteName: "frockbot-test",
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
const computer = await provider.open(
|
|
340
|
+
{ userId: "owner" },
|
|
341
|
+
{ botId: "health" },
|
|
342
|
+
{ providerId: "fly-sprite", generation: 1 },
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// The host would throw if this reached `open` or `viewer`: "The Agent
|
|
346
|
+
// loop, Memory, Skills, Package composition, and Routines function
|
|
347
|
+
// correctly while the Computer is hibernated and do not wake it."
|
|
348
|
+
await expect(
|
|
349
|
+
computer.workspace?.write({
|
|
350
|
+
path: {
|
|
351
|
+
root: { kind: "bot-instructions", userId: "owner", botId: "health" },
|
|
352
|
+
path: "profile.md",
|
|
353
|
+
},
|
|
354
|
+
bytes: new TextEncoder().encode("remember"),
|
|
355
|
+
writer: {
|
|
356
|
+
kind: "bot",
|
|
357
|
+
botId: "health",
|
|
358
|
+
sessionId: "session-1",
|
|
359
|
+
turnId: "turn-1",
|
|
360
|
+
runId: "run-1",
|
|
361
|
+
},
|
|
362
|
+
expectedGenerationId: null,
|
|
363
|
+
}),
|
|
364
|
+
).resolves.toMatchObject({ status: "ok" });
|
|
365
|
+
expect(host.viewerSessions).toEqual([]);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ADR 0012: one Sprite per User, every Bot a tenant on it. Two Bots resolve
|
|
369
|
+
// to one provider Computer, one Sprite name, and one browser profile; only
|
|
370
|
+
// their directories and desktops differ.
|
|
371
|
+
test("puts two Bots of one User on one Sprite", async () => {
|
|
372
|
+
const host = fakeHost();
|
|
373
|
+
const provider = new FlySpriteComputerProvider(undefined, host.factory);
|
|
374
|
+
|
|
375
|
+
const first = provider.computerFor({ userId: "owner" });
|
|
376
|
+
const second = provider.computerFor({ userId: "owner" });
|
|
377
|
+
const other = provider.computerFor({ userId: "owner-2" });
|
|
378
|
+
|
|
379
|
+
expect(first).toBe(second);
|
|
380
|
+
expect(first.spriteName).toBe(
|
|
381
|
+
flySpriteNameForComputer({ userId: "owner" }),
|
|
382
|
+
);
|
|
383
|
+
expect(other.spriteName).not.toBe(first.spriteName);
|
|
384
|
+
|
|
385
|
+
const shared = new FlySpriteComputerProvider(attach(host));
|
|
386
|
+
const assignment = { providerId: "fly-sprite", generation: 1 };
|
|
387
|
+
const health = await shared.open(
|
|
388
|
+
{ userId: "owner" },
|
|
389
|
+
{ botId: "health" },
|
|
390
|
+
assignment,
|
|
391
|
+
);
|
|
392
|
+
const general = await shared.open(
|
|
393
|
+
{ userId: "owner" },
|
|
394
|
+
{ botId: "general" },
|
|
395
|
+
assignment,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
expect(health.identity).toEqual(general.identity);
|
|
399
|
+
expect(health.assignment).toBe(general.assignment);
|
|
400
|
+
expect(health.tenant.directory).not.toBe(general.tenant.directory);
|
|
401
|
+
expect(health.tenant.directory).toBe(
|
|
402
|
+
`agent-data/agents/${computerBotKey("health")}`,
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("adapts Fly execution through the provider-neutral Computer interface", async () => {
|
|
407
|
+
const host = fakeHost();
|
|
408
|
+
const provider = new FlySpriteComputerProvider(attach(host));
|
|
409
|
+
const assignment = { providerId: "fly-sprite", generation: 1 };
|
|
410
|
+
const computer = await provider.open(
|
|
411
|
+
{ userId: "owner" },
|
|
412
|
+
{ botId: "health" },
|
|
413
|
+
assignment,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const result = await computer.exec?.execute(
|
|
417
|
+
{ executable: "/bin/bash", args: ["-lc", "pwd"] },
|
|
418
|
+
{ signal: signal() },
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
expect(result?.exitCode).toBe(0);
|
|
422
|
+
expect(new TextDecoder().decode(result?.stdout)).toBe("");
|
|
423
|
+
expect(new TextDecoder().decode(result?.stderr)).toBe("warning noise\n");
|
|
424
|
+
expect(result?.outputTruncated).toBe(false);
|
|
425
|
+
expect(
|
|
426
|
+
host.scripts.some((script) => script.includes("bash -c 'pwd'")),
|
|
427
|
+
).toBe(true);
|
|
428
|
+
|
|
429
|
+
const failed = await computer.exec?.execute(
|
|
430
|
+
{ executable: "/bin/bash", args: ["-lc", "echo boom; exit 7"] },
|
|
431
|
+
{ signal: signal() },
|
|
432
|
+
);
|
|
433
|
+
expect(failed?.exitCode).toBe(7);
|
|
434
|
+
expect(new TextDecoder().decode(failed?.stdout)).toBe("boom");
|
|
435
|
+
|
|
436
|
+
// `cwd`, `env`, and `stdin` were refused while every one of them had to
|
|
437
|
+
// travel in a request URL. The host compiles them into the script it
|
|
438
|
+
// delivers on the command's stdin, so they are ordinary now.
|
|
439
|
+
await computer.exec?.execute(
|
|
440
|
+
{
|
|
441
|
+
executable: "env",
|
|
442
|
+
cwd: "/tmp",
|
|
443
|
+
env: { FROCKBOT_PROBE: "yes" },
|
|
444
|
+
stdin: new TextEncoder().encode("piped\n"),
|
|
445
|
+
},
|
|
446
|
+
{ signal: signal() },
|
|
447
|
+
);
|
|
448
|
+
// The composed document reaches the tenant's shell inside `bash -c`, so
|
|
449
|
+
// its own quoting is escaped once here. What matters is that all three
|
|
450
|
+
// travel in the script and none of them in an argument list.
|
|
451
|
+
const composed = host.scripts.at(-1) ?? "";
|
|
452
|
+
expect(composed).toContain("export FROCKBOT_PROBE=");
|
|
453
|
+
expect(composed).toContain("yes");
|
|
454
|
+
expect(composed).toContain("/tmp");
|
|
455
|
+
expect(composed).toContain("piped\n");
|
|
456
|
+
expect(composed).toMatch(/FROCKBOT_STDIN_[0-9A-F]{16}/);
|
|
457
|
+
|
|
458
|
+
await expect(
|
|
459
|
+
computer.browser?.perform({ type: "snapshot" }, { signal: signal() }),
|
|
460
|
+
).resolves.toMatchObject({
|
|
461
|
+
url: "https://example.com",
|
|
462
|
+
accessibilitySnapshot: "- heading",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const skills = {
|
|
466
|
+
kind: "bot-instructions",
|
|
467
|
+
userId: "owner",
|
|
468
|
+
botId: "health",
|
|
469
|
+
} as const;
|
|
470
|
+
const written = await computer.workspace?.write({
|
|
471
|
+
path: { root: skills, path: "profile.md" },
|
|
472
|
+
bytes: new TextEncoder().encode("remember"),
|
|
473
|
+
// The handle is open for a Bot, so the Bot is the only writer it may
|
|
474
|
+
// record.
|
|
475
|
+
writer: {
|
|
476
|
+
kind: "bot",
|
|
477
|
+
botId: "health",
|
|
478
|
+
sessionId: "session-1",
|
|
479
|
+
turnId: "turn-1",
|
|
480
|
+
runId: "run-1",
|
|
481
|
+
},
|
|
482
|
+
expectedGenerationId: null,
|
|
483
|
+
});
|
|
484
|
+
const stored = await computer.workspace?.read({
|
|
485
|
+
root: skills,
|
|
486
|
+
path: "profile.md",
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
expect(written).toMatchObject({ status: "ok" });
|
|
490
|
+
expect(stored).toMatchObject({ status: "ok" });
|
|
491
|
+
expect(
|
|
492
|
+
await computer.workspace?.read({ root: skills, path: "missing.md" }),
|
|
493
|
+
).toMatchObject({ status: "not-found" });
|
|
494
|
+
// The mount path comes from the declared layout and matches the GrokBot
|
|
495
|
+
// parity layout: durable per-Bot state under agent-data/agents/<key>.
|
|
496
|
+
expect(
|
|
497
|
+
host.scripts.some((script) =>
|
|
498
|
+
script.includes(
|
|
499
|
+
`ROOT='/home/box/agent-data/agents/${computerBotKey("health")}/skills'`,
|
|
500
|
+
),
|
|
501
|
+
),
|
|
502
|
+
).toBe(true);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("a viewer and a control lease are reachable from the Durable Object", async () => {
|
|
506
|
+
const host = fakeHost();
|
|
507
|
+
const provider = new FlySpriteComputerProvider(attach(host));
|
|
508
|
+
const computer = await provider.open(
|
|
509
|
+
{ userId: "owner" },
|
|
510
|
+
{ botId: "health" },
|
|
511
|
+
{ providerId: "fly-sprite", generation: 1 },
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
// Neither was reachable before: both need the Sprite's URL and its
|
|
515
|
+
// `flock`, and neither was reachable from workerd (ADR 0004).
|
|
516
|
+
const session = await computer.viewer?.open({ signal: signal() });
|
|
517
|
+
expect(session?.url).toContain("/vnc.html#");
|
|
518
|
+
|
|
519
|
+
const lease = await computer.control?.acquire(undefined, {
|
|
520
|
+
signal: signal(),
|
|
521
|
+
});
|
|
522
|
+
expect(lease?.id).toBeTruthy();
|
|
523
|
+
expect(Date.parse(lease?.expiresAt ?? "")).toBeGreaterThan(Date.now());
|
|
524
|
+
const renewed = await computer.control?.renew(lease!, undefined, {
|
|
525
|
+
signal: signal(),
|
|
526
|
+
});
|
|
527
|
+
expect(renewed?.id).toBe(lease?.id);
|
|
528
|
+
await computer.control?.release(lease!, undefined, { signal: signal() });
|
|
529
|
+
expect(host.leases.size).toBe(0);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("satisfies plugin package conventions", () => {
|
|
533
|
+
expect(verifyPluginPackage({ packageJson, manifest })).toMatchObject({
|
|
534
|
+
name: "@frockbot/plugin-fly-sprite",
|
|
535
|
+
contributionKinds: ["runtime", "desktop"],
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
});
|