@fungi.computer/oyster 0.3.1 → 0.4.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 (75) hide show
  1. package/README.md +2 -1
  2. package/dist/agent-computer-source.d.ts +42 -0
  3. package/dist/agent-computer-source.js +88 -0
  4. package/dist/agent-computers.d.ts +104 -0
  5. package/dist/agent-computers.js +227 -0
  6. package/dist/agents.d.ts +10 -0
  7. package/dist/agents.js +33 -0
  8. package/dist/boot-capability.d.ts +1 -1
  9. package/dist/boot-capability.js +1 -1
  10. package/dist/browser/boot.d.ts +2 -2
  11. package/dist/browser/boot.js +10 -3
  12. package/dist/browser/browser.d.ts +1 -1
  13. package/dist/browser/browser.js +2 -1
  14. package/dist/browser/cdp-client.d.ts +2 -0
  15. package/dist/browser/errors.d.ts +2 -2
  16. package/dist/browser/errors.js +2 -2
  17. package/dist/browser/index.d.ts +3 -2
  18. package/dist/browser/index.js +3 -2
  19. package/dist/browser/page.js +11 -4
  20. package/dist/browser/sessions.d.ts +7 -4
  21. package/dist/browser/sessions.js +159 -0
  22. package/dist/browser/url-policy.d.ts +4 -2
  23. package/dist/browser/url-policy.js +4 -2
  24. package/dist/computer-control-port.d.ts +26 -0
  25. package/dist/computer-control-port.js +1 -0
  26. package/dist/host.d.ts +3 -0
  27. package/dist/machine-link/delivery.d.ts +1 -1
  28. package/dist/machine-link/delivery.js +2 -3
  29. package/dist/machine-link/host.d.ts +37 -6
  30. package/dist/machine-link/host.js +209 -14
  31. package/dist/machine-link/link.d.ts +13 -1
  32. package/dist/machine-link/link.js +20 -7
  33. package/dist/mcp.d.ts +152 -11
  34. package/dist/mcp.js +131 -13
  35. package/dist/page/computer-handles.d.ts +3 -3
  36. package/dist/page-script.js +1 -1
  37. package/dist/standalone/agent-computers.d.ts +29 -0
  38. package/dist/standalone/agent-computers.js +101 -0
  39. package/dist/standalone/agent-mail.d.ts +41 -18
  40. package/dist/standalone/agent-model-choice.d.ts +26 -0
  41. package/dist/standalone/agent-model-choice.js +72 -0
  42. package/dist/standalone/agents.d.ts +50 -5
  43. package/dist/standalone/agents.js +505 -53
  44. package/dist/standalone/app-builds-binding.js +1 -1
  45. package/dist/standalone/computer-control.d.ts +4 -0
  46. package/dist/standalone/computer-control.js +62 -0
  47. package/dist/standalone/computer-fleet.d.ts +1 -0
  48. package/dist/standalone/computer-fleet.js +1 -1
  49. package/dist/standalone/computer.d.ts +21 -2
  50. package/dist/standalone/computer.js +12 -0
  51. package/dist/standalone/machine-link.d.ts +10 -0
  52. package/dist/standalone/machine-link.js +6 -0
  53. package/dist/standalone/registry.d.ts +12 -0
  54. package/dist/standalone/registry.js +18 -0
  55. package/dist/standalone/tenancy.d.ts +14 -1
  56. package/dist/standalone/tenancy.js +37 -3
  57. package/dist/standalone/worker.js +36 -9
  58. package/dist/standalone/workflow-binding.d.ts +2 -2
  59. package/dist/standalone/workflow-host.d.ts +11 -11
  60. package/dist/standalone/workflows.d.ts +22 -22
  61. package/dist/storage-frame.d.ts +3 -3
  62. package/dist/workers-git.d.ts +2 -0
  63. package/dist/workers-git.js +11 -1
  64. package/dist/workers.d.ts +8 -8
  65. package/docs/api/agents.md +66 -0
  66. package/docs/api/apps.md +104 -104
  67. package/docs/api/build-contract.md +35 -35
  68. package/docs/api/build.md +12 -12
  69. package/docs/api/host.md +61 -61
  70. package/docs/api/mcp.md +141 -13
  71. package/docs/api/root-data.md +1 -1
  72. package/docs/api/tenancy.md +1 -1
  73. package/docs/api/workers-git.md +3 -2
  74. package/docs/api/workers.md +81 -83
  75. package/package.json +25 -17
