@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.
Files changed (291) hide show
  1. package/README.md +396 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +213 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/older-oyster-store-refusal.d.ts +4 -0
  130. package/dist/older-oyster-store-refusal.js +18 -0
  131. package/dist/older-oyster-store.d.ts +21 -0
  132. package/dist/older-oyster-store.js +43 -0
  133. package/dist/pack.d.ts +14 -3
  134. package/dist/pack.js +44 -19
  135. package/dist/page/app-access.d.ts +7 -0
  136. package/dist/page/app-access.js +75 -0
  137. package/dist/page/boot-host.js +57 -0
  138. package/dist/page/capabilities.d.ts +3 -3
  139. package/dist/page/capabilities.js +16 -12
  140. package/dist/page/computer-handles.d.ts +3 -3
  141. package/dist/page/computer-target.d.ts +3 -0
  142. package/dist/page/computer-target.js +9 -0
  143. package/dist/page/mount.d.ts +23 -3
  144. package/dist/page/mount.js +152 -16
  145. package/dist/page-assets.d.ts +22 -0
  146. package/dist/page-assets.js +28 -0
  147. package/dist/page-script.d.ts +3 -0
  148. package/dist/page-script.js +5 -4
  149. package/dist/release.d.ts +13 -6
  150. package/dist/release.js +10 -7
  151. package/dist/root-data.d.ts +16 -6
  152. package/dist/root-data.js +2 -2
  153. package/dist/source/index.d.ts +1 -1
  154. package/dist/source/team-source.d.ts +20 -8
  155. package/dist/source/team-source.js +39 -10
  156. package/dist/source/template-seed.d.ts +2 -2
  157. package/dist/standalone/agent-mail.d.ts +24 -0
  158. package/dist/standalone/agent-mail.js +62 -0
  159. package/dist/standalone/agent-store.d.ts +9 -0
  160. package/dist/standalone/agent-store.js +30 -0
  161. package/dist/standalone/agents.d.ts +68 -9
  162. package/dist/standalone/agents.js +428 -56
  163. package/dist/standalone/app-builds-binding.d.ts +20 -2
  164. package/dist/standalone/app-builds-binding.js +147 -23
  165. package/dist/standalone/app-set.d.ts +5 -2
  166. package/dist/standalone/app-set.js +186 -144
  167. package/dist/standalone/browser.d.ts +11 -0
  168. package/dist/standalone/browser.js +72 -0
  169. package/dist/standalone/builds.d.ts +13 -4
  170. package/dist/standalone/builds.js +28 -44
  171. package/dist/standalone/computer-egress.d.ts +2 -0
  172. package/dist/standalone/computer-egress.js +13 -4
  173. package/dist/standalone/computer-fleet.d.ts +43 -0
  174. package/dist/standalone/computer-fleet.js +200 -0
  175. package/dist/standalone/computer.d.ts +44 -9
  176. package/dist/standalone/computer.js +129 -22
  177. package/dist/standalone/install-apps-cli.js +43 -6
  178. package/dist/standalone/instance.d.ts +2 -2
  179. package/dist/standalone/instance.js +8 -11
  180. package/dist/standalone/machine-link.d.ts +354 -0
  181. package/dist/standalone/machine-link.js +368 -0
  182. package/dist/standalone/media.d.ts +2 -2
  183. package/dist/standalone/media.js +19 -16
  184. package/dist/standalone/preview.d.ts +2 -2
  185. package/dist/standalone/preview.js +37 -10
  186. package/dist/standalone/registry.d.ts +151 -9
  187. package/dist/standalone/registry.js +305 -8
  188. package/dist/standalone/worker.d.ts +19 -5
  189. package/dist/standalone/worker.js +90 -54
  190. package/dist/standalone/workflow-binding.d.ts +18 -22
  191. package/dist/standalone/workflow-binding.js +18 -18
  192. package/dist/standalone/workflow-control.d.ts +4 -0
  193. package/dist/standalone/workflow-control.js +40 -0
  194. package/dist/standalone/workflow-events.d.ts +2 -2
  195. package/dist/standalone/workflow-events.js +9 -10
  196. package/dist/standalone/workflow-git-capture.js +4 -3
  197. package/dist/standalone/workflow-host.d.ts +162 -161
  198. package/dist/standalone/workflow-host.js +101 -21
  199. package/dist/standalone/workflow-node-capture.js +11 -4
  200. package/dist/standalone/workflow-schedules.d.ts +5 -2
  201. package/dist/standalone/workflow-schedules.js +9 -10
  202. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  203. package/dist/standalone/workflow-trigger-host.js +2 -2
  204. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  205. package/dist/standalone/workflow-webhooks.js +39 -0
  206. package/dist/standalone/workflows.d.ts +140 -43
  207. package/dist/standalone/workflows.js +198 -39
  208. package/dist/storage-frame.d.ts +6 -6
  209. package/dist/testing/capabilities.js +14 -3
  210. package/dist/testing/installation.js +26 -2
  211. package/dist/testing/worker.js +1 -1
  212. package/dist/testing/workers-runtime.d.ts +8 -2
  213. package/dist/testing/workers-runtime.js +20 -2
  214. package/dist/webhook-abuse.d.ts +25 -0
  215. package/dist/webhook-abuse.js +54 -0
  216. package/dist/webhook-custody.d.ts +144 -0
  217. package/dist/webhook-custody.js +222 -0
  218. package/dist/webhook-ingress.d.ts +36 -0
  219. package/dist/webhook-ingress.js +146 -0
  220. package/dist/webhooks.d.ts +111 -0
  221. package/dist/webhooks.js +189 -0
  222. package/dist/workers-git.d.ts +12 -17
  223. package/dist/workers-git.js +64 -52
  224. package/dist/workers.d.ts +226 -13
  225. package/dist/workers.js +363 -51
  226. package/dist/workflow-agent-authority.d.ts +8 -0
  227. package/dist/workflow-agent-authority.js +42 -0
  228. package/dist/workflows.d.ts +66 -0
  229. package/dist/workflows.js +97 -0
  230. package/docs/api/README.md +63 -37
  231. package/docs/api/agents.md +17 -0
  232. package/docs/api/app-storage.md +1 -1
  233. package/docs/api/apps.md +1394 -543
  234. package/docs/api/browser/boot.md +132 -0
  235. package/docs/api/browser.md +1440 -0
  236. package/docs/api/build-contract.md +38 -25
  237. package/docs/api/build.md +263 -84
  238. package/docs/api/cloudflare.md +93 -7
  239. package/docs/api/git.md +3 -13
  240. package/docs/api/host.md +944 -118
  241. package/docs/api/index.md +320 -18
  242. package/docs/api/keys.md +24 -1
  243. package/docs/api/mcp.md +59 -12
  244. package/docs/api/modules.md +33 -40
  245. package/docs/api/pack.md +57 -8
  246. package/docs/api/page/mount.md +92 -4
  247. package/docs/api/workers-git.md +103 -24
  248. package/docs/api/workers.md +1594 -244
  249. package/docs/site/agent-quickstart.mdx +28 -14
  250. package/examples/standalone/worker.ts +3 -1
  251. package/examples/standalone/wrangler.jsonc +2 -0
  252. package/package.json +73 -71
  253. package/dist/installation-consent.d.ts +0 -81
  254. package/dist/installation-consent.js +0 -84
  255. package/dist/shell/app-directory.d.ts +0 -542
  256. package/dist/shell/app-directory.js +0 -196
  257. package/dist/shell/browser-actions.d.ts +0 -7
  258. package/dist/shell/browser-actions.js +0 -56
  259. package/dist/shell/command-host.d.ts +0 -112
  260. package/dist/shell/command-host.js +0 -502
  261. package/dist/shell/commands.d.ts +0 -25
  262. package/dist/shell/commands.js +0 -78
  263. package/dist/shell/index.d.ts +0 -8
  264. package/dist/shell/index.js +0 -8
  265. package/dist/shell/keys.d.ts +0 -41
  266. package/dist/shell/keys.js +0 -134
  267. package/dist/shell/shell-module.d.ts +0 -55
  268. package/dist/shell/shell-module.js +0 -86
  269. package/dist/shell/window-manager.d.ts +0 -128
  270. package/dist/shell/window-manager.js +0 -730
  271. package/dist/shell/workbench-contract.d.ts +0 -68
  272. package/dist/shell/workbench-directory.d.ts +0 -332
  273. package/dist/shell/workbench-directory.js +0 -35
  274. package/dist/shell/workbench-host-types.d.ts +0 -96
  275. package/dist/shell/workbench-host-types.js +0 -1
  276. package/dist/shell/workbench-host.d.ts +0 -34
  277. package/dist/shell/workbench-host.js +0 -352
  278. package/dist/shell/workbench-model.d.ts +0 -194
  279. package/dist/shell/workbench-model.js +0 -53
  280. package/dist/shell/workbench.d.ts +0 -4
  281. package/dist/shell/workbench.js +0 -2
  282. package/docs/api/shell/app-directory.md +0 -599
  283. package/docs/api/shell/command-host.md +0 -224
  284. package/docs/api/shell/commands.md +0 -197
  285. package/docs/api/shell/keys.md +0 -183
  286. package/docs/api/shell/shell-module.md +0 -204
  287. package/docs/api/shell/window-manager.md +0 -267
  288. package/docs/api/shell/workbench-directory.md +0 -119
  289. package/docs/api/shell/workbench.md +0 -205
  290. package/docs/api/shell.md +0 -1915
  291. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
