@fungi.computer/oyster 0.2.0 → 0.3.0

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