@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
@@ -4,146 +4,55 @@ import { join } from "node:path";
4
4
  import { setTimeout as sleep } from "node:timers/promises";
5
5
  import * as Schema from "effect/Schema";
6
6
  import { publishSource } from "../pack.js";
7
+ import { appBuildRecipe, readBuildManifest } from "../build/workspace-build.js";
7
8
  import { parseSourceRevision } from "@fungi.computer/puffball";
8
- // Existing closed source recipes, shared by the standalone install command and
9
- // its real-root proof. The canonical Oyster builder owns every compilation, on
10
- // the Oyster's own Computer.
11
- const assets = {
12
- maxManifestBytes: 1_048_576,
13
- maxObjects: 1024,
14
- maxObjectBytes: 16_777_216,
15
- maxTotalBytes: 67_108_864,
16
- };
17
- const facet = { kind: "durable-facet", stateSchema: 1 };
9
+ import { releaseSchema } from "@fungi.computer/puffball/effect";
10
+ import { grantCommandSchema, installationSchema, } from "../apps/installation.js";
11
+ import { appPermissionPageSchema } from "../apps/permissions.js";
12
+ import { installOnOyster, InstallationObservationError, } from "../installation-client.js";
18
13
  export const standaloneAppSet = [
19
14
  {
20
15
  installationId: "tourtois",
16
+ dashboard: true,
21
17
  directory: "apps/tourtois",
22
18
  repository: "tourtois",
23
19
  appId: "app_tourtois",
24
- recipe: {
25
- entry: "apps/tourtois/index.html",
26
- assets,
27
- assetBase: "./",
28
- backend: {
29
- entry: "apps/tourtois/backend/index.ts",
30
- exportName: "Tourtois",
31
- compatibilityDate: "2026-09-04",
32
- runtime: facet,
33
- },
34
- maxOutputBytes: 67_108_864,
35
- },
36
20
  },
37
21
  {
38
22
  installationId: "bonsai",
39
23
  directory: "apps/bonsai",
40
24
  repository: "bonsai",
41
25
  appId: "app_bonsai",
42
- recipe: {
43
- entry: "apps/bonsai/web/index.html",
44
- assets,
45
- assetBase: "./",
46
- maxOutputBytes: 67_108_864,
47
- backend: {
48
- entry: "apps/bonsai/backend/index.ts",
49
- exportName: "BonsaiApp",
50
- compatibilityDate: "2026-09-04",
51
- runtime: facet,
52
- },
53
- },
54
26
  },
55
27
  {
56
28
  installationId: "shed",
57
29
  directory: "apps/shed",
58
30
  repository: "shed",
59
31
  appId: "app_shed",
60
- recipe: {
61
- entry: "apps/shed/index.html",
62
- assets: {
63
- maxManifestBytes: 1_048_576,
64
- maxObjects: 512,
65
- maxObjectBytes: 4_194_304,
66
- maxTotalBytes: 16_777_216,
67
- },
68
- assetBase: "./",
69
- maxOutputBytes: 16_777_216,
70
- backend: {
71
- entry: "apps/shed/backend/index.ts",
72
- exportName: "Shed",
73
- compatibilityDate: "2026-09-25",
74
- runtime: facet,
75
- },
76
- },
77
32
  },
78
33
  {
79
34
  installationId: "inkcap",
80
35
  directory: "apps/inkcap",
81
36
  repository: "inkcap",
82
37
  appId: "app_inkcap",
83
- recipe: {
84
- entry: "apps/inkcap/index.html",
85
- assets,
86
- assetBase: "./",
87
- maxOutputBytes: 16_777_216,
88
- backend: {
89
- entry: "apps/inkcap/backend/index.ts",
90
- exportName: "Inkcap",
91
- compatibilityDate: "2026-09-25",
92
- runtime: facet,
93
- },
94
- },
95
38
  },
96
39
  {
97
40
  installationId: "trellis",
98
41
  directory: "apps/trellis",
99
42
  repository: "trellis",
100
43
  appId: "app_trellis",
101
- recipe: {
102
- entry: "apps/trellis/index.html",
103
- maxOutputBytes: 16_777_216,
104
- backend: {
105
- entry: "apps/trellis/backend/index.ts",
106
- exportName: "Trellis",
107
- compatibilityDate: "2026-09-04",
108
- runtime: facet,
109
- },
110
- },
111
44
  },
112
45
  {
113
46
  installationId: "prints",
114
47
  directory: "apps/prints",
115
48
  repository: "prints",
116
49
  appId: "app_prints",
117
- recipe: {
118
- entry: "apps/prints/index.html",
119
- assets,
120
- assetBase: "./",
121
- maxOutputBytes: 16_777_216,
122
- backend: {
123
- entry: "apps/prints/backend/index.ts",
124
- exportName: "Prints",
125
- compatibilityDate: "2026-09-04",
126
- runtime: facet,
127
- },
128
- },
129
50
  },
130
51
  {
131
52
  installationId: "grove",
132
53
  directory: "apps/grove",
133
54
  repository: "grove",
134
55
  appId: "app_grove",
135
- recipe: {
136
- entry: "apps/grove/index.html",
137
- assets,
138
- assetBase: "./",
139
- maxOutputBytes: 67_108_864,
140
- backend: {
141
- entry: "apps/grove/backend/index.ts",
142
- exportName: "Grove",
143
- compatibilityDate: "2026-09-04",
144
- runtime: facet,
145
- },
146
- },
147
56
  },
148
57
  ];
