@fungi.computer/oyster 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +396 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +213 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/older-oyster-store-refusal.d.ts +4 -0
  130. package/dist/older-oyster-store-refusal.js +18 -0
  131. package/dist/older-oyster-store.d.ts +21 -0
  132. package/dist/older-oyster-store.js +43 -0
  133. package/dist/pack.d.ts +14 -3
  134. package/dist/pack.js +44 -19
  135. package/dist/page/app-access.d.ts +7 -0
  136. package/dist/page/app-access.js +75 -0
  137. package/dist/page/boot-host.js +57 -0
  138. package/dist/page/capabilities.d.ts +3 -3
  139. package/dist/page/capabilities.js +16 -12
  140. package/dist/page/computer-handles.d.ts +3 -3
  141. package/dist/page/computer-target.d.ts +3 -0
  142. package/dist/page/computer-target.js +9 -0
  143. package/dist/page/mount.d.ts +23 -3
  144. package/dist/page/mount.js +152 -16
  145. package/dist/page-assets.d.ts +22 -0
  146. package/dist/page-assets.js +28 -0
  147. package/dist/page-script.d.ts +3 -0
  148. package/dist/page-script.js +5 -4
  149. package/dist/release.d.ts +13 -6
  150. package/dist/release.js +10 -7
  151. package/dist/root-data.d.ts +16 -6
  152. package/dist/root-data.js +2 -2
  153. package/dist/source/index.d.ts +1 -1
  154. package/dist/source/team-source.d.ts +20 -8
  155. package/dist/source/team-source.js +39 -10
  156. package/dist/source/template-seed.d.ts +2 -2
  157. package/dist/standalone/agent-mail.d.ts +24 -0
  158. package/dist/standalone/agent-mail.js +62 -0
  159. package/dist/standalone/agent-store.d.ts +9 -0
  160. package/dist/standalone/agent-store.js +30 -0
  161. package/dist/standalone/agents.d.ts +68 -9
  162. package/dist/standalone/agents.js +428 -56
  163. package/dist/standalone/app-builds-binding.d.ts +20 -2
  164. package/dist/standalone/app-builds-binding.js +147 -23
  165. package/dist/standalone/app-set.d.ts +5 -2
  166. package/dist/standalone/app-set.js +186 -144
  167. package/dist/standalone/browser.d.ts +11 -0
  168. package/dist/standalone/browser.js +72 -0
  169. package/dist/standalone/builds.d.ts +13 -4
  170. package/dist/standalone/builds.js +28 -44
  171. package/dist/standalone/computer-egress.d.ts +2 -0
  172. package/dist/standalone/computer-egress.js +13 -4
  173. package/dist/standalone/computer-fleet.d.ts +43 -0
  174. package/dist/standalone/computer-fleet.js +200 -0
  175. package/dist/standalone/computer.d.ts +44 -9
  176. package/dist/standalone/computer.js +129 -22
  177. package/dist/standalone/install-apps-cli.js +43 -6
  178. package/dist/standalone/instance.d.ts +2 -2
  179. package/dist/standalone/instance.js +8 -11
  180. package/dist/standalone/machine-link.d.ts +354 -0
  181. package/dist/standalone/machine-link.js +368 -0
  182. package/dist/standalone/media.d.ts +2 -2
  183. package/dist/standalone/media.js +19 -16
  184. package/dist/standalone/preview.d.ts +2 -2
  185. package/dist/standalone/preview.js +37 -10
  186. package/dist/standalone/registry.d.ts +151 -9
  187. package/dist/standalone/registry.js +305 -8
  188. package/dist/standalone/worker.d.ts +19 -5
  189. package/dist/standalone/worker.js +90 -54
  190. package/dist/standalone/workflow-binding.d.ts +18 -22
  191. package/dist/standalone/workflow-binding.js +18 -18
  192. package/dist/standalone/workflow-control.d.ts +4 -0
  193. package/dist/standalone/workflow-control.js +40 -0
  194. package/dist/standalone/workflow-events.d.ts +2 -2
  195. package/dist/standalone/workflow-events.js +9 -10
  196. package/dist/standalone/workflow-git-capture.js +4 -3
  197. package/dist/standalone/workflow-host.d.ts +162 -161
  198. package/dist/standalone/workflow-host.js +101 -21
  199. package/dist/standalone/workflow-node-capture.js +11 -4
  200. package/dist/standalone/workflow-schedules.d.ts +5 -2
  201. package/dist/standalone/workflow-schedules.js +9 -10
  202. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  203. package/dist/standalone/workflow-trigger-host.js +2 -2
  204. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  205. package/dist/standalone/workflow-webhooks.js +39 -0
  206. package/dist/standalone/workflows.d.ts +140 -43
  207. package/dist/standalone/workflows.js +198 -39
  208. package/dist/storage-frame.d.ts +6 -6
  209. package/dist/testing/capabilities.js +14 -3
  210. package/dist/testing/installation.js +26 -2
  211. package/dist/testing/worker.js +1 -1
  212. package/dist/testing/workers-runtime.d.ts +8 -2
  213. package/dist/testing/workers-runtime.js +20 -2
  214. package/dist/webhook-abuse.d.ts +25 -0
  215. package/dist/webhook-abuse.js +54 -0
  216. package/dist/webhook-custody.d.ts +144 -0
  217. package/dist/webhook-custody.js +222 -0
  218. package/dist/webhook-ingress.d.ts +36 -0
  219. package/dist/webhook-ingress.js +146 -0
  220. package/dist/webhooks.d.ts +111 -0
  221. package/dist/webhooks.js +189 -0
  222. package/dist/workers-git.d.ts +12 -17
  223. package/dist/workers-git.js +64 -52
  224. package/dist/workers.d.ts +226 -13
  225. package/dist/workers.js +363 -51
  226. package/dist/workflow-agent-authority.d.ts +8 -0
  227. package/dist/workflow-agent-authority.js +42 -0
  228. package/dist/workflows.d.ts +66 -0
  229. package/dist/workflows.js +97 -0
  230. package/docs/api/README.md +63 -37
  231. package/docs/api/agents.md +17 -0
  232. package/docs/api/app-storage.md +1 -1
  233. package/docs/api/apps.md +1394 -543
  234. package/docs/api/browser/boot.md +132 -0
  235. package/docs/api/browser.md +1440 -0
  236. package/docs/api/build-contract.md +38 -25
  237. package/docs/api/build.md +263 -84
  238. package/docs/api/cloudflare.md +93 -7
  239. package/docs/api/git.md +3 -13
  240. package/docs/api/host.md +944 -118
  241. package/docs/api/index.md +320 -18
  242. package/docs/api/keys.md +24 -1
  243. package/docs/api/mcp.md +59 -12
  244. package/docs/api/modules.md +33 -40
  245. package/docs/api/pack.md +57 -8
  246. package/docs/api/page/mount.md +92 -4
  247. package/docs/api/workers-git.md +103 -24
  248. package/docs/api/workers.md +1594 -244
  249. package/docs/site/agent-quickstart.mdx +28 -14
  250. package/examples/standalone/worker.ts +3 -1
  251. package/examples/standalone/wrangler.jsonc +2 -0
  252. package/package.json +73 -71
  253. package/dist/installation-consent.d.ts +0 -81
  254. package/dist/installation-consent.js +0 -84
  255. package/dist/shell/app-directory.d.ts +0 -542
  256. package/dist/shell/app-directory.js +0 -196
  257. package/dist/shell/browser-actions.d.ts +0 -7
  258. package/dist/shell/browser-actions.js +0 -56
  259. package/dist/shell/command-host.d.ts +0 -112
  260. package/dist/shell/command-host.js +0 -502
  261. package/dist/shell/commands.d.ts +0 -25
  262. package/dist/shell/commands.js +0 -78
  263. package/dist/shell/index.d.ts +0 -8
  264. package/dist/shell/index.js +0 -8
  265. package/dist/shell/keys.d.ts +0 -41
  266. package/dist/shell/keys.js +0 -134
  267. package/dist/shell/shell-module.d.ts +0 -55
  268. package/dist/shell/shell-module.js +0 -86
  269. package/dist/shell/window-manager.d.ts +0 -128
  270. package/dist/shell/window-manager.js +0 -730
  271. package/dist/shell/workbench-contract.d.ts +0 -68
  272. package/dist/shell/workbench-directory.d.ts +0 -332
  273. package/dist/shell/workbench-directory.js +0 -35
  274. package/dist/shell/workbench-host-types.d.ts +0 -96
  275. package/dist/shell/workbench-host-types.js +0 -1
  276. package/dist/shell/workbench-host.d.ts +0 -34
  277. package/dist/shell/workbench-host.js +0 -352
  278. package/dist/shell/workbench-model.d.ts +0 -194
  279. package/dist/shell/workbench-model.js +0 -53
  280. package/dist/shell/workbench.d.ts +0 -4
  281. package/dist/shell/workbench.js +0 -2
  282. package/docs/api/shell/app-directory.md +0 -599
  283. package/docs/api/shell/command-host.md +0 -224
  284. package/docs/api/shell/commands.md +0 -197
  285. package/docs/api/shell/keys.md +0 -183
  286. package/docs/api/shell/shell-module.md +0 -204
  287. package/docs/api/shell/window-manager.md +0 -267
  288. package/docs/api/shell/workbench-directory.md +0 -119
  289. package/docs/api/shell/workbench.md +0 -205
  290. package/docs/api/shell.md +0 -1915
  291. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
