@fungi.computer/oyster 0.1.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 (292) hide show
  1. package/README.md +376 -72
  2. package/dist/agent-avatars.d.ts +103 -0
  3. package/dist/agent-avatars.js +66 -0
  4. package/dist/agents.d.ts +15 -4
  5. package/dist/agents.js +43 -4
  6. package/dist/app-builds.d.ts +81 -8
  7. package/dist/app-builds.js +210 -60
  8. package/dist/app-storage.d.ts +3 -3
  9. package/dist/approval-capability.d.ts +5 -0
  10. package/dist/approval-capability.js +46 -0
  11. package/dist/approval-home.d.ts +51 -0
  12. package/dist/approval-home.js +226 -0
  13. package/dist/apps/catalog.d.ts +335 -0
  14. package/dist/apps/catalog.js +46 -0
  15. package/dist/apps/gate.d.ts +26 -12
  16. package/dist/apps/gate.js +48 -22
  17. package/dist/apps/index.d.ts +3 -1
  18. package/dist/apps/index.js +3 -1
  19. package/dist/apps/installation.d.ts +147 -66
  20. package/dist/apps/installation.js +193 -86
  21. package/dist/apps/permissions.d.ts +247 -0
  22. package/dist/apps/permissions.js +255 -0
  23. package/dist/boot-capability-route.d.ts +15 -0
  24. package/dist/boot-capability-route.js +196 -0
  25. package/dist/boot-capability.d.ts +103 -0
  26. package/dist/boot-capability.js +141 -0
  27. package/dist/boot-construction.d.ts +12 -0
  28. package/dist/boot-construction.js +175 -0
  29. package/dist/boot-gate.d.ts +267 -0
  30. package/dist/boot-gate.js +236 -0
  31. package/dist/boot-host-script.d.ts +2 -0
  32. package/dist/boot-host-script.js +1 -0
  33. package/dist/boot-mount.d.ts +21 -0
  34. package/dist/boot-mount.js +65 -0
  35. package/dist/browser/boot.d.ts +73 -0
  36. package/dist/browser/boot.js +502 -0
  37. package/dist/browser/browser.d.ts +104 -0
  38. package/dist/browser/browser.js +374 -0
  39. package/dist/browser/capability-source.d.ts +76 -0
  40. package/dist/browser/capability-source.js +338 -0
  41. package/dist/browser/cdp-client.d.ts +56 -0
  42. package/dist/browser/cdp-client.js +184 -0
  43. package/dist/browser/errors.d.ts +79 -0
  44. package/dist/browser/errors.js +32 -0
  45. package/dist/browser/index.d.ts +16 -0
  46. package/dist/browser/index.js +14 -0
  47. package/dist/browser/lease-registry.d.ts +61 -0
  48. package/dist/browser/lease-registry.js +101 -0
  49. package/dist/browser/operations.d.ts +225 -0
  50. package/dist/browser/operations.js +137 -0
  51. package/dist/browser/page.d.ts +25 -0
  52. package/dist/browser/page.js +172 -0
  53. package/dist/browser/quick-reads.d.ts +29 -0
  54. package/dist/browser/quick-reads.js +160 -0
  55. package/dist/browser/sessions.d.ts +52 -0
  56. package/dist/browser/sessions.js +118 -0
  57. package/dist/browser/url-policy.d.ts +18 -0
  58. package/dist/browser/url-policy.js +58 -0
  59. package/dist/build/app-imports.d.ts +12 -0
  60. package/dist/build/app-imports.js +105 -0
  61. package/dist/build/build-policy.d.ts +5 -3
  62. package/dist/build/build-policy.js +5 -3
  63. package/dist/build/candidate-builder.d.ts +1 -1
  64. package/dist/build/candidate-cli.mjs +8428 -337
  65. package/dist/build/local-build.d.ts +2 -0
  66. package/dist/build/local-build.js +28 -6
  67. package/dist/build/snapshot-package-store.d.ts +1 -1
  68. package/dist/build/snapshot-package-store.js +7 -2
  69. package/dist/build/workspace-build.d.ts +92 -5
  70. package/dist/build/workspace-build.js +93 -19
  71. package/dist/build-egress.js +4 -4
  72. package/dist/build.d.ts +1 -1
  73. package/dist/build.js +1 -1
  74. package/dist/catalog-frame.js +1 -1
  75. package/dist/cloudflare.d.ts +13 -3
  76. package/dist/cloudflare.js +106 -8
  77. package/dist/frame-binding-wire-script.js +1 -1
  78. package/dist/frame-binding-wire.d.ts +1 -1
  79. package/dist/frame-binding-wire.js +5 -3
  80. package/dist/git-auth.d.ts +0 -2
  81. package/dist/git-auth.js +0 -2
  82. package/dist/git-journal.js +4 -4
  83. package/dist/git.d.ts +0 -1
  84. package/dist/git.js +0 -2
  85. package/dist/host.d.ts +39 -13
  86. package/dist/host.js +268 -142
  87. package/dist/index.d.ts +26 -15
  88. package/dist/index.js +40 -16
  89. package/dist/install-release-cli.d.ts +2 -0
  90. package/dist/install-release-cli.js +46 -0
  91. package/dist/installation-client.d.ts +43 -0
  92. package/dist/installation-client.js +178 -0
  93. package/dist/installation-execution.d.ts +11 -0
  94. package/dist/installation-execution.js +184 -0
  95. package/dist/installation-operations.d.ts +1437 -0
  96. package/dist/installation-operations.js +664 -0
  97. package/dist/installation-store-generation.d.ts +3 -0
  98. package/dist/installation-store-generation.js +16 -0
  99. package/dist/instance-id.d.ts +10 -0
  100. package/dist/instance-id.js +10 -0
  101. package/dist/instance-refusal.d.ts +11 -0
  102. package/dist/instance-refusal.js +21 -0
  103. package/dist/internal/delivery.js +4 -2
  104. package/dist/internal/host-gate.js +7 -1
  105. package/dist/internal/registry-installations.js +4 -4
  106. package/dist/keys.d.ts +5 -1
  107. package/dist/keys.js +10 -2
  108. package/dist/machine-link/authority.d.ts +89 -0
  109. package/dist/machine-link/authority.js +37 -0
  110. package/dist/machine-link/client.d.ts +66 -0
  111. package/dist/machine-link/client.js +94 -0
  112. package/dist/machine-link/delivery.d.ts +18 -0
  113. package/dist/machine-link/delivery.js +55 -0
  114. package/dist/machine-link/enrollment.d.ts +111 -0
  115. package/dist/machine-link/enrollment.js +286 -0
  116. package/dist/machine-link/fences.d.ts +37 -0
  117. package/dist/machine-link/fences.js +166 -0
  118. package/dist/machine-link/home.d.ts +20 -0
  119. package/dist/machine-link/home.js +24 -0
  120. package/dist/machine-link/host.d.ts +43 -0
  121. package/dist/machine-link/host.js +189 -0
  122. package/dist/machine-link/index.d.ts +15 -0
  123. package/dist/machine-link/index.js +15 -0
  124. package/dist/machine-link/link.d.ts +48 -0
  125. package/dist/machine-link/link.js +223 -0
  126. package/dist/machine-link/presence.d.ts +10 -0
  127. package/dist/machine-link/presence.js +25 -0
  128. package/dist/machine-link/workers.d.ts +6 -0
  129. package/dist/machine-link/workers.js +6 -0
  130. package/dist/mcp-owner.d.ts +2 -2
  131. package/dist/mcp-owner.js +41 -33
  132. package/dist/mcp.d.ts +15 -3
  133. package/dist/mcp.js +146 -25
  134. package/dist/pack.d.ts +14 -3
  135. package/dist/pack.js +44 -19
  136. package/dist/page/app-access.d.ts +7 -0
  137. package/dist/page/app-access.js +75 -0
  138. package/dist/page/boot-host.js +57 -0
  139. package/dist/page/capabilities.d.ts +3 -3
  140. package/dist/page/capabilities.js +16 -12
  141. package/dist/page/computer-handles.d.ts +3 -3
  142. package/dist/page/computer-target.d.ts +3 -0
  143. package/dist/page/computer-target.js +9 -0
  144. package/dist/page/mount.d.ts +23 -7
  145. package/dist/page/mount.js +152 -16
  146. package/dist/page-assets.d.ts +22 -0
  147. package/dist/page-assets.js +28 -0
  148. package/dist/page-script.d.ts +3 -0
  149. package/dist/page-script.js +5 -4
  150. package/dist/release.d.ts +13 -6
  151. package/dist/release.js +10 -7
  152. package/dist/root-data.d.ts +16 -6
  153. package/dist/root-data.js +2 -2
  154. package/dist/source/index.d.ts +1 -1
  155. package/dist/source/team-source.d.ts +20 -8
  156. package/dist/source/team-source.js +39 -10
  157. package/dist/source/template-seed.d.ts +2 -2
  158. package/dist/standalone/agent-mail.d.ts +24 -0
  159. package/dist/standalone/agent-mail.js +62 -0
  160. package/dist/standalone/agent-store.d.ts +9 -0
  161. package/dist/standalone/agent-store.js +30 -0
  162. package/dist/standalone/agents.d.ts +78 -12
  163. package/dist/standalone/agents.js +470 -59
  164. package/dist/standalone/app-builds-binding.d.ts +20 -2
  165. package/dist/standalone/app-builds-binding.js +151 -27
  166. package/dist/standalone/app-set.d.ts +5 -2
  167. package/dist/standalone/app-set.js +174 -143
  168. package/dist/standalone/browser.d.ts +11 -0
  169. package/dist/standalone/browser.js +72 -0
  170. package/dist/standalone/builds.d.ts +13 -4
  171. package/dist/standalone/builds.js +28 -44
  172. package/dist/standalone/computer-egress.d.ts +2 -0
  173. package/dist/standalone/computer-egress.js +13 -4
  174. package/dist/standalone/computer-fleet.d.ts +43 -0
  175. package/dist/standalone/computer-fleet.js +200 -0
  176. package/dist/standalone/computer.d.ts +44 -9
  177. package/dist/standalone/computer.js +132 -25
  178. package/dist/standalone/install-apps-cli.js +43 -6
  179. package/dist/standalone/instance.d.ts +2 -2
  180. package/dist/standalone/instance.js +8 -11
  181. package/dist/standalone/machine-link.d.ts +354 -0
  182. package/dist/standalone/machine-link.js +368 -0
  183. package/dist/standalone/media.d.ts +2 -2
  184. package/dist/standalone/media.js +19 -16
  185. package/dist/standalone/preview.d.ts +2 -2
  186. package/dist/standalone/preview.js +39 -12
  187. package/dist/standalone/registry.d.ts +149 -9
  188. package/dist/standalone/registry.js +299 -8
  189. package/dist/standalone/worker.d.ts +19 -5
  190. package/dist/standalone/worker.js +90 -54
  191. package/dist/standalone/workflow-binding.d.ts +28 -32
  192. package/dist/standalone/workflow-binding.js +20 -20
  193. package/dist/standalone/workflow-control.d.ts +4 -0
  194. package/dist/standalone/workflow-control.js +40 -0
  195. package/dist/standalone/workflow-events.d.ts +2 -2
  196. package/dist/standalone/workflow-events.js +9 -10
  197. package/dist/standalone/workflow-git-capture.js +4 -3
  198. package/dist/standalone/workflow-host.d.ts +177 -152
  199. package/dist/standalone/workflow-host.js +137 -30
  200. package/dist/standalone/workflow-node-capture.js +11 -4
  201. package/dist/standalone/workflow-schedules.d.ts +5 -2
  202. package/dist/standalone/workflow-schedules.js +9 -10
  203. package/dist/standalone/workflow-trigger-host.d.ts +88 -6
  204. package/dist/standalone/workflow-trigger-host.js +2 -2
  205. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  206. package/dist/standalone/workflow-webhooks.js +39 -0
  207. package/dist/standalone/workflows.d.ts +203 -50
  208. package/dist/standalone/workflows.js +198 -39
  209. package/dist/storage-frame.d.ts +6 -6
  210. package/dist/testing/capabilities.js +14 -3
  211. package/dist/testing/installation.js +26 -2
  212. package/dist/testing/worker.js +1 -1
  213. package/dist/testing/workers-runtime.d.ts +8 -2
  214. package/dist/testing/workers-runtime.js +20 -2
  215. package/dist/webhook-abuse.d.ts +25 -0
  216. package/dist/webhook-abuse.js +54 -0
  217. package/dist/webhook-custody.d.ts +144 -0
  218. package/dist/webhook-custody.js +222 -0
  219. package/dist/webhook-ingress.d.ts +36 -0
  220. package/dist/webhook-ingress.js +146 -0
  221. package/dist/webhooks.d.ts +111 -0
  222. package/dist/webhooks.js +189 -0
  223. package/dist/workers-git.d.ts +12 -17
  224. package/dist/workers-git.js +60 -55
  225. package/dist/workers.d.ts +224 -13
  226. package/dist/workers.js +351 -51
  227. package/dist/workflow-agent-authority.d.ts +8 -0
  228. package/dist/workflow-agent-authority.js +42 -0
  229. package/dist/workflows.d.ts +66 -0
  230. package/dist/workflows.js +97 -0
  231. package/docs/api/README.md +63 -37
  232. package/docs/api/agents.md +17 -0
  233. package/docs/api/app-storage.md +1 -1
  234. package/docs/api/apps.md +1394 -543
  235. package/docs/api/browser/boot.md +132 -0
  236. package/docs/api/browser.md +1440 -0
  237. package/docs/api/build-contract.md +38 -25
  238. package/docs/api/build.md +263 -84
  239. package/docs/api/cloudflare.md +93 -7
  240. package/docs/api/git.md +3 -13
  241. package/docs/api/host.md +944 -118
  242. package/docs/api/index.md +320 -18
  243. package/docs/api/keys.md +24 -1
  244. package/docs/api/mcp.md +59 -12
  245. package/docs/api/modules.md +33 -40
  246. package/docs/api/pack.md +57 -8
  247. package/docs/api/page/mount.md +92 -4
  248. package/docs/api/workers-git.md +103 -24
  249. package/docs/api/workers.md +1581 -244
  250. package/docs/site/agent-quickstart.mdx +28 -14
  251. package/examples/standalone/worker.ts +3 -1
  252. package/examples/standalone/wrangler.jsonc +2 -0
  253. package/package.json +73 -69
  254. package/dist/installation-consent.d.ts +0 -79
  255. package/dist/installation-consent.js +0 -82
  256. package/dist/shell/app-directory.d.ts +0 -542
  257. package/dist/shell/app-directory.js +0 -199
  258. package/dist/shell/browser-actions.d.ts +0 -7
  259. package/dist/shell/browser-actions.js +0 -56
  260. package/dist/shell/command-host.d.ts +0 -112
  261. package/dist/shell/command-host.js +0 -502
  262. package/dist/shell/commands.d.ts +0 -25
  263. package/dist/shell/commands.js +0 -78
  264. package/dist/shell/index.d.ts +0 -8
  265. package/dist/shell/index.js +0 -8
  266. package/dist/shell/keys.d.ts +0 -41
  267. package/dist/shell/keys.js +0 -134
  268. package/dist/shell/shell-module.d.ts +0 -55
  269. package/dist/shell/shell-module.js +0 -86
  270. package/dist/shell/window-manager.d.ts +0 -124
  271. package/dist/shell/window-manager.js +0 -711
  272. package/dist/shell/workbench-contract.d.ts +0 -68
  273. package/dist/shell/workbench-directory.d.ts +0 -332
  274. package/dist/shell/workbench-directory.js +0 -35
  275. package/dist/shell/workbench-host-types.d.ts +0 -96
  276. package/dist/shell/workbench-host-types.js +0 -1
  277. package/dist/shell/workbench-host.d.ts +0 -34
  278. package/dist/shell/workbench-host.js +0 -347
  279. package/dist/shell/workbench-model.d.ts +0 -192
  280. package/dist/shell/workbench-model.js +0 -53
  281. package/dist/shell/workbench.d.ts +0 -4
  282. package/dist/shell/workbench.js +0 -2
  283. package/docs/api/shell/app-directory.md +0 -599
  284. package/docs/api/shell/command-host.md +0 -224
  285. package/docs/api/shell/commands.md +0 -197
  286. package/docs/api/shell/keys.md +0 -183
  287. package/docs/api/shell/shell-module.md +0 -204
  288. package/docs/api/shell/window-manager.md +0 -267
  289. package/docs/api/shell/workbench-directory.md +0 -119
  290. package/docs/api/shell/workbench.md +0 -205
  291. package/docs/api/shell.md +0 -1915
  292. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -17,9 +17,11 @@
