@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
@@ -19,6 +19,7 @@
19
19
  import * as Schema from "effect/Schema";
20
20
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
21
21
  import { type Release } from "@fungi.computer/puffball";
22
+ import { type AppPublisherCoreConfigInput } from "./build/effect.js";
22
23
  import { type TeamAppCandidateManifest } from "./build/submission-contract.js";
23
24
  import { type ArtifactHeads, type ArtifactStore } from "./release.js";
24
25
  import type { PublisherTemplateSeedReceipts } from "./source/seed-receipt.js";
@@ -77,6 +78,19 @@ export declare const oysterBuildSourceSchema: Schema.Struct<{
77
78
  readonly buildRecipeDigest: Schema.refine<import("@fungi.computer/puffball").Sha256Digest, Schema.String>;
78
79
  }>;
79
80
  export type OysterBuildSource = Schema.Schema.Type<typeof oysterBuildSourceSchema>;
81
+ /** Installation is a build outcome, never a pending consent/approval record. */
82
+ export declare const oysterBuildInstallationSchema: Schema.Union<readonly [Schema.Struct<{
83
+ readonly kind: Schema.Literal<"pending">;
84
+ readonly installationId: Schema.String;
85
+ }>, Schema.Struct<{
86
+ readonly kind: Schema.Literals<readonly ["installed", "needs_consent"]>;
87
+ readonly installationId: Schema.String;
88
+ }>, Schema.Struct<{
89
+ readonly kind: Schema.Literal<"failed">;
90
+ readonly installationId: Schema.String;
91
+ readonly detail: Schema.String;
92
+ }>]>;
93
+ export type OysterBuildInstallation = Schema.Schema.Type<typeof oysterBuildInstallationSchema>;
80
94
  /** What an owner sees of one build. */
81
95
  export type OysterBuildView = Readonly<{
82
96
  jobId: string;
@@ -86,8 +100,42 @@ export type OysterBuildView = Readonly<{
86
100
  status: "running" | "succeeded" | "failed" | "revoked";
87
101
  detail: string | null;
88
102
  release: Release | null;
103
+ installation: OysterBuildInstallation | null;
104
+ }>;
105
+ /** Known preflight refusals, before a job or Computer run exists. */
106
+ export declare const oysterBuildStartReasonSchema: Schema.Literals<readonly ["invalid_revision", "no_source", "not_template_seed", "no_backend"]>;
107
+ export type OysterBuildStartReason = Schema.Schema.Type<typeof oysterBuildStartReasonSchema>;
108
+ export declare const oysterBuildStartMessages: {
109
+ invalid_revision: string;
110
+ no_source: string;
111
+ no_backend: string;
112
+ not_template_seed: string;
113
+ };
114
+ declare const OysterBuildStartError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
115
+ readonly _tag: "OysterBuildStartError";
116
+ } & Readonly<A>;
117
+ /** Local typed failure. The registry turns it into data before a DO RPC. */
118
+ export declare class OysterBuildStartError extends OysterBuildStartError_base<{
119
+ readonly reason: OysterBuildStartReason;
120
+ readonly message: string;
121
+ }> {
122
+ constructor(reason: OysterBuildStartReason);
123
+ }
124
+ export type OysterBuildStartResult = Readonly<{
125
+ kind: "started";
126
+ build: OysterBuildView;
127
+ }> | Readonly<{
128
+ kind: "refused";
129
+ reason: OysterBuildStartReason;
130
+ }>;
131
+ export type OysterBuildBackend = Readonly<{
132
+ kind: "hosted";
133
+ }> | Readonly<{
134
+ kind: "no_backend";
89
135
  }>;
