@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.
- package/README.md +2 -1
- package/dist/agent-computer-source.d.ts +42 -0
- package/dist/agent-computer-source.js +88 -0
- package/dist/agent-computers.d.ts +104 -0
- package/dist/agent-computers.js +227 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.js +33 -0
- package/dist/boot-capability.d.ts +1 -1
- package/dist/boot-capability.js +1 -1
- package/dist/browser/boot.d.ts +2 -2
- package/dist/browser/boot.js +10 -3
- package/dist/browser/browser.d.ts +1 -1
- package/dist/browser/browser.js +2 -1
- package/dist/browser/cdp-client.d.ts +2 -0
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +2 -2
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +3 -2
- package/dist/browser/page.js +11 -4
- package/dist/browser/sessions.d.ts +7 -4
- package/dist/browser/sessions.js +159 -0
- package/dist/browser/url-policy.d.ts +4 -2
- package/dist/browser/url-policy.js +4 -2
- package/dist/computer-control-port.d.ts +26 -0
- package/dist/computer-control-port.js +1 -0
- package/dist/host.d.ts +3 -0
- package/dist/machine-link/delivery.d.ts +1 -1
- package/dist/machine-link/delivery.js +2 -3
- package/dist/machine-link/host.d.ts +37 -6
- package/dist/machine-link/host.js +209 -14
- package/dist/machine-link/link.d.ts +13 -1
- package/dist/machine-link/link.js +20 -7
- package/dist/mcp.d.ts +152 -11
- package/dist/mcp.js +131 -13
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page-script.js +1 -1
- package/dist/standalone/agent-computers.d.ts +29 -0
- package/dist/standalone/agent-computers.js +101 -0
- package/dist/standalone/agent-mail.d.ts +41 -18
- package/dist/standalone/agent-model-choice.d.ts +26 -0
- package/dist/standalone/agent-model-choice.js +72 -0
- package/dist/standalone/agents.d.ts +50 -5
- package/dist/standalone/agents.js +505 -53
- package/dist/standalone/app-builds-binding.js +1 -1
- package/dist/standalone/computer-control.d.ts +4 -0
- package/dist/standalone/computer-control.js +62 -0
- package/dist/standalone/computer-fleet.d.ts +1 -0
- package/dist/standalone/computer-fleet.js +1 -1
- package/dist/standalone/computer.d.ts +21 -2
- package/dist/standalone/computer.js +12 -0
- package/dist/standalone/machine-link.d.ts +10 -0
- package/dist/standalone/machine-link.js +6 -0
- package/dist/standalone/registry.d.ts +12 -0
- package/dist/standalone/registry.js +18 -0
- package/dist/standalone/tenancy.d.ts +14 -1
- package/dist/standalone/tenancy.js +37 -3
- package/dist/standalone/worker.js +36 -9
- package/dist/standalone/workflow-binding.d.ts +2 -2
- package/dist/standalone/workflow-host.d.ts +11 -11
- package/dist/standalone/workflows.d.ts +22 -22
- package/dist/storage-frame.d.ts +3 -3
- package/dist/workers-git.d.ts +2 -0
- package/dist/workers-git.js +11 -1
- package/dist/workers.d.ts +8 -8
- package/docs/api/agents.md +66 -0
- package/docs/api/apps.md +104 -104
- package/docs/api/build-contract.md +35 -35
- package/docs/api/build.md +12 -12
- package/docs/api/host.md +61 -61
- package/docs/api/mcp.md +141 -13
- package/docs/api/root-data.md +1 -1
- package/docs/api/tenancy.md +1 -1
- package/docs/api/workers-git.md +3 -2
- package/docs/api/workers.md +81 -83
- 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.
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
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;
|