@fungi.computer/oyster 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/README.md +376 -72
  2. package/dist/agent-avatars.d.ts +103 -0
  3. package/dist/agent-avatars.js +66 -0
  4. package/dist/agents.d.ts +15 -4
  5. package/dist/agents.js +43 -4
  6. package/dist/app-builds.d.ts +81 -8
  7. package/dist/app-builds.js +210 -60
  8. package/dist/app-storage.d.ts +3 -3
  9. package/dist/approval-capability.d.ts +5 -0
  10. package/dist/approval-capability.js +46 -0
  11. package/dist/approval-home.d.ts +51 -0
  12. package/dist/approval-home.js +226 -0
  13. package/dist/apps/catalog.d.ts +335 -0
  14. package/dist/apps/catalog.js +46 -0
  15. package/dist/apps/gate.d.ts +26 -12
  16. package/dist/apps/gate.js +48 -22
  17. package/dist/apps/index.d.ts +3 -1
  18. package/dist/apps/index.js +3 -1
  19. package/dist/apps/installation.d.ts +147 -66
  20. package/dist/apps/installation.js +193 -86
  21. package/dist/apps/permissions.d.ts +247 -0
  22. package/dist/apps/permissions.js +255 -0
  23. package/dist/boot-capability-route.d.ts +15 -0
  24. package/dist/boot-capability-route.js +196 -0
  25. package/dist/boot-capability.d.ts +103 -0
  26. package/dist/boot-capability.js +141 -0
  27. package/dist/boot-construction.d.ts +12 -0
  28. package/dist/boot-construction.js +175 -0
  29. package/dist/boot-gate.d.ts +267 -0
  30. package/dist/boot-gate.js +236 -0
  31. package/dist/boot-host-script.d.ts +2 -0
  32. package/dist/boot-host-script.js +1 -0
  33. package/dist/boot-mount.d.ts +21 -0
  34. package/dist/boot-mount.js +65 -0
  35. package/dist/browser/boot.d.ts +73 -0
  36. package/dist/browser/boot.js +502 -0
  37. package/dist/browser/browser.d.ts +104 -0
  38. package/dist/browser/browser.js +374 -0
  39. package/dist/browser/capability-source.d.ts +76 -0
  40. package/dist/browser/capability-source.js +338 -0
  41. package/dist/browser/cdp-client.d.ts +56 -0
  42. package/dist/browser/cdp-client.js +184 -0
  43. package/dist/browser/errors.d.ts +79 -0
  44. package/dist/browser/errors.js +32 -0
  45. package/dist/browser/index.d.ts +16 -0
  46. package/dist/browser/index.js +14 -0
  47. package/dist/browser/lease-registry.d.ts +61 -0
  48. package/dist/browser/lease-registry.js +101 -0
  49. package/dist/browser/operations.d.ts +225 -0
  50. package/dist/browser/operations.js +137 -0
  51. package/dist/browser/page.d.ts +25 -0
  52. package/dist/browser/page.js +172 -0
  53. package/dist/browser/quick-reads.d.ts +29 -0
  54. package/dist/browser/quick-reads.js +160 -0
  55. package/dist/browser/sessions.d.ts +52 -0
  56. package/dist/browser/sessions.js +118 -0
  57. package/dist/browser/url-policy.d.ts +18 -0
  58. package/dist/browser/url-policy.js +58 -0
  59. package/dist/build/app-imports.d.ts +12 -0
  60. package/dist/build/app-imports.js +105 -0
  61. package/dist/build/build-policy.d.ts +5 -3
  62. package/dist/build/build-policy.js +5 -3
  63. package/dist/build/candidate-builder.d.ts +1 -1
  64. package/dist/build/candidate-cli.mjs +8428 -337
  65. package/dist/build/local-build.d.ts +2 -0
  66. package/dist/build/local-build.js +28 -6
  67. package/dist/build/snapshot-package-store.d.ts +1 -1
  68. package/dist/build/snapshot-package-store.js +7 -2
  69. package/dist/build/workspace-build.d.ts +92 -5
  70. package/dist/build/workspace-build.js +93 -19
  71. package/dist/build-egress.js +4 -4
  72. package/dist/build.d.ts +1 -1
  73. package/dist/build.js +1 -1
  74. package/dist/catalog-frame.js +1 -1
  75. package/dist/cloudflare.d.ts +13 -3
  76. package/dist/cloudflare.js +106 -8
  77. package/dist/frame-binding-wire-script.js +1 -1
  78. package/dist/frame-binding-wire.d.ts +1 -1
  79. package/dist/frame-binding-wire.js +5 -3
  80. package/dist/git-auth.d.ts +0 -2
  81. package/dist/git-auth.js +0 -2
  82. package/dist/git-journal.js +4 -4
  83. package/dist/git.d.ts +0 -1
  84. package/dist/git.js +0 -2
  85. package/dist/host.d.ts +39 -13
  86. package/dist/host.js +268 -142
  87. package/dist/index.d.ts +26 -15
  88. package/dist/index.js +40 -16
  89. package/dist/install-release-cli.d.ts +2 -0
  90. package/dist/install-release-cli.js +46 -0
  91. package/dist/installation-client.d.ts +43 -0
  92. package/dist/installation-client.js +178 -0
  93. package/dist/installation-execution.d.ts +11 -0
  94. package/dist/installation-execution.js +184 -0
  95. package/dist/installation-operations.d.ts +1437 -0
  96. package/dist/installation-operations.js +664 -0
  97. package/dist/installation-store-generation.d.ts +3 -0
  98. package/dist/installation-store-generation.js +16 -0
  99. package/dist/instance-id.d.ts +10 -0
  100. package/dist/instance-id.js +10 -0
  101. package/dist/instance-refusal.d.ts +11 -0
  102. package/dist/instance-refusal.js +21 -0
  103. package/dist/internal/delivery.js +4 -2
  104. package/dist/internal/host-gate.js +7 -1
  105. package/dist/internal/registry-installations.js +4 -4
  106. package/dist/keys.d.ts +5 -1
  107. package/dist/keys.js +10 -2
  108. package/dist/machine-link/authority.d.ts +89 -0
  109. package/dist/machine-link/authority.js +37 -0
  110. package/dist/machine-link/client.d.ts +66 -0
  111. package/dist/machine-link/client.js +94 -0
  112. package/dist/machine-link/delivery.d.ts +18 -0
  113. package/dist/machine-link/delivery.js +55 -0
  114. package/dist/machine-link/enrollment.d.ts +111 -0
  115. package/dist/machine-link/enrollment.js +286 -0
  116. package/dist/machine-link/fences.d.ts +37 -0
  117. package/dist/machine-link/fences.js +166 -0
  118. package/dist/machine-link/home.d.ts +20 -0
  119. package/dist/machine-link/home.js +24 -0
  120. package/dist/machine-link/host.d.ts +43 -0
  121. package/dist/machine-link/host.js +189 -0
  122. package/dist/machine-link/index.d.ts +15 -0
  123. package/dist/machine-link/index.js +15 -0
  124. package/dist/machine-link/link.d.ts +48 -0
  125. package/dist/machine-link/link.js +223 -0
  126. package/dist/machine-link/presence.d.ts +10 -0
  127. package/dist/machine-link/presence.js +25 -0
  128. package/dist/machine-link/workers.d.ts +6 -0
  129. package/dist/machine-link/workers.js +6 -0
  130. package/dist/mcp-owner.d.ts +2 -2
  131. package/dist/mcp-owner.js +41 -33
  132. package/dist/mcp.d.ts +15 -3
  133. package/dist/mcp.js +146 -25
  134. package/dist/pack.d.ts +14 -3
  135. package/dist/pack.js +44 -19
  136. package/dist/page/app-access.d.ts +7 -0
  137. package/dist/page/app-access.js +75 -0
  138. package/dist/page/boot-host.js +57 -0
  139. package/dist/page/capabilities.d.ts +3 -3
  140. package/dist/page/capabilities.js +16 -12
  141. package/dist/page/computer-handles.d.ts +3 -3
  142. package/dist/page/computer-target.d.ts +3 -0
  143. package/dist/page/computer-target.js +9 -0
  144. package/dist/page/mount.d.ts +23 -7
  145. package/dist/page/mount.js +152 -16
  146. package/dist/page-assets.d.ts +22 -0
  147. package/dist/page-assets.js +28 -0
  148. package/dist/page-script.d.ts +3 -0
  149. package/dist/page-script.js +5 -4
  150. package/dist/release.d.ts +13 -6
  151. package/dist/release.js +10 -7
  152. package/dist/root-data.d.ts +16 -6
  153. package/dist/root-data.js +2 -2
  154. package/dist/source/index.d.ts +1 -1
  155. package/dist/source/team-source.d.ts +20 -8
  156. package/dist/source/team-source.js +39 -10
  157. package/dist/source/template-seed.d.ts +2 -2
  158. package/dist/standalone/agent-mail.d.ts +24 -0
  159. package/dist/standalone/agent-mail.js +62 -0
  160. package/dist/standalone/agent-store.d.ts +9 -0
  161. package/dist/standalone/agent-store.js +30 -0
  162. package/dist/standalone/agents.d.ts +78 -12
  163. package/dist/standalone/agents.js +470 -59
  164. package/dist/standalone/app-builds-binding.d.ts +20 -2
  165. package/dist/standalone/app-builds-binding.js +151 -27
  166. package/dist/standalone/app-set.d.ts +5 -2
  167. package/dist/standalone/app-set.js +174 -143
  168. package/dist/standalone/browser.d.ts +11 -0
  169. package/dist/standalone/browser.js +72 -0
  170. package/dist/standalone/builds.d.ts +13 -4
  171. package/dist/standalone/builds.js +28 -44
  172. package/dist/standalone/computer-egress.d.ts +2 -0
  173. package/dist/standalone/computer-egress.js +13 -4
  174. package/dist/standalone/computer-fleet.d.ts +43 -0
  175. package/dist/standalone/computer-fleet.js +200 -0
  176. package/dist/standalone/computer.d.ts +44 -9
  177. package/dist/standalone/computer.js +132 -25
  178. package/dist/standalone/install-apps-cli.js +43 -6
  179. package/dist/standalone/instance.d.ts +2 -2
  180. package/dist/standalone/instance.js +8 -11
  181. package/dist/standalone/machine-link.d.ts +354 -0
  182. package/dist/standalone/machine-link.js +368 -0
  183. package/dist/standalone/media.d.ts +2 -2
  184. package/dist/standalone/media.js +19 -16
  185. package/dist/standalone/preview.d.ts +2 -2
  186. package/dist/standalone/preview.js +39 -12
  187. package/dist/standalone/registry.d.ts +149 -9
  188. package/dist/standalone/registry.js +299 -8
  189. package/dist/standalone/worker.d.ts +19 -5
  190. package/dist/standalone/worker.js +90 -54
  191. package/dist/standalone/workflow-binding.d.ts +28 -32
  192. package/dist/standalone/workflow-binding.js +20 -20
  193. package/dist/standalone/workflow-control.d.ts +4 -0
  194. package/dist/standalone/workflow-control.js +40 -0
  195. package/dist/standalone/workflow-events.d.ts +2 -2
  196. package/dist/standalone/workflow-events.js +9 -10
  197. package/dist/standalone/workflow-git-capture.js +4 -3
  198. package/dist/standalone/workflow-host.d.ts +177 -152
  199. package/dist/standalone/workflow-host.js +137 -30
  200. package/dist/standalone/workflow-node-capture.js +11 -4
  201. package/dist/standalone/workflow-schedules.d.ts +5 -2
  202. package/dist/standalone/workflow-schedules.js +9 -10
  203. package/dist/standalone/workflow-trigger-host.d.ts +88 -6
  204. package/dist/standalone/workflow-trigger-host.js +2 -2
  205. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  206. package/dist/standalone/workflow-webhooks.js +39 -0
  207. package/dist/standalone/workflows.d.ts +203 -50
  208. package/dist/standalone/workflows.js +198 -39
  209. package/dist/storage-frame.d.ts +6 -6
  210. package/dist/testing/capabilities.js +14 -3
  211. package/dist/testing/installation.js +26 -2
  212. package/dist/testing/worker.js +1 -1
  213. package/dist/testing/workers-runtime.d.ts +8 -2
  214. package/dist/testing/workers-runtime.js +20 -2
  215. package/dist/webhook-abuse.d.ts +25 -0
  216. package/dist/webhook-abuse.js +54 -0
  217. package/dist/webhook-custody.d.ts +144 -0
  218. package/dist/webhook-custody.js +222 -0
  219. package/dist/webhook-ingress.d.ts +36 -0
  220. package/dist/webhook-ingress.js +146 -0
  221. package/dist/webhooks.d.ts +111 -0
  222. package/dist/webhooks.js +189 -0
  223. package/dist/workers-git.d.ts +12 -17
  224. package/dist/workers-git.js +60 -55
  225. package/dist/workers.d.ts +224 -13
  226. package/dist/workers.js +351 -51
  227. package/dist/workflow-agent-authority.d.ts +8 -0
  228. package/dist/workflow-agent-authority.js +42 -0
  229. package/dist/workflows.d.ts +66 -0
  230. package/dist/workflows.js +97 -0
  231. package/docs/api/README.md +63 -37
  232. package/docs/api/agents.md +17 -0
  233. package/docs/api/app-storage.md +1 -1
  234. package/docs/api/apps.md +1394 -543
  235. package/docs/api/browser/boot.md +132 -0
  236. package/docs/api/browser.md +1440 -0
  237. package/docs/api/build-contract.md +38 -25
  238. package/docs/api/build.md +263 -84
  239. package/docs/api/cloudflare.md +93 -7
  240. package/docs/api/git.md +3 -13
  241. package/docs/api/host.md +944 -118
  242. package/docs/api/index.md +320 -18
  243. package/docs/api/keys.md +24 -1
  244. package/docs/api/mcp.md +59 -12
  245. package/docs/api/modules.md +33 -40
  246. package/docs/api/pack.md +57 -8
  247. package/docs/api/page/mount.md +92 -4
  248. package/docs/api/workers-git.md +103 -24
  249. package/docs/api/workers.md +1581 -244
  250. package/docs/site/agent-quickstart.mdx +28 -14
  251. package/examples/standalone/worker.ts +3 -1
  252. package/examples/standalone/wrangler.jsonc +2 -0
  253. package/package.json +73 -69
  254. package/dist/installation-consent.d.ts +0 -79
  255. package/dist/installation-consent.js +0 -82
  256. package/dist/shell/app-directory.d.ts +0 -542
  257. package/dist/shell/app-directory.js +0 -199
  258. package/dist/shell/browser-actions.d.ts +0 -7
  259. package/dist/shell/browser-actions.js +0 -56
  260. package/dist/shell/command-host.d.ts +0 -112
  261. package/dist/shell/command-host.js +0 -502
  262. package/dist/shell/commands.d.ts +0 -25
  263. package/dist/shell/commands.js +0 -78
  264. package/dist/shell/index.d.ts +0 -8
  265. package/dist/shell/index.js +0 -8
  266. package/dist/shell/keys.d.ts +0 -41
  267. package/dist/shell/keys.js +0 -134
  268. package/dist/shell/shell-module.d.ts +0 -55
  269. package/dist/shell/shell-module.js +0 -86
  270. package/dist/shell/window-manager.d.ts +0 -124
  271. package/dist/shell/window-manager.js +0 -711
  272. package/dist/shell/workbench-contract.d.ts +0 -68
  273. package/dist/shell/workbench-directory.d.ts +0 -332
  274. package/dist/shell/workbench-directory.js +0 -35
  275. package/dist/shell/workbench-host-types.d.ts +0 -96
  276. package/dist/shell/workbench-host-types.js +0 -1
  277. package/dist/shell/workbench-host.d.ts +0 -34
  278. package/dist/shell/workbench-host.js +0 -347
  279. package/dist/shell/workbench-model.d.ts +0 -192
  280. package/dist/shell/workbench-model.js +0 -53
  281. package/dist/shell/workbench.d.ts +0 -4
  282. package/dist/shell/workbench.js +0 -2
  283. package/docs/api/shell/app-directory.md +0 -599
  284. package/docs/api/shell/command-host.md +0 -224
  285. package/docs/api/shell/commands.md +0 -197
  286. package/docs/api/shell/keys.md +0 -183
  287. package/docs/api/shell/shell-module.md +0 -204
  288. package/docs/api/shell/window-manager.md +0 -267
  289. package/docs/api/shell/workbench-directory.md +0 -119
  290. package/docs/api/shell/workbench.md +0 -205
  291. package/docs/api/shell.md +0 -1915
  292. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -4,7 +4,8 @@ import * as Result from "effect/Result";