17
17
  * installed one, and records it; installing it is a separate owner act.
18
18
  */
19
19
  import * as Schema from "effect/Schema";
20
+ import * as Data from "effect/Data";
21
+ import * as Option from "effect/Option";
20
22
  import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
21
23
  import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
22
- import { capturedPinnedRecipeSchema, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
24
+ import { capturedPinnedRecipeSchema, normalizeAppPublisherRecipe, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
23
25
  import { candidateManifestObjects, decodeCandidateBackend, digestTeamAppCandidateManifest, } from "./build/submission-contract.js";
24
26
  import { contentObjectRef } from "./build/content-objects.js";
25
27
  import { createTeamBuildPublisher, teamAppBuildCommand, } from "./build/build-jobs.js";
@@ -52,6 +54,42 @@ export const oysterBuildSourceSchema = Schema.Struct({
52
54
  recipe: capturedPinnedRecipeSchema,
53
55
  buildRecipeDigest: sha256DigestSchema,
54
56
  });
57
+ /** Installation is a build outcome, never a pending consent/approval record. */
58
+ export const oysterBuildInstallationSchema = Schema.Union([
59
+ Schema.Struct({
60
+ kind: Schema.Literal("pending"),
61
+ installationId: Schema.String,
62
+ }),
63
+ Schema.Struct({
64
+ kind: Schema.Literals(["installed", "needs_consent"]),
65
+ installationId: Schema.String,
66
+ }),
67
+ Schema.Struct({
68
+ kind: Schema.Literal("failed"),
69
+ installationId: Schema.String,
70
+ detail: Schema.String,
71
+ }),
72
+ ]);
73
+ const installationCodec = Schema.fromJsonString(oysterBuildInstallationSchema);
74
+ /** Known preflight refusals, before a job or Computer run exists. */
75
+ export const oysterBuildStartReasonSchema = Schema.Literals([
76
+ "invalid_revision",
77
+ "no_source",
78
+ "not_template_seed",
79
+ "no_backend",
80
+ ]);
81
+ export const oysterBuildStartMessages = {
82
+ invalid_revision: "Source revision must be git: followed by a full commit hash",
83
+ no_source: "App has no build source",
84
+ no_backend: "No hosted build backend is attached to this Computer",
85
+ not_template_seed: "Revision is not a seed of this App's template",
86
+ };
87
+ /** Local typed failure. The registry turns it into data before a DO RPC. */
88
+ export class OysterBuildStartError extends Data.TaggedError("OysterBuildStartError") {
89
+ constructor(reason) {
90
+ super({ reason, message: oysterBuildStartMessages[reason] });
91
+ }
92
+ }
55
93
  /** One Computer as the build owner reaches it. */
56
94
  /** The Computer established no terminal result for a build command, for
57
95
  * example because its connection to the container dropped mid-command. */
@@ -76,10 +114,12 @@ const jobRow = Schema.Struct({
76
114
  requested_by: Schema.String,
77
115
  credential_id: Schema.String,
78
116
  upload_grant_digest: Schema.String,
117
+ request_id: Schema.NullOr(Schema.String),
79
118
  status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
80
119
  artifact_digest: Schema.NullOr(Schema.String),
81
120
  artifact_bytes: Schema.NullOr(Schema.Number),
82
121
  detail: Schema.NullOr(Schema.String),
122
+ installation: Schema.NullOr(installationCodec),
83
123
  issued_at: Schema.Number,
84
124
  expires_at: Schema.Number,
85
125
  executable_until: Schema.Number,
@@ -107,48 +147,21 @@ function sameDigest(left, right) {
107
147
  difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
108
148
  return difference === 0;
109
149
  }
110
- /**
111
- * The SQLite tables one Oyster build owner keeps. Creation and any carrying
112
- * forward of retained facts run as one transaction of the owner's storage, so
113
- * an interrupted conversion leaves the facts as they were for a retry.
114
- */
150
+ /** Create the current build tables and index atomically; never convert old stores. */
115
151
  export function migrateOysterAppBuilds(owner) {
116
152
  const { sql } = owner;
117
153
  owner.transactionSync(() => {
118
154
  sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
119
- // Sources recorded before the location union named only an installation
120
- // repository; carry each forward once, in place.
121
- sql.exec(`UPDATE oyster_build_sources SET source = json_set(
122
- json_remove(source, '$.installationId', '$.repository'), '$.location',
123
- json_object('kind', 'installation',
124
- 'installationId', json_extract(source, '$.installationId'),
125
- 'repository', json_extract(source, '$.repository')))
126
- WHERE json_type(source, '$.location') IS NULL`);
127
155
  const jobColumns = `
128
156
  job_id TEXT PRIMARY KEY, app_id TEXT NOT NULL, release_id TEXT NOT NULL UNIQUE,
129
157
  source_revision TEXT NOT NULL, location TEXT NOT NULL,
130
158
  recipe TEXT NOT NULL, recipe_digest TEXT NOT NULL, requested_by TEXT NOT NULL,
131
159
  credential_id TEXT NOT NULL, upload_grant_digest TEXT NOT NULL,
132
160
  status TEXT NOT NULL, artifact_digest TEXT, artifact_bytes INTEGER, detail TEXT,
161
+ installation TEXT, request_id TEXT,
133
162
  issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL`;
134
163
  sql.exec(`CREATE TABLE IF NOT EXISTS oyster_build_jobs (${jobColumns}) STRICT`);
135
- // Jobs recorded before the location union named an installation repository
136
- // in two columns; carry each into the one location column, once.
137
- if (sql
138
- .exec("SELECT name FROM pragma_table_info('oyster_build_jobs') WHERE name='installation_id'")
139
- .toArray().length > 0) {
140
- sql.exec(`CREATE TABLE oyster_build_jobs_current (${jobColumns}) STRICT`);
141
- sql.exec(`INSERT INTO oyster_build_jobs_current SELECT job_id, app_id, release_id,
142
- source_revision,
143
- json_object('kind', 'installation', 'installationId', installation_id,
144
- 'repository', repository),
145
- recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
146
- status, artifact_digest, artifact_bytes, detail, issued_at, expires_at,
147
- executable_until
148
- FROM oyster_build_jobs`);
149
- sql.exec("DROP TABLE oyster_build_jobs");
150
- sql.exec("ALTER TABLE oyster_build_jobs_current RENAME TO oyster_build_jobs");
151
- }
164
+ sql.exec("CREATE UNIQUE INDEX IF NOT EXISTS oyster_build_request ON oyster_build_jobs(request_id)");
152
165
  sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_releases (release_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE, release TEXT NOT NULL) STRICT");
153
166
  });
154
167
  }
@@ -172,7 +185,10 @@ export function createOysterAppBuilds(options) {
172
185
  // job, so an owner restart must not fail it. A job that reaches its expiry
173
186
  // without a result can never settle, so it fails then instead of reading
174
187
  // as running for as long as anyone asks.
175
- const expire = () => owner.sql.exec("UPDATE oyster_build_jobs SET status='failed', detail=? WHERE status='running' AND expires_at<=?", "Build expired without a result", now());
188
+ const failedInstallation = `CASE WHEN installation IS NULL THEN NULL ELSE
189
+ json_object('kind', 'failed', 'installationId', json_extract(installation, '$.installationId'),
190
+ 'detail', COALESCE(?, 'Build did not succeed')) END`;
191
+ const expire = () => owner.sql.exec(`UPDATE oyster_build_jobs SET installation=${failedInstallation}, status='failed', detail=? WHERE status='running' AND expires_at<=?`, "Build expired without a result", "Build expired without a result", now());
176
192
  expire();
177
193
  const decodeRecipe = (recipe) => Schema.decodeUnknownSync(recipeCodec)(recipe);
178
194
  const grantAdmits = async (job, grant) => sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant)));
@@ -209,7 +225,8 @@ export function createOysterAppBuilds(options) {
209
225
  detail: job.detail,
210
226
  });
211
227
  const settle = (jobId, status, detail) => {
212
- owner.sql.exec("UPDATE oyster_build_jobs SET status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'", status, detail, now(), jobId);
228
+ owner.sql.exec(`UPDATE oyster_build_jobs SET installation=CASE WHEN ?='succeeded' THEN installation ELSE ${failedInstallation} END,
229
+ status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'`, status, detail, status, detail, now(), jobId);
213
230
  };
214
231
  const required = (jobId) => {
215
232
  const job = readJob(jobId);
@@ -293,16 +310,128 @@ export function createOysterAppBuilds(options) {
293
310
  status: job.status,
294
311
  detail: job.detail,
295
312
  release: readRelease(job.job_id),
313
+ installation: job.installation,
296
314
  });
315
+ const registerSource = (source) => {
316
+ if (digestPinnedRecipe(source.recipe) !== source.buildRecipeDigest)
317
+ throw new Error("Build recipe digest does not match its recipe");
318
+ owner.sql.exec("INSERT INTO oyster_build_sources(app_id, source) VALUES(?, ?) ON CONFLICT(app_id) DO UPDATE SET source=excluded.source", source.appId, Schema.encodeUnknownSync(sourceCodec)(source));
319
+ return source;
320
+ };
321
+ const readRepositorySource = (installationId, repository) => {
322
+ const [row] = owner.sql
323
+ .exec("SELECT source FROM oyster_build_sources WHERE json_extract(source, '$.location.kind')='installation' AND json_extract(source, '$.location.installationId')=? AND json_extract(source, '$.location.repository')=? LIMIT 1", installationId, repository)
324
+ .toArray();
325
+ return row === undefined
326
+ ? null
327
+ : Schema.decodeUnknownSync(sourceCodec)(Schema.decodeUnknownSync(sourceRow)(row).source);
328
+ };
329
+ async function start(input) {
330
+ const revision = Schema.decodeUnknownOption(sourceRevisionSchema)(input.sourceRevision);
331
+ if (Option.isNone(revision))
332
+ throw new OysterBuildStartError("invalid_revision");
333
+ const sourceRevision = revision.value;
334
+ const source = readSource(input.appId);
335
+ if (source === null)
336
+ throw new OysterBuildStartError("no_source");
337
+ if (source.location.kind === "template") {
338
+ const receipt = await options.templates.read(source.appId, sourceRevision);
339
+ if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
340
+ throw new OysterBuildStartError("not_template_seed");
341
+ }
342
+ const reusedRequest = () => {
343
+ if (input.requestId !== undefined) {
344
+ const [row] = owner.sql
345
+ .exec("SELECT * FROM oyster_build_jobs WHERE request_id=?", input.requestId)
346
+ .toArray();
347
+ if (row !== undefined) {
348
+ const existing = decodeJob(row);
349
+ if (existing.app_id !== source.appId ||
350
+ existing.source_revision !== sourceRevision)
351
+ throw new Error("Build request identity conflict");
352
+ return existing.job_id;
353
+ }
354
+ }
355
+ return null;
356
+ };
357
+ const existing = reusedRequest();
358
+ if (existing !== null)
359
+ return {
360
+ build: view(required(existing)),
361
+ run: () => Promise.resolve(view(required(existing))),
362
+ };
363
+ if ((await options.computer.port.buildBackend()).kind === "no_backend")
364
+ throw new OysterBuildStartError("no_backend");
365
+ const jobId = `build_${randomHex(16)}`;
366
+ const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
367
+ const uploadGrant = randomHex(32);
368
+ const issuedAt = now();
369
+ const uploadGrantDigest = await sha256Hex(new TextEncoder().encode(uploadGrant));
370
+ const reused = owner.transactionSync(() => {
371
+ const existing = reusedRequest();
372
+ if (existing !== null)
373
+ return existing;
374
+ owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
375
+ recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
376
+ status, artifact_digest, artifact_bytes, detail, installation, request_id, issued_at, expires_at, executable_until)
377
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, Schema.encodeUnknownSync(locationCodec)(source.location), Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), uploadGrantDigest, input.installAt === undefined
378
+ ? null
379
+ : Schema.encodeUnknownSync(installationCodec)({
380
+ kind: "pending",
381
+ installationId: input.installAt,
382
+ }), input.requestId ?? null, issuedAt, issuedAt + jobTtlMs);
383
+ return null;
384
+ });
385
+ if (reused !== null)
386
+ return {
387
+ build: view(required(reused)),
388
+ run: () => Promise.resolve(view(required(reused))),
389
+ };
390
+ // The grant leaves this owner only inside the fixed command.
391
+ const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
392
+ return { build: view(required(jobId)), run: () => run(jobId, command) };
393
+ }
394
+ let installationBusy = false;
395
+ function recordInstallationOutcome(jobId, outcome) {
396
+ return owner.transactionSync(() => {
397
+ const job = readJob(jobId);
398
+ if (job?.status !== "succeeded" ||
399
+ readRelease(jobId) === null ||
400
+ job.installation?.kind !== "pending" ||
401
+ job.installation.installationId !== outcome.installationId)
402
+ return false;
403
+ owner.sql.exec("UPDATE oyster_build_jobs SET installation=? WHERE job_id=?", Schema.encodeUnknownSync(installationCodec)(outcome), jobId);
404
+ return true;
405
+ });
406
+ }
297
407
  return Object.freeze({
298
408
  /** Record (or replace) an App's build source; the caller is the owner. */
299
- registerSource(source) {
300
- if (digestPinnedRecipe(source.recipe) !== source.buildRecipeDigest)
301
- throw new Error("Build recipe digest does not match its recipe");
302
- owner.sql.exec("INSERT INTO oyster_build_sources(app_id, source) VALUES(?, ?) ON CONFLICT(app_id) DO UPDATE SET source=excluded.source", source.appId, Schema.encodeUnknownSync(sourceCodec)(source));
303
- return source;
409
+ registerSource,
410
+ /** Resolve repository identity and replace its current recipe as one transition.
411
+ * A fork's existing identity wins over the copied manifest's App id. */
412
+ registerRepositorySource(input) {
413
+ return owner.transactionSync(() => {
414
+ const existing = readRepositorySource(input.location.installationId, input.location.repository);
415
+ const appId = Schema.decodeUnknownSync(appIdSchema)(existing?.appId ?? input.appId);
416
+ if (existing === null && readSource(appId) !== null)
417
+ return { kind: "conflict" };
418
+ const pinned = normalizeAppPublisherRecipe({
419
+ appId,
420
+ recipe: input.recipe,
421
+ });
422
+ return {
423
+ kind: "registered",
424
+ source: registerSource({
425
+ appId,
426
+ location: input.location,
427
+ recipe: pinned.recipe,
428
+ buildRecipeDigest: pinned.buildRecipeDigest,
429
+ }),
430
+ };
431
+ });
304
432
  },
305
433
  readSource,
434
+ readRepositorySource,
306
435
  /**
307
436
  * Mint one build job for an App's registered source at one exact commit.
308
437
  * A template source builds only a seed revision recorded for its pinned
@@ -310,33 +439,54 @@ export function createOysterAppBuilds(options) {
310
439
  * Answers the job and its one run, which the caller keeps alive (the
311
440
  * upload grant exists only inside that run's fixed command).
312
441
  */
313
- async start(input) {
314
- const sourceRevision = Schema.decodeUnknownSync(sourceRevisionSchema)(input.sourceRevision);
315
- const source = readSource(input.appId);
316
- if (source === null)
317
- throw new Error("App has no build source");
318
- if (source.location.kind === "template") {
319
- const receipt = await options.templates.read(source.appId, sourceRevision);
320
- if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
321
- throw new Error("Revision is not a seed of this App's template");
322
- }
323
- const jobId = `build_${randomHex(16)}`;
324
- const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
325
- const uploadGrant = randomHex(32);
326
- const issuedAt = now();
327
- owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
328
- recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
329
- status, artifact_digest, artifact_bytes, detail, issued_at, expires_at, executable_until)
330
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, Schema.encodeUnknownSync(locationCodec)(source.location), Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), await sha256Hex(new TextEncoder().encode(uploadGrant)), issuedAt, issuedAt + jobTtlMs);
331
- // The grant leaves this owner only inside the fixed command.
332
- const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
333
- return { build: view(required(jobId)), run: () => run(jobId, command) };
334
- },
442
+ start,
335
443
  read(jobId) {
336
444
  expire();
337
445
  const job = readJob(jobId);
338
446
  return job === null ? null : view(job);
339
447
  },
