@frockbot/plugin-computer 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.
Files changed (42) hide show
  1. package/frockbot.json +25 -0
  2. package/package.json +54 -6
  3. package/src/agent.test.ts +271 -0
  4. package/src/agent.ts +1419 -0
  5. package/src/backend.test.ts +149 -0
  6. package/src/backend.ts +163 -0
  7. package/src/bot.test.ts +411 -0
  8. package/src/bot.ts +831 -0
  9. package/src/client/ComputerCard.test.ts +96 -0
  10. package/src/client/ComputerCard.vue +60 -0
  11. package/src/client/ComputerStrip.test.ts +54 -0
  12. package/src/client/ComputerStrip.vue +55 -0
  13. package/src/client/ComputerViewerOverlay.vue +252 -0
  14. package/src/client/application.test.ts +373 -0
  15. package/src/client/application.ts +340 -0
  16. package/src/client/cordis-client-shim.d.ts +16 -0
  17. package/src/client/dialog-focus.ts +13 -0
  18. package/src/client/index.ts +28 -0
  19. package/src/client/state-machine.test.ts +200 -0
  20. package/src/client/state-machine.ts +172 -0
  21. package/src/client/styles.css +594 -0
  22. package/src/client/viewer.ts +58 -0
  23. package/src/control-record.ts +57 -0
  24. package/src/doctor.test.ts +247 -0
  25. package/src/env.d.ts +12 -0
  26. package/src/index.ts +6 -0
  27. package/src/manifest.ts +3 -0
  28. package/src/process-records.test.ts +178 -0
  29. package/src/process-records.ts +278 -0
  30. package/src/process-store.ts +96 -0
  31. package/src/processes.test.ts +388 -0
  32. package/src/protocol.ts +405 -0
  33. package/src/roots.ts +6 -0
  34. package/src/screenshot.test.ts +253 -0
  35. package/src/shared-provider.test.ts +56 -0
  36. package/src/shared-provider.ts +121 -0
  37. package/src/shared.ts +54 -0
  38. package/src/sync.test.ts +255 -0
  39. package/src/workspace-fixture.ts +126 -0
  40. package/tsconfig.json +19 -0
  41. package/vite.config.ts +24 -0
  42. package/README.md +0 -3