4
4
  import { appCapabilityIdSchema, appApiMethodNameSchema, } from "@fungi.computer/puffball/app";
5
5
  import { hostActorSchema } from "@fungi.computer/puffball/app-method-runtime";
6
6
  import { appIdSchema, releaseSchema, sha256DigestSchema, } from "@fungi.computer/puffball/effect";
7
- import { instanceIdSchema } from "../index.js";
7
+ import { instanceIdSchema } from "../instance-id.js";
8
+ import { permissionCatalog } from "./catalog.js";
8
9
  /** Reuse the owning Effect codec at a JSON/storage boundary. */
9
10
  export function ownerCodec(schema) {
10
11
  const decode = Schema.decodeUnknownResult(schema, {
@@ -20,30 +21,28 @@ export function ownerCodec(schema) {
20
21
  });
21
22
  }
22
23
  /** Installation names cannot contain the tenant separator. */
23
- export const installationIdSchema = z
24
- .string()
25
- .regex(/^[a-z](?:[a-z0-9]|-(?!-)){0,62}$/u);
24
+ export const installationIdSchema = z.lazy(() => z.string().regex(/^[a-z](?:[a-z0-9]|-(?!-)){0,62}$/u));
26
25
  /** A resource identity minted and resolved by the host, never by this module. */
27
- const resourceRefSchema = z
26
+ export const resourceRefSchema = z.lazy(() => z
28
27
  .strictObject({
29
28
  kind: z.string().min(1),
30
29
  id: z.string().min(1),
31
30
  })
32
- .readonly();
33
- const operationsSchema = z
31
+ .readonly());
32
+ const operationsSchema = z.lazy(() => z
34
33
  .array(z.string().min(1))
35
34
  .refine((values) => new Set(values).size === values.length, "Operations must be unique")
36
- .readonly();
35
+ .readonly());
37
36
  /**
38
37
  * Consent approves capabilities, with explicit operation and resource ceilings.
39
38
  * A Release adding a method that requires an already-approved capability needs
40
39
  * no new consent; a new capability does. Release selection intersects stored
41
40
  * operations instead of adding approvals. Each Worker call is still restricted
42
41
  * to that method's declared requires, never the App's complete capability set.
43
- * Only an approved install or set-grants command mints capabilityApproved.
42
+ * Only an approved install or change-grants command mints capabilityApproved.
44
43
  * Empty operation rows retain that consent; removing the capability revokes it.
45
44
  * route:app.methods rows record approval detail, not a method allowlist: narrowing
46
- * them in set-grants does not restrict methods using an approved capability.
45
+ * them in change-grants does not restrict methods using an approved capability.
47
46
  * Exact operation rows are enforced for capability routes and backend bindings.
48
47
  */
49
48
  export const approvedGrantSchema = z
@@ -58,31 +57,139 @@ export const approvedGrantSchema = z
58
57
  })