448
+ /** Discover the current source's latest job from the caller's Git owner,
449
+ * never a job id remembered in a browser or another installation's repo. */
450
+ latestRepositoryBuild(installationId, repository) {
451
+ expire();
452
+ const source = readRepositorySource(installationId, repository);
453
+ if (source === null)
454
+ return null;
455
+ const [row] = owner.sql
456
+ .exec("SELECT * FROM oyster_build_jobs WHERE app_id=? AND json_extract(location, '$.kind')='installation' AND json_extract(location, '$.installationId')=? AND json_extract(location, '$.repository')=? ORDER BY rowid DESC LIMIT 1", source.appId, installationId, repository)
457
+ .toArray();
458
+ return row === undefined ? null : view(decodeJob(row));
459
+ },
460
+ /** One bounded host installation at a time. Only a verified successful
461
+ * build may reach the installer; the same existing row owns its outcome.
462
+ * A Workflow retry may resume a pending decision, never a terminal one. */
463
+ async install(jobId, decide) {
464
+ const job = readJob(jobId);
465
+ const release = readRelease(jobId);
466
+ if (installationBusy ||
467
+ job?.status !== "succeeded" ||
468
+ release === null ||
469
+ job.installation?.kind !== "pending")
470
+ return false;
471
+ installationBusy = true;
472
+ try {
473
+ let outcome;
474
+ try {
475
+ outcome = await decide(release, job.installation.installationId);
476
+ }
477
+ catch {
478
+ outcome = {
479
+ kind: "failed",
480
+ installationId: job.installation.installationId,
481
+ detail: "Automatic installation unavailable",
482
+ };
483
+ }
484
+ return recordInstallationOutcome(jobId, outcome);
485
+ }
486
+ finally {
487
+ installationBusy = false;
488
+ }
489
+ },
340
490
  /** A built Release by id, for an owner to install or select. */
