@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
@@ -2,150 +2,66 @@ import { execFile } from "node:child_process";
2
2
  import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { setTimeout as sleep } from "node:timers/promises";
5
+ import * as Option from "effect/Option";
5
6
  import * as Schema from "effect/Schema";
7
+ import { olderOysterStoreCode } from "../older-oyster-store.js";
6
8
  import { publishSource } from "../pack.js";
9
+ import { appBuildRecipe, readBuildManifest } from "../build/workspace-build.js";
7
10
  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 };
11
+ import { releaseSchema } from "@fungi.computer/puffball/effect";
12
+ import { grantCommandSchema, installationSchema, } from "../apps/installation.js";
13
+ import { appPermissionPageSchema } from "../apps/permissions.js";
14
+ import { installOnOyster, InstallationObservationError, } from "../installation-client.js";
18
15
  export const standaloneAppSet = [
19
16
  {
20
17
  installationId: "tourtois",
18
+ dashboard: true,
21
19
  directory: "apps/tourtois",
22
20
  repository: "tourtois",
23
21
  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
22
  },
37
23
  {
38
24
  installationId: "bonsai",
39
25
  directory: "apps/bonsai",
40
26
  repository: "bonsai",
41
27
  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
28
  },
55
29
  {
56
30
  installationId: "shed",
57
31
  directory: "apps/shed",
58
32
  repository: "shed",
59
33
  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
34
  },
78
35
  {
79
36
  installationId: "inkcap",
80
37
  directory: "apps/inkcap",
81
38
  repository: "inkcap",
82
39
  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
40
  },
96
41
  {
97
42
  installationId: "trellis",
98
43
  directory: "apps/trellis",
99
44
  repository: "trellis",
100
45
  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
46
  },
112
47
  {
113
48
  installationId: "prints",
114
49
  directory: "apps/prints",
115
50
  repository: "prints",
116
51
  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
52
  },
130
53
  {
131
54
  installationId: "grove",
132
55
  directory: "apps/grove",
133
56
  repository: "grove",
134
57
  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
58
  },
148
59
  ];
