@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
@@ -0,0 +1,664 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
4
+ import { Watchdog, } from "@fungi.computer/watchdog";
5
+ import { releaseSchema } from "@fungi.computer/puffball/effect";
6
+ import { hostActorSchema } from "@fungi.computer/puffball/app-method-runtime";
7
+ import { installationIdSchema, installationSchema, isCommittedInstallation, ownerCodec, } from "./apps/installation.js";
8
+ import { instanceIds, instanceIdSchema } from "./instance-id.js";
9
+ import { tenantIdSchema, tenantScope } from "./tenant.js";
10
+ import { uiBootCheckOutcomeSchema, } from "./boot-gate.js";
11
+ // Request identity has Watchdog's public job-key bound; it is not a retry cap.
12
+ export const installationRequestIdSchema = z.string().min(1).max(256);
13
+ const scopeSchema = z.strictObject({
14
+ tenantId: ownerCodec(tenantIdSchema),
15
+ origin: z.url(),
16
+ });
17
+ const releaseCodec = ownerCodec(releaseSchema);
18
+ const identity = {
19
+ requestId: installationRequestIdSchema,
20
+ requestedInstallationId: installationIdSchema,
21
+ installationId: installationIdSchema,
22
+ release: releaseCodec,
23
+ };
24
+ const creationSchema = z.enum(["fresh", "replacement", "reminted"]);
25
+ const refusalSchema = z.strictObject({
26
+ state: z.literal("refused"),
27
+ status: z.number().int().min(400).max(599),
28
+ code: z.string().min(1).max(200),
29
+ });
30
+ const successSchema = z.strictObject({
31
+ state: z.literal("succeeded"),
32
+ installation: installationSchema,
33
+ creation: creationSchema,
34
+ uiCheck: uiBootCheckOutcomeSchema.nullable(),
35
+ });
36
+ const unconfirmedSchema = z.strictObject({
37
+ state: z.literal("unconfirmed"),
38
+ outcome: z.enum(["outcome_unknown", "interrupted", "cancelled"]),
39
+ });
40
+ /** Application evidence, never a classification of an edge's HTTP response. */
41
+ const installationOutcomeSchema = z.discriminatedUnion("state", [
42
+ successSchema,
43
+ refusalSchema,
44
+ unconfirmedSchema,
45
+ ]);
46
+ const pendingOperationSchema = z.strictObject({
47
+ ...identity,
48
+ state: z.literal("pending"),
49
+ });
50
+ const successReferenceSchema = z.strictObject({
51
+ ...identity,
52
+ state: z.literal("succeeded"),
53
+ reference: z.strictObject({
54
+ generation: z.string().min(1),
55
+ instanceId: ownerCodec(instanceIdSchema),
56
+ authorityVersion: z.number().int().positive(),
57
+ status: z.enum(["enabled", "disabled", "deleted"]),
58
+ }),
59
+ creation: creationSchema,
60
+ uiCheck: uiBootCheckOutcomeSchema.nullable(),
61
+ });
62
+ function matchesReference(operation, installation) {
63
+ return isCommittedInstallation(installation, {
64
+ owner: { kind: "installation", id: operation.installationId },
65
+ instanceId: operation.reference.instanceId,
66
+ appId: operation.release.appId,
67
+ releaseId: operation.release.releaseId,
68
+ authorityVersion: operation.reference.authorityVersion,
69
+ status: operation.reference.status,
70
+ }, operation.release);
71
+ }
72
+ const terminalOperationSchema = z.discriminatedUnion("state", [
73
+ successReferenceSchema,
74
+ refusalSchema.extend(identity),
75
+ unconfirmedSchema.extend(identity),
76
+ ]);
77
+ /** One wire contract for admission and subsequent authoritative observation. */
78
+ export const installationOperationSchema = z.discriminatedUnion("state", [
79
+ pendingOperationSchema,
80
+ successReferenceSchema
81
+ .extend({ installation: installationSchema.nullable() })
82
+ .refine((operation) => operation.installation === null ||
83
+ matchesReference(operation, operation.installation), "Installation does not match its committed reference"),
84
+ refusalSchema.extend(identity),
85
+ unconfirmedSchema.extend(identity),
86
+ ]);
87
+ const installationAdmissionSchema = z.strictObject({
88
+ requestId: installationRequestIdSchema,
89
+ installationId: installationIdSchema,
90
+ release: releaseCodec,
91
+ actor: hostActorSchema,
92
+ intent: z.enum(["install-or-select", "install-or-remint", "create-only"]),
93
+ // A Workflow may reuse the owner's approval, never grant new authority.
94
+ origin: z.enum(["owner-direct", "workflow"]),
95
+ });
96
+ const planSchema = z
97
+ .strictObject({
98
+ installation: installationSchema,
99
+ hasBackend: z.boolean(),
100
+ prepared: z.boolean(),
101
+ uiCheck: uiBootCheckOutcomeSchema.nullable().optional(),
102
+ })
103
+ .refine((plan) => !plan.prepared ||
104
+ (plan.uiCheck !== undefined &&
105
+ (plan.installation.status !== "enabled" || plan.uiCheck !== null)));
106
+ /** Retained intent and exact baseline; recovery never re-reduces a newer row. */
107
+ export const retainedInstallationCommandSchema = z.strictObject({
108
+ admission: installationAdmissionSchema,
109
+ scope: scopeSchema,
110
+ installationId: installationIdSchema,
111
+ instanceId: ownerCodec(instanceIdSchema),
112
+ generation: z.string().min(1),
113
+ baseline: installationSchema.nullable(),
114
+ acceptedAt: z.number().int().nonnegative(),
115
+ creation: creationSchema,
116
+ plan: planSchema.nullable(),
117
+ operation: pendingOperationSchema,
118
+ });
119
+ const settledInstallationCommandSchema = z.strictObject({
120
+ kind: z.literal("settled"),
121
+ inputDigest: z.string().regex(/^[a-f0-9]{64}$/u),
122
+ scope: scopeSchema,
123
+ generation: z.string().min(1),
124
+ acceptedAt: z.number().int().nonnegative(),
125
+ settledAt: z.number().int().nonnegative(),
126
+ operation: terminalOperationSchema,
127
+ });
128
+ const storedCommandSchema = z.union([
129
+ retainedInstallationCommandSchema,
130
+ settledInstallationCommandSchema,
131
+ ]);
132
+ const inputDigest = (input) => createHash("sha256")
133
+ .update(JSON.stringify(installationAdmissionSchema.parse(input)))
134
+ .digest("hex");
135
+ const installationOperationRequestSchema = z.discriminatedUnion("action", [
136
+ z.strictObject({
137
+ action: z.literal("admit"),
138
+ scope: scopeSchema,
139
+ input: installationAdmissionSchema,
140
+ }),
141
+ z.strictObject({
142
+ action: z.literal("read"),
143
+ scope: scopeSchema,
144
+ requestId: installationRequestIdSchema,
145
+ installationId: installationIdSchema,
146
+ }),
147
+ z.strictObject({
148
+ action: z.literal("load"),
149
+ scope: scopeSchema,
150
+ requestId: installationRequestIdSchema,
151
+ }),
152
+ z.strictObject({
153
+ action: z.literal("plan"),
154
+ scope: scopeSchema,
155
+ requestId: installationRequestIdSchema,
156
+ installation: installationSchema,
157
+ hasBackend: z.boolean(),
158
+ }),
159
+ z.strictObject({
160
+ action: z.literal("commit"),
161
+ scope: scopeSchema,
162
+ requestId: installationRequestIdSchema,
163
+ uiCheck: uiBootCheckOutcomeSchema.nullable(),
164
+ }),
165
+ ]);
166
+ const installationOperationReplySchema = z.discriminatedUnion("kind", [
167
+ z.strictObject({
168
+ kind: z.literal("refused"),
169
+ code: z.enum([
170
+ "request_conflict",
171
+ "stale_authority",
172
+ "needs_consent",
173
+ "unavailable",
174
+ "stored_operation_invalid",
175
+ ]),
176
+ }),
177
+ z.strictObject({
178
+ kind: z.literal("operation"),
179
+ operation: installationOperationSchema.nullable(),
180
+ }),
181
+ z.strictObject({
182
+ kind: z.literal("command"),
183
+ command: retainedInstallationCommandSchema.nullable(),
184
+ }),
185
+ z.strictObject({
186
+ kind: z.literal("commit"),
187
+ commit: z.discriminatedUnion("ok", [
188
+ z.strictObject({ ok: z.literal(true) }),
189
+ z.strictObject({
190
+ ok: z.literal(false),
191
+ code: z.literal("stale_authority"),
192
+ }),
193
+ ]),
194
+ }),
195
+ ]);
196
+ export class InstallationOperationError extends Error {
197
+ code;
198
+ constructor(code) {
199
+ super(code === "stored_operation_invalid"
200
+ ? "Stored installation operation is incompatible or invalid; republish this standalone preview."
201
+ : code);
202
+ this.code = code;
203
+ }
204
+ }
205
+ /** Decode ordinary RPC replies and rehydrate only the owner's closed refusal. */
206
+ export function installationOperationsPort(send, scope) {
207
+ const call = async (request) => {
208
+ const reply = installationOperationReplySchema.parse(await send(request));
209
+ if (reply.kind === "refused")
210
+ throw new InstallationOperationError(reply.code);
211
+ return reply;
212
+ };
213
+ return {
214
+ async admit(input) {
215
+ const reply = await call({ action: "admit", scope, input });
216
+ if (reply.kind !== "operation" || reply.operation === null)
217
+ throw new InstallationOperationError("unavailable");
218
+ return reply.operation;
219
+ },
220
+ async read(requestId, installationId) {
221
+ const reply = await call({
222
+ action: "read",
223
+ scope,
224
+ requestId,
225
+ installationId,
226
+ });
227
+ if (reply.kind !== "operation")
228
+ throw new InstallationOperationError("unavailable");
229
+ return reply.operation;
230
+ },
231
+ async load(requestId) {
232
+ const reply = await call({ action: "load", scope, requestId });
233
+ if (reply.kind !== "command")
234
+ throw new InstallationOperationError("unavailable");
235
+ return reply.command;
236
+ },
237
+ async plan(requestId, installation, hasBackend) {
238
+ const reply = await call({
239
+ action: "plan",
240
+ scope,
241
+ requestId,
242
+ installation,
243
+ hasBackend,
244
+ });
245
+ if (reply.kind !== "command" || reply.command === null)
246
+ throw new InstallationOperationError("unavailable");
247
+ return reply.command;
248
+ },
249
+ async commit(requestId, uiCheck) {
250
+ const reply = await call({ action: "commit", scope, requestId, uiCheck });
251
+ if (reply.kind !== "commit")
252
+ throw new InstallationOperationError("unavailable");
253
+ return reply.commit;
254
+ },
255
+ };
256
+ }
257
+ export const installationCommandIds = defineIdFamily({
258
+ family: "app-installation",
259
+ version: 1,
260
+ kinds: {
261
+ installation: { prefix: "installation" },
262
+ request: { prefix: "installreq" },
263
+ },
264
+ });
265
+ const storedRow = z.strictObject({
266
+ command: z.string(),
267
+ provisional_outcome: z.string().nullable(),
268
+ });
269
+ const settledRetentionMs = 90 * 24 * 60 * 60 * 1_000;
270
+ // A maintenance quantum, never an admission quota or lifetime history limit.
271
+ const retentionBatchSize = 16;
272
+ const retentionCandidateRow = z.strictObject({
273
+ request_id: installationRequestIdSchema,
274
+ installation_id: installationIdSchema,
275
+ settled_at: z.number().int().nonnegative(),
276
+ retention_due_at: z.number().int().nonnegative(),
277
+ provisional_outcome: z.string().nullable(),
278
+ });
279
+ const retentionWakeRow = z.strictObject({
280
+ due: z.number().int().nonnegative().nullable(),
281
+ });
282
+ /** Current contract only: obsolete preview data is refused, never translated. */
283
+ function readStored(schema, encoded) {
284
+ try {
285
+ return schema.parse(JSON.parse(encoded));
286
+ }
287
+ catch (cause) {
288
+ if (cause instanceof SyntaxError || cause instanceof z.ZodError)
289
+ throw new InstallationOperationError("stored_operation_invalid");
290
+ throw cause;
291
+ }
292
+ }
293
+ const jobKey = (requestId) => `installation:${createHash("sha256").update(requestId).digest("hex")}`;
294
+ const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
295
+ const version = (record) => record?.authority.authorityVersion ?? 0;
296
+ /** Whether a plan only reuses, or narrows, the approval its baseline holds. */
297
+ function reusesApproval(baseline, next) {
298
+ const grants = baseline?.authority.approvedGrants ?? [];
299
+ const methods = baseline?.approvedAnonymousMethods ?? [];
300
+ return (next.authority.approvedGrants.every((grant) => grants.some((held) => held.capability === grant.capability &&
301
+ same(held.resources, grant.resources) &&
302
+ grant.operations.every((operation) => held.operations.includes(operation)))) &&
303
+ (next.authority.approval === null ||
304
+ same(next.authority.approval, baseline?.authority.approval ?? null)) &&
305
+ next.approvedAnonymousMethods.every((method) => methods.some((held) => held.name === method.name && held.digest === method.digest)) &&
306
+ (next.anonymousApproval === null ||
307
+ same(next.anonymousApproval, baseline?.anonymousApproval ?? null)));
308
+ }
309
+ /**
310
+ * One registry-owned command journal joined to maintained Watchdog transactions.
311
+ * The executor runs outside this owner. No release bytes or guest work enter it.
312
+ */
313
+ export async function createInstallationOperations(ports) {
314
+ const { owner } = ports;
315
+ owner.sql.exec(`CREATE TABLE IF NOT EXISTS installation_commands (
316
+ request_id TEXT PRIMARY KEY, installation_id TEXT NOT NULL,
317
+ command TEXT NOT NULL, provisional_outcome TEXT,
318
+ settled_at INTEGER, retention_due_at INTEGER
319
+ ) STRICT`);
320
+ owner.sql.exec(`CREATE UNIQUE INDEX IF NOT EXISTS installation_commands_latest
321
+ ON installation_commands(installation_id)
322
+ WHERE settled_at IS NOT NULL AND retention_due_at IS NULL`);
323
+ owner.sql.exec(`CREATE INDEX IF NOT EXISTS installation_commands_retention
324
+ ON installation_commands(retention_due_at)
325
+ WHERE retention_due_at IS NOT NULL`);
326
+ const load = (requestId) => {
327
+ const row = owner.sql
328
+ .exec("SELECT command,provisional_outcome FROM installation_commands WHERE request_id=?", requestId)
329
+ .toArray()[0];
330
+ if (row === undefined)
331
+ return null;
332
+ const stored = storedRow.parse(row);
333
+ // A stale provisional result must refuse owner reads too, not remain
334
+ // invisibly pending until an alarm tries to settle it.
335
+ if (stored.provisional_outcome !== null)
336
+ readStored(installationOutcomeSchema, stored.provisional_outcome);
337
+ return readStored(storedCommandSchema, stored.command);
338
+ };
339
+ const save = (command) => {
340
+ const stored = storedCommandSchema.parse(command);
341
+ if ("kind" in stored)
342
+ owner.sql.exec(`UPDATE installation_commands
343
+ SET retention_due_at=settled_at+?
344
+ WHERE installation_id=? AND settled_at IS NOT NULL
345
+ AND retention_due_at IS NULL AND request_id<>?`, settledRetentionMs, stored.operation.installationId, stored.operation.requestId);
346
+ owner.sql.exec(`INSERT INTO installation_commands
347
+ (request_id,installation_id,command,settled_at,retention_due_at)
348
+ VALUES (?,?,?,?,NULL)
349
+ ON CONFLICT(request_id) DO UPDATE SET
350
+ command=excluded.command,settled_at=excluded.settled_at,
351
+ retention_due_at=excluded.retention_due_at`, stored.operation.requestId, stored.operation.installationId, JSON.stringify(stored), "kind" in stored ? stored.settledAt : null);
352
+ };
353
+ const pruneSettled = () => {
354
+ owner.transactionSync(() => {
355
+ const candidates = owner.sql
356
+ .exec(`SELECT request_id,installation_id,settled_at,retention_due_at,provisional_outcome
357
+ FROM installation_commands WHERE retention_due_at<=?
358
+ ORDER BY retention_due_at,rowid LIMIT ?`, Date.now(), retentionBatchSize)
359
+ .toArray();
360
+ for (const candidate of candidates) {
361
+ const parsed = retentionCandidateRow.safeParse(candidate);
362
+ if (!parsed.success)
363
+ throw new InstallationOperationError("stored_operation_invalid");
364
+ const row = parsed.data;
365
+ const command = load(row.request_id);
366
+ if (command === null ||
367
+ !("kind" in command) ||
368
+ command.operation.requestId !== row.request_id ||
369
+ command.operation.installationId !== row.installation_id ||
370
+ command.settledAt !== row.settled_at ||
371
+ row.retention_due_at !== command.settledAt + settledRetentionMs ||
372
+ row.provisional_outcome !== null)
373
+ throw new InstallationOperationError("stored_operation_invalid");
374
+ owner.sql.exec("DELETE FROM installation_commands WHERE request_id=? AND retention_due_at=?", row.request_id, row.retention_due_at);
375
+ }
376
+ });
377
+ };
378
+ const nextRetentionWake = () => retentionWakeRow.parse(owner.sql
379
+ .exec(`SELECT MIN(retention_due_at) AS due FROM installation_commands
380
+ WHERE retention_due_at IS NOT NULL`)
381
+ .toArray()[0]).due;
382
+ const operationOf = (command) => {
383
+ const operation = command.operation;
384
+ if (operation.state !== "succeeded")
385
+ return operation;
386
+ const current = ports.generation() === command.generation &&
387
+ command.generation === operation.reference.generation
388
+ ? ports.get(operation.installationId)
389
+ : null;
390
+ return {
391
+ ...operation,
392
+ installation: matchesReference(operation, current) ? current : null,
393
+ };
394
+ };
395
+ // Only the running executor proposes evidence. Watchdog's private claim
396
+ // decides whether it may become terminal, in the settlement transaction.
397
+ const provisional = (requestId) => {
398
+ const row = owner.sql
399
+ .exec("SELECT provisional_outcome FROM installation_commands WHERE request_id=?", requestId)
400
+ .toArray()[0];
401
+ const encoded = row === undefined
402
+ ? null
403
+ : z
404
+ .strictObject({ provisional_outcome: z.string().nullable() })
405
+ .parse(row).provisional_outcome;
406
+ return encoded === null
407
+ ? null
408
+ : readStored(installationOutcomeSchema, encoded);
409
+ };
410
+ const watchdog = await Watchdog.make({
411
+ owner,
412
+ isAlive: () => Promise.resolve(false),
413
+ wake: { recompute: () => ports.scheduleWake(Date.now()) },
414
+ project: ({ settled }) => {
415
+ if (settled === undefined)
416
+ return;
417
+ const payload = installationRequestIdSchema.parse(settled.payload);
418
+ const command = load(payload);
419
+ if (command === null || !("admission" in command))
420
+ return;
421
+ const proposed = provisional(payload);
422
+ const definitive = (proposed?.state === "succeeded" &&
423
+ settled.outcome === "completed" &&
424
+ ports.generation() === command.generation &&
425
+ same(ports.get(command.installationId), proposed.installation)) ||
426
+ (proposed?.state === "refused" && settled.outcome === "failed");
427
+ const result = definitive
428
+ ? proposed
429
+ : {
430
+ state: "unconfirmed",
431
+ outcome: settled.outcome === "interrupted" ||
432
+ settled.outcome === "cancelled"
433
+ ? settled.outcome
434
+ : "outcome_unknown",
435
+ };
436
+ const operation = terminalOperationSchema.parse(result.state === "succeeded"
437
+ ? {
438
+ ...command.operation,
439
+ state: result.state,
440
+ reference: {
441
+ generation: command.generation,
442
+ instanceId: result.installation.instanceId,
443
+ authorityVersion: result.installation.authority.authorityVersion,
444
+ status: result.installation.status,
445
+ },
446
+ creation: result.creation,
447
+ uiCheck: result.uiCheck,
448
+ }
449
+ : { ...command.operation, ...result });
450
+ save({
451
+ kind: "settled",
452
+ inputDigest: inputDigest(command.admission),
453
+ scope: command.scope,
454
+ generation: command.generation,
455
+ acceptedAt: command.acceptedAt,
456
+ settledAt: Date.now(),
457
+ operation,
458
+ });
459
+ owner.sql.exec("UPDATE installation_commands SET provisional_outcome=NULL WHERE request_id=?", payload);
460
+ },
461
+ execute: async (job) => {
462
+ const command = load(installationRequestIdSchema.parse(job.payload));
463
+ if (command === null || !("admission" in command))
464
+ return { kind: "outcome_unknown" };
465
+ // Persist the wake handoff before awaiting work in another execution context.
466
+ await ports.scheduleWake(Date.now());
467
+ const result = installationOutcomeSchema.parse(await ports.execute(command));
468
+ const latest = load(command.admission.requestId);
469
+ if (latest === null || !("admission" in latest))
470
+ throw new InstallationOperationError("stale_authority");
471
+ if (result.state === "succeeded" &&
472
+ (latest.plan === null ||
473
+ !same(result.installation, latest.plan.installation) ||
474
+ result.creation !== latest.creation))
475
+ throw new InstallationOperationError("stale_authority");
476
+ owner.transactionSync(() => {
477
+ owner.sql.exec("UPDATE installation_commands SET provisional_outcome=? WHERE request_id=?", JSON.stringify(result), command.admission.requestId);
478
+ });
479
+ return result.state === "succeeded"
480
+ ? { kind: "completed" }
481
+ : result.state === "refused"
482
+ ? { kind: "failed", reason: result.code }
483
+ : result.outcome === "cancelled"
484
+ ? { kind: "cancelled" }
485
+ : { kind: "outcome_unknown" };
486
+ },
487
+ });
488
+ const requireCommand = (requestId, scope) => {
489
+ const command = load(requestId);
490
+ if (command === null || !same(command.scope, scope))
491
+ throw new InstallationOperationError("unavailable");
492
+ if (!("admission" in command))
493
+ throw new InstallationOperationError("stale_authority");
494
+ return command;
495
+ };
496
+ const dispatch = async (raw) => {
497
+ const request = installationOperationRequestSchema.parse(raw);
498
+ const scope = tenantScope(request.scope.tenantId, request.scope.origin);
499
+ if (scope.durableName("registry") !== ports.registryName)
500
+ throw new InstallationOperationError("unavailable");
501
+ if (request.action === "admit") {
502
+ if (request.input.actor.kind !== "member")
503
+ throw new InstallationOperationError("unavailable");
504
+ const scheduled = await watchdog.read(jobKey(request.input.requestId));
505
+ // An orphan wake is harmless; an accepted row with no wake is not.
506
+ // No external await separates this persisted wake from atomic admission.
507
+ await ports.scheduleWake(Date.now());
508
+ return owner.transactionSync(() => {
509
+ const prior = load(request.input.requestId);
510
+ if (prior !== null) {
511
+ if (("admission" in prior
512
+ ? !same(prior.admission, request.input)
513
+ : prior.inputDigest !== inputDigest(request.input)) ||
514
+ !same(prior.scope, request.scope))
515
+ throw new InstallationOperationError("request_conflict");
516
+ return { kind: "operation", operation: operationOf(prior) };
517
+ }
518
+ // Expiring App history does not erase Watchdog's job identity.
519
+ // A concurrent admission still replays through the journal above.
520
+ if (scheduled.status === "found")
521
+ throw new InstallationOperationError("unavailable");
522
+ const requested = ports.get(request.input.installationId);
523
+ let installationId = request.input.installationId;
524
+ let creation = requested === null ? "fresh" : "replacement";
525
+ if (requested?.status === "deleted" &&
526
+ request.input.intent === "install-or-remint") {
527
+ const suffix = installationCommandIds.installation
528
+ .generate()
529
+ .slice("installation_".length);
530
+ installationId = `${installationId.slice(0, 41).replace(/-+$/u, "")}-${suffix}`;
531
+ creation = "reminted";
532
+ }
533
+ const baseline = ports.get(installationId);
534
+ if ((creation !== "replacement" ||
535
+ request.input.intent === "create-only") &&
536
+ baseline !== null)
537
+ throw new InstallationOperationError("stale_authority");
538
+ const operation = pendingOperationSchema.parse({
539
+ requestId: request.input.requestId,
540
+ requestedInstallationId: request.input.installationId,
541
+ installationId,
542
+ release: request.input.release,
543
+ state: "pending",
544
+ });
545
+ save({
546
+ admission: request.input,
547
+ scope: request.scope,
548
+ installationId,
549
+ instanceId: baseline?.instanceId ?? instanceIds.instance.generate(),
550
+ baseline,
551
+ generation: ports.generation(),
552
+ acceptedAt: Date.now(),
553
+ creation,
554
+ plan: null,
555
+ operation,
556
+ });
557
+ watchdog.transactional.enqueueAcceptedJob({
558
+ jobId: jobKey(request.input.requestId),
559
+ queue: installationId,
560
+ lane: "installation",
561
+ priority: 0,
562
+ payload: request.input.requestId,
563
+ state: "queued",
564
+ });
565
+ return { kind: "operation", operation };
566
+ });
567
+ }
568
+ if (request.action === "read" || request.action === "load") {
569
+ const command = load(request.requestId);
570
+ const admitted = command !== null && same(command.scope, request.scope) ? command : null;
571
+ return request.action === "load"
572
+ ? {
573
+ kind: "command",
574
+ command: admitted !== null && "admission" in admitted ? admitted : null,
575
+ }
576
+ : {
577
+ kind: "operation",
578
+ operation: admitted !== null &&
579
+ (admitted.operation.installationId === request.installationId ||
580
+ admitted.operation.requestedInstallationId ===
581
+ request.installationId)
582
+ ? operationOf(admitted)
583
+ : null,
584
+ };
585
+ }
586
+ const command = requireCommand(request.requestId, request.scope);
587
+ if (request.action === "plan") {
588
+ const next = request.installation;
589
+ if (next.installationId !== command.installationId ||
590
+ next.instanceId !== command.instanceId ||
591
+ next.authority.authorityVersion !== version(command.baseline) + 1 ||
592
+ !same(next.release, command.admission.release))
593
+ throw new InstallationOperationError("stale_authority");
594
+ if (command.admission.origin === "workflow" &&
595
+ !reusesApproval(command.baseline, next))
596
+ throw new InstallationOperationError("needs_consent");
597
+ if (command.plan !== null &&
598
+ (!same(command.plan.installation, next) ||
599
+ command.plan.hasBackend !== request.hasBackend))
600
+ throw new InstallationOperationError("stale_authority");
601
+ const planned = {
602
+ ...command,
603
+ plan: command.plan ?? {
604
+ installation: next,
605
+ hasBackend: request.hasBackend,
606
+ prepared: false,
607
+ },
608
+ };
609
+ owner.transactionSync(() => {
610
+ save(planned);
611
+ });
612
+ return { kind: "command", command: planned };
613
+ }
614
+ if (command.plan === null)
615
+ throw new InstallationOperationError("unavailable");
616
+ if (command.plan.prepared &&
617
+ JSON.stringify(command.plan.uiCheck) !== JSON.stringify(request.uiCheck))
618
+ throw new InstallationOperationError("stale_authority");
619
+ const prepared = {
620
+ ...command,
621
+ plan: { ...command.plan, prepared: true, uiCheck: request.uiCheck },
622
+ };
623
+ owner.transactionSync(() => {
624
+ save(prepared);
625
+ });
626
+ if (ports.generation() !== command.generation)
627
+ return { kind: "commit", commit: { ok: false, code: "stale_authority" } };
628
+ const current = ports.get(command.installationId);
629
+ // Exact replay, not another reducer invocation/version increment.
630
+ const commit = same(current, command.plan.installation)
631
+ ? { ok: true }
632
+ : await ports.compareAndPut(command.plan.installation, version(command.baseline), command.generation, command.scope);
633
+ return { kind: "commit", commit };
634
+ };
635
+ return {
636
+ async dispatch(raw) {
637
+ try {
638
+ return await dispatch(raw);
639
+ }
640
+ catch (cause) {
641
+ if (cause instanceof InstallationOperationError)
642
+ return { kind: "refused", code: cause.code };
643
+ throw cause;
644
+ }
645
+ },
646
+ async tick() {
647
+ // A failed batch rolls back and uses the host's alarm failure handling,
648
+ // rather than repeatedly arming its already-expired deadline.
649
+ pruneSettled();
650
+ try {
651
+ return await watchdog.tick();
652
+ }
653
+ finally {
654
+ if (await watchdog.hasPendingWork())
655
+ await ports.scheduleWake(Date.now());
656
+ else {
657
+ const due = nextRetentionWake();
658
+ if (due !== null)
659
+ await ports.scheduleWake(due);
660
+ }
661
+ }
662
+ },
663
+ };
664
+ }
@@ -0,0 +1,3 @@
1
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
+ /** Own one persisted store incarnation, independent of installation row revisions. */
3
+ export declare function createInstallationStoreGeneration(sql: WatchdogSqliteOwner["sql"]): () => string;
@@ -0,0 +1,16 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
3
+ const installationStoreIds = defineIdFamily({
4
+ family: "installation-store",
5
+ version: 1,
6
+ kinds: { generation: { prefix: "store" } },
7
+ });
8
+ const generationRow = Schema.decodeUnknownSync(Schema.Struct({ generation: Schema.NonEmptyString }));
9
+ /** Own one persisted store incarnation, independent of installation row revisions. */
10
+ export function createInstallationStoreGeneration(sql) {
11
+ sql.exec("CREATE TABLE IF NOT EXISTS installation_store (id INTEGER PRIMARY KEY CHECK (id=1), generation TEXT NOT NULL) STRICT");
12
+ sql.exec("INSERT OR IGNORE INTO installation_store (id,generation) VALUES(1,?)", installationStoreIds.generation.generate());
13
+ return () => generationRow(sql
14
+ .exec("SELECT generation FROM installation_store WHERE id=1")
15
+ .toArray()[0]).generation;
16
+ }