59
58
  .readonly();
60
59
  /** One grant per capability keeps operation/resource pairs unambiguous. */
61
- const approvedGrantsSchema = z
60
+ const approvedGrantsSchema = z.lazy(() => z
62
61
  .array(approvedGrantSchema)
63
62
  .refine((grants) => new Set(grants.map((grant) => grant.capability)).size === grants.length, "Capabilities must be unique")
64
- .readonly();
65
- /** Shorthand is accepted only on a command and expanded at approval time. */
66
- const grantRequestSchema = z.union([
67
- z.literal("none"),
68
- z.literal("declared"),
69
- z
70
- .array(approvedGrantSchema
63
+ .readonly());
64
+ /** A bounded owner edit. Unnamed approvals are not part of the command. */
65
+ export const grantChangeSchema = z
66
+ .strictObject({
67
+ capability: ownerCodec(appCapabilityIdSchema),
68
+ grant: approvedGrantSchema
71
69
  .unwrap()
72
- .omit({ capabilityApproved: true })
73
- .readonly())
74
- .refine((grants) => new Set(grants.map((grant) => grant.capability)).size === grants.length, "Capabilities must be unique")
75
- .readonly(),
76
- ]);
77
- const versionSchema = z.number().int().positive();
70
+ .omit({ capabilityApproved: true, capability: true })
71
+ .readonly()
72
+ .nullable(),
73
+ })
74
+ .readonly();
75
+ const grantChangesSchema = z
76
+ .array(grantChangeSchema)
77
+ .min(1)
78
+ .refine((changes) => new Set(changes.map((change) => change.capability)).size ===
79
+ changes.length, "Capabilities must be unique")
80
+ .readonly();
81
+ const versionSchema = z.lazy(() => z.number().int().positive());
78
82
  /** An owner's approval of a fixed operation snapshot. */
