@fungi.computer/oyster 0.2.0 → 0.3.1
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 +396 -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 +213 -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/older-oyster-store-refusal.d.ts +4 -0
- package/dist/older-oyster-store-refusal.js +18 -0
- package/dist/older-oyster-store.d.ts +21 -0
- package/dist/older-oyster-store.js +43 -0
- 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 +186 -144
- 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 +151 -9
- package/dist/standalone/registry.js +305 -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 +64 -52
- package/dist/workers.d.ts +226 -13
- package/dist/workers.js +363 -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 +1594 -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,20 @@
|
|
|
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 { installationsFromOlderOyster } from "./older-oyster-store.js";
|
|
14
|
+
import { refuseOlderOysterStore } from "./older-oyster-store-refusal.js";
|
|
15
|
+
import { createInstallationOperations, } from "./installation-operations.js";
|
|
16
|
+
import { installationWorkerEntrypoint } from "./installation-execution.js";
|
|
17
|
+
export { installationWorkerEntrypoint };
|
|
3
18
|
import { approvedBindingGrants, scopedBindings } from "./apps/gate.js";
|
|
4
19
|
import { appMethodReplySchema } from "@fungi.computer/agar/backend";
|
|
5
20
|
import { nativeGitFrameMethods, nativeGitMethodNameSchema, } from "@fungi.computer/agar/git-rpc";
|
|
@@ -20,7 +35,7 @@ import * as Schema from "effect/Schema";
|
|
|
20
35
|
import * as Stream from "effect/Stream";
|
|
21
36
|
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
22
37
|
import { AppInstanceError, } from "./index.js";
|
|
23
|
-
import { createCloudflareAppInstance, createCloudflareInvocationGate, decodeFrameBindingProps, captureFrameBindingReply, frameBindingEntrypoint, backgroundFrameMethods, } from "./cloudflare.js";
|
|
38
|
+
import { createCloudflareAppInstance, r2ArtifactStore, createCloudflareInvocationGate, decodeFrameBindingProps, captureFrameBindingReply, frameBindingEntrypoint, backgroundFrameMethods, } from "./cloudflare.js";
|
|
24
39
|
import { invocationFactsSchema, assertBackgroundBinding, admitBackgroundFrame, } from "./invocation.js";
|
|
25
40
|
import { deliverBackground, deliverGitPush } from "./internal/delivery.js";
|
|
26
41
|
import { createOyster, verifyRelease, defaultAssetPolicy, } from "./host.js";
|
|
@@ -30,31 +45,106 @@ import { storageFrameMethods } from "./storage-frame.js";
|
|
|
30
45
|
import { catalogFrameMethods, oysterAppCatalog } from "./catalog-frame.js";
|
|
31
46
|
import { appStorageKeyNameSchema, appStorageWriteSchema, appStorageResetSchema, } from "@fungi.computer/agar/storage";
|
|
32
47
|
import { createOysterRootData } from "./root-data.js";
|
|
33
|
-
import { backendCache, resolveBackend
|
|
34
|
-
import { headContentObject } from "./build/content-objects.js";
|
|
48
|
+
import { backendCache, resolveBackend } from "./release.js";
|
|
35
49
|
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
|
-
}
|
|
50
|
+
export { r2ArtifactStore, r2FileSource, ownerKeyAuth };
|
|
52
51
|
const recordRow = Schema.Struct({
|
|
53
52
|
installation_id: Schema.String,
|
|
54
53
|
record: Schema.String,
|
|
55
54
|
});
|
|
55
|
+
const bootCapabilityRows = z.array(z.strictObject({
|
|
56
|
+
token_digest: z.string().regex(/^[0-9a-f]{64}$/),
|
|
57
|
+
expires_at: z.number().int(),
|
|
58
|
+
record: z.string().max(2_000_000),
|
|
59
|
+
}));
|
|
60
|
+
const approvalHomePinRow = Schema.Struct({
|
|
61
|
+
tenant_id: tenantIdSchema,
|
|
62
|
+
origin: Schema.String,
|
|
63
|
+
});
|
|
56
64
|
/** The installation registry: one SQLite-backed Durable Object per Oyster. */
|
|
57
65
|
export class OysterRegistry extends DurableObject {
|
|
66
|
+
#generation;
|
|
67
|
+
/** Decided once, before this version writes anything, and never revised. */
|
|
68
|
+
#installationsFromOlderOyster;
|
|
69
|
+
#approval;
|
|
70
|
+
#installationOperations;
|
|
71
|
+
/** The plain root has the same single owner-key host as OysterInstance. */
|
|
72
|
+
hostFor(scope) {
|
|
73
|
+
return Promise.resolve(ownerKeyRootHost(this.env, { scope }));
|
|
74
|
+
}
|
|
75
|
+
/** Trusted host RPC; an operation input cannot select actor, namespace or tenant. */
|
|
76
|
+
approvalSubmit(pin, call) {
|
|
77
|
+
return this.approvalHome(pin).submit(call);
|
|
78
|
+
}
|
|
79
|
+
approvalHome(pin) {
|
|
80
|
+
const scope = tenantScope(Schema.decodeUnknownSync(tenantIdSchema)(pin.tenantId), pin.origin);
|
|
81
|
+
if (this.ctx.id.name !== scope.durableName("registry"))
|
|
82
|
+
throw new Error("Approval home namespace does not match tenant");
|
|
83
|
+
this.ctx.storage.transactionSync(() => {
|
|
84
|
+
const raw = this.ctx.storage.sql
|
|
85
|
+
.exec("SELECT tenant_id, origin FROM oyster_approval_home WHERE id = 1")
|
|
86
|
+
.toArray()[0];
|
|
87
|
+
if (raw === undefined) {
|
|
88
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_approval_home (id, tenant_id, origin) VALUES (1, ?, ?)", scope.tenantId, scope.origin);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const retained = Schema.decodeUnknownSync(approvalHomePinRow)(raw);
|
|
92
|
+
if (retained.tenant_id !== scope.tenantId ||
|
|
93
|
+
retained.origin !== scope.origin)
|
|
94
|
+
throw new Error("Approval home cannot change tenant or origin");
|
|
95
|
+
});
|
|
96
|
+
this.#approval ??= new OysterApprovalHome({
|
|
97
|
+
owner: {
|
|
98
|
+
sql: this.ctx.storage.sql,
|
|
99
|
+
transactionSync: (body) => this.ctx.storage.transactionSync(body),
|
|
100
|
+
},
|
|
101
|
+
scope,
|
|
102
|
+
current: () => this.hostFor(scope),
|
|
103
|
+
commit: (write, nextWakeAt) => this.ctx.storage.transaction(async (transaction) => {
|
|
104
|
+
const result = await write();
|
|
105
|
+
const at = nextWakeAt();
|
|
106
|
+
const current = await transaction.getAlarm();
|
|
107
|
+
if (at !== null && (current === null || current > at))
|
|
108
|
+
await transaction.setAlarm(at);
|
|
109
|
+
return result;
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
return this.#approval;
|
|
113
|
+
}
|
|
114
|
+
/** Recover approved work from the durable home, not a last-request memory pin. */
|
|
115
|
+
async approvalAlarm() {
|
|
116
|
+
const raw = this.ctx.storage.sql
|
|
117
|
+
.exec("SELECT tenant_id, origin FROM oyster_approval_home WHERE id = 1")
|
|
118
|
+
.toArray()[0];
|
|
119
|
+
if (raw === undefined)
|
|
120
|
+
return;
|
|
121
|
+
const pin = Schema.decodeUnknownSync(approvalHomePinRow)(raw);
|
|
122
|
+
const at = await this.approvalHome({
|
|
123
|
+
tenantId: pin.tenant_id,
|
|
124
|
+
origin: pin.origin,
|
|
125
|
+
}).reconcile();
|
|
126
|
+
if (at !== null) {
|
|
127
|
+
const current = await this.ctx.storage.getAlarm();
|
|
128
|
+
if (current === null || current > at)
|
|
129
|
+
await this.ctx.storage.setAlarm(at);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Read a bounded page for the current member of the pinned native home. */
|
|
133
|
+
approvalList(pin, actor, input) {
|
|
134
|
+
return this.approvalHome(pin).capability(actor).list(input);
|
|
135
|
+
}
|
|
136
|
+
/** Read one member-scoped view; the native home resolves membership again. */
|
|
137
|
+
approvalGet(pin, actor, input) {
|
|
138
|
+
return this.approvalHome(pin).capability(actor).get(input);
|
|
139
|
+
}
|
|
140
|
+
/** Persist an eligible answer and attempt its native decision work once. */
|
|
141
|
+
approvalAnswer(pin, actor, input) {
|
|
142
|
+
return this.approvalHome(pin).capability(actor).answer(input);
|
|
143
|
+
}
|
|
144
|
+
/** Policy never grants operation authority; only the live Oyster owner may change it. */
|
|
145
|
+
approvalSetAutoApprove(pin, actor, input) {
|
|
146
|
+
return this.approvalHome(pin).capability(actor).setAutoApprove(input);
|
|
147
|
+
}
|
|
58
148
|
#rootData;
|
|
59
149
|
/** Append-only App source seed receipts; a Git-capable subclass forks over these. */
|
|
60
150
|
appSourceReceipts;
|
|
@@ -64,8 +154,32 @@ export class OysterRegistry extends DurableObject {
|
|
|
64
154
|
artifactsRepositories;
|
|
65
155
|
constructor(ctx, env) {
|
|
66
156
|
super(ctx, env);
|
|
157
|
+
this.#installationsFromOlderOyster = installationsFromOlderOyster(ctx.storage.sql);
|
|
158
|
+
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
159
|
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS installations (installation_id TEXT PRIMARY KEY, record TEXT NOT NULL) STRICT");
|
|
160
|
+
this.#generation = createInstallationStoreGeneration(ctx.storage.sql);
|
|
161
|
+
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
162
|
ensureAppSourceTables(ctx.storage.sql);
|
|
163
|
+
this.#installationOperations = createInstallationOperations({
|
|
164
|
+
owner: ctx.storage,
|
|
165
|
+
registryName: ctx.id.name ?? "",
|
|
166
|
+
generation: () => this.generation(),
|
|
167
|
+
get: (installationId) => this.get(installationId),
|
|
168
|
+
compareAndPut: (installation, version, generation, scope) => this.compareAndPut(installation, version, generation, scope.tenantId),
|
|
169
|
+
execute: (command) => ctx.exports
|
|
170
|
+
.OysterInstallationExecutor({
|
|
171
|
+
props: {
|
|
172
|
+
requestId: command.admission.requestId,
|
|
173
|
+
scope: command.scope,
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
.run(),
|
|
177
|
+
scheduleWake: (at) => ctx.storage.transaction(async (transaction) => {
|
|
178
|
+
const current = await transaction.getAlarm();
|
|
179
|
+
if (current === null || at < current)
|
|
180
|
+
await transaction.setAlarm(at);
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
69
183
|
this.appSourceReceipts = createOysterAppSourceReceipts(ctx.storage.sql);
|
|
70
184
|
this.appRegistrations = createOysterAppRegistrations(ctx.storage.sql);
|
|
71
185
|
this.artifactsRepositories = createOysterArtifactsRepositoryMetadataStore(ctx.storage.sql);
|
|
@@ -83,6 +197,24 @@ export class OysterRegistry extends DurableObject {
|
|
|
83
197
|
resolveRelease: (release) => verifyRelease(r2ArtifactStore(env.RELEASES), release, defaultAssetPolicy, false),
|
|
84
198
|
});
|
|
85
199
|
}
|
|
200
|
+
/** The installation owner's RPC: admit, read, load, plan or commit one
|
|
201
|
+
* retained command for the caller's tenant scope. */
|
|
202
|
+
async installationOperation(input) {
|
|
203
|
+
return (await this.#installationOperations).dispatch(input);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* One Registry wake serves both owners: installation work, then the native
|
|
207
|
+
* approval home. Each re-arms only an earlier wake, so neither postpones the
|
|
208
|
+
* other, and one owner's failure never skips the other's quantum.
|
|
209
|
+
*/
|
|
210
|
+
async alarm() {
|
|
211
|
+
try {
|
|
212
|
+
await (await this.#installationOperations).tick();
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
await this.approvalAlarm();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
86
218
|
/** Current desktop metadata; roles are the ADR's derived default, never Hub's stored choice. */
|
|
87
219
|
directory() {
|
|
88
220
|
return this.#rootData.directory();
|
|
@@ -179,7 +311,7 @@ export class OysterRegistry extends DurableObject {
|
|
|
179
311
|
(expected !== undefined &&
|
|
180
312
|
(expected.selection.owner.kind !== "installation" ||
|
|
181
313
|
installation.instanceId !== expected.instanceId ||
|
|
182
|
-
installation.authorityVersion !==
|
|
314
|
+
installation.authority.authorityVersion !==
|
|
183
315
|
expected.selection.authorityVersion ||
|
|
184
316
|
installation.release.releaseId !==
|
|
185
317
|
expected.selection.releaseId ||
|
|
@@ -197,7 +329,8 @@ export class OysterRegistry extends DurableObject {
|
|
|
197
329
|
throw new AppStorageError("unavailable");
|
|
198
330
|
const current = this.get(installationId);
|
|
199
331
|
if (current?.status !== "enabled" ||
|
|
200
|
-
current.authorityVersion !==
|
|
332
|
+
current.authority.authorityVersion !==
|
|
333
|
+
installation.authority.authorityVersion ||
|
|
201
334
|
current.release.releaseId !== installation.release.releaseId ||
|
|
202
335
|
current.release.artifact.artifactDigest !==
|
|
203
336
|
installation.release.artifact.artifactDigest)
|
|
@@ -219,6 +352,10 @@ export class OysterRegistry extends DurableObject {
|
|
|
219
352
|
: new AppStorageError("unavailable", { cause }),
|
|
220
353
|
}));
|
|
221
354
|
}
|
|
355
|
+
/** Whether this registry's data was written by Oyster 0.2 or earlier; hosts refuse it. */
|
|
356
|
+
storeFromOlderOyster() {
|
|
357
|
+
return this.#installationsFromOlderOyster;
|
|
358
|
+
}
|
|
222
359
|
/** Every installation record. */
|
|
223
360
|
list() {
|
|
224
361
|
return this.ctx.storage.sql
|
|
@@ -233,14 +370,14 @@ export class OysterRegistry extends DurableObject {
|
|
|
233
370
|
.toArray()[0];
|
|
234
371
|
return row === undefined ? null : this.decode(row);
|
|
235
372
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
373
|
+
/** Durable store identity, regenerated only when the store itself is replaced. */
|
|
374
|
+
generation() {
|
|
375
|
+
return this.#generation();
|
|
239
376
|
}
|
|
240
377
|
/** Native RPC returns a closed result; callers never classify a transported Error. */
|
|
241
|
-
async compareAndPut(installation, expectedAuthorityVersion, tenantId = defaultTenant) {
|
|
378
|
+
async compareAndPut(installation, expectedAuthorityVersion, expectedGeneration, tenantId = defaultTenant) {
|
|
242
379
|
try {
|
|
243
|
-
await this.writeInstallation(installation, tenantId, expectedAuthorityVersion);
|
|
380
|
+
await this.writeInstallation(installation, tenantId, expectedAuthorityVersion, expectedGeneration);
|
|
244
381
|
return { ok: true };
|
|
245
382
|
}
|
|
246
383
|
catch (cause) {
|
|
@@ -249,10 +386,12 @@ export class OysterRegistry extends DurableObject {
|
|
|
249
386
|
throw cause;
|
|
250
387
|
}
|
|
251
388
|
}
|
|
252
|
-
async writeInstallation(installation, tenantId, expectedAuthorityVersion) {
|
|
389
|
+
async writeInstallation(installation, tenantId, expectedAuthorityVersion, expectedGeneration) {
|
|
390
|
+
// Version 0 names an absent record.
|
|
391
|
+
const versionOf = (record) => record?.authority.authorityVersion ?? 0;
|
|
253
392
|
const previous = this.get(installation.installationId);
|
|
254
|
-
if (
|
|
255
|
-
previous
|
|
393
|
+
if (this.generation() !== expectedGeneration ||
|
|
394
|
+
versionOf(previous) !== expectedAuthorityVersion)
|
|
256
395
|
throw new AppInstanceError("stale_authority");
|
|
257
396
|
const encode = (value) => JSON.stringify(installationSchema.parse(value));
|
|
258
397
|
const record = encode(installation);
|
|
@@ -260,21 +399,22 @@ export class OysterRegistry extends DurableObject {
|
|
|
260
399
|
if (previous !== null &&
|
|
261
400
|
previous.instanceId !== null &&
|
|
262
401
|
(previous.instanceId !== installation.instanceId ||
|
|
263
|
-
previous.authorityVersion !==
|
|
402
|
+
previous.authority.authorityVersion !==
|
|
403
|
+
installation.authority.authorityVersion ||
|
|
264
404
|
previous.status !== installation.status ||
|
|
265
405
|
previous.release.artifact.artifactDigest !==
|
|
266
406
|
installation.release.artifact.artifactDigest))
|
|
267
407
|
if ((await resolveBackend(r2ArtifactStore(this.env.RELEASES), previous.release)) !== null) {
|
|
268
|
-
if (
|
|
269
|
-
this.get(installation.installationId)
|
|
408
|
+
if (this.generation() !== expectedGeneration ||
|
|
409
|
+
versionOf(this.get(installation.installationId)) !==
|
|
270
410
|
expectedAuthorityVersion)
|
|
271
411
|
throw new AppInstanceError("stale_authority");
|
|
272
412
|
await this.env.INSTANCES.getByName(previous.instanceId).revoke(previous.installationId, undefined, tenantId);
|
|
273
413
|
}
|
|
274
414
|
this.ctx.storage.transactionSync(() => {
|
|
275
415
|
const current = this.get(installation.installationId);
|
|
276
|
-
if (
|
|
277
|
-
current
|
|
416
|
+
if (this.generation() !== expectedGeneration ||
|
|
417
|
+
versionOf(current) !== expectedAuthorityVersion)
|
|
278
418
|
throw new AppInstanceError("stale_authority");
|
|
279
419
|
if ((current === null ? null : encode(current)) !== previousRecord)
|
|
280
420
|
throw new AppInstanceError("stale_authority");
|
|
@@ -287,6 +427,49 @@ export class OysterRegistry extends DurableObject {
|
|
|
287
427
|
const { record } = Schema.decodeUnknownSync(recordRow)(row);
|
|
288
428
|
return installationSchema.parse(JSON.parse(record));
|
|
289
429
|
}
|
|
430
|
+
/** Issue, resolve, or revoke one bounded capability without storing its token. */
|
|
431
|
+
bootCapability(input) {
|
|
432
|
+
if (input.kind === "issue") {
|
|
433
|
+
const { lease, tokenDigest } = bootCapabilityGrantSchema.parse(input.grant);
|
|
434
|
+
validateCapabilityAssetSet(lease);
|
|
435
|
+
const now = Date.now();
|
|
436
|
+
if (lease.expiresAt <= now ||
|
|
437
|
+
lease.expiresAt > now + BOOT_DEADLINE_MS + BOOT_TRANSPORT_GRACE_MS)
|
|
438
|
+
throw new Error("Boot capability lease is outside its deadline");
|
|
439
|
+
this.ctx.storage.sql.exec("DELETE FROM boot_capabilities WHERE expires_at <= ?", now);
|
|
440
|
+
const count = this.ctx.storage.sql
|
|
441
|
+
.exec("SELECT id FROM boot_capabilities LIMIT 8")
|
|
442
|
+
.toArray().length;
|
|
443
|
+
if (count >= 8)
|
|
444
|
+
throw new Error("Boot capability lease capacity reached");
|
|
445
|
+
this.ctx.storage.sql.exec("INSERT INTO boot_capabilities (id, token_digest, expires_at, record) VALUES(?,?,?,?)", lease.id, tokenDigest, lease.expiresAt, JSON.stringify(lease));
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
if (input.kind === "revoke") {
|
|
449
|
+
const id = z.string().min(1).max(128).parse(input.id);
|
|
450
|
+
this.ctx.storage.sql.exec("DELETE FROM boot_capabilities WHERE id = ?", id);
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
const tokenDigest = z
|
|
454
|
+
.string()
|
|
455
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
456
|
+
.parse(input.tokenDigest);
|
|
457
|
+
const now = z.number().int().parse(input.now);
|
|
458
|
+
const rows = bootCapabilityRows.parse(this.ctx.storage.sql
|
|
459
|
+
.exec("SELECT token_digest, expires_at, record FROM boot_capabilities LIMIT 8")
|
|
460
|
+
.toArray());
|
|
461
|
+
const encoder = new TextEncoder();
|
|
462
|
+
const candidate = encoder.encode(tokenDigest);
|
|
463
|
+
let matched = null;
|
|
464
|
+
for (const row of rows) {
|
|
465
|
+
const digest = encoder.encode(row.token_digest);
|
|
466
|
+
const equal = digest.byteLength === candidate.byteLength &&
|
|
467
|
+
timingSafeEqual(candidate, digest);
|
|
468
|
+
if (equal && row.expires_at > now)
|
|
469
|
+
matched = bootCapabilityLeaseSchema.parse(JSON.parse(row.record));
|
|
470
|
+
}
|
|
471
|
+
return matched;
|
|
472
|
+
}
|
|
290
473
|
}
|
|
291
474
|
const releaseRow = Schema.Struct({ release: Schema.String });
|
|
292
475
|
/**
|
|
@@ -368,6 +551,7 @@ export class OysterBinding extends oysterBindingEntrypoint(oysterBindings) {
|
|
|
368
551
|
function instanceHandle(target, scope, budget) {
|
|
369
552
|
const { tenantId, origin } = scope;
|
|
370
553
|
return {
|
|
554
|
+
prepare: (state, release) => target.prepare(state, release, budget, { tenantId, origin }),
|
|
371
555
|
apply: (state, release) => target.apply(state, release, budget, { tenantId, origin }),
|
|
372
556
|
fetch: (facts, request) => target.backend(facts, request, budget, tenantId),
|
|
373
557
|
call: async (facts, api, name, input) => {
|
|
@@ -456,7 +640,7 @@ export class OysterInstance extends DurableObject {
|
|
|
456
640
|
}
|
|
457
641
|
/** The deployable root's host for one pinned tenant; this plain root is the single owner key. */
|
|
458
642
|
hostFor(scope) {
|
|
459
|
-
return ownerKeyRootHost(this.env, { scope });
|
|
643
|
+
return Promise.resolve(ownerKeyRootHost(this.env, { scope }));
|
|
460
644
|
}
|
|
461
645
|
/** Reconstruct current host admission for each wake; non-default tenants get no wakes. */
|
|
462
646
|
backgroundHost() {
|
|
@@ -495,19 +679,21 @@ export class OysterInstance extends DurableObject {
|
|
|
495
679
|
},
|
|
496
680
|
});
|
|
497
681
|
}
|
|
498
|
-
createRuntime(limits) {
|
|
682
|
+
createRuntime(limits, candidate, bootScope) {
|
|
499
683
|
return createCloudflareAppInstance(this.ctx, this.env.LOADER, (releaseId, scope) => Effect.tryPromise({
|
|
500
684
|
try: async () => {
|
|
501
685
|
if (scope.kind === "invocation")
|
|
502
686
|
return scope.frame.host.artifact;
|
|
503
|
-
const release = this.release(releaseId);
|
|
687
|
+
const release = candidate ?? this.release(releaseId);
|
|
688
|
+
if (release.releaseId !== releaseId)
|
|
689
|
+
throw new AppInstanceError("instance_release_mismatch");
|
|
504
690
|
const resolved = await this.backends(release);
|
|
505
691
|
if (resolved === null)
|
|
506
692
|
throw new Error("Release has no backend");
|
|
507
693
|
return { release, ...resolved };
|
|
508
694
|
},
|
|
509
695
|
catch: (cause) => cause instanceof Error ? cause : new Error("Release unavailable"),
|
|
510
|
-
}), (frame) => this.guestEnv(frame), limits);
|
|
696
|
+
}), (frame) => this.guestEnv(frame), (release) => oysterBootUi(this.env, bootScope ?? this.scope())(release), limits);
|
|
511
697
|
}
|
|
512
698
|
runtimeFor(input) {
|
|
513
699
|
const limits = guestLimits(input);
|
|
@@ -618,6 +804,14 @@ export class OysterInstance extends DurableObject {
|
|
|
618
804
|
}
|
|
619
805
|
/** Installation changes end guest work; a Workflow cancel targets only its run. */
|
|
620
806
|
async revoke(installationId, runId, tenantId = defaultTenant) {
|
|
807
|
+
// CAS can complete without the first apply ever arriving. An unclaimed
|
|
808
|
+
// Instance cannot admit work: withFrame requires both ownership pins.
|
|
809
|
+
// There is nothing to revoke, and this must not invent a tenant/receipt.
|
|
810
|
+
if (this.installationId() === null &&
|
|
811
|
+
this.ctx.storage.sql
|
|
812
|
+
.exec("SELECT id FROM oyster_tenant WHERE id = 1")
|
|
813
|
+
.toArray().length === 0)
|
|
814
|
+
return;
|
|
621
815
|
this.requireTenant(tenantId);
|
|
622
816
|
if (installationId !== this.installationId())
|
|
623
817
|
throw new AppInstanceError("instance_owner_mismatch");
|
|
@@ -633,7 +827,7 @@ export class OysterInstance extends DurableObject {
|
|
|
633
827
|
"kind" in current ||
|
|
634
828
|
current.status !== "enabled" ||
|
|
635
829
|
current.instanceId !== selected.instanceId ||
|
|
636
|
-
current.authorityVersion !== selected.authorityVersion ||
|
|
830
|
+
current.authority.authorityVersion !== selected.authorityVersion ||
|
|
637
831
|
current.release.releaseId !== selected.releaseId ||
|
|
638
832
|
current.release.artifact.artifactDigest !==
|
|
639
833
|
frame.host.artifact.release.artifact.artifactDigest)
|
|
@@ -710,7 +904,33 @@ export class OysterInstance extends DurableObject {
|
|
|
710
904
|
throw new Error("Release unknown to this Instance");
|
|
711
905
|
return Schema.decodeUnknownSync(Schema.fromJsonString(releaseSchema))(Schema.decodeUnknownSync(releaseRow)(row).release);
|
|
712
906
|
}
|
|
713
|
-
|
|
907
|
+
rememberRelease(release) {
|
|
908
|
+
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));
|
|
909
|
+
}
|
|
910
|
+
/** Reuse lifecycle preflight without changing the active runtime or its budgets. */
|
|
911
|
+
async prepare(state, release, limits, tenant) {
|
|
912
|
+
let bootScope;
|
|
913
|
+
if (tenant !== undefined) {
|
|
914
|
+
bootScope = tenantScope(Schema.decodeUnknownSync(tenantIdSchema)(tenant.tenantId), tenant.origin);
|
|
915
|
+
const stored = this.ctx.storage.sql
|
|
916
|
+
.exec("SELECT tenant_id, origin FROM oyster_tenant WHERE id = 1")
|
|
917
|
+
.toArray()[0];
|
|
918
|
+
if (stored !== undefined) {
|
|
919
|
+
const pin = this.scope();
|
|
920
|
+
if (pin.tenantId !== bootScope.tenantId ||
|
|
921
|
+
pin.origin !== bootScope.origin)
|
|
922
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
923
|
+
const owner = this.installationId();
|
|
924
|
+
if (owner !== null && owner !== state.owner.id)
|
|
925
|
+
throw new AppInstanceError("instance_owner_mismatch");
|
|
926
|
+
}
|
|
927
|
+
else if (this.installationId() !== null) {
|
|
928
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return result(this.createRuntime(guestLimits(limits), release, bootScope).prepare(state));
|
|
932
|
+
}
|
|
933
|
+
/** Activate the exact committed selection under the host's tenant pin. */
|
|
714
934
|
async apply(state, release, limits, tenant) {
|
|
715
935
|
const selected = tenantScope(Schema.decodeUnknownSync(tenantIdSchema)(tenant.tenantId), tenant.origin);
|
|
716
936
|
const checkTenant = () => {
|
|
@@ -725,17 +945,30 @@ export class OysterInstance extends DurableObject {
|
|
|
725
945
|
const pin = this.scope();
|
|
726
946
|
if (pin.tenantId !== selected.tenantId || pin.origin !== selected.origin)
|
|
727
947
|
throw new AppInstanceError("installation_tenant_mismatch");
|
|
948
|
+
const owner = this.installationId();
|
|
949
|
+
if (owner !== null && owner !== state.owner.id)
|
|
950
|
+
throw new AppInstanceError("instance_owner_mismatch");
|
|
728
951
|
};
|
|
952
|
+
const committed = async () => isCommittedInstallation(await this.env.REGISTRY.getByName(selected.durableName("registry")).get(state.owner.id), state, release);
|
|
729
953
|
checkTenant();
|
|
954
|
+
if (!(await committed()))
|
|
955
|
+
return { ok: false, code: "stale_authority" };
|
|
730
956
|
await this.frames.revoke();
|
|
731
957
|
return this.frames.exclusive(async () => {
|
|
732
958
|
checkTenant();
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
959
|
+
if (!(await committed()))
|
|
960
|
+
return { ok: false, code: "stale_authority" };
|
|
961
|
+
// These pins attest the committed owner, not successful activation.
|
|
962
|
+
// Persist before boot so a failed first activation can be retried or
|
|
963
|
+
// revoked/deleted without ever claiming an active lifecycle receipt.
|
|
964
|
+
this.ctx.storage.transactionSync(() => {
|
|
736
965
|
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
|
-
}
|
|
966
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_owner (id, installation_id) VALUES (1, ?) ON CONFLICT(id) DO NOTHING", state.owner.id);
|
|
967
|
+
});
|
|
968
|
+
this.rememberRelease(release);
|
|
969
|
+
const applied = await result(this.runtimeFor(limits).apply(state));
|
|
970
|
+
if (!(await committed()))
|
|
971
|
+
return { ok: false, code: "stale_authority" };
|
|
739
972
|
return applied;
|
|
740
973
|
});
|
|
741
974
|
}
|
|
@@ -791,12 +1024,74 @@ export class OysterInstance extends DurableObject {
|
|
|
791
1024
|
}
|
|
792
1025
|
}
|
|
793
1026
|
/** Host adapters for a standalone Oyster Worker over its own bindings. */
|
|
1027
|
+
function registryBootCapabilities(env, scope) {
|
|
1028
|
+
const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
|
|
1029
|
+
return {
|
|
1030
|
+
issue: async (grant) => {
|
|
1031
|
+
await registry().bootCapability({ kind: "issue", grant });
|
|
1032
|
+
},
|
|
1033
|
+
resolve: async (tokenDigest, now) => {
|
|
1034
|
+
const result = await registry().bootCapability({
|
|
1035
|
+
kind: "resolve",
|
|
1036
|
+
tokenDigest,
|
|
1037
|
+
now,
|
|
1038
|
+
});
|
|
1039
|
+
if (result === undefined)
|
|
1040
|
+
throw new Error("Boot capability lookup returned no result");
|
|
1041
|
+
return result === null ? null : bootCapabilityLeaseSchema.parse(result);
|
|
1042
|
+
},
|
|
1043
|
+
revoke: async (id) => {
|
|
1044
|
+
await registry().bootCapability({ kind: "revoke", id });
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/** Resolve a scoped boot URL through the registry-backed HTTPS asset route. */
|
|
1049
|
+
export function serveOysterBootCapability(request, env, scope) {
|
|
1050
|
+
return serveBootCapability(request, {
|
|
1051
|
+
configuredOrigin: scope.origin,
|
|
1052
|
+
bucket: env.RELEASES,
|
|
1053
|
+
capabilities: registryBootCapabilities(env, scope),
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* The standalone UI boot check: the Worker's own BROWSER binding opens the
|
|
1058
|
+
* pinned `/_boot/` page on the host-selected origin, with a capability in
|
|
1059
|
+
* that tenant's registry. Without a BROWSER binding the UI is not checked.
|
|
1060
|
+
*/
|
|
1061
|
+
function oysterBootUi(env, scope) {
|
|
1062
|
+
const artifacts = r2ArtifactStore(env.RELEASES);
|
|
1063
|
+
const capabilities = registryBootCapabilities(env, scope);
|
|
1064
|
+
const sessions = env.BROWSER === undefined ? null : bindingSessions(env.BROWSER);
|
|
1065
|
+
return (release) => reportBootPage({
|
|
1066
|
+
origin: scope.origin,
|
|
1067
|
+
check: sessions === null ? null : (target) => checkBootPage(sessions, target),
|
|
1068
|
+
readUi: () => readBootUi(artifacts, release),
|
|
1069
|
+
bucket: env.RELEASES,
|
|
1070
|
+
capabilities,
|
|
1071
|
+
now: Date.now,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Create an Oyster host backed by this Worker environment's registry,
|
|
1076
|
+
* artifacts, Instances, and boot-check service.
|
|
1077
|
+
*/
|
|
794
1078
|
export function oysterWorkerHost(env, auth, options = {}) {
|
|
795
1079
|
const scope = options.scope ?? tenantScope(defaultTenant, env.OYSTER_ORIGIN);
|
|
796
1080
|
const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
|
|
797
1081
|
const installations = registryInstallations(env, scope);
|
|
1082
|
+
const artifacts = r2ArtifactStore(env.RELEASES);
|
|
798
1083
|
const originSecret = Schema.decodeUnknownOption(Schema.NonEmptyString)(env.OYSTER_OWNER_KEY);
|
|
1084
|
+
const pin = { tenantId: scope.tenantId, origin: scope.origin };
|
|
1085
|
+
const approvalControl = (actor) => ({
|
|
1086
|
+
list: (input) => registry().approvalList(pin, actor, input),
|
|
1087
|
+
get: (input) => registry().approvalGet(pin, actor, input),
|
|
1088
|
+
answer: (input) => registry().approvalAnswer(pin, actor, input),
|
|
1089
|
+
setAutoApprove: (input) => registry().approvalSetAutoApprove(pin, actor, input),
|
|
1090
|
+
});
|
|
799
1091
|
let host = {
|
|
1092
|
+
approvals: {
|
|
1093
|
+
submit: (call) => registry().approvalSubmit({ tenantId: scope.tenantId, origin: scope.origin }, call),
|
|
1094
|
+
},
|
|
800
1095
|
auth,
|
|
801
1096
|
workbench: {
|
|
802
1097
|
directory: async () => {
|
|
@@ -811,10 +1106,14 @@ export function oysterWorkerHost(env, auth, options = {}) {
|
|
|
811
1106
|
}),
|
|
812
1107
|
},
|
|
813
1108
|
guestLimits: guestLimits(options.guestLimits),
|
|
814
|
-
artifacts
|
|
1109
|
+
artifacts,
|
|
1110
|
+
bootUi: oysterBootUi(env, scope),
|
|
815
1111
|
// File-only hosts may keep workspace files beside their Release artifacts.
|
|
816
|
-
capabilities:
|
|
817
|
-
|
|
1112
|
+
capabilities: [
|
|
1113
|
+
...fileCapabilities(options.workspace?.files ??
|
|
1114
|
+
r2FileSource(env.RELEASES, scope.workspacePrefix)),
|
|
1115
|
+
approvalCapability(approvalControl),
|
|
1116
|
+
],
|
|
818
1117
|
installations,
|
|
819
1118
|
instances: {
|
|
820
1119
|
get: (instanceId, limits) => instanceHandle(env.INSTANCES.getByName(instanceId), scope, guestLimits(limits)),
|
|
@@ -848,6 +1147,9 @@ function ownerKeyRootHost(env, options) {
|
|
|
848
1147
|
/** The non-git standalone root's MCP owner. */
|
|
849
1148
|
export class OysterMcp extends createOysterMcpOwner((env, scope) => ownerKeyRootHost(env, { scope })) {
|
|
850
1149
|
}
|
|
1150
|
+
/** Ordinary Worker execution; REGISTRY receives references and receipts only. */
|
|
1151
|
+
export class OysterInstallationExecutor extends installationWorkerEntrypoint((env, scope) => ownerKeyRootHost(env, { scope })) {
|
|
1152
|
+
}
|
|
851
1153
|
/**
|
|
852
1154
|
* The standalone Worker's `fetch` handler; frontend hosts are cached per env.
|
|
853
1155
|
* MCP transports belong to the tenant Durable Object, independent of that cache.
|
|
@@ -856,10 +1158,20 @@ export function createOysterWorker(options = {}) {
|
|
|
856
1158
|
const limits = guestLimits(options.guestLimits);
|
|
857
1159
|
const apps = new WeakMap();
|
|
858
1160
|
return {
|
|
859
|
-
fetch(request, env) {
|
|
1161
|
+
async fetch(request, env) {
|
|
1162
|
+
const scope = tenantScope(defaultTenant, env.OYSTER_ORIGIN);
|
|
1163
|
+
const refused = await refuseOlderOysterStore(request, env, scope);
|
|
1164
|
+
if (refused !== null)
|
|
1165
|
+
return refused;
|
|
1166
|
+
const boot = await serveOysterBootCapability(request, env, scope);
|
|
1167
|
+
if (boot !== null)
|
|
1168
|
+
return boot;
|
|
860
1169
|
let app = apps.get(env);
|
|
861
1170
|
if (app === undefined) {
|
|
862
|
-
app = createOyster(ownerKeyRootHost(env, {
|
|
1171
|
+
app = createOyster(ownerKeyRootHost(env, {
|
|
1172
|
+
guestLimits: limits,
|
|
1173
|
+
scope,
|
|
1174
|
+
}));
|
|
863
1175
|
apps.set(env, app);
|
|
864
1176
|
}
|
|
865
1177
|
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
|
+
}
|