@fungi.computer/oyster 0.2.0 → 0.3.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 +376 -72
- package/dist/agents.d.ts +3 -2
- package/dist/agents.js +4 -2
- package/dist/app-builds.d.ts +81 -8
- package/dist/app-builds.js +210 -60
- package/dist/app-storage.d.ts +3 -3
- package/dist/approval-capability.d.ts +5 -0
- package/dist/approval-capability.js +46 -0
- package/dist/approval-home.d.ts +51 -0
- package/dist/approval-home.js +226 -0
- package/dist/apps/catalog.d.ts +335 -0
- package/dist/apps/catalog.js +46 -0
- package/dist/apps/gate.d.ts +20 -6
- package/dist/apps/gate.js +42 -16
- package/dist/apps/index.d.ts +3 -1
- package/dist/apps/index.js +3 -1
- package/dist/apps/installation.d.ts +132 -51
- package/dist/apps/installation.js +176 -70
- package/dist/apps/permissions.d.ts +247 -0
- package/dist/apps/permissions.js +255 -0
- package/dist/boot-capability-route.d.ts +15 -0
- package/dist/boot-capability-route.js +196 -0
- package/dist/boot-capability.d.ts +103 -0
- package/dist/boot-capability.js +141 -0
- package/dist/boot-construction.d.ts +12 -0
- package/dist/boot-construction.js +175 -0
- package/dist/boot-gate.d.ts +267 -0
- package/dist/boot-gate.js +236 -0
- package/dist/boot-host-script.d.ts +2 -0
- package/dist/boot-host-script.js +1 -0
- package/dist/boot-mount.d.ts +21 -0
- package/dist/boot-mount.js +65 -0
- package/dist/browser/boot.d.ts +73 -0
- package/dist/browser/boot.js +502 -0
- package/dist/browser/browser.d.ts +104 -0
- package/dist/browser/browser.js +374 -0
- package/dist/browser/capability-source.d.ts +76 -0
- package/dist/browser/capability-source.js +338 -0
- package/dist/browser/cdp-client.d.ts +56 -0
- package/dist/browser/cdp-client.js +184 -0
- package/dist/browser/errors.d.ts +79 -0
- package/dist/browser/errors.js +32 -0
- package/dist/browser/index.d.ts +16 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/lease-registry.d.ts +61 -0
- package/dist/browser/lease-registry.js +101 -0
- package/dist/browser/operations.d.ts +225 -0
- package/dist/browser/operations.js +137 -0
- package/dist/browser/page.d.ts +25 -0
- package/dist/browser/page.js +172 -0
- package/dist/browser/quick-reads.d.ts +29 -0
- package/dist/browser/quick-reads.js +160 -0
- package/dist/browser/sessions.d.ts +52 -0
- package/dist/browser/sessions.js +118 -0
- package/dist/browser/url-policy.d.ts +18 -0
- package/dist/browser/url-policy.js +58 -0
- package/dist/build/app-imports.d.ts +12 -0
- package/dist/build/app-imports.js +105 -0
- package/dist/build/build-policy.d.ts +5 -3
- package/dist/build/build-policy.js +5 -3
- package/dist/build/candidate-builder.d.ts +1 -1
- package/dist/build/candidate-cli.mjs +8375 -334
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +28 -6
- package/dist/build/snapshot-package-store.d.ts +1 -1
- package/dist/build/snapshot-package-store.js +7 -2
- package/dist/build/workspace-build.d.ts +92 -5
- package/dist/build/workspace-build.js +93 -19
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- package/dist/catalog-frame.js +1 -1
- package/dist/cloudflare.d.ts +13 -3
- package/dist/cloudflare.js +102 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.js +4 -2
- package/dist/git-auth.d.ts +0 -2
- package/dist/git-auth.js +0 -2
- package/dist/git.d.ts +0 -1
- package/dist/git.js +0 -2
- package/dist/host.d.ts +39 -13
- package/dist/host.js +268 -142
- package/dist/index.d.ts +25 -15
- package/dist/index.js +39 -16
- package/dist/install-release-cli.d.ts +2 -0
- package/dist/install-release-cli.js +46 -0
- package/dist/installation-client.d.ts +43 -0
- package/dist/installation-client.js +178 -0
- package/dist/installation-execution.d.ts +11 -0
- package/dist/installation-execution.js +184 -0
- package/dist/installation-operations.d.ts +1437 -0
- package/dist/installation-operations.js +664 -0
- package/dist/installation-store-generation.d.ts +3 -0
- package/dist/installation-store-generation.js +16 -0
- package/dist/instance-id.d.ts +10 -0
- package/dist/instance-id.js +10 -0
- package/dist/instance-refusal.d.ts +11 -0
- package/dist/instance-refusal.js +21 -0
- package/dist/internal/delivery.js +4 -2
- package/dist/internal/host-gate.js +7 -1
- package/dist/internal/registry-installations.js +4 -4
- package/dist/keys.d.ts +5 -1
- package/dist/keys.js +10 -2
- package/dist/machine-link/authority.d.ts +89 -0
- package/dist/machine-link/authority.js +37 -0
- package/dist/machine-link/client.d.ts +66 -0
- package/dist/machine-link/client.js +94 -0
- package/dist/machine-link/delivery.d.ts +18 -0
- package/dist/machine-link/delivery.js +55 -0
- package/dist/machine-link/enrollment.d.ts +111 -0
- package/dist/machine-link/enrollment.js +286 -0
- package/dist/machine-link/fences.d.ts +37 -0
- package/dist/machine-link/fences.js +166 -0
- package/dist/machine-link/home.d.ts +20 -0
- package/dist/machine-link/home.js +24 -0
- package/dist/machine-link/host.d.ts +43 -0
- package/dist/machine-link/host.js +189 -0
- package/dist/machine-link/index.d.ts +15 -0
- package/dist/machine-link/index.js +15 -0
- package/dist/machine-link/link.d.ts +48 -0
- package/dist/machine-link/link.js +223 -0
- package/dist/machine-link/presence.d.ts +10 -0
- package/dist/machine-link/presence.js +25 -0
- package/dist/machine-link/workers.d.ts +6 -0
- package/dist/machine-link/workers.js +6 -0
- package/dist/mcp-owner.d.ts +2 -2
- package/dist/mcp-owner.js +41 -33
- package/dist/mcp.d.ts +15 -3
- package/dist/mcp.js +146 -25
- package/dist/pack.d.ts +14 -3
- package/dist/pack.js +44 -19
- package/dist/page/app-access.d.ts +7 -0
- package/dist/page/app-access.js +75 -0
- package/dist/page/boot-host.js +57 -0
- package/dist/page/capabilities.d.ts +3 -3
- package/dist/page/capabilities.js +16 -12
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page/computer-target.d.ts +3 -0
- package/dist/page/computer-target.js +9 -0
- package/dist/page/mount.d.ts +23 -3
- package/dist/page/mount.js +152 -16
- package/dist/page-assets.d.ts +22 -0
- package/dist/page-assets.js +28 -0
- package/dist/page-script.d.ts +3 -0
- package/dist/page-script.js +5 -4
- package/dist/release.d.ts +13 -6
- package/dist/release.js +10 -7
- package/dist/root-data.d.ts +16 -6
- package/dist/root-data.js +2 -2
- package/dist/source/index.d.ts +1 -1
- package/dist/source/team-source.d.ts +20 -8
- package/dist/source/team-source.js +39 -10
- package/dist/source/template-seed.d.ts +2 -2
- package/dist/standalone/agent-mail.d.ts +24 -0
- package/dist/standalone/agent-mail.js +62 -0
- package/dist/standalone/agent-store.d.ts +9 -0
- package/dist/standalone/agent-store.js +30 -0
- package/dist/standalone/agents.d.ts +68 -9
- package/dist/standalone/agents.js +428 -56
- package/dist/standalone/app-builds-binding.d.ts +20 -2
- package/dist/standalone/app-builds-binding.js +147 -23
- package/dist/standalone/app-set.d.ts +5 -2
- package/dist/standalone/app-set.js +174 -143
- package/dist/standalone/browser.d.ts +11 -0
- package/dist/standalone/browser.js +72 -0
- package/dist/standalone/builds.d.ts +13 -4
- package/dist/standalone/builds.js +28 -44
- package/dist/standalone/computer-egress.d.ts +2 -0
- package/dist/standalone/computer-egress.js +13 -4
- package/dist/standalone/computer-fleet.d.ts +43 -0
- package/dist/standalone/computer-fleet.js +200 -0
- package/dist/standalone/computer.d.ts +44 -9
- package/dist/standalone/computer.js +129 -22
- package/dist/standalone/install-apps-cli.js +43 -6
- package/dist/standalone/instance.d.ts +2 -2
- package/dist/standalone/instance.js +8 -11
- package/dist/standalone/machine-link.d.ts +354 -0
- package/dist/standalone/machine-link.js +368 -0
- package/dist/standalone/media.d.ts +2 -2
- package/dist/standalone/media.js +19 -16
- package/dist/standalone/preview.d.ts +2 -2
- package/dist/standalone/preview.js +37 -10
- package/dist/standalone/registry.d.ts +149 -9
- package/dist/standalone/registry.js +299 -8
- package/dist/standalone/worker.d.ts +19 -5
- package/dist/standalone/worker.js +90 -54
- package/dist/standalone/workflow-binding.d.ts +18 -22
- package/dist/standalone/workflow-binding.js +18 -18
- package/dist/standalone/workflow-control.d.ts +4 -0
- package/dist/standalone/workflow-control.js +40 -0
- package/dist/standalone/workflow-events.d.ts +2 -2
- package/dist/standalone/workflow-events.js +9 -10
- package/dist/standalone/workflow-git-capture.js +4 -3
- package/dist/standalone/workflow-host.d.ts +162 -161
- package/dist/standalone/workflow-host.js +101 -21
- package/dist/standalone/workflow-node-capture.js +11 -4
- package/dist/standalone/workflow-schedules.d.ts +5 -2
- package/dist/standalone/workflow-schedules.js +9 -10
- package/dist/standalone/workflow-trigger-host.d.ts +65 -5
- package/dist/standalone/workflow-trigger-host.js +2 -2
- package/dist/standalone/workflow-webhooks.d.ts +22 -0
- package/dist/standalone/workflow-webhooks.js +39 -0
- package/dist/standalone/workflows.d.ts +140 -43
- package/dist/standalone/workflows.js +198 -39
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.js +14 -3
- package/dist/testing/installation.js +26 -2
- package/dist/testing/worker.js +1 -1
- package/dist/testing/workers-runtime.d.ts +8 -2
- package/dist/testing/workers-runtime.js +20 -2
- package/dist/webhook-abuse.d.ts +25 -0
- package/dist/webhook-abuse.js +54 -0
- package/dist/webhook-custody.d.ts +144 -0
- package/dist/webhook-custody.js +222 -0
- package/dist/webhook-ingress.d.ts +36 -0
- package/dist/webhook-ingress.js +146 -0
- package/dist/webhooks.d.ts +111 -0
- package/dist/webhooks.js +189 -0
- package/dist/workers-git.d.ts +12 -17
- package/dist/workers-git.js +60 -52
- package/dist/workers.d.ts +224 -13
- package/dist/workers.js +351 -51
- package/dist/workflow-agent-authority.d.ts +8 -0
- package/dist/workflow-agent-authority.js +42 -0
- package/dist/workflows.d.ts +66 -0
- package/dist/workflows.js +97 -0
- package/docs/api/README.md +63 -37
- package/docs/api/agents.md +17 -0
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +1394 -543
- package/docs/api/browser/boot.md +132 -0
- package/docs/api/browser.md +1440 -0
- package/docs/api/build-contract.md +38 -25
- package/docs/api/build.md +263 -84
- package/docs/api/cloudflare.md +93 -7
- package/docs/api/git.md +3 -13
- package/docs/api/host.md +944 -118
- package/docs/api/index.md +320 -18
- package/docs/api/keys.md +24 -1
- package/docs/api/mcp.md +59 -12
- package/docs/api/modules.md +33 -40
- package/docs/api/pack.md +57 -8
- package/docs/api/page/mount.md +92 -4
- package/docs/api/workers-git.md +103 -24
- package/docs/api/workers.md +1581 -244
- package/docs/site/agent-quickstart.mdx +28 -14
- package/examples/standalone/worker.ts +3 -1
- package/examples/standalone/wrangler.jsonc +2 -0
- package/package.json +73 -71
- package/dist/installation-consent.d.ts +0 -81
- package/dist/installation-consent.js +0 -84
- package/dist/shell/app-directory.d.ts +0 -542
- package/dist/shell/app-directory.js +0 -196
- package/dist/shell/browser-actions.d.ts +0 -7
- package/dist/shell/browser-actions.js +0 -56
- package/dist/shell/command-host.d.ts +0 -112
- package/dist/shell/command-host.js +0 -502
- package/dist/shell/commands.d.ts +0 -25
- package/dist/shell/commands.js +0 -78
- package/dist/shell/index.d.ts +0 -8
- package/dist/shell/index.js +0 -8
- package/dist/shell/keys.d.ts +0 -41
- package/dist/shell/keys.js +0 -134
- package/dist/shell/shell-module.d.ts +0 -55
- package/dist/shell/shell-module.js +0 -86
- package/dist/shell/window-manager.d.ts +0 -128
- package/dist/shell/window-manager.js +0 -730
- package/dist/shell/workbench-contract.d.ts +0 -68
- package/dist/shell/workbench-directory.d.ts +0 -332
- package/dist/shell/workbench-directory.js +0 -35
- package/dist/shell/workbench-host-types.d.ts +0 -96
- package/dist/shell/workbench-host-types.js +0 -1
- package/dist/shell/workbench-host.d.ts +0 -34
- package/dist/shell/workbench-host.js +0 -352
- package/dist/shell/workbench-model.d.ts +0 -194
- package/dist/shell/workbench-model.js +0 -53
- package/dist/shell/workbench.d.ts +0 -4
- package/dist/shell/workbench.js +0 -2
- package/docs/api/shell/app-directory.md +0 -599
- package/docs/api/shell/command-host.md +0 -224
- package/docs/api/shell/commands.md +0 -197
- package/docs/api/shell/keys.md +0 -183
- package/docs/api/shell/shell-module.md +0 -204
- package/docs/api/shell/window-manager.md +0 -267
- package/docs/api/shell/workbench-directory.md +0 -119
- package/docs/api/shell/workbench.md +0 -205
- package/docs/api/shell.md +0 -1915
- /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
package/dist/workers.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { approvalCapability } from "./approval-capability.js";
|
|
2
|
+
import { createInstallationStoreGeneration } from "./installation-store-generation.js";
|
|
3
|
+
import { OysterApprovalHome, } from "./approval-home.js";
|
|
4
|
+
import { timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { bootCapabilityGrantSchema, bootCapabilityLeaseSchema, reportBootPage, validateCapabilityAssetSet, } from "./boot-capability.js";
|
|
7
|
+
import { serveBootCapability } from "./boot-capability-route.js";
|
|
8
|
+
import { installationSchema, isCommittedInstallation, } from "./apps/installation.js";
|
|
9
|
+
import { BOOT_DEADLINE_MS, BOOT_TRANSPORT_GRACE_MS, readBootUi, } from "./boot-gate.js";
|
|
10
|
+
import { checkBootPage } from "./browser/boot.js";
|
|
11
|
+
import { bindingSessions } from "./browser/sessions.js";
|
|
2
12
|
import { registryInstallations } from "./internal/registry-installations.js";
|
|
13
|
+
import { createInstallationOperations, } from "./installation-operations.js";
|
|
14
|
+
import { installationWorkerEntrypoint } from "./installation-execution.js";
|
|
15
|
+
export { installationWorkerEntrypoint };
|
|
3
16
|
import { approvedBindingGrants, scopedBindings } from "./apps/gate.js";
|
|
4
17
|
import { appMethodReplySchema } from "@fungi.computer/agar/backend";
|
|
5
18
|
import { nativeGitFrameMethods, nativeGitMethodNameSchema, } from "@fungi.computer/agar/git-rpc";
|
|
@@ -20,7 +33,7 @@ import * as Schema from "effect/Schema";
|
|
|
20
33
|
import * as Stream from "effect/Stream";
|
|
21
34
|
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
22
35
|
import { AppInstanceError, } from "./index.js";
|
|
23
|
-
import { createCloudflareAppInstance, createCloudflareInvocationGate, decodeFrameBindingProps, captureFrameBindingReply, frameBindingEntrypoint, backgroundFrameMethods, } from "./cloudflare.js";
|
|
36
|
+
import { createCloudflareAppInstance, r2ArtifactStore, createCloudflareInvocationGate, decodeFrameBindingProps, captureFrameBindingReply, frameBindingEntrypoint, backgroundFrameMethods, } from "./cloudflare.js";
|
|
24
37
|
import { invocationFactsSchema, assertBackgroundBinding, admitBackgroundFrame, } from "./invocation.js";
|
|
25
38
|
import { deliverBackground, deliverGitPush } from "./internal/delivery.js";
|
|
26
39
|
import { createOyster, verifyRelease, defaultAssetPolicy, } from "./host.js";
|
|
@@ -30,31 +43,104 @@ import { storageFrameMethods } from "./storage-frame.js";
|
|
|
30
43
|
import { catalogFrameMethods, oysterAppCatalog } from "./catalog-frame.js";
|
|
31
44
|
import { appStorageKeyNameSchema, appStorageWriteSchema, appStorageResetSchema, } from "@fungi.computer/agar/storage";
|
|
32
45
|
import { createOysterRootData } from "./root-data.js";
|
|
33
|
-
import { backendCache, resolveBackend
|
|
34
|
-
import { headContentObject } from "./build/content-objects.js";
|
|
46
|
+
import { backendCache, resolveBackend } from "./release.js";
|
|
35
47
|
import { ensureAppSourceTables, createOysterAppSourceReceipts, createOysterAppRegistrations, createOysterArtifactsRepositoryMetadataStore, } from "./app-sources.js";
|
|
36
|
-
export { r2FileSource, ownerKeyAuth };
|
|
37
|
-
/** Store Release artifacts in an R2 bucket. */
|
|
38
|
-
export function r2ArtifactStore(bucket) {
|
|
39
|
-
return {
|
|
40
|
-
head: (artifactRef) => headContentObject(bucket, artifactRef),
|
|
41
|
-
async get(artifactRef) {
|
|
42
|
-
const object = await bucket.get(artifactRef);
|
|
43
|
-
return object === null
|
|
44
|
-
? null
|
|
45
|
-
: new Uint8Array(await object.arrayBuffer());
|
|
46
|
-
},
|
|
47
|
-
async put(artifactRef, bytes) {
|
|
48
|
-
await bucket.put(artifactRef, bytes);
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
48
|
+
export { r2ArtifactStore, r2FileSource, ownerKeyAuth };
|
|
52
49
|
const recordRow = Schema.Struct({
|
|
53
50
|
installation_id: Schema.String,
|
|
54
51
|
record: Schema.String,
|
|
55
52
|
});
|
|
53
|
+
const bootCapabilityRows = z.array(z.strictObject({
|
|
54
|
+
token_digest: z.string().regex(/^[0-9a-f]{64}$/),
|
|
55
|
+
expires_at: z.number().int(),
|
|
56
|
+
record: z.string().max(2_000_000),
|
|
57
|
+
}));
|
|
58
|
+
const approvalHomePinRow = Schema.Struct({
|
|
59
|
+
tenant_id: tenantIdSchema,
|
|
60
|
+
origin: Schema.String,
|
|
61
|
+
});
|
|
56
62
|
/** The installation registry: one SQLite-backed Durable Object per Oyster. */
|
|
57
63
|
export class OysterRegistry extends DurableObject {
|
|
64
|
+
#generation;
|
|
65
|
+
#approval;
|
|
66
|
+
#installationOperations;
|
|
67
|
+
/** The plain root has the same single owner-key host as OysterInstance. */
|
|
68
|
+
hostFor(scope) {
|
|
69
|
+
return Promise.resolve(ownerKeyRootHost(this.env, { scope }));
|
|
70
|
+
}
|
|
71
|
+
/** Trusted host RPC; an operation input cannot select actor, namespace or tenant. */
|
|
72
|
+
approvalSubmit(pin, call) {
|
|
73
|
+
return this.approvalHome(pin).submit(call);
|
|
74
|
+
}
|
|
75
|
+
approvalHome(pin) {
|
|
76
|
+
const scope = tenantScope(Schema.decodeUnknownSync(tenantIdSchema)(pin.tenantId), pin.origin);
|
|
77
|
+
if (this.ctx.id.name !== scope.durableName("registry"))
|
|
78
|
+
throw new Error("Approval home namespace does not match tenant");
|
|
79
|
+
this.ctx.storage.transactionSync(() => {
|
|
80
|
+
const raw = this.ctx.storage.sql
|
|
81
|
+
.exec("SELECT tenant_id, origin FROM oyster_approval_home WHERE id = 1")
|
|
82
|
+
.toArray()[0];
|
|
83
|
+
if (raw === undefined) {
|
|
84
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_approval_home (id, tenant_id, origin) VALUES (1, ?, ?)", scope.tenantId, scope.origin);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const retained = Schema.decodeUnknownSync(approvalHomePinRow)(raw);
|
|
88
|
+
if (retained.tenant_id !== scope.tenantId ||
|
|
89
|
+
retained.origin !== scope.origin)
|
|
90
|
+
throw new Error("Approval home cannot change tenant or origin");
|
|
91
|
+
});
|
|
92
|
+
this.#approval ??= new OysterApprovalHome({
|
|
93
|
+
owner: {
|
|
94
|
+
sql: this.ctx.storage.sql,
|
|
95
|
+
transactionSync: (body) => this.ctx.storage.transactionSync(body),
|
|
96
|
+
},
|
|
97
|
+
scope,
|
|
98
|
+
current: () => this.hostFor(scope),
|
|
99
|
+
commit: (write, nextWakeAt) => this.ctx.storage.transaction(async (transaction) => {
|
|
100
|
+
const result = await write();
|
|
101
|
+
const at = nextWakeAt();
|
|
102
|
+
const current = await transaction.getAlarm();
|
|
103
|
+
if (at !== null && (current === null || current > at))
|
|
104
|
+
await transaction.setAlarm(at);
|
|
105
|
+
return result;
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
return this.#approval;
|
|
109
|
+
}
|
|
110
|
+
/** Recover approved work from the durable home, not a last-request memory pin. */
|
|
111
|
+
async approvalAlarm() {
|
|
112
|
+
const raw = this.ctx.storage.sql
|
|
113
|
+
.exec("SELECT tenant_id, origin FROM oyster_approval_home WHERE id = 1")
|
|
114
|
+
.toArray()[0];
|
|
115
|
+
if (raw === undefined)
|
|
116
|
+
return;
|
|
117
|
+
const pin = Schema.decodeUnknownSync(approvalHomePinRow)(raw);
|
|
118
|
+
const at = await this.approvalHome({
|
|
119
|
+
tenantId: pin.tenant_id,
|
|
120
|
+
origin: pin.origin,
|
|
121
|
+
}).reconcile();
|
|
122
|
+
if (at !== null) {
|
|
123
|
+
const current = await this.ctx.storage.getAlarm();
|
|
124
|
+
if (current === null || current > at)
|
|
125
|
+
await this.ctx.storage.setAlarm(at);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Read a bounded page for the current member of the pinned native home. */
|
|
129
|
+
approvalList(pin, actor, input) {
|
|
130
|
+
return this.approvalHome(pin).capability(actor).list(input);
|
|
131
|
+
}
|
|
132
|
+
/** Read one member-scoped view; the native home resolves membership again. */
|
|
133
|
+
approvalGet(pin, actor, input) {
|
|
134
|
+
return this.approvalHome(pin).capability(actor).get(input);
|
|
135
|
+
}
|
|
136
|
+
/** Persist an eligible answer and attempt its native decision work once. */
|
|
137
|
+
approvalAnswer(pin, actor, input) {
|
|
138
|
+
return this.approvalHome(pin).capability(actor).answer(input);
|
|
139
|
+
}
|
|
140
|
+
/** Policy never grants operation authority; only the live Oyster owner may change it. */
|
|
141
|
+
approvalSetAutoApprove(pin, actor, input) {
|
|
142
|
+
return this.approvalHome(pin).capability(actor).setAutoApprove(input);
|
|
143
|
+
}
|
|
58
144
|
#rootData;
|
|
59
145
|
/** Append-only App source seed receipts; a Git-capable subclass forks over these. */
|
|
60
146
|
appSourceReceipts;
|
|
@@ -64,8 +150,31 @@ export class OysterRegistry extends DurableObject {
|
|
|
64
150
|
artifactsRepositories;
|
|
65
151
|
constructor(ctx, env) {
|
|
66
152
|
super(ctx, env);
|
|
153
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_approval_home (id INTEGER PRIMARY KEY CHECK (id = 1), tenant_id TEXT NOT NULL, origin TEXT NOT NULL) STRICT");
|
|
67
154
|
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS installations (installation_id TEXT PRIMARY KEY, record TEXT NOT NULL) STRICT");
|
|
155
|
+
this.#generation = createInstallationStoreGeneration(ctx.storage.sql);
|
|
156
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS boot_capabilities (id TEXT PRIMARY KEY, token_digest TEXT NOT NULL, expires_at INTEGER NOT NULL, record TEXT NOT NULL) STRICT");
|
|
68
157
|
ensureAppSourceTables(ctx.storage.sql);
|
|
158
|
+
this.#installationOperations = createInstallationOperations({
|
|
159
|
+
owner: ctx.storage,
|
|
160
|
+
registryName: ctx.id.name ?? "",
|
|
161
|
+
generation: () => this.generation(),
|
|
162
|
+
get: (installationId) => this.get(installationId),
|
|
163
|
+
compareAndPut: (installation, version, generation, scope) => this.compareAndPut(installation, version, generation, scope.tenantId),
|
|
164
|
+
execute: (command) => ctx.exports
|
|
165
|
+
.OysterInstallationExecutor({
|
|
166
|
+
props: {
|
|
167
|
+
requestId: command.admission.requestId,
|
|
168
|
+
scope: command.scope,
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
.run(),
|
|
172
|
+
scheduleWake: (at) => ctx.storage.transaction(async (transaction) => {
|
|
173
|
+
const current = await transaction.getAlarm();
|
|
174
|
+
if (current === null || at < current)
|
|
175
|
+
await transaction.setAlarm(at);
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
69
178
|
this.appSourceReceipts = createOysterAppSourceReceipts(ctx.storage.sql);
|
|
70
179
|
this.appRegistrations = createOysterAppRegistrations(ctx.storage.sql);
|
|
71
180
|
this.artifactsRepositories = createOysterArtifactsRepositoryMetadataStore(ctx.storage.sql);
|
|
@@ -83,6 +192,24 @@ export class OysterRegistry extends DurableObject {
|
|
|
83
192
|
resolveRelease: (release) => verifyRelease(r2ArtifactStore(env.RELEASES), release, defaultAssetPolicy, false),
|
|
84
193
|
});
|
|
85
194
|
}
|
|
195
|
+
/** The installation owner's RPC: admit, read, load, plan or commit one
|
|
196
|
+
* retained command for the caller's tenant scope. */
|
|
197
|
+
async installationOperation(input) {
|
|
198
|
+
return (await this.#installationOperations).dispatch(input);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* One Registry wake serves both owners: installation work, then the native
|
|
202
|
+
* approval home. Each re-arms only an earlier wake, so neither postpones the
|
|
203
|
+
* other, and one owner's failure never skips the other's quantum.
|
|
204
|
+
*/
|
|
205
|
+
async alarm() {
|
|
206
|
+
try {
|
|
207
|
+
await (await this.#installationOperations).tick();
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
await this.approvalAlarm();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
86
213
|
/** Current desktop metadata; roles are the ADR's derived default, never Hub's stored choice. */
|
|
87
214
|
directory() {
|
|
88
215
|
return this.#rootData.directory();
|
|
@@ -179,7 +306,7 @@ export class OysterRegistry extends DurableObject {
|
|
|
179
306
|
(expected !== undefined &&
|
|
180
307
|
(expected.selection.owner.kind !== "installation" ||
|
|
181
308
|
installation.instanceId !== expected.instanceId ||
|
|
182
|
-
installation.authorityVersion !==
|
|
309
|
+
installation.authority.authorityVersion !==
|
|
183
310
|
expected.selection.authorityVersion ||
|
|
184
311
|
installation.release.releaseId !==
|
|
185
312
|
expected.selection.releaseId ||
|
|
@@ -197,7 +324,8 @@ export class OysterRegistry extends DurableObject {
|
|
|
197
324
|
throw new AppStorageError("unavailable");
|
|
198
325
|
const current = this.get(installationId);
|
|
199
326
|
if (current?.status !== "enabled" ||
|
|
200
|
-
current.authorityVersion !==
|
|
327
|
+
current.authority.authorityVersion !==
|
|
328
|
+
installation.authority.authorityVersion ||
|
|
201
329
|
current.release.releaseId !== installation.release.releaseId ||
|
|
202
330
|
current.release.artifact.artifactDigest !==
|
|
203
331
|
installation.release.artifact.artifactDigest)
|
|
@@ -233,14 +361,14 @@ export class OysterRegistry extends DurableObject {
|
|
|
233
361
|
.toArray()[0];
|
|
234
362
|
return row === undefined ? null : this.decode(row);
|
|
235
363
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
364
|
+
/** Durable store identity, regenerated only when the store itself is replaced. */
|
|
365
|
+
generation() {
|
|
366
|
+
return this.#generation();
|
|
239
367
|
}
|
|
240
368
|
/** Native RPC returns a closed result; callers never classify a transported Error. */
|
|
241
|
-
async compareAndPut(installation, expectedAuthorityVersion, tenantId = defaultTenant) {
|
|
369
|
+
async compareAndPut(installation, expectedAuthorityVersion, expectedGeneration, tenantId = defaultTenant) {
|
|
242
370
|
try {
|
|
243
|
-
await this.writeInstallation(installation, tenantId, expectedAuthorityVersion);
|
|
371
|
+
await this.writeInstallation(installation, tenantId, expectedAuthorityVersion, expectedGeneration);
|
|
244
372
|
return { ok: true };
|
|
245
373
|
}
|
|
246
374
|
catch (cause) {
|
|
@@ -249,10 +377,12 @@ export class OysterRegistry extends DurableObject {
|
|
|
249
377
|
throw cause;
|
|
250
378
|
}
|
|
251
379
|
}
|
|
252
|
-
async writeInstallation(installation, tenantId, expectedAuthorityVersion) {
|
|
380
|
+
async writeInstallation(installation, tenantId, expectedAuthorityVersion, expectedGeneration) {
|
|
381
|
+
// Version 0 names an absent record.
|
|
382
|
+
const versionOf = (record) => record?.authority.authorityVersion ?? 0;
|
|
253
383
|
const previous = this.get(installation.installationId);
|
|
254
|
-
if (
|
|
255
|
-
previous
|
|
384
|
+
if (this.generation() !== expectedGeneration ||
|
|
385
|
+
versionOf(previous) !== expectedAuthorityVersion)
|
|
256
386
|
throw new AppInstanceError("stale_authority");
|
|
257
387
|
const encode = (value) => JSON.stringify(installationSchema.parse(value));
|
|
258
388
|
const record = encode(installation);
|
|
@@ -260,21 +390,22 @@ export class OysterRegistry extends DurableObject {
|
|
|
260
390
|
if (previous !== null &&
|
|
261
391
|
previous.instanceId !== null &&
|
|
262
392
|
(previous.instanceId !== installation.instanceId ||
|
|
263
|
-
previous.authorityVersion !==
|
|
393
|
+
previous.authority.authorityVersion !==
|
|
394
|
+
installation.authority.authorityVersion ||
|
|
264
395
|
previous.status !== installation.status ||
|
|
265
396
|
previous.release.artifact.artifactDigest !==
|
|
266
397
|
installation.release.artifact.artifactDigest))
|
|
267
398
|
if ((await resolveBackend(r2ArtifactStore(this.env.RELEASES), previous.release)) !== null) {
|
|
268
|
-
if (
|
|
269
|
-
this.get(installation.installationId)
|
|
399
|
+
if (this.generation() !== expectedGeneration ||
|
|
400
|
+
versionOf(this.get(installation.installationId)) !==
|
|
270
401
|
expectedAuthorityVersion)
|
|
271
402
|
throw new AppInstanceError("stale_authority");
|
|
272
403
|
await this.env.INSTANCES.getByName(previous.instanceId).revoke(previous.installationId, undefined, tenantId);
|
|
273
404
|
}
|
|
274
405
|
this.ctx.storage.transactionSync(() => {
|
|
275
406
|
const current = this.get(installation.installationId);
|
|
276
|
-
if (
|
|
277
|
-
current
|
|
407
|
+
if (this.generation() !== expectedGeneration ||
|
|
408
|
+
versionOf(current) !== expectedAuthorityVersion)
|
|
278
409
|
throw new AppInstanceError("stale_authority");
|
|
279
410
|
if ((current === null ? null : encode(current)) !== previousRecord)
|
|
280
411
|
throw new AppInstanceError("stale_authority");
|
|
@@ -287,6 +418,49 @@ export class OysterRegistry extends DurableObject {
|
|
|
287
418
|
const { record } = Schema.decodeUnknownSync(recordRow)(row);
|
|
288
419
|
return installationSchema.parse(JSON.parse(record));
|
|
289
420
|
}
|
|
421
|
+
/** Issue, resolve, or revoke one bounded capability without storing its token. */
|
|
422
|
+
bootCapability(input) {
|
|
423
|
+
if (input.kind === "issue") {
|
|
424
|
+
const { lease, tokenDigest } = bootCapabilityGrantSchema.parse(input.grant);
|
|
425
|
+
validateCapabilityAssetSet(lease);
|
|
426
|
+
const now = Date.now();
|
|
427
|
+
if (lease.expiresAt <= now ||
|
|
428
|
+
lease.expiresAt > now + BOOT_DEADLINE_MS + BOOT_TRANSPORT_GRACE_MS)
|
|
429
|
+
throw new Error("Boot capability lease is outside its deadline");
|
|
430
|
+
this.ctx.storage.sql.exec("DELETE FROM boot_capabilities WHERE expires_at <= ?", now);
|
|
431
|
+
const count = this.ctx.storage.sql
|
|
432
|
+
.exec("SELECT id FROM boot_capabilities LIMIT 8")
|
|
433
|
+
.toArray().length;
|
|
434
|
+
if (count >= 8)
|
|
435
|
+
throw new Error("Boot capability lease capacity reached");
|
|
436
|
+
this.ctx.storage.sql.exec("INSERT INTO boot_capabilities (id, token_digest, expires_at, record) VALUES(?,?,?,?)", lease.id, tokenDigest, lease.expiresAt, JSON.stringify(lease));
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
if (input.kind === "revoke") {
|
|
440
|
+
const id = z.string().min(1).max(128).parse(input.id);
|
|
441
|
+
this.ctx.storage.sql.exec("DELETE FROM boot_capabilities WHERE id = ?", id);
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
const tokenDigest = z
|
|
445
|
+
.string()
|
|
446
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
447
|
+
.parse(input.tokenDigest);
|
|
448
|
+
const now = z.number().int().parse(input.now);
|
|
449
|
+
const rows = bootCapabilityRows.parse(this.ctx.storage.sql
|
|
450
|
+
.exec("SELECT token_digest, expires_at, record FROM boot_capabilities LIMIT 8")
|
|
451
|
+
.toArray());
|
|
452
|
+
const encoder = new TextEncoder();
|
|
453
|
+
const candidate = encoder.encode(tokenDigest);
|
|
454
|
+
let matched = null;
|
|
455
|
+
for (const row of rows) {
|
|
456
|
+
const digest = encoder.encode(row.token_digest);
|
|
457
|
+
const equal = digest.byteLength === candidate.byteLength &&
|
|
458
|
+
timingSafeEqual(candidate, digest);
|
|
459
|
+
if (equal && row.expires_at > now)
|
|
460
|
+
matched = bootCapabilityLeaseSchema.parse(JSON.parse(row.record));
|
|
461
|
+
}
|
|
462
|
+
return matched;
|
|
463
|
+
}
|
|
290
464
|
}
|
|
291
465
|
const releaseRow = Schema.Struct({ release: Schema.String });
|
|
292
466
|
/**
|
|
@@ -368,6 +542,7 @@ export class OysterBinding extends oysterBindingEntrypoint(oysterBindings) {
|
|
|
368
542
|
function instanceHandle(target, scope, budget) {
|
|
369
543
|
const { tenantId, origin } = scope;
|
|
370
544
|
return {
|
|
545
|
+
prepare: (state, release) => target.prepare(state, release, budget, { tenantId, origin }),
|
|
371
546
|
apply: (state, release) => target.apply(state, release, budget, { tenantId, origin }),
|
|
372
547
|
fetch: (facts, request) => target.backend(facts, request, budget, tenantId),
|
|
373
548
|
call: async (facts, api, name, input) => {
|
|
@@ -456,7 +631,7 @@ export class OysterInstance extends DurableObject {
|
|
|
456
631
|
}
|
|
457
632
|
/** The deployable root's host for one pinned tenant; this plain root is the single owner key. */
|
|
458
633
|
hostFor(scope) {
|
|
459
|
-
return ownerKeyRootHost(this.env, { scope });
|
|
634
|
+
return Promise.resolve(ownerKeyRootHost(this.env, { scope }));
|
|
460
635
|
}
|
|
461
636
|
/** Reconstruct current host admission for each wake; non-default tenants get no wakes. */
|
|
462
637
|
backgroundHost() {
|
|
@@ -495,19 +670,21 @@ export class OysterInstance extends DurableObject {
|
|
|
495
670
|
},
|
|
496
671
|
});
|
|
497
672
|
}
|
|
498
|
-
createRuntime(limits) {
|
|
673
|
+
createRuntime(limits, candidate, bootScope) {
|
|
499
674
|
return createCloudflareAppInstance(this.ctx, this.env.LOADER, (releaseId, scope) => Effect.tryPromise({
|
|
500
675
|
try: async () => {
|
|
501
676
|
if (scope.kind === "invocation")
|
|
502
677
|
return scope.frame.host.artifact;
|
|
503
|
-
const release = this.release(releaseId);
|
|
678
|
+
const release = candidate ?? this.release(releaseId);
|
|
679
|
+
if (release.releaseId !== releaseId)
|
|
680
|
+
throw new AppInstanceError("instance_release_mismatch");
|
|
504
681
|
const resolved = await this.backends(release);
|
|
505
682
|
if (resolved === null)
|
|
506
683
|
throw new Error("Release has no backend");
|
|
507
684
|
return { release, ...resolved };
|
|
508
685
|
},
|
|
509
686
|
catch: (cause) => cause instanceof Error ? cause : new Error("Release unavailable"),
|
|
510
|
-
}), (frame) => this.guestEnv(frame), limits);
|
|
687
|
+
}), (frame) => this.guestEnv(frame), (release) => oysterBootUi(this.env, bootScope ?? this.scope())(release), limits);
|
|
511
688
|
}
|
|
512
689
|
runtimeFor(input) {
|
|
513
690
|
const limits = guestLimits(input);
|
|
@@ -618,6 +795,14 @@ export class OysterInstance extends DurableObject {
|
|
|
618
795
|
}
|
|
619
796
|
/** Installation changes end guest work; a Workflow cancel targets only its run. */
|
|
620
797
|
async revoke(installationId, runId, tenantId = defaultTenant) {
|
|
798
|
+
// CAS can complete without the first apply ever arriving. An unclaimed
|
|
799
|
+
// Instance cannot admit work: withFrame requires both ownership pins.
|
|
800
|
+
// There is nothing to revoke, and this must not invent a tenant/receipt.
|
|
801
|
+
if (this.installationId() === null &&
|
|
802
|
+
this.ctx.storage.sql
|
|
803
|
+
.exec("SELECT id FROM oyster_tenant WHERE id = 1")
|
|
804
|
+
.toArray().length === 0)
|
|
805
|
+
return;
|
|
621
806
|
this.requireTenant(tenantId);
|
|
622
807
|
if (installationId !== this.installationId())
|
|
623
808
|
throw new AppInstanceError("instance_owner_mismatch");
|
|
@@ -633,7 +818,7 @@ export class OysterInstance extends DurableObject {
|
|
|
633
818
|
"kind" in current ||
|
|
634
819
|
current.status !== "enabled" ||
|
|
635
820
|
current.instanceId !== selected.instanceId ||
|
|
636
|
-
current.authorityVersion !== selected.authorityVersion ||
|
|
821
|
+
current.authority.authorityVersion !== selected.authorityVersion ||
|
|
637
822
|
current.release.releaseId !== selected.releaseId ||
|
|
638
823
|
current.release.artifact.artifactDigest !==
|
|
639
824
|
frame.host.artifact.release.artifact.artifactDigest)
|
|
@@ -710,7 +895,33 @@ export class OysterInstance extends DurableObject {
|
|
|
710
895
|
throw new Error("Release unknown to this Instance");
|
|
711
896
|
return Schema.decodeUnknownSync(Schema.fromJsonString(releaseSchema))(Schema.decodeUnknownSync(releaseRow)(row).release);
|
|
712
897
|
}
|
|
713
|
-
|
|
898
|
+
rememberRelease(release) {
|
|
899
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_releases(release_id, release) VALUES(?,?) ON CONFLICT(release_id) DO NOTHING", release.releaseId, Schema.encodeUnknownSync(Schema.fromJsonString(releaseSchema))(release));
|
|
900
|
+
}
|
|
901
|
+
/** Reuse lifecycle preflight without changing the active runtime or its budgets. */
|
|
902
|
+
async prepare(state, release, limits, tenant) {
|
|
903
|
+
let bootScope;
|
|
904
|
+
if (tenant !== undefined) {
|
|
905
|
+
bootScope = tenantScope(Schema.decodeUnknownSync(tenantIdSchema)(tenant.tenantId), tenant.origin);
|
|
906
|
+
const stored = this.ctx.storage.sql
|
|
907
|
+
.exec("SELECT tenant_id, origin FROM oyster_tenant WHERE id = 1")
|
|
908
|
+
.toArray()[0];
|
|
909
|
+
if (stored !== undefined) {
|
|
910
|
+
const pin = this.scope();
|
|
911
|
+
if (pin.tenantId !== bootScope.tenantId ||
|
|
912
|
+
pin.origin !== bootScope.origin)
|
|
913
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
914
|
+
const owner = this.installationId();
|
|
915
|
+
if (owner !== null && owner !== state.owner.id)
|
|
916
|
+
throw new AppInstanceError("instance_owner_mismatch");
|
|
917
|
+
}
|
|
918
|
+
else if (this.installationId() !== null) {
|
|
919
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return result(this.createRuntime(guestLimits(limits), release, bootScope).prepare(state));
|
|
923
|
+
}
|
|
924
|
+
/** Activate the exact committed selection under the host's tenant pin. */
|
|
714
925
|
async apply(state, release, limits, tenant) {
|
|
715
926
|
const selected = tenantScope(Schema.decodeUnknownSync(tenantIdSchema)(tenant.tenantId), tenant.origin);
|
|
716
927
|
const checkTenant = () => {
|
|
@@ -725,17 +936,30 @@ export class OysterInstance extends DurableObject {
|
|
|
725
936
|
const pin = this.scope();
|
|
726
937
|
if (pin.tenantId !== selected.tenantId || pin.origin !== selected.origin)
|
|
727
938
|
throw new AppInstanceError("installation_tenant_mismatch");
|
|
939
|
+
const owner = this.installationId();
|
|
940
|
+
if (owner !== null && owner !== state.owner.id)
|
|
941
|
+
throw new AppInstanceError("instance_owner_mismatch");
|
|
728
942
|
};
|
|
943
|
+
const committed = async () => isCommittedInstallation(await this.env.REGISTRY.getByName(selected.durableName("registry")).get(state.owner.id), state, release);
|
|
729
944
|
checkTenant();
|
|
945
|
+
if (!(await committed()))
|
|
946
|
+
return { ok: false, code: "stale_authority" };
|
|
730
947
|
await this.frames.revoke();
|
|
731
948
|
return this.frames.exclusive(async () => {
|
|
732
949
|
checkTenant();
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
950
|
+
if (!(await committed()))
|
|
951
|
+
return { ok: false, code: "stale_authority" };
|
|
952
|
+
// These pins attest the committed owner, not successful activation.
|
|
953
|
+
// Persist before boot so a failed first activation can be retried or
|
|
954
|
+
// revoked/deleted without ever claiming an active lifecycle receipt.
|
|
955
|
+
this.ctx.storage.transactionSync(() => {
|
|
736
956
|
this.ctx.storage.sql.exec("INSERT INTO oyster_tenant (id, tenant_id, origin) VALUES (1, ?, ?) ON CONFLICT(id) DO NOTHING", selected.tenantId, selected.origin);
|
|
737
|
-
this.ctx.storage.sql.exec("INSERT INTO oyster_owner (id, installation_id) VALUES (1, ?) ON CONFLICT(id) DO
|
|
738
|
-
}
|
|
957
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_owner (id, installation_id) VALUES (1, ?) ON CONFLICT(id) DO NOTHING", state.owner.id);
|
|
958
|
+
});
|
|
959
|
+
this.rememberRelease(release);
|
|
960
|
+
const applied = await result(this.runtimeFor(limits).apply(state));
|
|
961
|
+
if (!(await committed()))
|
|
962
|
+
return { ok: false, code: "stale_authority" };
|
|
739
963
|
return applied;
|
|
740
964
|
});
|
|
741
965
|
}
|
|
@@ -791,12 +1015,74 @@ export class OysterInstance extends DurableObject {
|
|
|
791
1015
|
}
|
|
792
1016
|
}
|
|
793
1017
|
/** Host adapters for a standalone Oyster Worker over its own bindings. */
|
|
1018
|
+
function registryBootCapabilities(env, scope) {
|
|
1019
|
+
const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
|
|
1020
|
+
return {
|
|
1021
|
+
issue: async (grant) => {
|
|
1022
|
+
await registry().bootCapability({ kind: "issue", grant });
|
|
1023
|
+
},
|
|
1024
|
+
resolve: async (tokenDigest, now) => {
|
|
1025
|
+
const result = await registry().bootCapability({
|
|
1026
|
+
kind: "resolve",
|
|
1027
|
+
tokenDigest,
|
|
1028
|
+
now,
|
|
1029
|
+
});
|
|
1030
|
+
if (result === undefined)
|
|
1031
|
+
throw new Error("Boot capability lookup returned no result");
|
|
1032
|
+
return result === null ? null : bootCapabilityLeaseSchema.parse(result);
|
|
1033
|
+
},
|
|
1034
|
+
revoke: async (id) => {
|
|
1035
|
+
await registry().bootCapability({ kind: "revoke", id });
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
/** Resolve a scoped boot URL through the registry-backed HTTPS asset route. */
|
|
1040
|
+
export function serveOysterBootCapability(request, env, scope) {
|
|
1041
|
+
return serveBootCapability(request, {
|
|
1042
|
+
configuredOrigin: scope.origin,
|
|
1043
|
+
bucket: env.RELEASES,
|
|
1044
|
+
capabilities: registryBootCapabilities(env, scope),
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* The standalone UI boot check: the Worker's own BROWSER binding opens the
|
|
1049
|
+
* pinned `/_boot/` page on the host-selected origin, with a capability in
|
|
1050
|
+
* that tenant's registry. Without a BROWSER binding the UI is not checked.
|
|
1051
|
+
*/
|
|
1052
|
+
function oysterBootUi(env, scope) {
|
|
1053
|
+
const artifacts = r2ArtifactStore(env.RELEASES);
|
|
1054
|
+
const capabilities = registryBootCapabilities(env, scope);
|
|
1055
|
+
const sessions = env.BROWSER === undefined ? null : bindingSessions(env.BROWSER);
|
|
1056
|
+
return (release) => reportBootPage({
|
|
1057
|
+
origin: scope.origin,
|
|
1058
|
+
check: sessions === null ? null : (target) => checkBootPage(sessions, target),
|
|
1059
|
+
readUi: () => readBootUi(artifacts, release),
|
|
1060
|
+
bucket: env.RELEASES,
|
|
1061
|
+
capabilities,
|
|
1062
|
+
now: Date.now,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Create an Oyster host backed by this Worker environment's registry,
|
|
1067
|
+
* artifacts, Instances, and boot-check service.
|
|
1068
|
+
*/
|
|
794
1069
|
export function oysterWorkerHost(env, auth, options = {}) {
|
|
795
1070
|
const scope = options.scope ?? tenantScope(defaultTenant, env.OYSTER_ORIGIN);
|
|
796
1071
|
const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
|
|
797
1072
|
const installations = registryInstallations(env, scope);
|
|
1073
|
+
const artifacts = r2ArtifactStore(env.RELEASES);
|
|
798
1074
|
const originSecret = Schema.decodeUnknownOption(Schema.NonEmptyString)(env.OYSTER_OWNER_KEY);
|
|
1075
|
+
const pin = { tenantId: scope.tenantId, origin: scope.origin };
|
|
1076
|
+
const approvalControl = (actor) => ({
|
|
1077
|
+
list: (input) => registry().approvalList(pin, actor, input),
|
|
1078
|
+
get: (input) => registry().approvalGet(pin, actor, input),
|
|
1079
|
+
answer: (input) => registry().approvalAnswer(pin, actor, input),
|
|
1080
|
+
setAutoApprove: (input) => registry().approvalSetAutoApprove(pin, actor, input),
|
|
1081
|
+
});
|
|
799
1082
|
let host = {
|
|
1083
|
+
approvals: {
|
|
1084
|
+
submit: (call) => registry().approvalSubmit({ tenantId: scope.tenantId, origin: scope.origin }, call),
|
|
1085
|
+
},
|
|
800
1086
|
auth,
|
|
801
1087
|
workbench: {
|
|
802
1088
|
directory: async () => {
|
|
@@ -811,10 +1097,14 @@ export function oysterWorkerHost(env, auth, options = {}) {
|
|
|
811
1097
|
}),
|
|
812
1098
|
},
|
|
813
1099
|
guestLimits: guestLimits(options.guestLimits),
|
|
814
|
-
artifacts
|
|
1100
|
+
artifacts,
|
|
1101
|
+
bootUi: oysterBootUi(env, scope),
|
|
815
1102
|
// File-only hosts may keep workspace files beside their Release artifacts.
|
|
816
|
-
capabilities:
|
|
817
|
-
|
|
1103
|
+
capabilities: [
|
|
1104
|
+
...fileCapabilities(options.workspace?.files ??
|
|
1105
|
+
r2FileSource(env.RELEASES, scope.workspacePrefix)),
|
|
1106
|
+
approvalCapability(approvalControl),
|
|
1107
|
+
],
|
|
818
1108
|
installations,
|
|
819
1109
|
instances: {
|
|
820
1110
|
get: (instanceId, limits) => instanceHandle(env.INSTANCES.getByName(instanceId), scope, guestLimits(limits)),
|
|
@@ -848,6 +1138,9 @@ function ownerKeyRootHost(env, options) {
|
|
|
848
1138
|
/** The non-git standalone root's MCP owner. */
|
|
849
1139
|
export class OysterMcp extends createOysterMcpOwner((env, scope) => ownerKeyRootHost(env, { scope })) {
|
|
850
1140
|
}
|
|
1141
|
+
/** Ordinary Worker execution; REGISTRY receives references and receipts only. */
|
|
1142
|
+
export class OysterInstallationExecutor extends installationWorkerEntrypoint((env, scope) => ownerKeyRootHost(env, { scope })) {
|
|
1143
|
+
}
|
|
851
1144
|
/**
|
|
852
1145
|
* The standalone Worker's `fetch` handler; frontend hosts are cached per env.
|
|
853
1146
|
* MCP transports belong to the tenant Durable Object, independent of that cache.
|
|
@@ -856,10 +1149,17 @@ export function createOysterWorker(options = {}) {
|
|
|
856
1149
|
const limits = guestLimits(options.guestLimits);
|
|
857
1150
|
const apps = new WeakMap();
|
|
858
1151
|
return {
|
|
859
|
-
fetch(request, env) {
|
|
1152
|
+
async fetch(request, env) {
|
|
1153
|
+
const scope = tenantScope(defaultTenant, env.OYSTER_ORIGIN);
|
|
1154
|
+
const boot = await serveOysterBootCapability(request, env, scope);
|
|
1155
|
+
if (boot !== null)
|
|
1156
|
+
return boot;
|
|
860
1157
|
let app = apps.get(env);
|
|
861
1158
|
if (app === undefined) {
|
|
862
|
-
app = createOyster(ownerKeyRootHost(env, {
|
|
1159
|
+
app = createOyster(ownerKeyRootHost(env, {
|
|
1160
|
+
guestLimits: limits,
|
|
1161
|
+
scope,
|
|
1162
|
+
}));
|
|
863
1163
|
apps.set(env, app);
|
|
864
1164
|
}
|
|
865
1165
|
return app.fetch(request);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WorkflowArmer, WorkflowTriggerPrincipal } from "@fungi.computer/braid/runtime";
|
|
2
|
+
import type { AppToolPolicy } from "./apps/index.js";
|
|
3
|
+
import type { InvocationFacts } from "./invocation.js";
|
|
4
|
+
/** Use the actual host-captured policy, never an Agent-supplied grantor or revision. */
|
|
5
|
+
export declare function workflowAgentAuthority(facts: InvocationFacts, operation: string, readPolicy: (agentId: string) => Promise<AppToolPolicy | null>): Promise<{
|
|
6
|
+
armer: WorkflowArmer;
|
|
7
|
+
principal: WorkflowTriggerPrincipal;
|
|
8
|
+
} | null>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AppInstanceError } from "./index.js";
|
|
2
|
+
import { agentWorkflowOperationSchema } from "./workflows.js";
|
|
3
|
+
/** Use the actual host-captured policy, never an Agent-supplied grantor or revision. */
|
|
4
|
+
export async function workflowAgentAuthority(facts, operation, readPolicy) {
|
|
5
|
+
if (facts.subject.kind !== "call")
|
|
6
|
+
throw new AppInstanceError("revoked");
|
|
7
|
+
const actor = facts.subject.context.actor;
|
|
8
|
+
if (actor.kind === "member")
|
|
9
|
+
return {
|
|
10
|
+
armer: { armedBy: actor.memberId, armedVia: null },
|
|
11
|
+
principal: { kind: "member", memberId: actor.memberId },
|
|
12
|
+
};
|
|
13
|
+
if (actor.kind !== "agent" ||
|
|
14
|
+
!agentWorkflowOperationSchema.safeParse(operation).success)
|
|
15
|
+
return null;
|
|
16
|
+
const captured = facts.subject.agentPolicy;
|
|
17
|
+
const policy = await readPolicy(actor.agentId);
|
|
18
|
+
const write = operation !== "catalog" &&
|
|
19
|
+
operation !== "schedules" &&
|
|
20
|
+
operation !== "eventTriggers";
|
|
21
|
+
if (policy === null ||
|
|
22
|
+
captured?.policyRevision !== policy.revision ||
|
|
23
|
+
captured.grantorMemberId !== policy.grantedBy ||
|
|
24
|
+
(write && policy.level !== "write"))
|
|
25
|
+
throw new AppInstanceError("revoked");
|
|
26
|
+
return {
|
|
27
|
+
armer: {
|
|
28
|
+
armedBy: policy.grantedBy,
|
|
29
|
+
armedVia: {
|
|
30
|
+
kind: "policy",
|
|
31
|
+
agentId: actor.agentId,
|
|
32
|
+
policyRevision: policy.revision,
|
|
33
|
+
grantorMemberId: policy.grantedBy,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
principal: {
|
|
37
|
+
kind: "agent",
|
|
38
|
+
agentId: actor.agentId,
|
|
39
|
+
grantorMemberId: policy.grantedBy,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|