package/dist/pack.js CHANGED
@@ -4,11 +4,13 @@ import { join, relative, resolve } from "node:path";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as Schema from "effect/Schema";
6
6
  import { releaseSchema } from "@fungi.computer/puffball/effect";
7
+ import { installOnOyster, installationObservationSignal, completedInstallationResponse, } from "./installation-client.js";
7
8
  import { parseArtifactRef, parseReleaseId, parseSha256Digest, parseSourceRevision, } from "@fungi.computer/puffball";
8
9
  import { commitPreparedCandidate, createAppCandidatePreparer, createArtifactsSourceCapture, createFirstPartyAppSourceDistribution, createLocalArtifactWriter, createLocalBuildExecutor, createLocalCommitSourceCapture, normalizeAppPublisherRecipe, } from "./build.js";
9
10
  import { templateSeedClaimSchema, } from "./source/template-seed.js";
10
11
  import { pushTemplateSource } from "./source/template-push.js";
11
12
  import { sha256 } from "./release.js";
13
+ export { InstallationObservationError } from "./installation-client.js";
12
14
  async function filesUnder(directory) {
13
15
  const entries = await readdir(directory, {
14
16
  recursive: true,
@@ -26,9 +28,9 @@ async function packCapturedSource(options, sourceCapture) {
26
28
  const preparer = createAppCandidatePreparer({
27
29
  config: { appId: options.appId, recipe: options.recipe },
28
30
  sourceCapture,
29
- buildExecutor: createLocalBuildExecutor({
30
- packageStoreDirectory: options.packageStoreDirectory,
31
- }),
31
+ buildExecutor: createLocalBuildExecutor(options.packageStoreDirectory === undefined
32
+ ? {}
33
+ : { packageStoreDirectory: options.packageStoreDirectory }),
32
34
  });
33
35
  const prepared = await Effect.runPromise(Effect.scoped(preparer.prepareCandidate(parseReleaseId(options.releaseId ??
34
36
  `rel_${revision.slice("git:".length, "git:".length + 12)}`))));
@@ -52,27 +54,40 @@ export async function packRelease(options) {
52
54
  * Capture an Artifacts Git commit, build its Release, upload it to an Oyster,
53
55
  * and install it, or switch an existing installation to it
54
56
  * ({@link installRelease}).
57
+ * `waitMs` starts with the upload/observation phase after packing; a supplied
58
+ * caller signal retains its original lifetime through that phase.
55
59
  */
56
60
  export async function installFromSource(input) {
57
61
  const outDirectory = await mkdtemp(join(tmpdir(), "oyster-source-release-"));
58
62
  try {
59
- await packCapturedSource({
63
+ let options = {
60
64
  appId: input.appId,
61
- releaseId: input.releaseId,
62
65
  recipe: input.recipe,
63
66
  sourceRevision: input.source.sourceRevision,
64
- packageStoreDirectory: input.packageStoreDirectory,
65
67
  outDirectory,
66
- }, createArtifactsSourceCapture({
68
+ };
69
+ if (input.releaseId !== undefined)
70
+ options = { ...options, releaseId: input.releaseId };
71
+ if (input.packageStoreDirectory !== undefined)
72
+ options = {
73
+ ...options,
74
+ packageStoreDirectory: input.packageStoreDirectory,
75
+ };
76
+ await packCapturedSource(options, createArtifactsSourceCapture({
67
77
  ...input.source,
68
78
  sourceRevision: parseSourceRevision(input.source.sourceRevision),
69
79
  }));
70
- return await installRelease({
80
+ let installation = {
71
81
  directory: outDirectory,
72
82
  url: input.url,
73
83
  key: input.key,
74
84
  installationId: input.installationId,
75
- });
85
+ };
86
+ if (input.waitMs !== undefined)
87
+ installation = { ...installation, waitMs: input.waitMs };
88
+ if (input.signal !== undefined)
89
+ installation = { ...installation, signal: input.signal };
90
+ return await installRelease(installation);
76
91
  }
77
92
  finally {
78
93
  await rm(outDirectory, { recursive: true, force: true });
@@ -81,9 +96,16 @@ export async function installFromSource(input) {
81
96
  /**
82
97
  * Upload a packed Release to an Oyster and install it. The host installs a
83
98
  * new installation (201) or switches an existing one to this Release (200),
84
- * keeping its state; both go through its one install-or-select owner.
99
+ * keeping its state. Uploads precede admission; this helper then waits for the
100
+ * correlated terminal owner receipt, not merely the HTTP acknowledgement.
101
+ * Uploads and observation share the caller signal or explicit `waitMs`; absent
102
+ * either, the installer owns a twenty-minute default. Expiry does not cancel
103
+ * admitted work and is reported as an unconfirmed observation, not refusal.
85
104
  */
86
105
  export async function installRelease(input) {
106
+ // One installer budget for uploads + observation, not an App authority grant.
107
+ const signal = installationObservationSignal(input);
108
+ signal.throwIfAborted();
87
109
  const directory = resolve(input.directory);
88
110
  const release = Schema.decodeUnknownSync(Schema.fromJsonString(releaseSchema))(await readFile(join(directory, "release.json"), "utf8"));
89
111
  const authorization = { Authorization: `Bearer ${input.key}` };
@@ -91,18 +113,21 @@ export async function installRelease(input) {
91
113
  const bytes = new Uint8Array(await readFile(file));
92
114
  const artifactRef = `artifact:${relative(join(directory, "artifacts"), file)}`;
93
115
  const digest = parseSha256Digest(await sha256(bytes));
94
- const response = await fetch(new URL(`/api/artifacts/${encodeURIComponent(artifactRef)}?digest=${digest}`, input.url), { method: "PUT", headers: authorization, body: bytes });
116
+ const response = await fetch(new URL(`/api/artifacts/${encodeURIComponent(artifactRef)}?digest=${digest}`, input.url), { method: "PUT", headers: authorization, body: bytes, signal });
95
117
  if (!response.ok)
96
118
  throw new Error(`Artifact upload failed (${String(response.status)}): ${artifactRef}`);
97
119
  }
98
- return fetch(new URL("/api/installations", input.url), {
99
- method: "POST",
100
- headers: { ...authorization, "Content-Type": "application/json" },
101
- body: JSON.stringify({
102
- release: Schema.encodeUnknownSync(releaseSchema)(release),
103
- installationId: input.installationId,
104
- }),
105
- });
120
+ let installation = {
121
+ url: input.url,
122
+ key: input.key,
123
+ release,
124
+ installationId: input.installationId ?? release.appId.replaceAll("_", "-"),
125
+ signal,
126
+ };
127
+ if (input.onAdmitted !== undefined)
128
+ installation = { ...installation, onAdmitted: input.onAdmitted };
129
+ const operation = await installOnOyster(installation);
130
+ return completedInstallationResponse(operation);
106
131
  }
107
132
  /** Project one exact first-party commit's editable source, `git push` it to
108
133
  * an Oyster's own template for that App, and claim the pushed head, so
@@ -0,0 +1,7 @@
1
+ import type { RequestShellAppAccess } from "@fungi.computer/nacre/shell-module";
2
+ import type { WorkbenchDirectory } from "@fungi.computer/nacre/workbench-directory";
3
+ /**
4
+ * Keep standalone consent in the Oyster host: the picked shell draws the
5
+ * prompt, while Oyster bounds the request and commits through its owner CAS.
6
+ */
7
+ export declare function createShellAppAccess(directory: () => Promise<WorkbenchDirectory>): RequestShellAppAccess;
@@ -0,0 +1,75 @@
1
+ import { grantRequestFor, appPermissionPageSchema, } from "../apps/permissions.js";
2
+ /**
3
+ * Keep standalone consent in the Oyster host: the picked shell draws the
4
+ * prompt, while Oyster bounds the request and commits through its owner CAS.
5
+ */
6
+ export function createShellAppAccess(directory) {
7
+ return async (target, input, dialog) => {
8
+ const { surfaceId, computerId } = target;
9
+ const current = await directory();
10
+ const app = current.apps.find((row) => row.surfaceId === surfaceId);
11
+ const installationId = app?.installationId ?? null;
12
+ const customize = () => dialog.customize(installationId);
13
+ if (installationId === null)
14
+ return customize();
15
+ const path = `/api/installations/${encodeURIComponent(installationId)}/grants`;
16
+ const response = await fetch(path, {
17
+ headers: { Accept: "application/json" },
18
+ credentials: "same-origin",
19
+ cache: "no-store",
20
+ redirect: "error",
21
+ });
22
+ if (!response.ok)
23
+ return "declined";
24
+ const view = appPermissionPageSchema.parse(await response.json());
25
+ const { permissions } = view;
26
+ const decision = grantRequestFor(permissions.authority, {
27
+ declaration: {
28
+ capabilities: permissions.declaredOperations.capabilities.map((row) => row.capability),
29
+ },
30
+ declaredOperations: permissions.declaredOperations,
31
+ }, input, { computerId });
32
+ if (decision.kind === "covered")
33
+ return "granted";
34
+ if (decision.kind === "refused") {
35
+ if (decision.code !== "window_scope_unavailable")
36
+ return "declined";
37
+ const outcome = await dialog.ask({
38
+ appName: view.appName,
39
+ agents: input.agents ?? [],
40
+ computers: input.computers ?? [],
41
+ refusal: {
42
+ code: decision.code,
43
+ message: "Allow for this window is unavailable on this Oyster. No access was changed. Use Customize to review Computer permissions explicitly.",
44
+ },
45
+ allow: () => Promise.resolve(false),
46
+ });
47
+ return outcome === "customize" ? customize() : outcome;
48
+ }
49
+ if (decision.kind === "customize")
50
+ return customize();
51
+ const outcome = await dialog.ask({
52
+ appName: view.appName,
53
+ agents: input.agents ?? [],
54
+ computers: input.computers ?? [],
55
+ allow: async () => {
56
+ const saved = await fetch(path, {
57
+ method: "PUT",
58
+ headers: { "Content-Type": "application/json" },
59
+ credentials: "same-origin",
60
+ cache: "no-store",
61
+ redirect: "error",
62
+ body: JSON.stringify({
63
+ storeGeneration: view.storeGeneration,
64
+ authorityVersion: permissions.authority.authorityVersion,
65
+ artifactDigest: view.artifactDigest,
66
+ command: decision.command,
67
+ }),
68
+ });
69
+ return saved.ok;
70
+ },
71
+ });
72
+ // False saves settle declined; rejected saves leave the shell open for retry.
73
+ return outcome === "customize" ? customize() : outcome;
74
+ };
75
+ }
@@ -0,0 +1,57 @@
1
+ import { connectAppHost } from "@fungi.computer/agar/host";
2
+ import { z } from "zod";
3
+ import { bootUiSchema } from "../boot-gate.js";
4
+ /**
5
+ * Trusted, zero-grant parent for a candidate entry. App readiness messages
6
+ * establish the ordinary Agar connection, never the boot-gate verdict.
7
+ */
8
+ async function bootHost() {
9
+ const element = document.getElementById("boot-config");
10
+ if (element === null)
11
+ throw new Error("Host boot configuration unavailable");
12
+ const config = bootUiSchema
13
+ .pick({ identity: true })
14
+ .extend({ entry: z.url() })
15
+ .parse(JSON.parse(element.textContent));
16
+ const frame = document.getElementById("candidate");
17
+ if (!(frame instanceof HTMLIFrameElement) || frame.contentWindow === null)
18
+ throw new Error("Host candidate frame unavailable");
19
+ const denied = () => Promise.reject(new Error("No grants during host boot"));
20
+ const connection = connectAppHost({
21
+ appWindow: frame.contentWindow,
22
+ expectedAppOrigin: "null",
23
+ hostWindow: window,
24
+ launch: {
25
+ identity: config.identity,
26
+ capabilities: [],
27
+ context: {
28
+ version: 1,
29
+ value: {
30
+ computerId: null,
31
+ focus: { editing: false, surface: null },
32
+ openRequest: null,
33
+ visibility: "visible",
34
+ },
35
+ },
36
+ lifecycle: { version: 1, value: "connected" },
37
+ reservedChords: { version: 1, value: [] },
38
+ theme: { version: 1, value: "light" },
39
+ },
40
+ handlers: {
41
+ invokeReservedChord: denied,
42
+ onDisconnect: () => Promise.resolve(),
43
+ openNavigation: denied,
44
+ registerCommands: () => Promise.resolve({
45
+ update: () => undefined,
46
+ dispose: () => undefined,
47
+ }),
48
+ reportFocus: () => Promise.resolve(),
49
+ reportAgentSessionSelection: () => Promise.resolve(),
50
+ showToast: () => Promise.resolve(),
51
+ },
52
+ });
53
+ // Install the host connection listener before the candidate can announce ready.
54
+ frame.src = config.entry;
55
+ await connection;
56
+ }
57
+ await bootHost();
@@ -5,7 +5,7 @@ import type { CapabilityDescriptor } from "../capability-descriptor.js";
5
5
  /** Agar's opaque host handle, shared by the page's exported factory contracts. */
6
6
  export type HostCapabilityHandle = AppHostLaunch["capabilities"][number]["handle"];
7
7
  /** Bind a page-local JSON caller; cancellation never enters serialized input. */
8
- export declare function capabilityJsonCaller(descriptor: CapabilityDescriptor, installationId: string): (method: string, raw?: unknown, options?: {
8
+ export declare function capabilityJsonCaller(descriptor: CapabilityDescriptor, installationId: string, computerTarget?: string): (method: string, raw?: unknown, options?: {
9
9
  readonly signal?: AbortSignal;
10
10
  }) => Promise<import("zod").JSONType | undefined>;
11
11
  /** Local JSON calls carry cancellation separately from their serialized input. */
@@ -13,11 +13,11 @@ export type CapabilityJsonCaller = ReturnType<typeof capabilityJsonCaller>;
13
13
  /** Local factories may open only transports present in their descriptor. */
14
14
  export type CapabilityTransportOpener = (method: string, input?: Schema.Json) => WebSocket;
15
15
  /** Build the App's RPC handle from its admitted descriptor and installation. */
16
- export declare function capabilityHandle(descriptor: CapabilityDescriptor, installationId: string): HostCapabilityHandle;
16
+ export declare function capabilityHandle(descriptor: CapabilityDescriptor, installationId: string, computerTarget?: string): HostCapabilityHandle;
17
17
  /** A supported contract may adapt local JSON calls into its App-facing handle. */
18
18
  export type CapabilityRegistration = Readonly<{
19
19
  contract: CapabilityContract<unknown>;
20
20
  createHandle?: (call: CapabilityJsonCaller, transport: CapabilityTransportOpener) => HostCapabilityHandle | Promise<HostCapabilityHandle>;
21
21
  }>;
22
22
  /** Create a registered handle, using the descriptor's ordinary RPC handle by default. */
23
- export declare function registeredCapabilityHandle(registration: CapabilityRegistration, descriptor: CapabilityDescriptor, installationId: string): Promise<HostCapabilityHandle>;
23
+ export declare function registeredCapabilityHandle(registration: CapabilityRegistration, descriptor: CapabilityDescriptor, installationId: string, computerTarget?: string): Promise<HostCapabilityHandle>;
@@ -3,6 +3,7 @@ import { compile, parse, pathToRegexp } from "path-to-regexp";
3
3
  import * as Schema from "effect/Schema";
4
4
  import { AppError } from "@fungi.computer/agar";
5
5
  import { parseJsonValue } from "@fungi.computer/whistle/wire";
6
+ import { computerTargetUrl } from "./computer-target.js";
6
7
  const queryInput = Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Number, Schema.Boolean]));
7
8
  const capabilityInputSchema = Schema.Union([Schema.Json, Schema.Undefined]);
8
9
  function resolveRoute(path, input) {
@@ -163,7 +164,7 @@ class TransportHandle extends RpcTarget {
163
164
  return Promise.resolve();
164
165
  }
165
166
  }
166
- function capabilityRequest(descriptor, installationId, route, raw) {
167
+ function capabilityRequest(descriptor, installationId, route, raw, computerTarget) {
167
168
  const resolved = resolveRoute(route.path, raw);
168
169
  const input = resolved.input;
169
170
  const headers = new Headers({ "X-Oyster-Installation": installationId });
@@ -184,9 +185,12 @@ function capabilityRequest(descriptor, installationId, route, raw) {
184
185
  if (route.kind === "json" && route.method !== "GET") {
185
186
  headers.set("Content-Type", "application/json");
186
187
  init.body = JSON.stringify(input);
187
- return { url, init };
188
+ return { url: computerTargetUrl(url, computerTarget), init };
188
189
  }
189
- return { url: `${url}${query(input)}`, init };
190
+ return {
191
+ url: computerTargetUrl(`${url}${query(input)}`, computerTarget),
192
+ init,
193
+ };
190
194
  }
191
195
  async function jsonReply(url, init) {
192
196
  const response = await fetch(url, init);
@@ -196,7 +200,7 @@ async function jsonReply(url, init) {
196
200
  return parseJsonValue(await response.json());
197
201
  }
198
202
  /** Bind a page-local JSON caller; cancellation never enters serialized input. */
199
- export function capabilityJsonCaller(descriptor, installationId) {
203
+ export function capabilityJsonCaller(descriptor, installationId, computerTarget) {
200
204
  return async (method, raw, options) => {
201
205
  const input = Schema.decodeUnknownSync(capabilityInputSchema)(raw);
202
206
  const route = descriptor.methods[method];
@@ -205,12 +209,12 @@ export function capabilityJsonCaller(descriptor, installationId) {
205
209
  code: "INVALID_ARGUMENT",
206
210
  retryable: false,
207
211
  });
208
- const { url, init } = capabilityRequest(descriptor, installationId, route, input);
212
+ const { url, init } = capabilityRequest(descriptor, installationId, route, input, computerTarget);
209
213
  return jsonReply(url, { ...init, signal: options?.signal });
210
214
  };
211
215
  }
212
216
  /** Open a browser transport using the page's admitted installation. */
213
- function capabilityTransportOpener(descriptor, installationId) {
217
+ function capabilityTransportOpener(descriptor, installationId, computerTarget) {
214
218
  return (method, raw) => {
215
219
  const route = descriptor.methods[method];
216
220
  if (route?.kind !== "transport" ||
@@ -219,7 +223,7 @@ function capabilityTransportOpener(descriptor, installationId) {
219
223
  code: "INVALID_ARGUMENT",
220
224
  retryable: false,
221
225
  });
222
- const { url } = capabilityRequest(descriptor, installationId, route, Schema.decodeUnknownSync(capabilityInputSchema)(raw));
226
+ const { url } = capabilityRequest(descriptor, installationId, route, Schema.decodeUnknownSync(capabilityInputSchema)(raw), computerTarget);
223
227
  const target = new URL(url, "https://oyster.invalid");
224
228
  // Browser WebSockets cannot send the installation header used by Fetch.
225
229
  target.searchParams.set("oysterInstallation", installationId);
@@ -227,7 +231,7 @@ function capabilityTransportOpener(descriptor, installationId) {
227
231
  };
228
232
  }
229
233
  /** Build the App's RPC handle from its admitted descriptor and installation. */
230
- export function capabilityHandle(descriptor, installationId) {
234
+ export function capabilityHandle(descriptor, installationId, computerTarget) {
231
235
  // Cap'n Web exposes prototype methods, never an instance's own properties.
232
236
  // Only input crosses RPC; AbortSignal options belong to the local caller.
233
237
  class CapabilityHandle extends RpcTarget {
@@ -236,7 +240,7 @@ export function capabilityHandle(descriptor, installationId) {
236
240
  name,
237
241
  async (raw) => {
238
242
  const input = Schema.decodeUnknownSync(capabilityInputSchema)(raw);
239
- const { url, init } = capabilityRequest(descriptor, installationId, route, input);
243
+ const { url, init } = capabilityRequest(descriptor, installationId, route, input, computerTarget);
240
244
  if (route.kind === "read")
241
245
  return new ReadHandle(url, init);
242
246
  if (route.kind === "write")
@@ -249,8 +253,8 @@ export function capabilityHandle(descriptor, installationId) {
249
253
  return new CapabilityHandle();
250
254
  }
251
255
  /** Create a registered handle, using the descriptor's ordinary RPC handle by default. */
252
- export async function registeredCapabilityHandle(registration, descriptor, installationId) {
256
+ export async function registeredCapabilityHandle(registration, descriptor, installationId, computerTarget) {
253
257
  if (registration.createHandle === undefined)
254
- return capabilityHandle(descriptor, installationId);
255
- return await registration.createHandle(capabilityJsonCaller(descriptor, installationId), capabilityTransportOpener(descriptor, installationId));
258
+ return capabilityHandle(descriptor, installationId, computerTarget);
259
+ return await registration.createHandle(capabilityJsonCaller(descriptor, installationId, computerTarget), capabilityTransportOpener(descriptor, installationId, computerTarget));
256
260
  }
@@ -46,7 +46,7 @@ export declare class ComputerProcessesHandle extends RpcTarget implements Handle
46
46
  readonly startedAt: number;
47
47
  readonly state: "running" | "exited" | "uncertain";
48
48
  readonly exitCode: number | null;
49
- readonly sync: "synced" | "pending" | "unknown";
49
+ readonly sync: "unknown" | "pending" | "synced";
50
50
  readonly endedAt: number | null;
51
51
  }>;
52
52
  list(): Promise<{
@@ -58,7 +58,7 @@ export declare class ComputerProcessesHandle extends RpcTarget implements Handle
58
58
  readonly startedAt: number;
59
59
  readonly state: "running" | "exited" | "uncertain";
60
60
  readonly exitCode: number | null;
61
- readonly sync: "synced" | "pending" | "unknown";
61
+ readonly sync: "unknown" | "pending" | "synced";
62
62
  readonly endedAt: number | null;
63
63
  }[];
64
64
  }>;
@@ -71,7 +71,7 @@ export declare class ComputerProcessesHandle extends RpcTarget implements Handle
71
71
  readonly startedAt: number;
72
72
  readonly state: "running" | "exited" | "uncertain";
73
73
  readonly exitCode: number | null;
74
- readonly sync: "synced" | "pending" | "unknown";
74
+ readonly sync: "unknown" | "pending" | "synced";
75
75
  readonly endedAt: number | null;
76
76
  };
77
77
  readonly signalled: boolean;
@@ -0,0 +1,3 @@
1
+ /** Serialize a window's routing hint. The server must admit it through the live
2
+ * owner and fleet before constructing that request's workspace. */
3
+ export declare function computerTargetUrl(path: string, computerId?: string): string;
@@ -0,0 +1,9 @@
1
+ /** Serialize a window's routing hint. The server must admit it through the live
2
+ * owner and fleet before constructing that request's workspace. */
3
+ export function computerTargetUrl(path, computerId) {
4
+ if (computerId === undefined)
5
+ return path;
6
+ const url = new URL(path, "https://oyster.invalid");
7
+ url.searchParams.set("computerId", computerId);
8
+ return url.pathname + url.search;
9
+ }
@@ -1,7 +1,21 @@
1
1
  import { type AppOpenInput, type AppRoleChoice } from "@fungi.computer/agar/apps";
2
+ import type { WhistleRuntime } from "@fungi.computer/whistle";
2
3
  import * as Schema from "effect/Schema";
4
+ import { type CapabilityDirectory, type AgentSessionSelection } from "@fungi.computer/agar";
3
5
  import { type AppHost, type AppHostLaunch, type AppHostHandlers } from "@fungi.computer/agar/host";
4
6
  import type { WhistleJsonValue } from "@fungi.computer/whistle";
7
+ /** One page-wide selected Session, using only its reporting App's admitted view. */
8
+ export declare class OysterAgentBrowserHost {
9
+ #private;
10
+ /** End the selected connection with the page's own lifetime. */
11
+ constructor(signal: AbortSignal);
12
+ /** A departing or deselecting mount cannot close a different active mount. */
13
+ release(frame: HTMLIFrameElement): void;
14
+ /** Release the page's current selection; later reports cannot reconnect it. */
15
+ dispose(): void;
16
+ /** Bind the reporting mount's current command view to its explicitly selected Session. */
17
+ report(frame: HTMLIFrameElement, selection: AgentSessionSelection | null, capabilities: CapabilityDirectory, whistle: Pick<WhistleRuntime, "snapshot" | "execute">): Promise<void>;
18
+ }
5
19
  /** Current server-admitted configuration for one installed App page. */
6
20
  export declare const oysterAppConfigSchema: Schema.Struct<{
7
21
  readonly workbench: Schema.Boolean;
@@ -32,7 +46,7 @@ export declare const oysterAppConfigSchema: Schema.Struct<{
32
46
  help?: Readonly<{
33
47
  order?: number | undefined;
34
48
  }> | undefined;
35
- menu? /** Which App that role's open would reach, or null when none fills it. */: Readonly<{
49
+ menu?: Readonly<{
36
50
  order?: number | undefined;
37
51
  }> | undefined;
38
52
  palette?: Readonly<{
@@ -64,7 +78,7 @@ export declare const oysterAppConfigSchema: Schema.Struct<{
64
78
  help?: Readonly<{
65
79
  order?: number | undefined;
66
80
  }> | undefined;
67
- menu? /** Which App that role's open would reach, or null when none fills it. */: Readonly<{
81
+ menu?: Readonly<{
68
82
  order?: number | undefined;
69
83
  }> | undefined;
70
84
  palette?: Readonly<{
@@ -141,6 +155,12 @@ export type OysterAppConfig = typeof oysterAppConfigSchema.Type;
141
155
  /** One isolated App connection, with the slots this host admitted for it. */
142
156
  export type OysterAppConnectionOptions = Readonly<{
143
157
  config: OysterAppConfig;
158
+ /** The page-wide selected Session owner shared by all child mounts. */
159
+ agentBrowser: OysterAgentBrowserHost;
160
+ /** The existing command owner's live, mount-scoped Agent view. */
161
+ agentCommands: Pick<WhistleRuntime, "snapshot" | "execute">;
162
+ /** An independently admitted per-window target; absent on fixed/default hosts. */
163
+ computerTarget?: string | undefined;
144
164
  frame: HTMLIFrameElement;
145
165
  signal: AbortSignal;
146
166
  handlers: AppHostHandlers;
@@ -156,4 +176,4 @@ export declare function connectOysterApp(options: OysterAppConnectionOptions): P
156
176
  export { oysterStorageHandle } from "./storage.js";
157
177
  export { appSandbox } from "../app-sandbox.js";
158
178
  /** Resolve only the slots admitted for this installation by its current host. */
159
- export declare function oysterAppCapabilities(options: Pick<OysterAppConnectionOptions, "config" | "signal" | "appOpen">): Promise<AppHostLaunch["capabilities"]>;
179
+ export declare function oysterAppCapabilities(options: Pick<OysterAppConnectionOptions, "config" | "signal" | "appOpen" | "computerTarget">): Promise<AppHostLaunch["capabilities"]>;