79
- const approvalSchema = z
83
+ const approvalSchema = z.lazy(() => z
80
84
  .strictObject({
81
85
  by: hostActorSchema,
82
86
  at: z.number().int().nonnegative(),
83
87
  authorityVersion: versionSchema,
84
88
  })
85
- .readonly();
89
+ .readonly());
90
+ /** The complete grant state. Hosts own storage and compare-and-set commits. */
91
+ export const appAuthoritySchema = z.strictObject({
92
+ authorityVersion: versionSchema,
93
+ declaredOperationsDigest: ownerCodec(sha256DigestSchema),
94
+ approvedGrants: approvedGrantsSchema,
95
+ approval: approvalSchema.nullable(),
96
+ });
97
+ export const grantCommandSchema = z.discriminatedUnion("kind", [
98
+ z.strictObject({ kind: z.literal("install") }),
99
+ z.strictObject({
100
+ kind: z.literal("change-grants"),
101
+ changes: grantChangesSchema,
102
+ }),
103
+ z.strictObject({ kind: z.literal("select-release") }),
104
+ z.strictObject({ kind: z.literal("delete") }),
105
+ ]);
106
+ /** One host-independent approval policy; selection only intersects existing grants. */
107
+ export function applyGrantCommand(current, rawCommand, context) {
108
+ const refuse = (code) => Result.fail({ kind: "refused", code });
109
+ const parsed = grantCommandSchema.safeParse(rawCommand);
110
+ if (!parsed.success)
111
+ return refuse("invalid_command");
112
+ const command = parsed.data;
113
+ if ((current === null && command.kind !== "install") ||
114
+ (current !== null && command.kind === "install"))
115
+ return refuse("invalid_command");
116
+ if ((context.expectedVersion ?? 0) !== (current?.authorityVersion ?? 0) ||
117
+ (current !== null &&
118
+ command.kind !== "select-release" &&
119
+ current.declaredOperationsDigest !== context.declaredOperations.digest))
120
+ return refuse("stale_authority");
121
+ if (context.principal.role === null ||
122
+ context.principal.actor.kind === "visitor" ||
123
+ (command.kind === "change-grants" && context.principal.role !== "owner"))
124
+ return refuse("not_owner");
125
+ const declared = (capability) => context.declaredOperations.capabilities.find((row) => row.capability === capability)?.operations ?? [];
126
+ const version = (current?.authorityVersion ?? 0) + 1;
127
+ let grants = current?.approvedGrants ?? [];
128
+ let approval = current?.approval ?? null;
129
+ switch (command.kind) {
130
+ case "install":
131
+ grants =
132
+ context.principal.role === "owner"
133
+ ? context.declaration.capabilities
134
+ .filter((capability) => permissionCatalog[capability].consent === "install")
135
+ .map((capability) => ({
136
+ capability,
137
+ capabilityApproved: true,
138
+ operations: declared(capability),
139
+ resources: "tenant-scope",
140
+ }))
141
+ : [];
142
+ break;
143
+ case "change-grants": {
144
+ for (const { capability, grant } of command.changes) {
145
+ if (!context.declaration.capabilities.includes(capability) ||
146
+ grant?.operations.some((operation) => !declared(capability).includes(operation)))
147
+ return refuse("not_declared");
148
+ const scope = permissionCatalog[capability].scope;
149
+ if (grant !== null &&
150
+ grant.resources !== "tenant-scope" &&
151
+ (scope === null ||
152
+ grant.resources.some((resource) => resource.kind !== scope)))
153
+ return refuse("invalid_command");
154
+ }
155
+ const changed = new Map(command.changes.map((change) => [change.capability, change.grant]));
156
+ grants = [
157
+ ...grants.filter((grant) => !changed.has(grant.capability)),
158
+ ...command.changes.flatMap(({ capability, grant }) => grant === null ||
159
+ (grant.resources !== "tenant-scope" && grant.resources.length === 0)
160
+ ? []
161
+ : [{ capability, ...grant, capabilityApproved: true }]),
162
+ ];
163
+ break;
164
+ }
165
+ case "select-release":
166
+ grants = grants
167
+ .filter((grant) => context.declaration.capabilities.includes(grant.capability))
168
+ .map((grant) => ({
169
+ ...grant,
170
+ operations: grant.operations.filter((operation) => declared(grant.capability).includes(operation)),
171
+ }));
172
+ break;
173
+ case "delete":
174
+ grants = [];
175
+ break;
176
+ }
177
+ if (grants.length === 0)
178
+ approval = null;
179
+ else if (command.kind === "install" || command.kind === "change-grants") {
180
+ approval = {
181
+ by: context.principal.actor,
182
+ at: context.now,
183
+ authorityVersion: version,
184
+ };
185
+ }
186
+ return Result.succeed({
187
+ authorityVersion: version,
188
+ declaredOperationsDigest: context.declaredOperations.digest,
189
+ approvedGrants: grants,
190
+ approval,
191
+ });
192
+ }
86
193
  const anonymousMethodsSchema = z