90
136
  export type OysterBuildComputer = Readonly<{
137
+ /** Read the configured hosted route without starting a machine. Local machines never build Apps. */
138
+ buildBackend(): Promise<OysterBuildBackend>;
91
139
  /** Run the fixed build command; the Computer re-admits the job first.
92
140
  * A missing `output` means no terminal result was established: the
93
141
  * command may still be running, so it is never read as an exit. */
@@ -118,17 +166,28 @@ export type OysterAppBuildOptions = Readonly<{
118
166
  templates: Pick<PublisherTemplateSeedReceipts, "read">;
119
167
  now?: () => number;
120
168
  }>;
121
- /**
122
- * The SQLite tables one Oyster build owner keeps. Creation and any carrying
123
- * forward of retained facts run as one transaction of the owner's storage, so
124
- * an interrupted conversion leaves the facts as they were for a retry.
125
- */
169
+ /** Create the current build tables and index atomically; an older store is refused, never converted. */
126
170
  export declare function migrateOysterAppBuilds(owner: WatchdogSqliteOwner): void;
127
171
  /** The build owner over one tenant's SQLite, Computer and artifact store. */
128
172
  export declare function createOysterAppBuilds(options: OysterAppBuildOptions): Readonly<{
129
173
  /** Record (or replace) an App's build source; the caller is the owner. */
130
- registerSource(source: OysterBuildSource): OysterBuildSource;
174
+ registerSource: (source: OysterBuildSource) => OysterBuildSource;
175
+ /** Resolve repository identity and replace its current recipe as one transition.
176
+ * A fork's existing identity wins over the copied manifest's App id. */
177
+ registerRepositorySource(input: {
178
+ appId: string;
179
+ location: Extract<OysterBuildLocation, {
180
+ kind: "installation";
181
+ }>;
182
+ recipe: AppPublisherCoreConfigInput["recipe"];
183
+ }): {
184
+ kind: "registered";
185
+ source: OysterBuildSource;
186
+ } | {
187
+ kind: "conflict";
188
+ };
131
189
  readSource: (appId: string) => OysterBuildSource | null;
190
+ readRepositorySource: (installationId: string, repository: string) => OysterBuildSource | null;
132
191
  /**
133
192
  * Mint one build job for an App's registered source at one exact commit.
134
193
  * A template source builds only a seed revision recorded for its pinned
@@ -136,15 +195,28 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
136
195
  * Answers the job and its one run, which the caller keeps alive (the
137
196
  * upload grant exists only inside that run's fixed command).
138
197
  */
139
- start(input: {
198
+ start: (input: {
140
199
  appId: string;
141
200
  sourceRevision: string;
142
201
  requestedBy: string;
143
- }): Promise<Readonly<{
202
+ /** The Workflow's installation target. Absence leaves manual builds manual. */
203
+ installAt?: string;
204
+ /** One host-owned Workflow node reuses its job, including after restart. */
205
+ requestId?: string;
206
+ }) => Promise<Readonly<{
144
207
  build: OysterBuildView;
145
208
  run: () => Promise<OysterBuildView>;
146
209
  }>>;
147
210
  read(jobId: string): OysterBuildView | null;
211
+ /** Discover the current source's latest job from the caller's Git owner,
212
+ * never a job id remembered in a browser or another installation's repo. */
213
+ latestRepositoryBuild(installationId: string, repository: string): OysterBuildView | null;
214
+ /** One bounded host installation at a time. Only a verified successful
215
+ * build may reach the installer; the same existing row owns its outcome.
216
+ * A Workflow retry may resume a pending decision, never a terminal one. */
217
+ install(jobId: string, decide: (release: Release, installationId: string) => Promise<Exclude<OysterBuildInstallation, {
218
+ kind: "pending";
219
+ }>>): Promise<boolean>;
148
220
  /** A built Release by id, for an owner to install or select. */
149
221
  release(releaseId: string): Release | null;
150
222
  /** May this Computer execute this job now? Re-checked by the Computer. */
@@ -208,3 +280,4 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
208
280
  submitCandidate(jobId: string, manifest: TeamAppCandidateManifest, grant: string, credentialId: string): Promise<Response>;
209
281
  }>;
210
282
  export type OysterAppBuilds = ReturnType<typeof createOysterAppBuilds>;
283
+ export {};
@@ -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";
@@ -27,6 +29,7 @@ import { writeArtifactRoot, } from "./build/artifact-writer.js";
27
29
  import { buildGitPassPrefix } from "./git-auth.js";
28
30
  import { defaultAssetPolicy, readVerified, verifyRelease, } from "./release.js";
29
31
  import { appForTemplateRepository, templateRepositoryForApp, } from "./source/template-repository.js";
32
+ import { buildJobsFromOlderOyster, OlderOysterStoreError, } from "./older-oyster-store.js";
30
33
  /** A Computer's whole-build budget: a cold build on a 6 GiB Computer measured 185-270 s. */
31
34
  export const oysterAppBuildTimeoutMs = 900_000;
32
35
  /** The job, its clone admission and its upload grant end together. */
@@ -52,6 +55,42 @@ export const oysterBuildSourceSchema = Schema.Struct({
52
55
  recipe: capturedPinnedRecipeSchema,
53
56
  buildRecipeDigest: sha256DigestSchema,
54
57
  });
58
+ /** Installation is a build outcome, never a pending consent/approval record. */
59
+ export const oysterBuildInstallationSchema = Schema.Union([
60
+ Schema.Struct({
61
+ kind: Schema.Literal("pending"),
62
+ installationId: Schema.String,
63
+ }),
64
+ Schema.Struct({
65
+ kind: Schema.Literals(["installed", "needs_consent"]),
66
+ installationId: Schema.String,
67
+ }),
68
+ Schema.Struct({
69
+ kind: Schema.Literal("failed"),
70
+ installationId: Schema.String,
71
+ detail: Schema.String,
72
+ }),
73
+ ]);
74
+ const installationCodec = Schema.fromJsonString(oysterBuildInstallationSchema);
75
+ /** Known preflight refusals, before a job or Computer run exists. */
76
+ export const oysterBuildStartReasonSchema = Schema.Literals([
77
+ "invalid_revision",
78
+ "no_source",
79
+ "not_template_seed",
80
+ "no_backend",
81
+ ]);
82
+ export const oysterBuildStartMessages = {
83
+ invalid_revision: "Source revision must be git: followed by a full commit hash",
84
+ no_source: "App has no build source",
85
+ no_backend: "No hosted build backend is attached to this Computer",
86
+ not_template_seed: "Revision is not a seed of this App's template",
87
+ };
88
+ /** Local typed failure. The registry turns it into data before a DO RPC. */
89
+ export class OysterBuildStartError extends Data.TaggedError("OysterBuildStartError") {
90
+ constructor(reason) {
91
+ super({ reason, message: oysterBuildStartMessages[reason] });
92
+ }
93
+ }
55
94
  /** One Computer as the build owner reaches it. */
56
95
  /** The Computer established no terminal result for a build command, for
57
96
  * example because its connection to the container dropped mid-command. */
@@ -76,10 +115,12 @@ const jobRow = Schema.Struct({
76
115
  requested_by: Schema.String,
77
116
  credential_id: Schema.String,
78
117
  upload_grant_digest: Schema.String,
118
+ request_id: Schema.NullOr(Schema.String),
79
119
  status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
80
120
  artifact_digest: Schema.NullOr(Schema.String),
81
121
  artifact_bytes: Schema.NullOr(Schema.Number),
82
122
  detail: Schema.NullOr(Schema.String),
123
+ installation: Schema.NullOr(installationCodec),
83
124
  issued_at: Schema.Number,
84
125
  expires_at: Schema.Number,
85
126
  executable_until: Schema.Number,
@@ -107,48 +148,23 @@ function sameDigest(left, right) {
107
148
  difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
108
149
  return difference === 0;
109
150
  }
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
- */
151
+ /** Create the current build tables and index atomically; an older store is refused, never converted. */
115
152
  export function migrateOysterAppBuilds(owner) {
116
153
  const { sql } = owner;
154
+ if (buildJobsFromOlderOyster(sql))
155
+ throw new OlderOysterStoreError();
117
156
  owner.transactionSync(() => {
118
157
  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
158
  const jobColumns = `
128
159
  job_id TEXT PRIMARY KEY, app_id TEXT NOT NULL, release_id TEXT NOT NULL UNIQUE,
129
160
  source_revision TEXT NOT NULL, location TEXT NOT NULL,
130
161
  recipe TEXT NOT NULL, recipe_digest TEXT NOT NULL, requested_by TEXT NOT NULL,
131
162
  credential_id TEXT NOT NULL, upload_grant_digest TEXT NOT NULL,
132
163
  status TEXT NOT NULL, artifact_digest TEXT, artifact_bytes INTEGER, detail TEXT,
164
+ installation TEXT, request_id TEXT,
133
165
  issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL`;
134
166
  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
- }
167
+ sql.exec("CREATE UNIQUE INDEX IF NOT EXISTS oyster_build_request ON oyster_build_jobs(request_id)");
152
168
  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
169
  });
154
170
  }
@@ -172,7 +188,10 @@ export function createOysterAppBuilds(options) {
172
188
  // job, so an owner restart must not fail it. A job that reaches its expiry
173
189
  // without a result can never settle, so it fails then instead of reading
174
190
  // 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());
191
+ const failedInstallation = `CASE WHEN installation IS NULL THEN NULL ELSE
192
+ json_object('kind', 'failed', 'installationId', json_extract(installation, '$.installationId'),
193
+ 'detail', COALESCE(?, 'Build did not succeed')) END`;
194
+ 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
195
  expire();
177
196
  const decodeRecipe = (recipe) => Schema.decodeUnknownSync(recipeCodec)(recipe);
178
197
  const grantAdmits = async (job, grant) => sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant)));
@@ -209,7 +228,8 @@ export function createOysterAppBuilds(options) {
209
228
  detail: job.detail,
210
229
  });
