@fungi.computer/oyster 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +376 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +210 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/pack.d.ts +14 -3
  130. package/dist/pack.js +44 -19
  131. package/dist/page/app-access.d.ts +7 -0
  132. package/dist/page/app-access.js +75 -0
  133. package/dist/page/boot-host.js +57 -0
  134. package/dist/page/capabilities.d.ts +3 -3
  135. package/dist/page/capabilities.js +16 -12
  136. package/dist/page/computer-handles.d.ts +3 -3
  137. package/dist/page/computer-target.d.ts +3 -0
  138. package/dist/page/computer-target.js +9 -0
  139. package/dist/page/mount.d.ts +23 -3
  140. package/dist/page/mount.js +152 -16
  141. package/dist/page-assets.d.ts +22 -0
  142. package/dist/page-assets.js +28 -0
  143. package/dist/page-script.d.ts +3 -0
  144. package/dist/page-script.js +5 -4
  145. package/dist/release.d.ts +13 -6
  146. package/dist/release.js +10 -7
  147. package/dist/root-data.d.ts +16 -6
  148. package/dist/root-data.js +2 -2
  149. package/dist/source/index.d.ts +1 -1
  150. package/dist/source/team-source.d.ts +20 -8
  151. package/dist/source/team-source.js +39 -10
  152. package/dist/source/template-seed.d.ts +2 -2
  153. package/dist/standalone/agent-mail.d.ts +24 -0
  154. package/dist/standalone/agent-mail.js +62 -0
  155. package/dist/standalone/agent-store.d.ts +9 -0
  156. package/dist/standalone/agent-store.js +30 -0
  157. package/dist/standalone/agents.d.ts +68 -9
  158. package/dist/standalone/agents.js +428 -56
  159. package/dist/standalone/app-builds-binding.d.ts +20 -2
  160. package/dist/standalone/app-builds-binding.js +147 -23
  161. package/dist/standalone/app-set.d.ts +5 -2
  162. package/dist/standalone/app-set.js +174 -143
  163. package/dist/standalone/browser.d.ts +11 -0
  164. package/dist/standalone/browser.js +72 -0
  165. package/dist/standalone/builds.d.ts +13 -4
  166. package/dist/standalone/builds.js +28 -44
  167. package/dist/standalone/computer-egress.d.ts +2 -0
  168. package/dist/standalone/computer-egress.js +13 -4
  169. package/dist/standalone/computer-fleet.d.ts +43 -0
  170. package/dist/standalone/computer-fleet.js +200 -0
  171. package/dist/standalone/computer.d.ts +44 -9
  172. package/dist/standalone/computer.js +129 -22
  173. package/dist/standalone/install-apps-cli.js +43 -6
  174. package/dist/standalone/instance.d.ts +2 -2
  175. package/dist/standalone/instance.js +8 -11
  176. package/dist/standalone/machine-link.d.ts +354 -0
  177. package/dist/standalone/machine-link.js +368 -0
  178. package/dist/standalone/media.d.ts +2 -2
  179. package/dist/standalone/media.js +19 -16
  180. package/dist/standalone/preview.d.ts +2 -2
  181. package/dist/standalone/preview.js +37 -10
  182. package/dist/standalone/registry.d.ts +149 -9
  183. package/dist/standalone/registry.js +299 -8
  184. package/dist/standalone/worker.d.ts +19 -5
  185. package/dist/standalone/worker.js +90 -54
  186. package/dist/standalone/workflow-binding.d.ts +18 -22
  187. package/dist/standalone/workflow-binding.js +18 -18
  188. package/dist/standalone/workflow-control.d.ts +4 -0
  189. package/dist/standalone/workflow-control.js +40 -0
  190. package/dist/standalone/workflow-events.d.ts +2 -2
  191. package/dist/standalone/workflow-events.js +9 -10
  192. package/dist/standalone/workflow-git-capture.js +4 -3
  193. package/dist/standalone/workflow-host.d.ts +162 -161
  194. package/dist/standalone/workflow-host.js +101 -21
  195. package/dist/standalone/workflow-node-capture.js +11 -4
  196. package/dist/standalone/workflow-schedules.d.ts +5 -2
  197. package/dist/standalone/workflow-schedules.js +9 -10
  198. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  199. package/dist/standalone/workflow-trigger-host.js +2 -2
  200. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  201. package/dist/standalone/workflow-webhooks.js +39 -0
  202. package/dist/standalone/workflows.d.ts +140 -43
  203. package/dist/standalone/workflows.js +198 -39
  204. package/dist/storage-frame.d.ts +6 -6
  205. package/dist/testing/capabilities.js +14 -3
  206. package/dist/testing/installation.js +26 -2
  207. package/dist/testing/worker.js +1 -1
  208. package/dist/testing/workers-runtime.d.ts +8 -2
  209. package/dist/testing/workers-runtime.js +20 -2
  210. package/dist/webhook-abuse.d.ts +25 -0
  211. package/dist/webhook-abuse.js +54 -0
  212. package/dist/webhook-custody.d.ts +144 -0
  213. package/dist/webhook-custody.js +222 -0
  214. package/dist/webhook-ingress.d.ts +36 -0
  215. package/dist/webhook-ingress.js +146 -0
  216. package/dist/webhooks.d.ts +111 -0
  217. package/dist/webhooks.js +189 -0
  218. package/dist/workers-git.d.ts +12 -17
  219. package/dist/workers-git.js +60 -52
  220. package/dist/workers.d.ts +224 -13
  221. package/dist/workers.js +351 -51
  222. package/dist/workflow-agent-authority.d.ts +8 -0
  223. package/dist/workflow-agent-authority.js +42 -0
  224. package/dist/workflows.d.ts +66 -0
  225. package/dist/workflows.js +97 -0
  226. package/docs/api/README.md +63 -37
  227. package/docs/api/agents.md +17 -0
  228. package/docs/api/app-storage.md +1 -1
  229. package/docs/api/apps.md +1394 -543
  230. package/docs/api/browser/boot.md +132 -0
  231. package/docs/api/browser.md +1440 -0
  232. package/docs/api/build-contract.md +38 -25
  233. package/docs/api/build.md +263 -84
  234. package/docs/api/cloudflare.md +93 -7
  235. package/docs/api/git.md +3 -13
  236. package/docs/api/host.md +944 -118
  237. package/docs/api/index.md +320 -18
  238. package/docs/api/keys.md +24 -1
  239. package/docs/api/mcp.md +59 -12
  240. package/docs/api/modules.md +33 -40
  241. package/docs/api/pack.md +57 -8
  242. package/docs/api/page/mount.md +92 -4
  243. package/docs/api/workers-git.md +103 -24
  244. package/docs/api/workers.md +1581 -244
  245. package/docs/site/agent-quickstart.mdx +28 -14
  246. package/examples/standalone/worker.ts +3 -1
  247. package/examples/standalone/wrangler.jsonc +2 -0
  248. package/package.json +73 -71
  249. package/dist/installation-consent.d.ts +0 -81
  250. package/dist/installation-consent.js +0 -84
  251. package/dist/shell/app-directory.d.ts +0 -542
  252. package/dist/shell/app-directory.js +0 -196
  253. package/dist/shell/browser-actions.d.ts +0 -7
  254. package/dist/shell/browser-actions.js +0 -56
  255. package/dist/shell/command-host.d.ts +0 -112
  256. package/dist/shell/command-host.js +0 -502
  257. package/dist/shell/commands.d.ts +0 -25
  258. package/dist/shell/commands.js +0 -78
  259. package/dist/shell/index.d.ts +0 -8
  260. package/dist/shell/index.js +0 -8
  261. package/dist/shell/keys.d.ts +0 -41
  262. package/dist/shell/keys.js +0 -134
  263. package/dist/shell/shell-module.d.ts +0 -55
  264. package/dist/shell/shell-module.js +0 -86
  265. package/dist/shell/window-manager.d.ts +0 -128
  266. package/dist/shell/window-manager.js +0 -730
  267. package/dist/shell/workbench-contract.d.ts +0 -68
  268. package/dist/shell/workbench-directory.d.ts +0 -332
  269. package/dist/shell/workbench-directory.js +0 -35
  270. package/dist/shell/workbench-host-types.d.ts +0 -96
  271. package/dist/shell/workbench-host-types.js +0 -1
  272. package/dist/shell/workbench-host.d.ts +0 -34
  273. package/dist/shell/workbench-host.js +0 -352
  274. package/dist/shell/workbench-model.d.ts +0 -194
  275. package/dist/shell/workbench-model.js +0 -53
  276. package/dist/shell/workbench.d.ts +0 -4
  277. package/dist/shell/workbench.js +0 -2
  278. package/docs/api/shell/app-directory.md +0 -599
  279. package/docs/api/shell/command-host.md +0 -224
  280. package/docs/api/shell/commands.md +0 -197
  281. package/docs/api/shell/keys.md +0 -183
  282. package/docs/api/shell/shell-module.md +0 -204
  283. package/docs/api/shell/window-manager.md +0 -267
  284. package/docs/api/shell/workbench-directory.md +0 -119
  285. package/docs/api/shell/workbench.md +0 -205
  286. package/docs/api/shell.md +0 -1915
  287. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -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; never convert old stores. */
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";
@@ -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 {};