87
194
  .array(z
88
195
  .strictObject({
@@ -96,7 +203,7 @@ const anonymousMethodsSchema = z
96
203
  }), "Anonymous method identities must have unique, sorted names")
97
204
  .readonly();
98
205
  /** Immutable operation facts derived from descriptor methods and the binding registry. */
99
- export const declaredOperationsSchema = z.strictObject({
206
+ export const declaredOperationsSchema = z.lazy(() => z.strictObject({
100
207
  artifactDigest: ownerCodec(sha256DigestSchema),
101
208
  digest: ownerCodec(sha256DigestSchema),
102
209
  anonymousMethods: anonymousMethodsSchema,
@@ -108,7 +215,7 @@ export const declaredOperationsSchema = z.strictObject({
108
215
  })
109
216
  .readonly())
110
217
  .readonly(),
111
- });
218
+ }));
112
219
  /** Resolved immutable Release plus host-owned installation and approval state. */
113
220
  export const installationSchema = z
114
221
  .strictObject({
@@ -117,12 +224,9 @@ export const installationSchema = z
117
224
  appId: ownerCodec(appIdSchema),
118
225
  release: ownerCodec(releaseSchema),
119
226
  releasePin: z.enum(["pinned", "floating"]),
120
- authorityVersion: versionSchema,
227
+ authority: appAuthoritySchema,
121
228
  status: z.enum(["enabled", "disabled", "deleted"]),
122
229
  visibility: z.enum(["private", "public"]),
123
- declaredOperationsDigest: ownerCodec(sha256DigestSchema),
124
- approvedGrants: approvedGrantsSchema,
125
- approval: approvalSchema.nullable(),
126
230
  approvedAnonymousMethods: anonymousMethodsSchema,
127
231
  anonymousApproval: approvalSchema.nullable(),
128
232
  })