@@ -203,7 +203,7 @@ export async function dispatchAppsBuildsBinding(env, scope, frame, props, method
203
203
  const registry = buildRegistry(env, scope);
204
204
  const build = "repo" in input
205
205
  ? await registry.readRepositoryBuild(frame.facts.selection.owner.id, input.repo)
206
- : await registry.readBuild(input.jobId);
206
+ : await registry.readInstallationBuild(frame.facts.selection.owner.id, input.jobId);
207
207
  frame.signal.throwIfAborted();
208
208
  const missingJob = build === null && "jobId" in input;
209
209
  return Schema.decodeUnknownSync(Schema.Json)({
@@ -0,0 +1,4 @@
1
+ import type { ComputerControlPort } from "../mcp.js";
2
+ import type { StandaloneRegistryRpc } from "./registry.js";
3
+ /** MCP control speaks to the same owner-admitted durable fleet as HTTP. */
4
+ export declare function standaloneComputerControl(registry: Pick<StandaloneRegistryRpc, "computerFleet">, memberId: string): ComputerControlPort;
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import { computerStateSchema, } from "@fungi.computer/computer/fleet";
3
+ import { teamComputerDirectorySchema } from "@fungi.computer/nacre/workbench";
4
+ const inventoryComputer = z.strictObject({
5
+ computerId: z.string(),
6
+ name: z.string(),
7
+ createdAt: z.number(),
8
+ updatedAt: z.number(),
9
+ });
10
+ const inventoryPage = z.strictObject({
11
+ computers: z.array(inventoryComputer),
12
+ nextOffset: z.number().int().nonnegative().nullable(),
13
+ });
14
+ const inventoryError = z.strictObject({
15
+ error: z.string(),
16
+ });
17
+ /** MCP control speaks to the same owner-admitted durable fleet as HTTP. */
18
+ export function standaloneComputerControl(registry, memberId) {
19
+ const fleet = (request) => registry.computerFleet(memberId, request);
20
+ const available = (result) => {
21
+ if (result === null || inventoryError.safeParse(result).success)
22
+ throw new Error("Computer fleet refused the request");
23
+ return result;
24
+ };
25
+ return {
26
+ list: async (page) => teamComputerDirectorySchema.parse(inventoryPage.parse(available(await fleet({ operation: "list", page })))),
27
+ state: async (computerId) => computerStateSchema.parse(available(await fleet({ operation: "state", computerId }))),
28
+ start: async (computerId) => computerStateSchema.parse(available(await fleet({ operation: "start", computerId }))),
29
+ create: async (input) => {
30
+ let result;
31
+ try {
32
+ result = await fleet({ operation: "create", request: input });
33
+ }
34
+ catch {
35
+ // Creation may have persisted before the RPC failed. Surface its
36
+ // identity so the caller can retry, never silently mint another one.
37
+ return { kind: "uncertain" };
38
+ }
39
+ if (result === null)
40
+ return { kind: "denied" };
41
+ const error = inventoryError.safeParse(result);
42
+ if (error.success) {
43
+ switch (error.data.error) {
44
+ case "owner_required":
45
+ return { kind: "denied" };
46
+ case "name_taken":
47
+ case "request_conflict":
48
+ return { kind: "conflict" };
49
+ case "partial_init":
50
+ return { kind: "uncertain" };
51
+ default:
52
+ return { kind: "invalid" };
53
+ }
54
+ }
55
+ const computer = inventoryComputer.parse(result);
56
+ return {
57
+ kind: "created",
58
+ computer: { computerId: computer.computerId, name: computer.name },
59
+ };
60
+ },
61
+ };
62
+ }
@@ -23,6 +23,7 @@ export declare const standaloneFleetRequestSchema: z.ZodDiscriminatedUnion<[z.Zo
23
23
  resolve: "resolve";
24
24
  select: "select";
25
25
  start: "start";
26
+ state: "state";
26
27
  }>;
27
28
  computerId: z.ZodString;
28
29
  }, z.core.$strict>, z.ZodObject<{
@@ -15,7 +15,7 @@ export const standaloneFleetRequestSchema = z.discriminatedUnion("operation", [
15
15
  page: computerPageSchema,
16
16
  }),
17
17
  z.strictObject({
18
- operation: z.enum(["read", "resolve", "start", "select"]),
18
+ operation: z.enum(["read", "resolve", "state", "start", "select"]),
19
19
  computerId: fleetComputerId,
20
20
  }),
21
21
  z.strictObject({
@@ -4,7 +4,8 @@ import { type PreviewSelector } from "./preview.js";
4
4
  import { type StandaloneTenancyEnv } from "./tenancy.js";
5
5
  import * as z from "zod";
6
6
  import { computerProcessSchema, computerToolResultSchema, readOutputSchema } from "@fungi.computer/computer-contract";
7
- import { ComputerDO as NativeComputerDO, type AttachedBackend, type ComputerEnv } from "@fungi.computer/computer";
7
+ import { ComputerDO as NativeComputerDO, type AttachedBackend, type ComputerEnv, type ComputerFilesRpc } from "@fungi.computer/computer";
8
+ import { patchInputSchema, patchOutputSchema } from "@fungi.computer/knapsack/files-patch";
8
9
  import { listOutputSchema, writeOutputSchema, mkdirOutputSchema, computerPortsOutputSchema, computerPortOutputSchema, computerPortRemovalOutputSchema, processListOutputSchema, processKillOutputSchema, computerOperationResultSchema, type ComputerExecutionHostHooks, type ComputerValue } from "@fungi.computer/computer-contract";
9
10
  import { type WorkflowGitCredential, type WorkflowGitPassGrant } from "@fungi.computer/braid/runtime";
10
11
  import { type ComputerAction, type ComputerGrant } from "@fungi.computer/computer/fleet";
@@ -66,8 +67,13 @@ export type StandaloneWorkflowGitIssuer = Readonly<{
66
67
  export type ComputerFetch = {
67
68
  fetch(request: Request): Promise<Response>;
68
69
  };
70
+ /** The additional native doors an attached Agent may use on this Computer. */
71
+ export type StandaloneAgentComputerRpc = ComputerFilesRpc & Readonly<{
72
+ tool(authority: ComputerGrant, capabilityId: string, input: ComputerValue): Promise<StandaloneComputerResult>;
73
+ filePatch(authority: ComputerGrant, input: z.output<typeof patchInputSchema>): Promise<z.output<typeof patchOutputSchema>>;
74
+ }>;
69
75
  /** Durable workspace and existing backings, with fleet checks at native acceptance. */
70
- export declare class ComputerDO extends NativeComputerDO implements ComputerFetch {
76
+ export declare class ComputerDO extends NativeComputerDO implements ComputerFetch, StandaloneAgentComputerRpc {
71
77
  #private;
72
78
  private readonly previewBindings;
73
79
  constructor(state: DurableObjectState, previewBindings: StandaloneComputerOwnerEnv);
@@ -77,6 +83,17 @@ export declare class ComputerDO extends NativeComputerDO implements ComputerFetc
77
83
  startHostedComputer(authority: ComputerGrant): Promise<{
78
84
  state: "running";
79
85
  }>;
86
+ /** Observe an admitted fleet Computer without starting its backend. */
87
+ readHostedComputerState(authority: ComputerGrant): Promise<{
88
+ state: "queued";
89
+ position: number;
90
+ } | {
91
+ state: "running" | "starting";
92
+ position?: never;
93
+ } | {
94
+ state: "running" | "sleeping";
95
+ position?: never;
96
+ }>;
80
97
  /** A read-only preflight over the maintained hosted-route selector. */
81
98
  buildBackend(): OysterBuildBackend;
82
99
  /**
@@ -120,6 +137,8 @@ export declare class ComputerDO extends NativeComputerDO implements ComputerFetc
120
137
  /** Binary files share the same native filesystem as commands and PTYs. */
121
138
  fileRead(authority: ComputerGrant, path: string): Promise<Uint8Array | null>;
122
139
  fileWrite(authority: ComputerGrant, path: string, bytes: Uint8Array): Promise<void>;
140
+ /** Patch is handled by the Computer's pinned native engine, not by the Agent. */
141
+ filePatch(authority: ComputerGrant, input: z.output<typeof patchInputSchema>): Promise<z.output<typeof patchOutputSchema>>;
123
142
  fileReplace(authority: ComputerGrant, path: string, expectedContent: string, content: string): Promise<{
124
143
  path: string;
125
144
  bytes: number;
@@ -11,6 +11,7 @@ import { Base64 } from "effect/encoding";
11
11
  import * as z from "zod";
12
12
  import { ComputerPortConflictError, computerByteReadReplySchema, computerProcessSchema, installationProcessScope, processStartInputSchema, processStartToolInputSchema, computerCapabilities, computerToolResultSchema, readOutputSchema, } from "@fungi.computer/computer-contract";
13
13
  import { ComputerDO as NativeComputerDO, ComputerOperationConflictError, } from "@fungi.computer/computer";
14
+ import { patchInputSchema, patchOutputSchema, } from "@fungi.computer/knapsack/files-patch";
14
15
  import { ComputerFileConflict, computerExecutionRequestSchema, computerValueSchema, computerPortInputSchema, listInputSchema, listOutputSchema, readBytesInputSchema, writeInputSchema, replaceInputSchema, writeOutputSchema, mkdirOutputSchema, computerPortsOutputSchema, computerPortOutputSchema, computerPortRemovalOutputSchema, processListOutputSchema, processKillOutputSchema, computerOperationResultSchema, mintComputerRequestId, processIdInputSchema, } from "@fungi.computer/computer-contract";
15
16
  import { workflowGitExecutionEnvironment, workflowGitPassGrantSchema, } from "@fungi.computer/braid/runtime";
16
17
  import { checkComputerGrant, computerActionSchema, computerGrantSchema, createFleetBacking, FleetGrantRefused, FleetLifecycle, } from "@fungi.computer/computer/fleet";
@@ -173,6 +174,11 @@ export class ComputerDO extends NativeComputerDO {
173
174
  await run();
174
175
  });
175
176
  }
177
+ /** Observe an admitted fleet Computer without starting its backend. */
178
+ readHostedComputerState(authority) {
179
+ this.#accept(authority, "start");
180
+ return this.#fleetLifecycle().readComputerState();
181
+ }
176
182
  #previewGrant(ownerId) {
177
183
  return {
178
184
  computerId: this.#id(),
@@ -455,6 +461,12 @@ export class ComputerDO extends NativeComputerDO {
455
461
  this.#accept(authority, "files.writeBytes");
456
462
  });
457
463
  }
464
+ /** Patch is handled by the Computer's pinned native engine, not by the Agent. */
465
+ async filePatch(authority, input) {
466
+ return this.applyPatchAfterAcceptance(input, () => {
467
+ this.#accept(authority, "files.edit");
468
+ });
469
+ }
458
470
  async fileReplace(authority, path, expectedContent, content) {
459
471
  try {
460
472
  const result = await this.replaceFileIfUnchangedAfterAcceptance({ path, expectedContent, content }, () => {
@@ -265,6 +265,7 @@ export declare function standaloneMachineLinkRoutes(env: StandaloneMachineLinkEn
265
265
  /** One device's link; directory, credentials and owner state remain elsewhere. */
266
266
  export declare class MachineLinkDO extends DurableObject<StandaloneMachineLinkEnv> {
267
267
  private readonly link;
268
+ alarm(): Promise<void>;
268
269
  fetch(request: Request): Promise<Response>;
269
270
  deliver(id: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
270
271
  operation(id: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, operation: MachineAgentOperation): Promise<import("effect/Schema").Json>;
@@ -276,6 +277,15 @@ export declare class MachineLinkDO extends DurableObject<StandaloneMachineLinkEn
276
277
  readErasure(): Promise<{
277
278
  erased: boolean;
278
279
  }>;
280
+ deadLetters(id: MachineLinkIdentity, afterKey?: string): Promise<Readonly<{
281
+ letters: readonly Readonly<{
282
+ key: string;
283
+ reason: "refused" | "exhausted" | "expired";
284
+ envelope: PostEnvelope;
285
+ attempts: number;
286
+ }>[];
287
+ nextCursor: string | undefined;
288
+ }>>;
279
289
  webSocketMessage(socket: WebSocket, message: string | ArrayBuffer): Promise<void>;
280
290
  webSocketClose(socket: WebSocket, code: number, reason: string): void;
281
291
  webSocketError(socket: WebSocket): void;
@@ -176,6 +176,9 @@ export class MachineLinkDO extends DurableObject {
176
176
  console.error(operation, cause);
177
177
  },
178
178
  });
179
+ alarm() {
180
+ return this.link.alarm();
181
+ }
179
182
  fetch(request) {
180
183
  return this.link.fetch(request);
181
184
  }
@@ -197,6 +200,9 @@ export class MachineLinkDO extends DurableObject {
197
200
  readErasure() {
198
201
  return this.link.readErasure();
199
202
  }
203
+ deadLetters(id, afterKey) {
204
+ return this.link.deadLetters(id, afterKey);
205
+ }
200
206
  webSocketMessage(socket, message) {
201
207
  return this.link.webSocketMessage(socket, message);
202
208
  }
@@ -37,6 +37,7 @@ export type StandaloneRegistryRpc = {
37
37
  registerRepositoryApp(input: Parameters<StandaloneRegistry["registerRepositoryApp"]>[0]): ReturnType<StandaloneRegistry["registerRepositoryApp"]>;
38
38
  repositoryBuildSource(installationId: string, repository: string): ReturnType<StandaloneRegistry["repositoryBuildSource"]>;
39
39
  readRepositoryBuild(installationId: string, repository: string): ReturnType<StandaloneRegistry["readRepositoryBuild"]>;
40
+ readInstallationBuild(installationId: string, jobId: string): ReturnType<StandaloneRegistry["readInstallationBuild"]>;
40
41
  installWorkflowBuild(jobId: string, target: string, release: Release, memberId: string, callerInstallationId: string): ReturnType<StandaloneRegistry["installWorkflowBuild"]>;
41
42
  };
42
43
  export declare abstract class StandaloneRegistry extends OysterRegistry implements StandaloneRegistryRpc {
@@ -60,6 +61,15 @@ export declare abstract class StandaloneRegistry extends OysterRegistry implemen
60
61
  }>[];
61
62
  readonly nextOffset: number | null;
62
63
  } | ({
64
+ state: "queued";
65
+ position: number;
66
+ } & Disposable) | ({
67
+ state: "running" | "starting";
68
+ position?: never | undefined;
69
+ } & Disposable) | ({
70
+ state: "running" | "sleeping";
71
+ position?: never | undefined;
72
+ } & Disposable) | ({
63
73
  state: "running";
64
74
  } & Disposable) | {
65
75
  error: string;
@@ -169,6 +179,8 @@ export declare abstract class StandaloneRegistry extends OysterRegistry implemen
169
179
  appId: import("@fungi.computer/puffball").AppId;
170
180
  } | null>;
171
181
  readBuild(jobId: string): Promise<OysterBuildView | null>;
182
+ /** A job ID alone never grants another installation access to its status. */
183
+ readInstallationBuild(installationId: string, jobId: string): Promise<OysterBuildView | null>;
172
184
  /** Repository status belongs to the same captured Git owner as registration. */
173
185
  readRepositoryBuild(installationId: string, repository: string): Promise<Readonly<{
174
186
  jobId: string;
@@ -129,6 +129,15 @@ export class StandaloneRegistry extends OysterRegistry {
129
129
  await this.#fleetStorage.put("selected-computer", input.computerId);
130
130
  return computer;
131
131
  }
132
+ case "state":
133
+ if (this.#computers.read(input.computerId) === null)
134
+ return null;
135
+ return this.#env.COMPUTER.getByName(input.computerId).readHostedComputerState({
136
+ computerId: input.computerId,
137
+ actions: new Set(["start"]),
138
+ expiresAt: Date.now() + 60_000,
139
+ provenance: { actor: memberId },
140
+ });
132
141
  case "start":
133
142
  if (this.#computers.read(input.computerId) === null)
134
143
  return null;
@@ -363,6 +372,15 @@ export class StandaloneRegistry extends OysterRegistry {
363
372
  async readBuild(jobId) {
364
373
  return (await this.#appBuilds()).read(jobId);
365
374
  }
375
+ /** A job ID alone never grants another installation access to its status. */
376
+ async readInstallationBuild(installationId, jobId) {
377
+ const builds = await this.#appBuilds();
378
+ const build = builds.read(jobId);
379
+ return build !== null &&
380
+ sourceOwnedBy(builds.readSource(build.appId), installationId)
381
+ ? build
382
+ : null;
383
+ }
366
384
  /** Repository status belongs to the same captured Git owner as registration. */
367
385
  async readRepositoryBuild(installationId, repository) {
368
386
  const latest = (await this.#appBuilds()).latestRepositoryBuild(installationId, repository);
@@ -7,12 +7,23 @@ export type StandaloneSeed = Readonly<{
7
7
  origin: string;
8
8
  owner: Identity;
9
9
  }>;
10
+ export type StandaloneSeedRefusal = Readonly<{
11
+ kind: "refused";
12
+ code: "seed_configuration_changed";
13
+ seededOrigin: string;
14
+ configuredOrigin: string;
15
+ recovery: "fresh_worker_name_or_reset_worker_data";
16
+ }>;
17
+ export declare class StandaloneSeedConfigurationChanged extends Error {
18
+ readonly refusal: StandaloneSeedRefusal;
19
+ constructor(refusal: StandaloneSeedRefusal);
20
+ }
10
21
  /** A fresh registry namespace with one host-provisioned standalone tenant. */
11
22
  export declare class StandaloneTenants extends TenancyRegistryDO {
12
23
  #private;
13
24
  constructor(state: DurableObjectState);
14
25
  /** Reuse only the exact configured tenant and its current canonical owner. */
15
- seed(input: StandaloneSeed): Promise<Tenant>;
26
+ seed(input: StandaloneSeed): Promise<Tenant | StandaloneSeedRefusal>;
16
27
  }
17
28
  /** The same trusted bindings are used by HTTP, MCP and cold root readers. */
18
29
  export type StandaloneTenancyEnv = Pick<OysterGitWorkerEnv, "OYSTER_ORIGIN" | "OYSTER_OWNER_NAME" | "OYSTER_OWNER_KEY" | "KEYS"> & Readonly<{
@@ -38,5 +49,7 @@ export type StandaloneTenancy = Readonly<{
38
49
  oyster: OysterTenancy;
39
50
  owner(): Promise<StandaloneOwner | null>;
40
51
  }>;
52
+ /** A request may seed the standalone tenant only at its configured origin. */
53
+ export declare function standaloneTenancyForRequest(env: StandaloneTenancyEnv, request: Request): Promise<StandaloneTenancy>;
41
54
  /** Seed once per environment, then admit every entry from one fresh registry read. */
42
55
  export declare function standaloneTenancy(env: StandaloneTenancyEnv): Promise<StandaloneTenancy>;
@@ -3,6 +3,7 @@ import { createTenancy, } from "../tenancy/index.js";
3
3
  import { ownerKey } from "../tenancy/owner-key.js";
4
4
  import { hosts } from "../tenancy/resolve.js";
5
5
  import { ownerOrKeyAuth } from "../auth.js";
6
+ import { HTTPException } from "hono/http-exception";
6
7
  import { defaultTenant, tenantScope, } from "../tenant.js";
7
8
  import * as Schema from "effect/Schema";
8
9
  const seedSchema = Schema.Struct({
@@ -13,6 +14,14 @@ const seedSchema = Schema.Struct({
13
14
  }),
14
15
  });
15
16
  const seedKey = "standalone-owner-seed";
17
+ export class StandaloneSeedConfigurationChanged extends Error {
18
+ refusal;
19
+ constructor(refusal) {
20
+ super("Standalone seed configuration changed");
21
+ this.name = "StandaloneSeedConfigurationChanged";
22
+ this.refusal = refusal;
23
+ }
24
+ }
16
25
  /** A fresh registry namespace with one host-provisioned standalone tenant. */
17
26
  export class StandaloneTenants extends TenancyRegistryDO {
18
27
  #state;
@@ -31,8 +40,15 @@ export class StandaloneTenants extends TenancyRegistryDO {
31
40
  const marker = Schema.decodeUnknownSync(seedSchema)(raw);
32
41
  if (marker.origin !== scope.origin ||
33
42
  marker.owner.provider !== input.owner.provider ||
34
- marker.owner.subject !== input.owner.subject)
35
- throw new Error("Standalone seed configuration changed");
43
+ marker.owner.subject !== input.owner.subject) {
44
+ return {
45
+ kind: "refused",
46
+ code: "seed_configuration_changed",
47
+ seededOrigin: marker.origin,
48
+ configuredOrigin: scope.origin,
49
+ recovery: "fresh_worker_name_or_reset_worker_data",
50
+ };
51
+ }
36
52
  }
37
53
  let tenant = await this.get(defaultTenant);
38
54
  if (tenant === null) {
@@ -69,6 +85,22 @@ export class StandaloneTenants extends TenancyRegistryDO {
69
85
  });
70
86
  }
71
87
  }
88
+ /** A request may seed the standalone tenant only at its configured origin. */
89
+ export async function standaloneTenancyForRequest(env, request) {
90
+ const configuredOrigin = tenantScope(defaultTenant, env.OYSTER_ORIGIN).origin;
91
+ const requestOrigin = new URL(request.url).origin;
92
+ if (requestOrigin !== configuredOrigin)
93
+ throw new HTTPException(404, {
94
+ res: Response.json({
95
+ kind: "refused",
96
+ code: "origin_mismatch",
97
+ configuredOrigin,
98
+ requestOrigin,
99
+ message: `Request origin ${requestOrigin} does not match configured OYSTER_ORIGIN ${configuredOrigin}. Set OYSTER_ORIGIN to this Worker's URL.`,
100
+ }, { status: 404 }),
101
+ });
102
+ return standaloneTenancy(env);
103
+ }
72
104
  const environments = new WeakMap();
73
105
  /** Seed once per environment, then admit every entry from one fresh registry read. */
74
106
  export function standaloneTenancy(env) {
@@ -90,10 +122,12 @@ async function composeTenancy(env) {
90
122
  provider: "standalone-owner",
91
123
  subject: env.OYSTER_OWNER_NAME ?? "owner",
92
124
  };
93
- await env.TENANTS.getByName("registry").seed({
125
+ const seeded = await env.TENANTS.getByName("registry").seed({
94
126
  origin: scope.origin,
95
127
  owner: identity,
96
128
  });
129
+ if ("kind" in seeded)
130
+ throw new StandaloneSeedConfigurationChanged(seeded);
97
131
  const registry = durableObjectRegistry(env.TENANTS, { readonly: true });
98
132
  /** The canonical owner, only while the one admitted record still proves it. */
99
133
  const ownerOf = ({ tenant, membership, }) => tenant.id !== defaultTenant ||
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { createOysterGitWorker, createOysterGitMcpOwner, oysterGitRoot, } from "../workers-git.js";
7
7
  import { defaultTenant, tenantScope } from "../tenant.js";
8
+ import { oysterPrincipalIsOwner } from "../host.js";
9
+ import { standaloneComputerControl } from "./computer-control.js";
8
10
  import { onBoxVmIngress } from "@fungi.computer/computer";
9
11
  import { WorkerEntrypoint } from "cloudflare:workers";
10
12
  import { installationWorkerEntrypoint } from "../installation-execution.js";
@@ -17,7 +19,7 @@ import { StandaloneInstance as ComputerInstance } from "./instance.js";
17
19
  // Every guest binding in every frame is this one entrypoint, routed by props.
18
20
  export { OysterBinding } from "./instance.js";
19
21
  import { StandaloneWorkflowRunsBase, } from "./workflows.js";
20
- import { standaloneTenancy } from "./tenancy.js";
22
+ import { standaloneTenancyForRequest, StandaloneSeedConfigurationChanged, } from "./tenancy.js";
21
23
  export { StandaloneTenants } from "./tenancy.js";
22
24
  import { standalonePreviewRequest } from "./preview.js";
23
25
  import { standaloneMachineLinkRoutes, } from "./machine-link.js";
@@ -25,8 +27,11 @@ export { MachineLinkDO } from "./machine-link.js";
25
27
  import { standaloneWorkflowControl } from "./workflow-control.js";
26
28
  import { standaloneWorkflowWebhookRoutes, standaloneWorkflowWebhookControls, } from "./workflow-webhooks.js";
27
29
  import { Hono } from "hono";
30
+ import { HTTPException } from "hono/http-exception";
28
31
  import { standaloneBuildRoutes } from "./builds.js";
29
32
  import { standaloneComputerFleetRoutes, standaloneComputerSelection, } from "./computer-fleet.js";
33
+ import { standaloneAgentComputerRoutes } from "./agent-computers.js";
34
+ import { agentDirectoryForScope } from "./agent-directory.js";
30
35
  import { serveOysterComputerEgress, } from "./computer-egress.js";
31
36
  import { appSourceOwnerRoutes, } from "./app-source.js";
32
37
  export { StandaloneGraphWorkflow } from "./workflows.js";
@@ -59,11 +64,14 @@ export { AgentDirectory } from "./agents.js";
59
64
  const standaloneRoot = (selectComputer) => {
60
65
  const root = oysterGitRoot({
61
66
  tenancy: (env) => ({
62
- resolve: async (request) => (await standaloneTenancy(env)).oyster.resolve(request),
67
+ resolve: async (request) => (await standaloneTenancyForRequest(env, request)).oyster.resolve(request),
63
68
  }),
64
69
  homeInstallationId: (env) => env.OYSTER_HOME_INSTALLATION,
65
70
  agents: standaloneAgents,
66
71
  workflows: standaloneWorkflowControl,
72
+ computerControl: (env, scope) => (principal) => oysterPrincipalIsOwner(principal)
73
+ ? standaloneComputerControl(tenantRegistry(env, scope), principal.actor.memberId)
74
+ : null,
67
75
  workspace: async (env, keysLive, scope, request) => standaloneWorkspace(env, keysLive, await selectComputer(tenantRegistry(env, scope), request)),
68
76
  capabilities: (env, workspace, keysLive) => [
69
77
  ...standaloneComputerCapabilities(env, workspace, keysLive),
@@ -96,6 +104,11 @@ const standaloneRoot = (selectComputer) => {
96
104
  ownerRoutes: (env, scope) => appSourceOwnerRoutes(env, scope)
97
105
  .route("/", standaloneBuildRoutes(() => tenantRegistry(env, scope)))
98
106
  .route("/", standaloneComputerFleetRoutes(() => tenantRegistry(env, scope)))
107
+ .route("/", standaloneAgentComputerRoutes({
108
+ attach: (principal, command) => agentDirectoryForScope(env, scope).attachComputer(command, principal, scope.tenantId),
109
+ detach: (principal, command) => agentDirectoryForScope(env, scope).detachComputer(command, principal, scope.tenantId),
110
+ read: (principal, agentId) => agentDirectoryForScope(env, scope).readAgentComputers(agentId, principal, scope.tenantId),
111
+ }))
99
112
  .route("/", standaloneWorkflowWebhookControls(env, () => root.hostFor(env, scope))),
100
113
  });
101
114
  return root;
@@ -153,14 +166,21 @@ export class OysterComputerEgress extends WorkerEntrypoint {
153
166
  });
154
167
  }
155
168
  }
169
+ const standaloneWorkerErrorHandler = (error, context) => {
170
+ if (error instanceof StandaloneSeedConfigurationChanged) {
171
+ const { refusal } = error;
172
+ return context.json({
173
+ ...refusal,
174
+ message: `Standalone seed/origin mismatch: existing data was seeded for ${refusal.seededOrigin}, but OYSTER_ORIGIN is ${refusal.configuredOrigin}. Deploy under a fresh Worker name or reset this Worker's data.`,
175
+ }, 409);
176
+ }
177
+ if (error instanceof HTTPException)
178
+ return error.getResponse();
179
+ console.error(error);
180
+ return context.text("Internal Server Error", 500);
181
+ };
156
182
  export default {
157
183
  async fetch(request, env, context) {
158
- const path = new URL(request.url).pathname;
159
- if (path === "/local/mesh/join" ||
160
- new URLPattern({
161
- pathname: "/api/teams/:teamId/local/mesh/:deviceId/connect",
162
- }).test(request.url))
163
- return standaloneMachineLinkRoutes(env).fetch(request);
164
184
  const preview = await standalonePreviewRequest(request, env, (computerId) => computerRegistry(env).ownsComputer(computerId), (computerId, signature, port, service) => env.COMPUTER.getByName(computerId).portPreview(signature, port, service));
165
185
  if (preview !== null)
166
186
  return preview;
@@ -171,6 +191,13 @@ export default {
171
191
  if (fetchOyster === undefined)
172
192
  throw new Error("Oyster fetch handler is missing");
173
193
  return new Hono()
194
+ .onError(standaloneWorkerErrorHandler)
195
+ .all("/local/mesh/join", () => standaloneMachineLinkRoutes(env)
196
+ .onError(standaloneWorkerErrorHandler)
197
+ .fetch(request, env, context))
198
+ .all("/api/teams/:teamId/local/mesh/:deviceId/connect", () => standaloneMachineLinkRoutes(env)
199
+ .onError(standaloneWorkerErrorHandler)
200
+ .fetch(request, env, context))
174
201
  .route("/", standaloneWorkflowWebhookRoutes(env, async (incoming) => {
175
202
  const admitted = await oysterRoot.admit(env, incoming);
176
203
  return admitted === null
@@ -178,6 +205,6 @@ export default {
178
205
  : oysterRoot.hostFor(env, admitted.scope);
179
206
  }))
180
207
  .all("*", () => fetchOyster(request, env, context))
181
- .fetch(request);
208
+ .fetch(request, env, context);
182
209
  },
183
210
  };
@@ -72,10 +72,10 @@ export declare const workflowFrameMethods: {
72
72
  readonly input: {
73
73
  readonly [x: string]: S.Json;
74
74
  };
75
+ readonly requestId: string;
75
76
  readonly bindings: Record<string, string | {
76
77
  kind: "team-source";
77
78
  }>;
78
- readonly requestId: string;
79
79
  readonly definition: unknown;
80
80
  readonly delivery?: {
81
81
  readonly agentId: string;
@@ -86,10 +86,10 @@ export declare const workflowFrameMethods: {
86
86
  readonly input: {
87
87
  readonly [x: string]: S.Json;
88
88
  };
89
+ readonly requestId: string;
89
90
  readonly bindings: Record<string, string | {
90
91
  kind: "team-source";
91
92
  }>;
92
- readonly requestId: string;
93
93
  readonly definition: S.Json;
94
94
  readonly published: {
95
95
  workflowId: string;