60
+ /** The host's refusal of a store written by Oyster 0.2 or earlier. */
61
+ const olderOysterStoreRefusal = Schema.Struct({
62
+ error: Schema.Literal(olderOysterStoreCode),
63
+ message: Schema.String,
64
+ });
149
65
  const publishedSchema = Schema.Struct({
150
66
  kind: Schema.Literals(["recorded", "existing"]),
151
67
  receipt: Schema.Struct({
@@ -181,23 +97,6 @@ const appBuildWaitMs = 20 * 60_000;
181
97
  export const defaultStarterBuildConcurrency = 2;
182
98
  /** The most builds at once on the 12 GiB Computer. */
183
99
  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
100
  const run = (command, args, cwd) => new Promise((resolveRun, reject) => {
202
101
  execFile(command, args, { cwd, encoding: "utf8", maxBuffer: 1_048_576 }, (error, stdout) => {
203
102
  if (error)
@@ -220,7 +119,10 @@ export async function installStandaloneAppSet(options) {
220
119
  concurrency < 1 ||
221
120
  concurrency > maxStarterBuildConcurrency)
222
121
  throw new Error(`Starter builds run 1 to ${String(maxStarterBuildConcurrency)} at a time`);
223
- const authorization = { Authorization: `Bearer ${options.key}` };
122
+ const authorization = {
123
+ Authorization: `Bearer ${options.key}`,
124
+ Accept: "application/json",
125
+ };
224
126
  const owner = async (method, path, body, signal) => {
225
127
  const init = body === undefined
226
128
  ? { method, headers: authorization }
@@ -233,6 +135,59 @@ export async function installStandaloneAppSet(options) {
233
135
  init.signal = signal;
234
136
  return fetch(new URL(path, options.url), init);
235
137
  };
138
+ const approveChosenDashboard = async (installed, generation) => {
139
+ const name = installed.installationId;
140
+ const path = `/api/installations/${encodeURIComponent(name)}/grants`;
141
+ const projectionResponse = await owner("GET", path);
142
+ if (!projectionResponse.ok)
143
+ throw new Error(`${name} approval read failed (${String(projectionResponse.status)})`);
144
+ const projection = appPermissionPageSchema.parse(await projectionResponse.json());
145
+ const { permissions } = projection;
146
+ const digest = installed.release.artifact.artifactDigest;
147
+ if (projection.storeGeneration !== generation ||
148
+ projection.installationId !== name ||
149
+ projection.artifactDigest !== digest ||
150
+ permissions.declaredOperations.artifactDigest !== digest ||
151
+ permissions.authority.authorityVersion !==
152
+ installed.authority.authorityVersion ||
153
+ permissions.authority.declaredOperationsDigest !==
154
+ installed.authority.declaredOperationsDigest ||
155
+ permissions.declaredOperations.digest !==
156
+ installed.authority.declaredOperationsDigest)
157
+ throw new Error(`${name} approval snapshot changed`);
158
+ const shell = permissions.declaredOperations.capabilities.find((row) => row.capability === "team.workbenchHost");
159
+ if (shell === undefined)
160
+ return;
161
+ const command = grantCommandSchema.parse({
162
+ kind: "change-grants",
163
+ changes: [
164
+ {
165
+ capability: shell.capability,
166
+ grant: { operations: shell.operations, resources: "tenant-scope" },
167
+ },
168
+ ],
169
+ });
170
+ const saved = await owner("PUT", path, Schema.decodeUnknownSync(Schema.Json)({
171
+ storeGeneration: projection.storeGeneration,
172
+ authorityVersion: installed.authority.authorityVersion,
173
+ artifactDigest: digest,
174
+ command,
175
+ }));
176
+ if (!saved.ok)
177
+ throw new Error(`${name} approval failed (${String(saved.status)})`);
178
+ const receipt = installationSchema.parse(await saved.json());
179
+ if (receipt.installationId !== name ||
180
+ receipt.instanceId !== installed.instanceId ||
181
+ receipt.release.artifact.artifactDigest !== digest ||
182
+ receipt.authority.authorityVersion !==
183
+ installed.authority.authorityVersion + 1 ||
184
+ !receipt.authority.approvedGrants.some((grant) => grant.capability === "team.workbenchHost" &&
185
+ grant.resources === "tenant-scope" &&
186
+ shell.operations.every((operation) => grant.operations.includes(operation))))
187
+ throw new Error(`${name} approval incomplete`);
188
+ };
189
+ const idle = options.idle ??
190
+ ((ms, signal) => sleep(ms, undefined, { signal }));
236
191
  /** Start one build and wait for its terminal outcome within the build
237
192
  * budget; the budget cancels every request and body read it covers. */
238
193
  const buildOnComputer = async (name, appId, seedRevision) => {
@@ -246,11 +201,13 @@ export async function installStandaloneAppSet(options) {
246
201
  jobId = build.jobId;
247
202
  options.report(`Building ${name} on the Computer (${build.jobId})`);
248
203
  while (build.status === "running") {
249
- await sleep(options.pollMs ?? 5_000, undefined, { signal: budget });
250
204
  const read = await owner("GET", `/api/builds/${encodeURIComponent(build.jobId)}`, undefined, budget);
251
205
  if (!read.ok)
252
206
  throw new Error(`${name} build is missing (${String(read.status)})`);
253
207
  build = Schema.decodeUnknownSync(buildSchema)(await read.json());
208
+ // Read first: a build already finished costs no idle wait.
209
+ if (build.status === "running")
210
+ await idle(options.pollMs ?? 5_000, budget);
254
211
  }
255
212
  return build;
256
213
  }
@@ -276,13 +233,19 @@ export async function installStandaloneAppSet(options) {
276
233
  * own repository; `release` removes a clone once its template is seeded. */
277
234
  const locate = async (app) => {
278
235
  const source = options.source;
279
- if (source.kind === "checkout")
236
+ if (source.kind === "checkout") {
237
+ const sourceRevision = parseSourceRevision(source.revision);
238
+ const metadata = await run("git", [
239
+ "show",
240
+ sourceRevision.slice(4) + ":" + app.directory + "/package.json",
241
+ ], source.directory);
280
242
  return {
281
243
  repositoryPath: source.directory,
282
- sourceRevision: parseSourceRevision(source.revision),
283
- recipe: app.recipe,
244
+ sourceRevision,
245
+ recipe: appBuildRecipe(readBuildManifest(metadata), app.directory),
284
246
  release: () => Promise.resolve(),
285
247
  };
248
+ }
286
249
  const clone = await mkdtemp(join(source.scratch, `${app.repository}-`));
287
250
  try {
288
251
  await run("git", [
@@ -298,7 +261,7 @@ export async function installStandaloneAppSet(options) {
298
261
  return {
299
262
  repositoryPath: clone,
300
263
  sourceRevision: parseSourceRevision(`git:${head}`),
301
- recipe: relocateRecipe(app.recipe, app.directory, ""),
264
+ recipe: appBuildRecipe(readBuildManifest(await run("git", ["show", head + ":package.json"], clone)), ""),
302
265
  release: () => rm(clone, { recursive: true, force: true }),
303
266
  };
304
267
  }
@@ -307,21 +270,38 @@ export async function installStandaloneAppSet(options) {
307
270
  throw cause;
308
271
  }
309
272
  };
310
- // Seed every template first: a git push each, quick beside a build.
311
- const seeded = [];
312
- for (const app of standaloneAppSet) {
273
+ const installed = new Map();
274
+ /** Set by the first failure, so no further seed or build starts. */
275
+ const stop = { requested: false };
276
+ /** Every failure, in the order it happened; the first is the one thrown. */
277
+ const failures = [];
278
+ const seededQueue = [];
279
+ const waiting = [];
280
+ const seeding = { finished: false };
281
+ const takeSeeded = () => new Promise((resolveTake) => {
282
+ const next = seededQueue.shift();
283
+ if (next !== undefined || seeding.finished)
284
+ resolveTake(next);
285
+ else
286
+ waiting.push(resolveTake);
287
+ });
288
+ const seedApp = async (app) => {
313
289
  const name = app.installationId;
314
290
  const fetchStarted = performance.now();
315
291
  const located = await locate(app);
316
292
  const publishStarted = performance.now();
317
293
  try {
318
294
  const published = await publish(app, located);
319
- if (!published.ok)
295
+ if (!published.ok) {
296
+ const refusal = Schema.decodeUnknownOption(Schema.fromJsonString(olderOysterStoreRefusal))(await published.text());
297
+ if (Option.isSome(refusal))
298
+ throw new Error(refusal.value.message);
320
299
  throw new Error(`${name} source publish failed (${String(published.status)})`);
300
+ }
321
301
  const { kind, receipt } = Schema.decodeUnknownSync(publishedSchema)(await published.json());
322
302
  options.report(`${name} source ${kind} at ${receipt.seedRevision}`);
323
303
  const ready = performance.now();
324
- seeded.push({
304
+ return {
325
305
  app,
326
306
  kind,
327
307
  seedRevision: receipt.seedRevision,
@@ -329,45 +309,99 @@ export async function installStandaloneAppSet(options) {
329
309
  fetch: publishStarted - fetchStarted,
330
310
  publish: ready - publishStarted,
331
311
  ready,
332
- });
312
+ };
333
313
  }
334
314
  finally {
335
315
  await located.release();
336
316
  }
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 };
317
+ };
318
+ const seedAll = async () => {
319
+ try {
320
+ for (const app of standaloneAppSet) {
321
+ if (stop.requested)
322
+ break;
323
+ const seeded = await seedApp(app);
324
+ const waiter = waiting.shift();
325
+ if (waiter === undefined)
326
+ seededQueue.push(seeded);
327
+ else
328
+ waiter(seeded);
329
+ }
330
+ }
331
+ catch (cause) {
332
+ stop.requested = true;
333
+ failures.push(cause);
334
+ throw cause;
335
+ }
336
+ finally {
337
+ seeding.finished = true;
338
+ for (const waiter of waiting.splice(0))
339
+ waiter(undefined);
340
+ }
341
+ };
342
+ // `concurrency` builds at once, each installing as its build succeeds.
345
343
  const slot = async () => {
346
- for (let next = stop.requested ? undefined : pending.shift(); next !== undefined; next = stop.requested ? undefined : pending.shift()) {
344
+ for (let next = await takeSeeded(); next !== undefined && !stop.requested; next = await takeSeeded()) {
347
345
  const name = next.app.installationId;
348
346
  try {
349
347
  const buildStarted = performance.now();
350
348
  const build = await buildOnComputer(name, next.app.appId, next.seedRevision);
351
349
  if (build.status !== "succeeded" || build.release === null)
352
350
  throw new Error(`${name} build ${build.status}: ${build.detail ?? "no Release"}`);
351
+ const release = Schema.decodeUnknownSync(releaseSchema)(build.release, {
352
+ onExcessProperty: "error",
353
+ });
354
+ if (release.appId !== next.app.appId)
355
+ throw new Error(`${name} build belongs to another App`);
353
356
  const installStarted = performance.now();
354
- const installation = await owner("POST", "/api/installations", {
355
- release: build.release,
357
+ let installation = {
358
+ url: options.url,
359
+ key: options.key,
360
+ release,
356
361
  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}`);
362
+ signal: AbortSignal.timeout(options.waitMs ?? appBuildWaitMs),
363
+ idle,
364
+ };
365
+ if (options.pollMs !== undefined)
366
+ installation = { ...installation, pollMs: options.pollMs };
367
+ const operation = await installOnOyster(installation);
368
+ if (operation.state === "unconfirmed")
369
+ throw new InstallationObservationError(operation.requestId);
370
+ if (operation.state === "refused")
371
+ throw new Error(`${name} installation refused (${operation.code})`);
372
+ const record = operation.installation;
373
+ if (record === null)
374
+ throw new Error(`${name} completed installation version is unavailable`);
375
+ if (record.appId !== release.appId ||
376
+ record.release.releaseId !== release.releaseId ||
377
+ record.release.artifact.artifactDigest !==
378
+ release.artifact.artifactDigest)
379
+ throw new Error(`${name} installation does not match its build`);
380
+ // Only the owner can attest exclusive fresh creation. A replacement or
381
+ // tombstone remint never replays a default grant.
382
+ if (operation.creation === "fresh") {
383
+ if (record.status !== "enabled")
384
+ throw new Error(`${name} fresh installation is not enabled`);
385
+ if (next.app.dashboard === true)
386
+ await approveChosenDashboard(record, operation.reference.generation);
387
+ }
388
+ options.report(`${operation.creation === "replacement" ? "Replaced" : "Installed"} ${name} from its build ${build.jobId}`);
389
+ // Report-only UI check: never blocks, but the owner sees it here.
390
+ const uiCheck = operation.uiCheck;
391
+ if (uiCheck?.kind === "failed")
392
+ options.report(`Warning: ${name} is installed, but its UI check failed (${uiCheck.code}${uiCheck.diagnostics === undefined ? "" : `: ${uiCheck.diagnostics.failedPredicate}`}).`);
393
+ else if (uiCheck?.kind === "not_checked")
394
+ options.report(`Warning: ${name} is installed, but its UI was not checked (${uiCheck.code}).`);
361
395
  const phases = build.detail === null
362
396
  ? undefined
363
397
  : Schema.decodeUnknownOption(computerPhasesSchema)(build.detail);
364
398
  installed.set(name, {
365
- installationId: name,
399
+ installationId: record.installationId,
366
400
  published: next.kind,
367
401
  seedRevision: next.seedRevision,
368
402
  sourceRevision: next.sourceRevision,
369
403
  jobId: build.jobId,
370
- installed: installation.status === 201 ? "installed" : "replaced",
404
+ installed: operation.creation === "replacement" ? "replaced" : "installed",
371
405
  release: build.release,
372
406
  timings: {
373
407
  fetch: next.fetch,
@@ -383,11 +417,19 @@ export async function installStandaloneAppSet(options) {
383
417
  }
384
418
  catch (cause) {
385
419
  stop.requested = true;
420
+ failures.push(cause);
386
421
  throw cause;
387
422
  }
388
423
  }
389
424
  };
390
- await Promise.all(Array.from({ length: concurrency }, slot));
425
+ // Wait for every seed and build to settle before reporting: nothing of this
426
+ // call may still run, or install, after it rejects.
427
+ await Promise.allSettled([
428
+ seedAll(),
429
+ ...Array.from({ length: concurrency }, slot),
430
+ ]);
431
+ if (failures.length > 0)
432
+ throw failures[0];
391
433
  return standaloneAppSet.flatMap((app) => {
392
434
  const result = installed.get(app.installationId);
393
435
  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>;