341
491
  release(releaseId) {
342
492
  const [row] = owner.sql
@@ -86,7 +86,7 @@ export declare function createAppStorage(options: {
86
86
  /** Read the declared team layer without exposing any member's private value. */
87
87
  read: (key: string) => Effect.Effect<{
88
88
  readonly value: Schema.Json;
89
- readonly source: "team" | "member" | "none";
89
+ readonly source: "member" | "team" | "none";
90
90
  readonly team: {
91
91
  readonly value: Schema.Json;
92
92
  readonly revision: number;
@@ -111,7 +111,7 @@ export declare function createAppStorage(options: {
111
111
  /** Resolve declared member and team layers under current live admission. */
112
112
  read: (key: string) => Effect.Effect<{
113
113
  readonly value: Schema.Json;
114
- readonly source: "team" | "member" | "none";
114
+ readonly source: "member" | "team" | "none";
115
115
  readonly team: {
116
116
  readonly value: Schema.Json;
117
117
  readonly revision: number;
@@ -134,7 +134,7 @@ export declare function createAppStorage(options: {
134
134
  events: (key: string) => Stream.Stream<{
135
135
  readonly appId: string;
136
136
  readonly key: string;
137
- readonly scope: "team" | "member";
137
+ readonly scope: "member" | "team";
138
138
  readonly revision: number;
139
139
  } | null, AppStorageError, never>;
140
140
  };
@@ -0,0 +1,5 @@
1
+ import { type Approvals } from "@fungi.computer/approvals";
2
+ import type { HostActor } from "@fungi.computer/puffball/app";
3
+ import type { OysterCapability } from "./host.js";
4
+ /** Ordinary capability routes; the outer App gate supplies identity and consent. */
5
+ export declare function approvalCapability(control: (actor: HostActor) => Approvals): OysterCapability;
@@ -0,0 +1,46 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { causeClass } from "./cause-class.js";
4
+ import * as Schema from "effect/Schema";
5
+ import { ApprovalListInputSchema, ApprovalGetInputSchema, ApprovalAnswerInputSchema, AutoApproveInputSchema, } from "@fungi.computer/approvals";
6
+ /** Ordinary capability routes; the outer App gate supplies identity and consent. */
7
+ export function approvalCapability(control) {
8
+ const routes = new Hono();
9
+ routes.use("*", bodyLimit({ maxSize: 4096 }));
10
+ routes.use("*", async (context, next) => {
11
+ if (context.env.principal.actor.kind !== "member")
12
+ return context.json({ kind: "refused", reason: "not-eligible" }, 403);
13
+ await next();
14
+ return undefined;
15
+ });
16
+ routes.onError((cause, context) => {
17
+ if (cause instanceof SyntaxError || cause instanceof Schema.SchemaError)
18
+ return context.json({ error: "Invalid approval request" }, 400);
19
+ console.warn("Native approval control failed", {
20
+ causeClass: causeClass(cause),
21
+ });
22
+ return context.json({ error: "Approval home unavailable" }, 503);
23
+ });
24
+ const strict = { onExcessProperty: "error" };
25
+ routes.post("/list", async (c) => c.json(await control(c.env.principal.actor).list(Schema.decodeUnknownSync(ApprovalListInputSchema, strict)(await c.req.json()))));
26
+ routes.post("/get", async (c) => c.json(await control(c.env.principal.actor).get(Schema.decodeUnknownSync(ApprovalGetInputSchema, strict)(await c.req.json()))));
27
+ routes.post("/answer", async (c) => c.json(await control(c.env.principal.actor).answer(Schema.decodeUnknownSync(ApprovalAnswerInputSchema, strict)(await c.req.json()))));
28
+ routes.post("/set-auto-approve", async (c) => c.json(await control(c.env.principal.actor).setAutoApprove(Schema.decodeUnknownSync(AutoApproveInputSchema, strict)(await c.req.json()))));
29
+ return {
30
+ id: "approvals",
31
+ version: 1,
32
+ permission: "approvals",
33
+ base: "/approvals/",
34
+ routes,
35
+ methods: {
36
+ list: { kind: "json", method: "POST", path: "list" },
37
+ get: { kind: "json", method: "POST", path: "get" },
38
+ answer: { kind: "json", method: "POST", path: "answer" },
39
+ setAutoApprove: {
40
+ kind: "json",
41
+ method: "POST",
42
+ path: "set-auto-approve",
43
+ },
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import { ApprovalLedger, type Approvals, type PendingApproval, type ApprovalApplyResult } from "@fungi.computer/approvals";
3
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
4
+ import { type OysterHost } from "./host.js";
5
+ import type { HostActor } from "@fungi.computer/puffball/app";
6
+ import type { TenantScope } from "./tenant.js";
7
+ declare const callSchema: z.ZodObject<{
8
+ actor: import("zod/mini").ZodMiniDiscriminatedUnion<[import("zod/mini").ZodMiniObject<{
9
+ kind: import("zod/mini").ZodMiniLiteral<"member">;
10
+ memberId: import("zod/mini").ZodMiniString<string>;
11
+ }, z.core.$strip>, import("zod/mini").ZodMiniObject<{
12
+ kind: import("zod/mini").ZodMiniLiteral<"agent">;
13
+ agentId: import("zod/mini").ZodMiniString<string>;
14
+ }, z.core.$strip>, import("zod/mini").ZodMiniObject<{
15
+ kind: import("zod/mini").ZodMiniLiteral<"team-key">;
16
+ keyId: import("zod/mini").ZodMiniString<string>;
17
+ }, z.core.$strip>], "kind">;
18
+ installationId: z.ZodString;
19
+ sessionId: z.ZodNullable<z.ZodString>;
20
+ operation: z.ZodString;
21
+ access: z.ZodEnum<{
22
+ write: "write";
23
+ external: "external";
24
+ }>;
25
+ authorityFence: z.ZodString;
26
+ idempotencyKey: z.ZodString;
27
+ input: z.ZodJSONSchema;
28
+ }, z.core.$strict>;
29
+ /** Host-generated App call metadata; the operation's input contains no actor or approval choice. */
30
+ export type OysterApprovalCall = z.output<typeof callSchema>;
31
+ /** The registry's durable App effect home. Mycelium still owns operation/schema invocation. */
32
+ export declare class OysterApprovalHome {
33
+ private readonly host;
34
+ readonly ledger: ApprovalLedger;
35
+ constructor(host: Readonly<{
36
+ owner: WatchdogSqliteOwner;
37
+ scope: TenantScope;
38
+ current: () => Promise<OysterHost>;
39
+ /** Commit only intent state and its wake, never an App effect. */
40
+ commit: <T>(write: () => Promise<T>, nextWakeAt: () => number | null) => Promise<T>;
41
+ }>);
42
+ private createLedger;
43
+ submit(raw: OysterApprovalCall): Promise<PendingApproval | ApprovalApplyResult>;
44
+ /** Bind each read/decision to current membership, not the renderer's actor bytes. */
45
+ capability(rawActor: HostActor): Approvals;
46
+ /** One native alarm quantum; apply owns the fresh grant/fence check and single claim. */
47
+ reconcile(): Promise<number | null>;
48
+ private resolve;
49
+ private execution;
50
+ }
51
+ export {};