package/frockbot.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "id": "computer",
4
+ "displayName": "Computer",
5
+ "version": "0.0.1",
6
+ "compatibility": { "frockbot": ">=0.0.1" },
7
+ "dependencies": { "shell": ">=0.0.1", "ui-theme": ">=0.0.1" },
8
+ "contributions": {
9
+ "backend": [
10
+ { "entry": "./backend", "host": "gateway" },
11
+ { "entry": "./bot", "host": "bot" }
12
+ ],
13
+ "runtime": { "entry": "./agent" },
14
+ "client": {
15
+ "entry": "./client",
16
+ "mounts": [
17
+ { "slot": "frockbot.computer" },
18
+ { "slot": "frockbot.sidebar-computer" },
19
+ { "slot": "frockbot.overlays" }
20
+ ],
21
+ "outlets": []
22
+ }
23
+ },
24
+ "permissions": ["computer:execute", "computer:browser", "webui:remote-frame"]
25
+ }
package/package.json CHANGED
@@ -1,14 +1,62 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-computer",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving this name for trusted publishing. Superseded by the first release.",
5
- "license": "UNLICENSED",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./agent": "./src/agent.ts",
9
+ "./backend": "./src/backend.ts",
10
+ "./bot": "./src/bot.ts",
11
+ "./client": "./src/client/application.ts",
12
+ "./client-state-machine": "./src/client/state-machine.ts",
13
+ "./protocol": "./src/protocol.ts",
14
+ "./roots": "./src/roots.ts",
15
+ "./src/client/index.ts": "./src/client/index.ts",
16
+ "./shared": "./src/shared.ts",
17
+ "./shared-provider": "./src/shared-provider.ts",
18
+ "./manifest": "./src/manifest.ts",
19
+ "./frockbot.json": "./frockbot.json",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "frockbot": {
23
+ "manifest": "./frockbot.json"
24
+ },
25
+ "scripts": {
26
+ "build": "vite build",
27
+ "test": "bun test src",
28
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json"
29
+ },
30
+ "dependencies": {
31
+ "@cordisjs/client": "0.8.2",
32
+ "@frockbot/client-core": "0.1.0",
33
+ "@frockbot/client-ui": "0.1.0",
34
+ "@frockbot/computer-core": "0.1.0",
35
+ "@frockbot/computer-host-runtime": "0.1.0",
36
+ "@frockbot/kernel-agent-loop": "0.1.0",
37
+ "@frockbot/kernel-contracts": "0.1.0",
38
+ "@frockbot/plugin-prompt": "0.1.0",
39
+ "@frockbot/plugin-shell": "0.1.0",
40
+ "@frockbot/plugin-tools": "0.1.0",
41
+ "cordis": "4.0.0-rc.8",
42
+ "vue": "3.5.41"
43
+ },
44
+ "devDependencies": {
45
+ "@frockbot/plugin-models": "0.1.0",
46
+ "@frockbot/plugin-testkit": "0.1.0",
47
+ "@types/bun": "1.4.0",
48
+ "@types/node": "26.2.0",
49
+ "@vitejs/plugin-vue": "6.0.8",
50
+ "typescript": "5.9.3",
51
+ "vite": "8.2.2",
52
+ "vue-tsc": "3.3.10"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
6
57
  "repository": {
7
58
  "type": "git",
8
59
  "url": "git+https://github.com/timoconnellaus/frockbot.git",
9
60
  "directory": "packages/plugin-computer"
10
- },
11
- "publishConfig": {
12
- "access": "public"
13
61
  }
14
62
  }