211
230
  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);
231
+ owner.sql.exec(`UPDATE oyster_build_jobs SET installation=CASE WHEN ?='succeeded' THEN installation ELSE ${failedInstallation} END,
232
+ status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'`, status, detail, status, detail, now(), jobId);
213
233
  };
214
234
  const required = (jobId) => {
215
235
  const job = readJob(jobId);
@@ -293,16 +313,128 @@ export function createOysterAppBuilds(options) {
293
313
  status: job.status,
294
314
  detail: job.detail,
295
315
  release: readRelease(job.job_id),
316
+ installation: job.installation,
296
317
  });
318
+ const registerSource = (source) => {
319
+ if (digestPinnedRecipe(source.recipe) !== source.buildRecipeDigest)
320
+ throw new Error("Build recipe digest does not match its recipe");
321
+ 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));
322
+ return source;
323
+ };
324
+ const readRepositorySource = (installationId, repository) => {
325
+ const [row] = owner.sql
326
+ .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)
327
+ .toArray();
328
+ return row === undefined
329
+ ? null
330
+ : Schema.decodeUnknownSync(sourceCodec)(Schema.decodeUnknownSync(sourceRow)(row).source);
331
+ };
332
+ async function start(input) {
333
+ const revision = Schema.decodeUnknownOption(sourceRevisionSchema)(input.sourceRevision);
334
+ if (Option.isNone(revision))
335
+ throw new OysterBuildStartError("invalid_revision");
336
+ const sourceRevision = revision.value;
337
+ const source = readSource(input.appId);
338
+ if (source === null)
339
+ throw new OysterBuildStartError("no_source");
340
+ if (source.location.kind === "template") {
341
+ const receipt = await options.templates.read(source.appId, sourceRevision);
342
+ if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
343
+ throw new OysterBuildStartError("not_template_seed");
344
+ }
345
+ const reusedRequest = () => {
346
+ if (input.requestId !== undefined) {
347
+ const [row] = owner.sql
348
+ .exec("SELECT * FROM oyster_build_jobs WHERE request_id=?", input.requestId)
349
+ .toArray();
350
+ if (row !== undefined) {
351
+ const existing = decodeJob(row);
352
+ if (existing.app_id !== source.appId ||
353
+ existing.source_revision !== sourceRevision)
354
+ throw new Error("Build request identity conflict");
355
+ return existing.job_id;
356
+ }
357
+ }
358
+ return null;
359
+ };
360
+ const existing = reusedRequest();
361
+ if (existing !== null)
362
+ return {
363
+ build: view(required(existing)),
364
+ run: () => Promise.resolve(view(required(existing))),
365
+ };
366
+ if ((await options.computer.port.buildBackend()).kind === "no_backend")
367
+ throw new OysterBuildStartError("no_backend");
368
+ const jobId = `build_${randomHex(16)}`;
369
+ const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
370
+ const uploadGrant = randomHex(32);
371
+ const issuedAt = now();
372
+ const uploadGrantDigest = await sha256Hex(new TextEncoder().encode(uploadGrant));
373
+ const reused = owner.transactionSync(() => {
374
+ const existing = reusedRequest();
375
+ if (existing !== null)
376
+ return existing;
377
+ owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
378
+ recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
379
+ status, artifact_digest, artifact_bytes, detail, installation, request_id, issued_at, expires_at, executable_until)
380
+ 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
381
+ ? null
382
+ : Schema.encodeUnknownSync(installationCodec)({
383
+ kind: "pending",
384
+ installationId: input.installAt,
385
+ }), input.requestId ?? null, issuedAt, issuedAt + jobTtlMs);
386
+ return null;
387
+ });
388
+ if (reused !== null)
389
+ return {
390
+ build: view(required(reused)),
391
+ run: () => Promise.resolve(view(required(reused))),
392
+ };
393
+ // The grant leaves this owner only inside the fixed command.
394
+ const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
395
+ return { build: view(required(jobId)), run: () => run(jobId, command) };
396
+ }
397
+ let installationBusy = false;
398
+ function recordInstallationOutcome(jobId, outcome) {
399
+ return owner.transactionSync(() => {
400
+ const job = readJob(jobId);
401
+ if (job?.status !== "succeeded" ||
402
+ readRelease(jobId) === null ||
403
+ job.installation?.kind !== "pending" ||
404
+ job.installation.installationId !== outcome.installationId)
405
+ return false;
406
+ owner.sql.exec("UPDATE oyster_build_jobs SET installation=? WHERE job_id=?", Schema.encodeUnknownSync(installationCodec)(outcome), jobId);
407
+ return true;
408
+ });
409
+ }
297
410
  return Object.freeze({
298
411
  /** 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;
412
+ registerSource,
413
+ /** Resolve repository identity and replace its current recipe as one transition.
414
+ * A fork's existing identity wins over the copied manifest's App id. */
415
+ registerRepositorySource(input) {
416
+ return owner.transactionSync(() => {
417
+ const existing = readRepositorySource(input.location.installationId, input.location.repository);
418
+ const appId = Schema.decodeUnknownSync(appIdSchema)(existing?.appId ?? input.appId);
419
+ if (existing === null && readSource(appId) !== null)
420
+ return { kind: "conflict" };
421
+ const pinned = normalizeAppPublisherRecipe({
422
+ appId,
423
+ recipe: input.recipe,
424
+ });
425
+ return {
426
+ kind: "registered",
427
+ source: registerSource({
428
+ appId,
429
+ location: input.location,
430
+ recipe: pinned.recipe,
431
+ buildRecipeDigest: pinned.buildRecipeDigest,
432
+ }),
433
+ };
434
+ });
304
435
  },
305
436
  readSource,
437
+ readRepositorySource,
306
438
  /**
307
439
  * Mint one build job for an App's registered source at one exact commit.
308
440
  * A template source builds only a seed revision recorded for its pinned
@@ -310,33 +442,54 @@ export function createOysterAppBuilds(options) {
310
442
  * Answers the job and its one run, which the caller keeps alive (the
311
443
  * upload grant exists only inside that run's fixed command).
312
444
  */
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
- },
445
+ start,
335
446
  read(jobId) {
336
447
  expire();
337
448
  const job = readJob(jobId);
338
449
  return job === null ? null : view(job);
339
450
  },
451
+ /** Discover the current source's latest job from the caller's Git owner,
452
+ * never a job id remembered in a browser or another installation's repo. */
453
+ latestRepositoryBuild(installationId, repository) {
454
+ expire();
455
+ const source = readRepositorySource(installationId, repository);
456
+ if (source === null)
457
+ return null;
458
+ const [row] = owner.sql
459
+ .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)
460
+ .toArray();
461
+ return row === undefined ? null : view(decodeJob(row));
462
+ },
463
+ /** One bounded host installation at a time. Only a verified successful
464
+ * build may reach the installer; the same existing row owns its outcome.
465
+ * A Workflow retry may resume a pending decision, never a terminal one. */
466
+ async install(jobId, decide) {
467
+ const job = readJob(jobId);
468
+ const release = readRelease(jobId);
469
+ if (installationBusy ||
470
+ job?.status !== "succeeded" ||
471
+ release === null ||
472
+ job.installation?.kind !== "pending")
473
+ return false;
474
+ installationBusy = true;
475
+ try {
476
+ let outcome;
477
+ try {
478
+ outcome = await decide(release, job.installation.installationId);
479
+ }
480
+ catch {
481
+ outcome = {
482
+ kind: "failed",
483
+ installationId: job.installation.installationId,
484
+ detail: "Automatic installation unavailable",
485
+ };
486
+ }
487
+ return recordInstallationOutcome(jobId, outcome);
488
+ }
489
+ finally {
490
+ installationBusy = false;
491
+ }
492
+ },
340
493
  /** A built Release by id, for an owner to install or select. */
341
494
  release(releaseId) {
342
495
  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 {};