@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/src/provider.ts
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ComputerError,
|
|
4
|
+
computerIdentityKeyV1,
|
|
5
|
+
computerSyncSummaryV1,
|
|
6
|
+
computerTenantBotIdV1,
|
|
7
|
+
type ComputerAssignment,
|
|
8
|
+
type ComputerBrowserAction,
|
|
9
|
+
type ComputerBrowserState,
|
|
10
|
+
type ComputerControlLease,
|
|
11
|
+
type ComputerExecRequest,
|
|
12
|
+
type ComputerHandle,
|
|
13
|
+
type ComputerIdentityV1,
|
|
14
|
+
type ComputerOperationOptions,
|
|
15
|
+
type ComputerProvider,
|
|
16
|
+
type ComputerSyncHostV1,
|
|
17
|
+
type ComputerSyncReasonV1,
|
|
18
|
+
type ComputerSyncSummaryV1,
|
|
19
|
+
type ComputerSyncV1,
|
|
20
|
+
type ComputerTenantV1,
|
|
21
|
+
type WorkspaceLayoutV1,
|
|
22
|
+
} from "@frockbot/computer-core";
|
|
23
|
+
import type { Plugin } from "cordis";
|
|
24
|
+
import {
|
|
25
|
+
computerBotKey,
|
|
26
|
+
type BrowserAction,
|
|
27
|
+
type ComputerHostFactoryV1,
|
|
28
|
+
FlySpriteComputer,
|
|
29
|
+
type FlySpriteAgentComputer,
|
|
30
|
+
flySpriteNameForBot,
|
|
31
|
+
} from "./computer.js";
|
|
32
|
+
import { FlyComputerWorkspace } from "./workspace.js";
|
|
33
|
+
import { createFlySpriteSyncV1, type WorkspaceSyncReportV1 } from "./sync.js";
|
|
34
|
+
|
|
35
|
+
const encoder = new TextEncoder();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The durable roots this provider guarantees, laid out to match GrokBot's box
|
|
39
|
+
* (`docs/research/grokbot-computer.md`): `HOME=/home/box`, durable application
|
|
40
|
+
* data under `agent-data`, per-Bot state under `agents/<key>`, and User-shared
|
|
41
|
+
* memory beside it.
|
|
42
|
+
*
|
|
43
|
+
* Memory roots are `read-only` from the Computer's point of view. "The Memory
|
|
44
|
+
* Package is the single writer of Memory roots ... the Workspace presents
|
|
45
|
+
* Memory roots read-only through the durable-root sync" (ADR 0013). That sync
|
|
46
|
+
* is `./sync.ts`: it materializes Memory roots at these mount paths from
|
|
47
|
+
* object storage and never pushes a change back out of them. The User-global
|
|
48
|
+
* instruction root is read-only for the same reason and by the same mechanism
|
|
49
|
+
* (ADR 0016): the Skills Package is its single writer, so a shared root needs
|
|
50
|
+
* no conflict machinery and no Turn has to wake a Computer to read a Skill.
|
|
51
|
+
*
|
|
52
|
+
* `package-declared` roots are User-scoped, because Package availability is
|
|
53
|
+
* User-level and `WorkspaceRootV1` names a Package root by User and Package.
|
|
54
|
+
*/
|
|
55
|
+
export const FLY_WORKSPACE_LAYOUT: WorkspaceLayoutV1 = {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
home: "/home/box",
|
|
58
|
+
roots: [
|
|
59
|
+
{
|
|
60
|
+
kind: "bot-instructions",
|
|
61
|
+
scope: "bot",
|
|
62
|
+
mountPath: "/home/box/agent-data/agents/{bot}/skills",
|
|
63
|
+
access: "read-write",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
// GrokBot's `agent-data/workflows/<slug>/SKILL.md`: the User's own
|
|
67
|
+
// Skills, global across all of their assistants. Read-only on the
|
|
68
|
+
// Computer — the Skills Package writes it through object storage and
|
|
69
|
+
// the sync only materializes it (ADR 0016, extending ADR 0013's Memory
|
|
70
|
+
// exception).
|
|
71
|
+
kind: "user-instructions",
|
|
72
|
+
scope: "user",
|
|
73
|
+
mountPath: "/home/box/agent-data/workflows",
|
|
74
|
+
access: "read-only",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
kind: "bot-memory",
|
|
78
|
+
scope: "bot",
|
|
79
|
+
mountPath: "/home/box/agent-data/agents/{bot}/memory",
|
|
80
|
+
access: "read-only",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
kind: "user-memory",
|
|
84
|
+
scope: "user",
|
|
85
|
+
mountPath: "/home/box/agent-data/user-memory",
|
|
86
|
+
access: "read-only",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
kind: "package-declared",
|
|
90
|
+
scope: "user",
|
|
91
|
+
mountPath: "/home/box/agent-data/user-packages/{package}/{root}",
|
|
92
|
+
access: "read-write",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function browserAction(action: ComputerBrowserAction): BrowserAction {
|
|
98
|
+
switch (action.type) {
|
|
99
|
+
case "snapshot":
|
|
100
|
+
return { action: "snapshot" };
|
|
101
|
+
case "navigate":
|
|
102
|
+
return { action: "navigate", url: action.url };
|
|
103
|
+
case "click":
|
|
104
|
+
return {
|
|
105
|
+
action: "click",
|
|
106
|
+
role: action.role,
|
|
107
|
+
name: action.name,
|
|
108
|
+
exact: action.exact,
|
|
109
|
+
};
|
|
110
|
+
case "fill":
|
|
111
|
+
return {
|
|
112
|
+
action: "fill",
|
|
113
|
+
label: action.label,
|
|
114
|
+
text: action.text,
|
|
115
|
+
exact: action.exact,
|
|
116
|
+
};
|
|
117
|
+
case "press":
|
|
118
|
+
return { action: "press", key: action.key };
|
|
119
|
+
case "wait":
|
|
120
|
+
return { action: "wait", milliseconds: action.milliseconds };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function browserState(output: string): ComputerBrowserState {
|
|
125
|
+
try {
|
|
126
|
+
const parsed: unknown = JSON.parse(output);
|
|
127
|
+
if (
|
|
128
|
+
typeof parsed === "object" &&
|
|
129
|
+
parsed !== null &&
|
|
130
|
+
"snapshot" in parsed &&
|
|
131
|
+
typeof parsed.snapshot === "string"
|
|
132
|
+
) {
|
|
133
|
+
const value = parsed as {
|
|
134
|
+
url?: unknown;
|
|
135
|
+
title?: unknown;
|
|
136
|
+
snapshot: string;
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
url: typeof value.url === "string" ? value.url : undefined,
|
|
140
|
+
title: typeof value.title === "string" ? value.title : undefined,
|
|
141
|
+
accessibilitySnapshot: value.snapshot,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Preserve provider output as a diagnostic snapshot.
|
|
146
|
+
}
|
|
147
|
+
return { accessibilitySnapshot: output };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function shellQuote(value: string): string {
|
|
151
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function commandFor(
|
|
155
|
+
executable: string,
|
|
156
|
+
args: readonly string[] | undefined,
|
|
157
|
+
): string {
|
|
158
|
+
if (
|
|
159
|
+
(executable === "/bin/bash" || executable === "bash") &&
|
|
160
|
+
args?.[0] === "-lc" &&
|
|
161
|
+
typeof args[1] === "string"
|
|
162
|
+
) {
|
|
163
|
+
return args[1];
|
|
164
|
+
}
|
|
165
|
+
return [executable, ...(args ?? [])].map(shellQuote).join(" ");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The durable-root sync of ADR 0013, behind the provider-neutral
|
|
170
|
+
* `ComputerSyncV1`.
|
|
171
|
+
*
|
|
172
|
+
* Everything Fly-specific stops here: the reconciliation itself is
|
|
173
|
+
* `./sync.ts`, the object-storage side and the Durable Object records come
|
|
174
|
+
* from the host, and what leaves this class is counts and a status. It exists
|
|
175
|
+
* only while a Computer is open for a Bot, so it can never be the reason a
|
|
176
|
+
* hibernated Computer wakes.
|
|
177
|
+
*
|
|
178
|
+
* `reconcile` never throws. A paused Sprite, a dropped connection, a store
|
|
179
|
+
* that refuses: each is a declared outcome its caller records on the Turn and
|
|
180
|
+
* carries on — "a dropped connection is an outcome, not a failure."
|
|
181
|
+
*/
|
|
182
|
+
class FlySpriteComputerSync implements ComputerSyncV1 {
|
|
183
|
+
private readonly sync: ReturnType<typeof createFlySpriteSyncV1>;
|
|
184
|
+
|
|
185
|
+
constructor(
|
|
186
|
+
computer: FlySpriteAgentComputer,
|
|
187
|
+
identity: ComputerIdentityV1,
|
|
188
|
+
tenant: ComputerTenantV1,
|
|
189
|
+
host: ComputerSyncHostV1,
|
|
190
|
+
) {
|
|
191
|
+
this.sync = createFlySpriteSyncV1({
|
|
192
|
+
computer,
|
|
193
|
+
layout: FLY_WORKSPACE_LAYOUT,
|
|
194
|
+
userId: identity.userId,
|
|
195
|
+
botDirectoryKey: computerBotKey,
|
|
196
|
+
botIds: [tenant.botId],
|
|
197
|
+
store: host.store,
|
|
198
|
+
...(host.effects ? { effects: host.effects } : {}),
|
|
199
|
+
...(host.generations ? { generations: host.generations } : {}),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async reconcile(
|
|
204
|
+
_reason: ComputerSyncReasonV1,
|
|
205
|
+
options?: ComputerOperationOptions,
|
|
206
|
+
): Promise<ComputerSyncSummaryV1> {
|
|
207
|
+
if (options?.signal?.aborted) {
|
|
208
|
+
return computerSyncSummaryV1("skipped", "the Turn was cancelled");
|
|
209
|
+
}
|
|
210
|
+
let report: WorkspaceSyncReportV1;
|
|
211
|
+
try {
|
|
212
|
+
report = await this.sync.sync();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return computerSyncSummaryV1(
|
|
215
|
+
"unavailable",
|
|
216
|
+
error instanceof Error ? error.message : String(error),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return summarize(report);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async signal(
|
|
223
|
+
options?: ComputerOperationOptions,
|
|
224
|
+
): Promise<string | undefined> {
|
|
225
|
+
if (options?.signal?.aborted) return undefined;
|
|
226
|
+
try {
|
|
227
|
+
const outcome = await this.sync.signal();
|
|
228
|
+
return outcome.status === "ok" ? (outcome.text ?? "") : undefined;
|
|
229
|
+
} catch {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function summarize(report: WorkspaceSyncReportV1): ComputerSyncSummaryV1 {
|
|
236
|
+
const total = (
|
|
237
|
+
pick: (root: WorkspaceSyncReportV1["roots"][number]) => number,
|
|
238
|
+
) => report.roots.reduce((sum, root) => sum + pick(root), 0);
|
|
239
|
+
const failed = report.failures[0];
|
|
240
|
+
const summary: ComputerSyncSummaryV1 = {
|
|
241
|
+
// Every root failing is `unavailable` — the usual shape of a paused
|
|
242
|
+
// Sprite. A partial failure is still an `ok` run that says what it missed.
|
|
243
|
+
status:
|
|
244
|
+
report.failures.length > 0 &&
|
|
245
|
+
report.roots.every((root) => root.failures.length > 0)
|
|
246
|
+
? "unavailable"
|
|
247
|
+
: "ok",
|
|
248
|
+
detail: failed ? `${failed.status}: ${failed.reason}`.slice(0, 512) : "",
|
|
249
|
+
pulled: total((root) => root.pulled.length),
|
|
250
|
+
pushed: total((root) => root.pushed.length),
|
|
251
|
+
restored: total((root) => root.restored.length),
|
|
252
|
+
removed: total(
|
|
253
|
+
(root) => root.removedOnComputer.length + root.removedInStore.length,
|
|
254
|
+
),
|
|
255
|
+
adopted: total((root) => root.adopted.length),
|
|
256
|
+
conflicts: report.conflicts.length,
|
|
257
|
+
failures: report.failures.length,
|
|
258
|
+
};
|
|
259
|
+
return summary;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One bash document for one exec request.
|
|
264
|
+
*
|
|
265
|
+
* `cwd` and `env` become `cd` and `export` lines rather than transport
|
|
266
|
+
* options, and `stdin` is fed to the command from a heredoc, because the whole
|
|
267
|
+
* request travels as a script on the command's own stdin. Nothing reaches an
|
|
268
|
+
* argv, which is what the 431 recorded in ADR 0004 cost to learn.
|
|
269
|
+
*/
|
|
270
|
+
function composed(request: ComputerExecRequest): string {
|
|
271
|
+
const command = commandFor(request.executable, request.args);
|
|
272
|
+
const lines: string[] = [];
|
|
273
|
+
for (const [key, value] of Object.entries(request.env ?? {})) {
|
|
274
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
275
|
+
}
|
|
276
|
+
if (request.cwd) lines.push(`cd ${shellQuote(request.cwd)}`);
|
|
277
|
+
if (request.stdin === undefined) {
|
|
278
|
+
lines.push(command);
|
|
279
|
+
} else {
|
|
280
|
+
// A quoted heredoc: the bytes reach the command unexpanded, and a
|
|
281
|
+
// delimiter derived from them cannot appear inside them.
|
|
282
|
+
const marker = `FROCKBOT_STDIN_${createHash("sha256")
|
|
283
|
+
.update(request.stdin)
|
|
284
|
+
.digest("hex")
|
|
285
|
+
.slice(0, 16)
|
|
286
|
+
.toUpperCase()}`;
|
|
287
|
+
lines.push(`${command} <<'${marker}'`);
|
|
288
|
+
lines.push(new TextDecoder().decode(request.stdin));
|
|
289
|
+
lines.push(marker);
|
|
290
|
+
}
|
|
291
|
+
return lines.join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function lease(result: {
|
|
295
|
+
ownerId: string;
|
|
296
|
+
expiresAt?: string;
|
|
297
|
+
}): ComputerControlLease {
|
|
298
|
+
return {
|
|
299
|
+
id: result.ownerId,
|
|
300
|
+
// A lease with no expiry would be a lease nothing can reclaim. The host
|
|
301
|
+
// always dates an acquire and a renew; this is the refusal if it ever
|
|
302
|
+
// does not.
|
|
303
|
+
expiresAt: result.expiresAt ?? new Date(0).toISOString(),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handle(
|
|
308
|
+
identity: ComputerIdentityV1,
|
|
309
|
+
tenant: ComputerTenantV1,
|
|
310
|
+
computer: FlySpriteAgentComputer,
|
|
311
|
+
assignment: ComputerAssignment,
|
|
312
|
+
syncHost?: ComputerSyncHostV1,
|
|
313
|
+
): ComputerHandle {
|
|
314
|
+
return {
|
|
315
|
+
assignment,
|
|
316
|
+
identity,
|
|
317
|
+
tenant,
|
|
318
|
+
...(syncHost
|
|
319
|
+
? {
|
|
320
|
+
sync: new FlySpriteComputerSync(computer, identity, tenant, syncHost),
|
|
321
|
+
}
|
|
322
|
+
: {}),
|
|
323
|
+
workspace: new FlyComputerWorkspace(FLY_WORKSPACE_LAYOUT, {
|
|
324
|
+
computer,
|
|
325
|
+
userId: identity.userId,
|
|
326
|
+
botId: tenant.botId,
|
|
327
|
+
botDirectoryKey: computerBotKey,
|
|
328
|
+
// The Durable Object's generation ledger, when the host supplied one.
|
|
329
|
+
// Without it the Computer's Workspace can attribute nothing, because a
|
|
330
|
+
// sidecar on the Computer is a hint and never an authority.
|
|
331
|
+
...(syncHost?.generations ? { generations: syncHost.generations } : {}),
|
|
332
|
+
}),
|
|
333
|
+
exec: {
|
|
334
|
+
execute: async (request, options) => {
|
|
335
|
+
// `cwd`, `env`, and `stdin` used to be refused because the Sprites SDK
|
|
336
|
+
// put every one of them into a request URL. The host compiles them
|
|
337
|
+
// into the script it delivers on the command's stdin instead, so they
|
|
338
|
+
// are ordinary parts of a request now.
|
|
339
|
+
const result = await computer.exec(
|
|
340
|
+
composed(request),
|
|
341
|
+
options?.signal ?? new AbortController().signal,
|
|
342
|
+
{
|
|
343
|
+
timeoutMs: request.timeoutMs,
|
|
344
|
+
maxOutputBytes: request.maxOutputBytes,
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
return {
|
|
348
|
+
exitCode: result.exitCode,
|
|
349
|
+
stdout: encoder.encode(result.stdout),
|
|
350
|
+
stderr: encoder.encode(result.stderr),
|
|
351
|
+
outputTruncated: result.outputTruncated,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
screenshot: {
|
|
356
|
+
capture: async (options) => {
|
|
357
|
+
const captured = await computer.screenshot(
|
|
358
|
+
options?.signal ?? new AbortController().signal,
|
|
359
|
+
);
|
|
360
|
+
return {
|
|
361
|
+
bytes: captured.bytes,
|
|
362
|
+
mediaType: "image/png",
|
|
363
|
+
display: captured.display,
|
|
364
|
+
capturedAt: captured.capturedAt,
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
// The Computer's self-check. Read-only, and not lease-guarded: a Computer
|
|
369
|
+
// under human control is exactly a Computer somebody may need to ask what
|
|
370
|
+
// is wrong with.
|
|
371
|
+
doctor: {
|
|
372
|
+
run: (options) =>
|
|
373
|
+
computer.doctor(options?.signal ?? new AbortController().signal),
|
|
374
|
+
},
|
|
375
|
+
presence: {
|
|
376
|
+
connect: async (options) => {
|
|
377
|
+
const connected = await computer.connect(options);
|
|
378
|
+
return {
|
|
379
|
+
id: connected.viewerSessionId,
|
|
380
|
+
url: connected.viewerUrl,
|
|
381
|
+
...(connected.viewerExpiresAt
|
|
382
|
+
? { expiresAt: connected.viewerExpiresAt }
|
|
383
|
+
: {}),
|
|
384
|
+
...(connected.message ? { message: connected.message } : {}),
|
|
385
|
+
};
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
processes: {
|
|
389
|
+
launch: async (request, options) => {
|
|
390
|
+
const launched = await computer.launchProcess(
|
|
391
|
+
request.processId,
|
|
392
|
+
request.command,
|
|
393
|
+
options?.signal ?? new AbortController().signal,
|
|
394
|
+
);
|
|
395
|
+
return {
|
|
396
|
+
pid: launched.pid,
|
|
397
|
+
logPath: launched.logPath,
|
|
398
|
+
cwd: launched.cwd,
|
|
399
|
+
// The generation the launch happened under, which is what a later
|
|
400
|
+
// check compares against to decide whether this is the same
|
|
401
|
+
// Computer at all.
|
|
402
|
+
generation: computer.generation ?? 0,
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
inspect: (processId, options) =>
|
|
406
|
+
computer.inspectProcess(
|
|
407
|
+
processId,
|
|
408
|
+
options?.signal ?? new AbortController().signal,
|
|
409
|
+
options?.tailBytes,
|
|
410
|
+
),
|
|
411
|
+
stop: (processId, options) =>
|
|
412
|
+
computer.stopProcess(
|
|
413
|
+
processId,
|
|
414
|
+
options?.signal ?? new AbortController().signal,
|
|
415
|
+
),
|
|
416
|
+
// Asked of the host every time, never read from the cached open: a
|
|
417
|
+
// process's whole reconciliation question is whether the Computer
|
|
418
|
+
// answering now is the one it was launched on.
|
|
419
|
+
generation: (options) => computer.currentGeneration(options?.signal),
|
|
420
|
+
},
|
|
421
|
+
browser: {
|
|
422
|
+
perform: async (action, options) =>
|
|
423
|
+
browserState(
|
|
424
|
+
await computer.browser(
|
|
425
|
+
browserAction(action),
|
|
426
|
+
options?.signal ?? new AbortController().signal,
|
|
427
|
+
),
|
|
428
|
+
),
|
|
429
|
+
},
|
|
430
|
+
// A viewer and a human-control lease are reachable from the Durable
|
|
431
|
+
// Object now. They were not before: both need the Sprite's URL and its
|
|
432
|
+
// `flock`, and neither was reachable from workerd (ADR 0004).
|
|
433
|
+
viewer: {
|
|
434
|
+
open: async (options) => {
|
|
435
|
+
const result = await computer.viewer(options);
|
|
436
|
+
if (!result.session) {
|
|
437
|
+
throw new ComputerError(
|
|
438
|
+
"provider-unavailable",
|
|
439
|
+
"The Computer host returned no viewer session",
|
|
440
|
+
true,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
id: result.session.id,
|
|
445
|
+
url: result.session.url,
|
|
446
|
+
...(result.session.expiresAt
|
|
447
|
+
? { expiresAt: result.session.expiresAt }
|
|
448
|
+
: {}),
|
|
449
|
+
};
|
|
450
|
+
},
|
|
451
|
+
renew: async (sessionId, options) => {
|
|
452
|
+
const result = await computer.refreshViewer(sessionId, options);
|
|
453
|
+
if (!result.session) {
|
|
454
|
+
throw new ComputerError(
|
|
455
|
+
"provider-unavailable",
|
|
456
|
+
"The Computer host did not renew the viewer session",
|
|
457
|
+
true,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
id: result.session.id,
|
|
462
|
+
url: result.session.url,
|
|
463
|
+
...(result.session.expiresAt
|
|
464
|
+
? { expiresAt: result.session.expiresAt }
|
|
465
|
+
: {}),
|
|
466
|
+
};
|
|
467
|
+
},
|
|
468
|
+
revoke: async (sessionId, options) => {
|
|
469
|
+
await computer.revokeViewer(sessionId, options);
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
control: {
|
|
473
|
+
// The scope and the owner travel with the call, so one Computer surface
|
|
474
|
+
// serves legacy per-tenant leases and the User-wide `desktop-gui` lease
|
|
475
|
+
// on which human sessions and `computerUse` subagents contend.
|
|
476
|
+
acquire: async (request, options) =>
|
|
477
|
+
lease(await computer.takeControl(options, request)),
|
|
478
|
+
renew: async (_current, request, options) =>
|
|
479
|
+
lease(await computer.refreshControl(options, request)),
|
|
480
|
+
release: (_current, request, options) =>
|
|
481
|
+
computer.releaseControl(options, request),
|
|
482
|
+
},
|
|
483
|
+
close: () => Promise.resolve(),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* One Sprite per User (ADR 0012). The Sprite name is derived from the User and
|
|
489
|
+
* from nothing else, so every Bot the User owns lands on the same Computer,
|
|
490
|
+
* sharing its browser profile, installed tooling, and Workspace.
|
|
491
|
+
*/
|
|
492
|
+
export function flySpriteNameForComputer(identity: ComputerIdentityV1): string {
|
|
493
|
+
return flySpriteNameForBot(JSON.stringify(["user", identity.userId.trim()]));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Provider adapter that keeps Fly-specific lifecycle behind Computer core. */
|
|
497
|
+
export class FlySpriteComputerProvider implements ComputerProvider {
|
|
498
|
+
readonly id = "fly-sprite";
|
|
499
|
+
readonly workspaceLayout = FLY_WORKSPACE_LAYOUT;
|
|
500
|
+
private readonly computers = new Map<string, FlySpriteComputer>();
|
|
501
|
+
|
|
502
|
+
constructor(
|
|
503
|
+
private readonly fixedComputer?: FlySpriteComputer,
|
|
504
|
+
/**
|
|
505
|
+
* The shared Computer host (ADR 0004). Absent, and every Computer this
|
|
506
|
+
* provider opens is unconfigured: the provider Package holds no Sprites
|
|
507
|
+
* SDK and no token, so without a host there is no compute to reach.
|
|
508
|
+
*/
|
|
509
|
+
private readonly host?: ComputerHostFactoryV1,
|
|
510
|
+
/**
|
|
511
|
+
* The object-storage side of the durable roots, and the Durable Object
|
|
512
|
+
* records a push depends on. Supplied by the host for one admitted Turn;
|
|
513
|
+
* absent outside one, and the handle then carries no `sync` at all rather
|
|
514
|
+
* than a sync with nowhere to record its intent.
|
|
515
|
+
*/
|
|
516
|
+
private readonly syncHost?: ComputerSyncHostV1,
|
|
517
|
+
/** The active `computerUse` task owner, on that task's child Turn. */
|
|
518
|
+
private readonly agentControlOwnerId?: string,
|
|
519
|
+
) {}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The one Sprite backing a User's Computer. One Computer per User (ADR
|
|
523
|
+
* 0012): every Bot the User owns is a tenant on the instance this returns.
|
|
524
|
+
*/
|
|
525
|
+
computerFor(identity: ComputerIdentityV1): FlySpriteComputer {
|
|
526
|
+
if (this.fixedComputer) return this.fixedComputer;
|
|
527
|
+
const key = computerIdentityKeyV1(identity);
|
|
528
|
+
let computer = this.computers.get(key);
|
|
529
|
+
if (!computer) {
|
|
530
|
+
computer = new FlySpriteComputer({
|
|
531
|
+
identity: { userId: identity.userId },
|
|
532
|
+
...(this.host ? { host: this.host } : {}),
|
|
533
|
+
respectHumanControl: true,
|
|
534
|
+
spriteName: flySpriteNameForComputer(identity),
|
|
535
|
+
...(this.agentControlOwnerId
|
|
536
|
+
? { agentControlOwnerId: this.agentControlOwnerId }
|
|
537
|
+
: {}),
|
|
538
|
+
});
|
|
539
|
+
this.computers.set(key, computer);
|
|
540
|
+
}
|
|
541
|
+
return computer;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
open(
|
|
545
|
+
identity: ComputerIdentityV1,
|
|
546
|
+
tenant: ComputerTenantV1,
|
|
547
|
+
assignment: ComputerAssignment,
|
|
548
|
+
_options?: ComputerOperationOptions,
|
|
549
|
+
): Promise<ComputerHandle> {
|
|
550
|
+
computerIdentityKeyV1(identity);
|
|
551
|
+
const botId = computerTenantBotIdV1(tenant);
|
|
552
|
+
const attached = this.computerFor(identity).bot(botId);
|
|
553
|
+
return Promise.resolve(
|
|
554
|
+
handle(
|
|
555
|
+
{ userId: identity.userId },
|
|
556
|
+
{
|
|
557
|
+
botId,
|
|
558
|
+
directory: attached.directory,
|
|
559
|
+
...(attached.display ? { display: attached.display } : {}),
|
|
560
|
+
},
|
|
561
|
+
attached,
|
|
562
|
+
assignment,
|
|
563
|
+
this.syncHost,
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function createFlySpriteProviderPlugin(
|
|
570
|
+
computer?: FlySpriteComputer,
|
|
571
|
+
options?: {
|
|
572
|
+
host?: ComputerHostFactoryV1;
|
|
573
|
+
sync?: ComputerSyncHostV1;
|
|
574
|
+
agentControlOwnerId?: string;
|
|
575
|
+
},
|
|
576
|
+
): Plugin.Function {
|
|
577
|
+
const plugin: Plugin.Function = (ctx) =>
|
|
578
|
+
ctx.computers.register(
|
|
579
|
+
new FlySpriteComputerProvider(
|
|
580
|
+
computer,
|
|
581
|
+
options?.host,
|
|
582
|
+
options?.sync,
|
|
583
|
+
options?.agentControlOwnerId,
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
plugin.inject = ["computers"];
|
|
587
|
+
return plugin;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export const flySpriteProviderPlugin = createFlySpriteProviderPlugin();
|
|
591
|
+
export default flySpriteProviderPlugin;
|