@@ -0,0 +1,271 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { SystemPromptRegistry } from "@frockbot/plugin-prompt";
3
+ import { ToolRegistry } from "@frockbot/plugin-tools";
4
+ import {
5
+ ComputerError,
6
+ ComputerRegistry,
7
+ type ComputerProvider,
8
+ } from "@frockbot/computer-core";
9
+ import {
10
+ createPluginHarness,
11
+ verifyPluginPackage,
12
+ } from "@frockbot/plugin-testkit";
13
+ import { SessionStore } from "@frockbot/kernel-contracts";
14
+ import manifest from "../frockbot.json" with { type: "json" };
15
+ import packageJson from "../package.json" with { type: "json" };
16
+ import {
17
+ createComputerAgentPlugin,
18
+ HUMAN_CONTROL_PROMPT_LINE,
19
+ } from "./agent.js";
20
+ import { COMPUTER_CONTROL_RECORD_KEY } from "./control-record.js";
21
+
22
+ async function execute(
23
+ harness: Awaited<ReturnType<typeof createPluginHarness>>,
24
+ name: string,
25
+ input: unknown,
26
+ ) {
27
+ const context = {
28
+ botId: "bot-1",
29
+ agentId: "run-9",
30
+ compositionGenerationId: "bootstrap",
31
+ turnType: "chat" as const,
32
+ sessionId: "session-1",
33
+ effectId: "tool:1:1:0",
34
+ signal: new AbortController().signal,
35
+ };
36
+ const prepared = await harness.root.tools.prepare(
37
+ { id: crypto.randomUUID(), name, input },
38
+ context,
39
+ );
40
+ if (prepared.kind !== "ready") throw new Error(prepared.result.content);
41
+ return harness.root.tools.executePrepared(prepared, context);
42
+ }
43
+
44
+ describe("computer agent contribution", () => {
45
+ test("routes generic tools through the Bot's selected Computer provider", async () => {
46
+ const calls: string[] = [];
47
+ const provider: ComputerProvider = {
48
+ id: "fixture",
49
+ open: async (identity, tenant, assignment) => {
50
+ calls.push(`open:${identity.userId}:${tenant.botId}`);
51
+ return {
52
+ assignment,
53
+ identity,
54
+ tenant,
55
+ exec: {
56
+ execute: async (request) => {
57
+ calls.push(
58
+ `exec:${request.executable}:${request.args?.join(" ")}`,
59
+ );
60
+ return {
61
+ exitCode: 0,
62
+ stdout: new TextEncoder().encode("/workspace/bot-1"),
63
+ stderr: new Uint8Array(),
64
+ outputTruncated: false,
65
+ };
66
+ },
67
+ },
68
+ browser: {
69
+ perform: async (action) => {
70
+ calls.push(`browser:${action.type}`);
71
+ return { accessibilitySnapshot: "button: Continue" };
72
+ },
73
+ },
74
+ close: () => Promise.resolve(),
75
+ };
76
+ },
77
+ };
78
+ const harness = await createPluginHarness([
79
+ ComputerRegistry,
80
+ ToolRegistry,
81
+ SystemPromptRegistry,
82
+ SessionStore,
83
+ ]);
84
+ harness.root.computers.register(provider);
85
+ await harness.mount(
86
+ createComputerAgentPlugin({
87
+ userId: "user-1",
88
+ defaultProviderId: "fixture",
89
+ }),
90
+ );
91
+
92
+ const exec = await execute(harness, "computer_exec", { command: "pwd" });
93
+ const browser = await execute(harness, "computer_browser", {
94
+ action: "snapshot",
95
+ });
96
+
97
+ expect(exec).toMatchObject({ content: "/workspace/bot-1", isError: false });
98
+ expect(browser).toMatchObject({
99
+ content: "button: Continue",
100
+ isError: false,
101
+ });
102
+ expect(calls).toEqual([
103
+ "open:user-1:bot-1",
104
+ "exec:/bin/bash:-lc pwd",
105
+ "open:user-1:bot-1",
106
+ "browser:snapshot",
107
+ ]);
108
+ await harness.dispose();
109
+ });
110
+
111
+ test("computer_exec during an update returns an actionable tool failure", async () => {
112
+ const provider: ComputerProvider = {
113
+ id: "fixture",
114
+ open: async (identity, tenant, assignment) => ({
115
+ assignment,
116
+ identity,
117
+ tenant,
118
+ exec: {
119
+ execute: async () => {
120
+ throw new ComputerError(
121
+ "updating",
122
+ "Updating the Computer runtime",
123
+ true,
124
+ );
125
+ },
126
+ },
127
+ close: () => Promise.resolve(),
128
+ }),
129
+ };
130
+ const harness = await createPluginHarness([
131
+ ComputerRegistry,
132
+ ToolRegistry,
133
+ SystemPromptRegistry,
134
+ SessionStore,
135
+ ]);
136
+ harness.root.computers.register(provider);
137
+ await harness.mount(
138
+ createComputerAgentPlugin({
139
+ userId: "user-1",
140
+ defaultProviderId: "fixture",
141
+ }),
142
+ );
143
+
144
+ const result = await execute(harness, "computer_exec", { command: "pwd" });
145
+
146
+ expect(result).toEqual({
147
+ content:
148
+ "The Computer is updating (Updating the Computer runtime); try again shortly",
149
+ isError: true,
150
+ });
151
+ await harness.dispose();
152
+ });
153
+
154
+ test("injects and records the human-control line only while the durable lease is fresh", async () => {
155
+ const records = new Map<string, unknown>([
156
+ [
157
+ COMPUTER_CONTROL_RECORD_KEY,
158
+ {
159
+ version: 1,
160
+ ownerId: "human:session-1",
161
+ acquiredAt: "2026-09-02T00:00:00.000Z",
162
+ expiresAt: "2026-09-02T00:01:30.000Z",
163
+ },
164
+ ],
165
+ ]);
166
+ let now = new Date("2026-09-02T00:00:30.000Z");
167
+ const harness = await createPluginHarness([
168
+ ComputerRegistry,
169
+ ToolRegistry,
170
+ SystemPromptRegistry,
171
+ SessionStore,
172
+ ]);
173
+ await harness.mount(
174
+ createComputerAgentPlugin({
175
+ userId: "user-1",
176
+ defaultProviderId: "fixture",
177
+ controlRecords: {
178
+ get: <T>(key: string) =>
179
+ Promise.resolve(records.get(key) as T | undefined),
180
+ now: () => now,
181
+ },
182
+ }),
183
+ );
184
+ const session = harness.root.sessions.create("session-1");
185
+ const preStep = (turn: number) =>
186
+ harness.root.waterfall(
187
+ "agent/pre-step",
188
+ { session } as never,
189
+ [],
190
+ turn,
191
+ 1,
192
+ () => Promise.resolve({ kind: "enter" as const, inputs: [] }),
193
+ );
194
+ const assemble = () =>
195
+ harness.root.systemPrompt.assemble({
196
+ sessionId: "session-1",
197
+ provider: "fixture",
198
+ model: "fixture",
199
+ turnType: "chat",
200
+ });
201
+
202
+ await preStep(1);
203
+ expect((await assemble()).text).toContain(HUMAN_CONTROL_PROMPT_LINE);
204
+ now = new Date("2026-09-02T00:02:00.000Z");
205
+ await preStep(2);
206
+ expect((await assemble()).text).not.toContain(HUMAN_CONTROL_PROMPT_LINE);
207
+
208
+ const injected = session.events.filter(
209
+ (event) => event.type === "computer/injected",
210
+ );
211
+ expect(injected).toMatchObject([
212
+ {
213
+ type: "computer/injected",
214
+ turn: 1,
215
+ text: HUMAN_CONTROL_PROMPT_LINE,
216
+ ownerId: "human:session-1",
217
+ expiresAt: "2026-09-02T00:01:30.000Z",
218
+ },
219
+ { type: "computer/injected", turn: 2, text: "" },
220
+ ]);
221
+ await harness.dispose();
222
+ });
223
+
224
+ test("human-control-active is a non-throwing actionable tool result", async () => {
225
+ const provider: ComputerProvider = {
226
+ id: "fixture",
227
+ open: async (identity, tenant, assignment) => ({
228
+ assignment,
229
+ identity,
230
+ tenant,
231
+ exec: {
232
+ execute: async () => {
233
+ throw new ComputerError(
234
+ "human-control-active",
235
+ "held by human:session-1",
236
+ );
237
+ },
238
+ },
239
+ close: () => Promise.resolve(),
240
+ }),
241
+ };
242
+ const harness = await createPluginHarness([
243
+ ComputerRegistry,
244
+ ToolRegistry,
245
+ SystemPromptRegistry,
246
+ SessionStore,
247
+ ]);
248
+ harness.root.computers.register(provider);
249
+ await harness.mount(
250
+ createComputerAgentPlugin({
251
+ userId: "user-1",
252
+ defaultProviderId: "fixture",
253
+ }),
254
+ );
255
+
256
+ await expect(
257
+ execute(harness, "computer_exec", { command: "pwd" }),
258
+ ).resolves.toEqual({
259
+ content: "The user is controlling this Computer; do not retry this Turn",
260
+ isError: true,
261
+ });
262
+ await harness.dispose();
263
+ });
264
+
265
+ test("satisfies plugin package conventions", () => {
266
+ expect(verifyPluginPackage({ packageJson, manifest })).toMatchObject({
267
+ name: "@frockbot/plugin-computer",
268
+ contributionKinds: ["backend", "runtime", "client"],
269
+ });
270
+ });
271
+ });