@@ -134,18 +238,17 @@ export const installationSchema = z
134
238
  : installation.status !== "deleted" &&
135
239
  installation.approvedAnonymousMethods.length > 0 &&
136
240
  approval.by.kind === "member" &&
137
- approval.authorityVersion <= installation.authorityVersion;
241
+ approval.authorityVersion <= installation.authority.authorityVersion;
138
242
  }, "Anonymous authority requires a retained human approval receipt");
139
243
  const releaseFields = {
140
244
  release: ownerCodec(releaseSchema),
141
245
  releasePin: installationSchema.shape.releasePin,
142
246
  };
143
247
  /** Host commands; floating targets have already been resolved to immutable Releases. */
144
- export const installationCommandSchema = z.discriminatedUnion("kind", [
248
+ export const installationCommandSchema = z.lazy(() => z.discriminatedUnion("kind", [
145
249
  z.strictObject({
146
250
  kind: z.literal("install"),
147
251
  ...releaseFields,
148
- grants: grantRequestSchema,
149
252
  }),
150
253
  z.strictObject({ kind: z.literal("select-release"), ...releaseFields }),
151
254
  z.strictObject({
@@ -156,14 +259,17 @@ export const installationCommandSchema = z.discriminatedUnion("kind", [
156
259
  kind: z.literal("set-visibility"),
157
260
  visibility: installationSchema.shape.visibility,
158
261
  }),
159
- z.strictObject({ kind: z.literal("set-grants"), grants: grantRequestSchema }),
262
+ z.strictObject({
263
+ kind: z.literal("change-grants"),
264
+ changes: grantChangesSchema,
265
+ }),
160
266
  z.strictObject({
161
267
  kind: z.literal("approve-anonymous"),
162
268
  methods: anonymousMethodsSchema,
163
269
  }),
164
- ]);
270
+ ]));
165
271
  /** Closed refusals: failed commands produce no installation update. */
166
- export const installationRefusalSchema = z.strictObject({
272
+ export const installationRefusalSchema = z.lazy(() => z.strictObject({
167
273
  kind: z.literal("refused"),
168
274
  code: z.enum([
169
275
  "unavailable",
@@ -173,7 +279,7 @@ export const installationRefusalSchema = z.strictObject({
173
279
  "release_app_mismatch",
174
280
  "invalid_command",
175
281
  ]),
176
- });
282
+ }));
177
283
  /** Pure transition; hosts retain transactions, receipts, epochs and delivery. */
178
284
  export function applyInstallationCommand(current, rawCommand, context) {
179
285
  const refuse = (code) => Result.fail({ kind: "refused", code });
@@ -190,12 +296,10 @@ export function applyInstallationCommand(current, rawCommand, context) {
190
296
  return refuse("stale_authority");
191
297
  }
192
298
  else if (command.kind === "install" ||
193
- context.expectedVersion !== current.authorityVersion) {
299
+ context.expectedVersion !== current.authority.authorityVersion) {
194
300
  return refuse("stale_authority");
195
301
  }
196
- if ((command.kind === "set-grants" ||
197
- (command.kind === "install" && command.grants !== "none")) &&
198
- context.role !== "owner")
302
+ if (command.kind === "change-grants" && context.role !== "owner")
199
303
  return refuse("not_owner");
200
304
  const anonymousConsent = context.by.kind === "member" &&
201
305
  context.anonymousConsent?.memberId === context.by.memberId;
@@ -215,14 +319,38 @@ export function applyInstallationCommand(current, rawCommand, context) {
215
319
  release.artifact.artifactDigest ||
216
320
  (current !== null &&
217
321
  command.kind !== "select-release" &&
218
- context.declaredOperations.digest !== current.declaredOperationsDigest))
322
+ context.declaredOperations.digest !==
323
+ current.authority.declaredOperationsDigest))
219
324
  return refuse("stale_authority");
220
- const declared = (capability) => context.declaration.capabilities.includes(capability)
221
- ? (context.declaredOperations.capabilities.find((row) => row.capability === capability)?.operations ?? [])
222
- : [];
223
- const version = (current?.authorityVersion ?? 0) + 1;
224
- let grants = current?.approvedGrants ?? [];
225
- let approval = current?.approval ?? null;
325
+ const version = (current?.authority.authorityVersion ?? 0) + 1;
326
+ const grantCommand = command.kind === "install" || command.kind === "select-release"
327
+ ? { kind: command.kind }
328
+ : command.kind === "change-grants"
329
+ ? command
330
+ : command.kind === "set-status" && command.status === "deleted"
331
+ ? { kind: "delete" }
332
+ : null;
333
+ const authority = grantCommand === null
334
+ ? current === null
335
+ ? Result.fail({
336
+ kind: "refused",
337
+ code: "invalid_command",
338
+ })
339
+ : Result.succeed({
340
+ authorityVersion: version,
341
+ declaredOperationsDigest: current.authority.declaredOperationsDigest,
342
+ approvedGrants: current.authority.approvedGrants,
343
+ approval: current.authority.approval,
344
+ })
345
+ : applyGrantCommand(current?.authority ?? null, grantCommand, {
346
+ principal: { actor: context.by, role: context.role },
347
+ expectedVersion: context.expectedVersion,
348
+ declaration: context.declaration,
349
+ declaredOperations: context.declaredOperations,
350
+ now: context.now,
351
+ });
352
+ if (Result.isFailure(authority))
353
+ return refuse(authority.failure.code);
226
354
  let anonymousMethods = current?.approvedAnonymousMethods ?? [];
227
355
  let anonymousApproval = current?.anonymousApproval ?? null;
228
356
  if (command.kind === "approve-anonymous") {
@@ -239,44 +367,12 @@ export function applyInstallationCommand(current, rawCommand, context) {
239
367
  authorityVersion: version,
240
368
  };
241
369
  }
242
- if (command.kind === "install" || command.kind === "set-grants") {
243
- grants =
244
- command.grants === "none"
245
- ? []
246
- : command.grants === "declared"
247
- ? context.declaredOperations.capabilities
248
- .filter((row) => context.declaration.capabilities.includes(row.capability))
249
- .map((row) => ({
250
- ...row,
251
- capabilityApproved: true,
252
- resources: "tenant-scope",
253
- }))
254
- : command.grants.map((grant) => ({
255
- ...grant,
256
- capabilityApproved: true,
257
- }));
258
- if (grants.some((grant) => !context.declaration.capabilities.includes(grant.capability) ||
259
- grant.operations.some((operation) => !declared(grant.capability).includes(operation))))
260
- return refuse("not_declared");
261
- approval =
262
- command.grants === "none"
263
- ? null
264
- : { by: context.by, at: context.now, authorityVersion: version };
265
- }
266
370
  if (command.kind === "select-release") {
267
371
  anonymousMethods = anonymousMethods.filter((method) => context.declaredOperations.anonymousMethods.some((declared) => declared.name === method.name && declared.digest === method.digest));
268
372
  if (anonymousMethods.length === 0)
269
373
  anonymousApproval = null;
270
- grants = grants
271
- .map((grant) => ({
272
- ...grant,
273
- operations: grant.operations.filter((operation) => declared(grant.capability).includes(operation)),
274
- }))
275
- .filter((grant) => context.declaration.capabilities.includes(grant.capability));
276
374
  }
277
375
  if (command.kind === "set-status" && command.status === "deleted") {
278
- grants = [];
279
- approval = null;
280
376
  anonymousMethods = [];
281
377
  anonymousApproval = null;
282
378
  }
@@ -288,16 +384,13 @@ export function applyInstallationCommand(current, rawCommand, context) {
288
384
  releasePin: command.kind === "install" || command.kind === "select-release"
289
385
  ? command.releasePin
290
386
  : current?.releasePin,
291
- authorityVersion: version,
292
387
  status: command.kind === "set-status"
293
388
  ? command.status
294
389
  : (current?.status ?? "enabled"),
295
390
  visibility: command.kind === "set-visibility"
296
391
  ? command.visibility
297
392
  : (current?.visibility ?? "private"),
298
- declaredOperationsDigest: context.declaredOperations.digest,
299
- approvedGrants: grants,
300
- approval,
393
+ authority: authority.success,
301
394
  approvedAnonymousMethods: anonymousMethods,
302
395
  anonymousApproval,
303
396
  });
@@ -305,3 +398,17 @@ export function applyInstallationCommand(current, rawCommand, context) {
305
398
  ? Result.succeed({ next: next.data, authorityChanged: true })
306
399
  : refuse("invalid_command");
307
400
  }
401
+ /** Only the committed installation may activate this exact owner selection. */
402
+ export function isCommittedInstallation(current, state, release) {
403
+ return (current !== null &&
404
+ state.owner.kind === "installation" &&
405
+ current.installationId === state.owner.id &&
406
+ current.instanceId === state.instanceId &&
407
+ current.appId === state.appId &&
408
+ current.authority.authorityVersion === state.authorityVersion &&
409
+ current.status === state.status &&
410
+ current.release.releaseId === state.releaseId &&
411
+ release.releaseId === state.releaseId &&
412
+ release.appId === state.appId &&
413
+ current.release.artifact.artifactDigest === release.artifact.artifactDigest);
414
+ }