package/dist/workers.js CHANGED
@@ -1,5 +1,20 @@
1
- import { installationSchema } from "./apps/installation.js";
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, } from "./release.js";
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 !== installation.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
- /** Create or replace one installation record. */
237
- async put(installation, tenantId = defaultTenant) {
238
- await this.writeInstallation(installation, tenantId);
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 (expectedAuthorityVersion !== undefined &&
255
- previous?.authorityVersion !== expectedAuthorityVersion)
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 !== installation.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 (expectedAuthorityVersion !== undefined &&
269
- this.get(installation.installationId)?.authorityVersion !==
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 (expectedAuthorityVersion !== undefined &&
277
- current?.authorityVersion !== expectedAuthorityVersion)
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
- /** Remember `release`, then apply the owner state under the host's tenant pin. */
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
- 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));
734
- const applied = await result(this.runtimeFor(limits).apply(state));
735
- if (applied.ok) {
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 UPDATE SET installation_id = excluded.installation_id", state.owner.id);
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: r2ArtifactStore(env.RELEASES),
1109
+ artifacts,
1110
+ bootUi: oysterBootUi(env, scope),
815
1111
  // File-only hosts may keep workspace files beside their Release artifacts.
816
- capabilities: fileCapabilities(options.workspace?.files ??
817
- r2FileSource(env.RELEASES, scope.workspacePrefix)),
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, { guestLimits: limits }));
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
+ }