149
58
  const publishedSchema = Schema.Struct({
@@ -181,23 +90,6 @@ const appBuildWaitMs = 20 * 60_000;
181
90
  export const defaultStarterBuildConcurrency = 2;
182
91
  /** The most builds at once on the 12 GiB Computer. */
183
92
  const maxStarterBuildConcurrency = 3;
184
- /** A recipe rooted at `from` (a checkout directory), rooted at `to` instead. */
185
- function relocateRecipe(recipe, from, to) {
186
- const move = (path) => {
187
- if (!path.startsWith(`${from}/`))
188
- throw new Error(`${path} is outside its App directory ${from}`);
189
- const rest = path.slice(from.length + 1);
190
- return to === "" ? rest : `${to}/${rest}`;
191
- };
192
- const backend = recipe.backend;
193
- if (backend?.entry === undefined)
194
- return { ...recipe, entry: move(recipe.entry) };
195
- return {
196
- ...recipe,
197
- entry: move(recipe.entry),
198
- backend: { ...backend, entry: move(backend.entry) },
199
- };
200
- }
201
93
  const run = (command, args, cwd) => new Promise((resolveRun, reject) => {
202
94
  execFile(command, args, { cwd, encoding: "utf8", maxBuffer: 1_048_576 }, (error, stdout) => {
203
95
  if (error)
@@ -220,7 +112,10 @@ export async function installStandaloneAppSet(options) {
220
112
  concurrency < 1 ||
221
113
  concurrency > maxStarterBuildConcurrency)
222
114
  throw new Error(`Starter builds run 1 to ${String(maxStarterBuildConcurrency)} at a time`);
223
- const authorization = { Authorization: `Bearer ${options.key}` };
115
+ const authorization = {
116
+ Authorization: `Bearer ${options.key}`,
117
+ Accept: "application/json",
118
+ };
224
119
  const owner = async (method, path, body, signal) => {
225
120
  const init = body === undefined
226
121
  ? { method, headers: authorization }
@@ -233,6 +128,59 @@ export async function installStandaloneAppSet(options) {
233
128
  init.signal = signal;
234
129
  return fetch(new URL(path, options.url), init);
235
130
  };
131
+ const approveChosenDashboard = async (installed, generation) => {
132
+ const name = installed.installationId;
133
+ const path = `/api/installations/${encodeURIComponent(name)}/grants`;
134
+ const projectionResponse = await owner("GET", path);
135
+ if (!projectionResponse.ok)
136
+ throw new Error(`${name} approval read failed (${String(projectionResponse.status)})`);
137
+ const projection = appPermissionPageSchema.parse(await projectionResponse.json());
138
+ const { permissions } = projection;
139
+ const digest = installed.release.artifact.artifactDigest;
140
+ if (projection.storeGeneration !== generation ||
141
+ projection.installationId !== name ||
142
+ projection.artifactDigest !== digest ||
143
+ permissions.declaredOperations.artifactDigest !== digest ||
144
+ permissions.authority.authorityVersion !==
145
+ installed.authority.authorityVersion ||
146
+ permissions.authority.declaredOperationsDigest !==
147
+ installed.authority.declaredOperationsDigest ||
148
+ permissions.declaredOperations.digest !==
149
+ installed.authority.declaredOperationsDigest)
150
+ throw new Error(`${name} approval snapshot changed`);
151
+ const shell = permissions.declaredOperations.capabilities.find((row) => row.capability === "team.workbenchHost");
152
+ if (shell === undefined)
153
+ return;
154
+ const command = grantCommandSchema.parse({
155
+ kind: "change-grants",
156
+ changes: [
157
+ {
158
+ capability: shell.capability,
159
+ grant: { operations: shell.operations, resources: "tenant-scope" },
160
+ },
161
+ ],
162
+ });
163
+ const saved = await owner("PUT", path, Schema.decodeUnknownSync(Schema.Json)({
164
+ storeGeneration: projection.storeGeneration,
165
+ authorityVersion: installed.authority.authorityVersion,
166
+ artifactDigest: digest,
167
+ command,
168
+ }));
169
+ if (!saved.ok)
170
+ throw new Error(`${name} approval failed (${String(saved.status)})`);
171
+ const receipt = installationSchema.parse(await saved.json());
172
+ if (receipt.installationId !== name ||
173
+ receipt.instanceId !== installed.instanceId ||
174
+ receipt.release.artifact.artifactDigest !== digest ||
175
+ receipt.authority.authorityVersion !==
176
+ installed.authority.authorityVersion + 1 ||
177
+ !receipt.authority.approvedGrants.some((grant) => grant.capability === "team.workbenchHost" &&
178
+ grant.resources === "tenant-scope" &&
179
+ shell.operations.every((operation) => grant.operations.includes(operation))))
180
+ throw new Error(`${name} approval incomplete`);
181
+ };
182
+ const idle = options.idle ??
183
+ ((ms, signal) => sleep(ms, undefined, { signal }));
236
184
  /** Start one build and wait for its terminal outcome within the build
237
185
  * budget; the budget cancels every request and body read it covers. */
238
186
  const buildOnComputer = async (name, appId, seedRevision) => {
@@ -246,11 +194,13 @@ export async function installStandaloneAppSet(options) {
246
194
  jobId = build.jobId;
247
195
  options.report(`Building ${name} on the Computer (${build.jobId})`);
248
196
  while (build.status === "running") {
249
- await sleep(options.pollMs ?? 5_000, undefined, { signal: budget });
250
197
  const read = await owner("GET", `/api/builds/${encodeURIComponent(build.jobId)}`, undefined, budget);
251
198
  if (!read.ok)
252
199
  throw new Error(`${name} build is missing (${String(read.status)})`);
253
200
  build = Schema.decodeUnknownSync(buildSchema)(await read.json());
201
+ // Read first: a build already finished costs no idle wait.
202
+ if (build.status === "running")
203
+ await idle(options.pollMs ?? 5_000, budget);
254
204
  }
255
205
  return build;
256
206
  }
@@ -276,13 +226,19 @@ export async function installStandaloneAppSet(options) {
276
226
  * own repository; `release` removes a clone once its template is seeded. */
277
227
  const locate = async (app) => {
278
228
  const source = options.source;
279
- if (source.kind === "checkout")
229
+ if (source.kind === "checkout") {
230
+ const sourceRevision = parseSourceRevision(source.revision);
231
+ const metadata = await run("git", [
232
+ "show",
233
+ sourceRevision.slice(4) + ":" + app.directory + "/package.json",
234
+ ], source.directory);
280
235
  return {
281
236
  repositoryPath: source.directory,
282
- sourceRevision: parseSourceRevision(source.revision),
283
- recipe: app.recipe,
237
+ sourceRevision,
238
+ recipe: appBuildRecipe(readBuildManifest(metadata), app.directory),
284
239
  release: () => Promise.resolve(),
285
240
  };
241
+ }
286
242
  const clone = await mkdtemp(join(source.scratch, `${app.repository}-`));
287
243
  try {
288
244
  await run("git", [
@@ -298,7 +254,7 @@ export async function installStandaloneAppSet(options) {
298
254
  return {
299
255
  repositoryPath: clone,
300
256
  sourceRevision: parseSourceRevision(`git:${head}`),
301
- recipe: relocateRecipe(app.recipe, app.directory, ""),
257
+ recipe: appBuildRecipe(readBuildManifest(await run("git", ["show", head + ":package.json"], clone)), ""),
302
258
  release: () => rm(clone, { recursive: true, force: true }),
303
259
  };
304
260
  }
@@ -307,9 +263,22 @@ export async function installStandaloneAppSet(options) {
307
263
  throw cause;
308
264
  }
309
265
  };
310
- // Seed every template first: a git push each, quick beside a build.
311
- const seeded = [];
312
- for (const app of standaloneAppSet) {
266
+ const installed = new Map();
267
+ /** Set by the first failure, so no further seed or build starts. */
268
+ const stop = { requested: false };
269
+ /** Every failure, in the order it happened; the first is the one thrown. */
270
+ const failures = [];
271
+ const seededQueue = [];
272
+ const waiting = [];
273
+ const seeding = { finished: false };
274
+ const takeSeeded = () => new Promise((resolveTake) => {
275
+ const next = seededQueue.shift();
276
+ if (next !== undefined || seeding.finished)
277
+ resolveTake(next);
278
+ else
279
+ waiting.push(resolveTake);
280
+ });
281
+ const seedApp = async (app) => {
313
282
  const name = app.installationId;
314
283
  const fetchStarted = performance.now();
315
284
  const located = await locate(app);
@@ -321,7 +290,7 @@ export async function installStandaloneAppSet(options) {
321
290
  const { kind, receipt } = Schema.decodeUnknownSync(publishedSchema)(await published.json());
322
291
  options.report(`${name} source ${kind} at ${receipt.seedRevision}`);
323
292
  const ready = performance.now();
324
- seeded.push({
293
+ return {
325
294
  app,
326
295
  kind,
327
296
  seedRevision: receipt.seedRevision,
@@ -329,45 +298,99 @@ export async function installStandaloneAppSet(options) {
329
298
  fetch: publishStarted - fetchStarted,
330
299
  publish: ready - publishStarted,
331
300
  ready,
332
- });
301
+ };
333
302
  }
334
303
  finally {
335
304
  await located.release();
336
305
  }
337
- }
338
- // Then build `concurrency` at a time, in set order, installing each as its
339
- // build succeeds. The first failure rejects the install and starts no
340
- // further build; builds already running finish and install.
341
- const installed = new Map();
342
- const pending = [...seeded];
343
- /** Set by the first failure, so no further build starts. */
344
- const stop = { requested: false };
306
+ };
307
+ const seedAll = async () => {
308
+ try {
309
+ for (const app of standaloneAppSet) {
310
+ if (stop.requested)
311
+ break;
312
+ const seeded = await seedApp(app);
313
+ const waiter = waiting.shift();
314
+ if (waiter === undefined)
315
+ seededQueue.push(seeded);
316
+ else
317
+ waiter(seeded);
318
+ }
319
+ }
320
+ catch (cause) {
321
+ stop.requested = true;
322
+ failures.push(cause);
323
+ throw cause;
324
+ }
325
+ finally {
326
+ seeding.finished = true;
327
+ for (const waiter of waiting.splice(0))
328
+ waiter(undefined);
329
+ }
330
+ };
331
+ // `concurrency` builds at once, each installing as its build succeeds.
345
332
  const slot = async () => {
346
- for (let next = stop.requested ? undefined : pending.shift(); next !== undefined; next = stop.requested ? undefined : pending.shift()) {
333
+ for (let next = await takeSeeded(); next !== undefined && !stop.requested; next = await takeSeeded()) {
347
334
  const name = next.app.installationId;
348
335
  try {
349
336
  const buildStarted = performance.now();
350
337
  const build = await buildOnComputer(name, next.app.appId, next.seedRevision);
351
338
  if (build.status !== "succeeded" || build.release === null)
352
339
  throw new Error(`${name} build ${build.status}: ${build.detail ?? "no Release"}`);
340
+ const release = Schema.decodeUnknownSync(releaseSchema)(build.release, {
341
+ onExcessProperty: "error",
342
+ });
343
+ if (release.appId !== next.app.appId)
344
+ throw new Error(`${name} build belongs to another App`);
353
345
  const installStarted = performance.now();
354
- const installation = await owner("POST", "/api/installations", {
355
- release: build.release,
346
+ let installation = {
347
+ url: options.url,
348
+ key: options.key,
349
+ release,
356
350
  installationId: name,
357
- });
358
- if (!installation.ok)
359
- throw new Error(`${name} installation failed (${String(installation.status)})`);
360
- options.report(`${installation.status === 201 ? "Installed" : "Replaced"} ${name} from its build ${build.jobId}`);
351
+ signal: AbortSignal.timeout(options.waitMs ?? appBuildWaitMs),
352
+ idle,
353
+ };
354
+ if (options.pollMs !== undefined)
355
+ installation = { ...installation, pollMs: options.pollMs };
356
+ const operation = await installOnOyster(installation);
357
+ if (operation.state === "unconfirmed")
358
+ throw new InstallationObservationError(operation.requestId);
359
+ if (operation.state === "refused")
360
+ throw new Error(`${name} installation refused (${operation.code})`);
361
+ const record = operation.installation;
362
+ if (record === null)
363
+ throw new Error(`${name} completed installation version is unavailable`);
364
+ if (record.appId !== release.appId ||
365
+ record.release.releaseId !== release.releaseId ||
366
+ record.release.artifact.artifactDigest !==
367
+ release.artifact.artifactDigest)
368
+ throw new Error(`${name} installation does not match its build`);
369
+ // Only the owner can attest exclusive fresh creation. A replacement or
370
+ // tombstone remint never replays a default grant.
371
+ if (operation.creation === "fresh") {
372
+ if (record.status !== "enabled")
373
+ throw new Error(`${name} fresh installation is not enabled`);
374
+ if (next.app.dashboard === true)
375
+ await approveChosenDashboard(record, operation.reference.generation);
376
+ }
377
+ options.report(`${operation.creation === "replacement" ? "Replaced" : "Installed"} ${name} from its build ${build.jobId}`);
378
+ // Report-only UI check: never blocks, but the owner sees it here.
379
+ const uiCheck = operation.uiCheck;
380
+ if (uiCheck?.kind === "failed")
381
+ options.report(`Warning: ${name} is installed, but its UI check failed (${uiCheck.code}${uiCheck.diagnostics === undefined ? "" : `: ${uiCheck.diagnostics.failedPredicate}`}).`);
382
+ else if (uiCheck?.kind === "not_checked")
383
+ options.report(`Warning: ${name} is installed, but its UI was not checked (${uiCheck.code}).`);
361
384
  const phases = build.detail === null
362
385
  ? undefined
363
386
  : Schema.decodeUnknownOption(computerPhasesSchema)(build.detail);
364
387
  installed.set(name, {
365
- installationId: name,
388
+ installationId: record.installationId,
366
389
  published: next.kind,
367
390
  seedRevision: next.seedRevision,
368
391
  sourceRevision: next.sourceRevision,
369
392
  jobId: build.jobId,
370
- installed: installation.status === 201 ? "installed" : "replaced",
393
+ installed: operation.creation === "replacement" ? "replaced" : "installed",
371
394
  release: build.release,
372
395
  timings: {
373
396
  fetch: next.fetch,
@@ -383,11 +406,19 @@ export async function installStandaloneAppSet(options) {
383
406
  }
384
407
  catch (cause) {
385
408
  stop.requested = true;
409
+ failures.push(cause);
386
410
  throw cause;
387
411
  }
388
412
  }
389
413
  };
390
- await Promise.all(Array.from({ length: concurrency }, slot));
414
+ // Wait for every seed and build to settle before reporting: nothing of this
415
+ // call may still run, or install, after it rejects.
416
+ await Promise.allSettled([
417
+ seedAll(),
418
+ ...Array.from({ length: concurrency }, slot),
419
+ ]);
420
+ if (failures.length > 0)
421
+ throw failures[0];
391
422
  return standaloneAppSet.flatMap((app) => {
392
423
  const result = installed.get(app.installationId);
393
424
  return result === undefined ? [] : [result];
@@ -0,0 +1,11 @@
1
+ import type { CapabilitySource } from "@fungi.computer/mycelium";
2
+ import { type BrowserBinding } from "../browser/index.js";
3
+ /**
4
+ * The agent's `cloudBrowser` tool on a standalone Oyster, or an empty source
5
+ * when the deployment declares no BROWSER binding. Driven sessions only: no
6
+ * Quick Reads, no Computer save, no member handoff on this host.
7
+ */
8
+ export declare function standaloneBrowserSource(binding: BrowserBinding | undefined, identity: () => Readonly<{
9
+ teamId: string;
10
+ agentId: string;
11
+ }>): CapabilitySource;
@@ -0,0 +1,72 @@
1
+ import { bindingSessions, browserCapabilitySource, createBrowser, } from "../browser/index.js";
2
+ import { causeClass } from "../cause-class.js";
3
+ /**
4
+ * A standalone Oyster's `BrowserHost`: its own Workers BROWSER binding, no
5
+ * Quick Reads (the binding has none), and the owner-funded meter below.
6
+ */
7
+ /**
8
+ * A standalone Oyster has no credit ledger: its owner's Cloudflare account
9
+ * pays Browser Rendering directly. This is that host's explicit metering
10
+ * decision, not a missing meter; the Hub supplies its Billing owner instead.
11
+ */
12
+ const ownerFundedMeter = {
13
+ hold: () => Promise.resolve(),
14
+ start: () => Promise.resolve(),
15
+ settle: () => Promise.resolve(),
16
+ release: () => Promise.resolve(),
17
+ };
18
+ /** One Browser per binding per isolate, so a lease survives between calls. */
19
+ const browsers = new WeakMap();
20
+ function standaloneBrowser(binding) {
21
+ let browser = browsers.get(binding);
22
+ if (browser === undefined) {
23
+ browser = createBrowser({
24
+ sessions: bindingSessions(binding),
25
+ quickReads: null,
26
+ meter: () => ownerFundedMeter,
27
+ report: (event) => {
28
+ // Code-only: never a URL, page text or provider body.
29
+ console.warn("Oyster browser", {
30
+ kind: event.kind,
31
+ executionId: event.executionId,
32
+ cause: causeClass(event.cause),
33
+ });
34
+ },
35
+ }, {
36
+ maxConcurrentPerTeam: 2,
37
+ maxLifetimeMs: 30 * 60_000,
38
+ idleTimeoutMs: 5 * 60_000,
39
+ });
40
+ browsers.set(binding, browser);
41
+ }
42
+ return browser;
43
+ }
44
+ /**
45
+ * The agent's `cloudBrowser` tool on a standalone Oyster, or an empty source
46
+ * when the deployment declares no BROWSER binding. Driven sessions only: no
47
+ * Quick Reads, no Computer save, no member handoff on this host.
48
+ */
49
+ export function standaloneBrowserSource(binding, identity) {
50
+ if (binding === undefined)
51
+ return {
52
+ id: "cloud-browser",
53
+ acquire: () => Promise.resolve({
54
+ snapshot: { version: "0", modules: [] },
55
+ release: () => Promise.resolve(),
56
+ }),
57
+ };
58
+ const browser = standaloneBrowser(binding);
59
+ return browserCapabilitySource({
60
+ identity,
61
+ browser: {
62
+ call: (owner, op, signal) => browser.call(owner, op, signal),
63
+ handoff: (owner, leaseId) => {
64
+ browser.handoff(owner, leaseId);
65
+ return Promise.resolve();
66
+ },
67
+ },
68
+ quickReads: false,
69
+ saveFile: null,
70
+ requestHandoff: null,
71
+ });
72
+ }
@@ -2,17 +2,26 @@
2
2
  * Owner routes for App builds on the standalone's Computer. They run after
3
3
  * the host's owner guard; the build owner is the registry.
4
4
  *
5
- * - `PUT /api/apps/:appId/build-source` records an installation repository
6
- * and recipe an App builds from (the recipe is normalized and pinned here).
7
- * Publishing an App's source (`PUT /api/apps/:appId/source`) instead makes
8
- * its template the build source, buildable at the returned seed revision.
5
+ * Repository Apps register through APPS.register from their committed root
6
+ * metadata. Publishing an App's source (PUT /api/apps/:appId/source) instead
7
+ * makes its template the build source at the returned seed revision.
8
+ *
9
9
  * - `POST /api/builds` starts a build of that source at one exact commit.
10
10
  * - `GET /api/builds/:jobId` reads it; a finished build carries its Release,
11
11
  * which the owner installs or selects through the installation routes.
12
12
  */
13
13
  import { Hono } from "hono";
14
+ import { type OysterBuildStartReason } from "../app-builds.js";
14
15
  import { type OysterEnv } from "../host.js";
15
16
  import type { StandaloneRegistry } from "./registry.js";
17
+ /** Both HTTP and APPS expose the same closed preflight reason. */
18
+ export declare function buildStartFailureReply(reason: OysterBuildStartReason | "unavailable"): {
19
+ status: number;
20
+ body: {
21
+ code: "unavailable" | "invalid_revision" | "no_source" | "not_template_seed" | "no_backend";
22
+ error: string;
23
+ };
24
+ };
16
25
  /** The build routes over the tenant registry that owns builds, resolved per
17
26
  * request so mounting the routes reaches no registry. */
18
27
  export declare function standaloneBuildRoutes(registryFor: () => DurableObjectStub<StandaloneRegistry>): Hono<OysterEnv>;