@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
@@ -0,0 +1,104 @@
1
+ import { type LeaseOwner } from "./lease-registry.js";
2
+ import type { BinaryResult, CloudBrowserOperation, CloudBrowserOperationResult } from "./operations.js";
3
+ import type { QuickReads } from "./quick-reads.js";
4
+ import type { BrowserSessions } from "./sessions.js";
5
+ /**
6
+ * The agent browser: one host port in, one owner-checked lease door out.
7
+ *
8
+ * The host supplies the provider (`sessions`, optional `quickReads`), the
9
+ * REQUIRED metering owner and a code-only event sink. The module owns every
10
+ * decision a host must not be able to skip: the egress URL policy on every
11
+ * agent-supplied URL, the per-Team concurrent cap, lease ownership, idle and
12
+ * lifetime reaping, and the billing order. A lease holds before the provider
13
+ * session opens, starts once it is attached, releases a hold that never
14
+ * opened and settles real elapsed time at close. A Quick Read holds before
15
+ * the provider call, settles the browser time the provider reports and
16
+ * releases its hold when the read fails.
17
+ */
18
+ /**
19
+ * One metered execution (a lease or a Quick Read), driven only by
20
+ * `createBrowser`. `maxMs` is the execution's own ceiling: the hold covers it
21
+ * and settlement never exceeds it.
22
+ */
23
+ export type BrowserMeter = Readonly<{
24
+ /** Hold credit for `maxMs`; throws to refuse the execution. */
25
+ hold: (input: Readonly<{
26
+ executionId: string;
27
+ maxMs: number;
28
+ }>) => Promise<void>;
29
+ /** The provider did the work; the hold can no longer be released. */
30
+ start: (input: Readonly<{
31
+ executionId: string;
32
+ }>) => Promise<void>;
33
+ /** Settle the real browser time used. */
34
+ settle: (input: Readonly<{
35
+ executionId: string;
36
+ elapsedMs: number;
37
+ maxMs: number;
38
+ }>) => Promise<void>;
39
+ /** Release a hold whose provider work never happened. */
40
+ release: (input: Readonly<{
41
+ executionId: string;
42
+ }>) => Promise<void>;
43
+ }>;
44
+ /** Code-only observations; never a URL, page text, token or provider body. */
45
+ export type BrowserEvent = Readonly<{
46
+ kind: "reap_close_failed" | "reap_settle_failed" | "settle_failed" | "open_cleanup_failed" | "read_release_failed" | "teardown_failed" | "teardown_abandoned";
47
+ teamId: string;
48
+ executionId: string;
49
+ cause: unknown;
50
+ }>;
51
+ /** The ONE host port for config, billing and provider egress. */
52
+ export type BrowserHost = Readonly<{
53
+ sessions: BrowserSessions;
54
+ /** Browser Run REST Quick Reads; `null` on a BROWSER binding. */
55
+ quickReads: QuickReads | null;
56
+ /** Required: every lease is metered by the owner the host names. */
57
+ meter: (owner: LeaseOwner) => BrowserMeter;
58
+ /** Receives code-only failure observations. */
59
+ report: (event: BrowserEvent) => void;
60
+ }>;
61
+ export type BrowserLimits = Readonly<{
62
+ /** Concurrent open leases one Team may hold. */
63
+ maxConcurrentPerTeam: number;
64
+ /** A lease older than this is reaped even if active. */
65
+ maxLifetimeMs: number;
66
+ /** A lease idle this long is reaped; also the provider keep-alive. */
67
+ idleTimeoutMs: number;
68
+ /** Per-navigation load deadline. */
69
+ navigationTimeoutMs?: number;
70
+ /** How long a close waits for the provider to confirm a "closing" ack. */
71
+ closeConfirmMs?: number;
72
+ /** Whole-call deadline of one Quick Read; also its billing ceiling. */
73
+ quickReadTimeoutMs?: number;
74
+ /** Clock for lease ages and billed time; `Date.now` by default. */
75
+ now?: () => number;
76
+ }>;
77
+ /** What a Team member may do to a lease an Agent handed off. */
78
+ export type BrowserMemberAction = Readonly<{
79
+ kind: "screenshot";
80
+ }> | Readonly<{
81
+ kind: "click";
82
+ x: number;
83
+ y: number;
84
+ }> | Readonly<{
85
+ kind: "type";
86
+ text: string;
87
+ }> | Readonly<{
88
+ kind: "scroll";
89
+ deltaX: number;
90
+ deltaY: number;
91
+ }> | Readonly<{
92
+ kind: "resume";
93
+ }>;
94
+ export type Browser = Readonly<{
95
+ /** Whether the stateless Quick Reads exist on this host. */
96
+ quickReads: boolean;
97
+ /** One agent call, bound to the exact (teamId, agentId, sessionId) owner. */
98
+ call: (owner: LeaseOwner, op: CloudBrowserOperation, signal: AbortSignal) => Promise<CloudBrowserOperationResult>;
99
+ /** Pause the owner's lease until a Team member presses "Done". */
100
+ handoff: (owner: LeaseOwner, leaseId: string) => void;
101
+ /** A Team member's door into a paused lease (membership checked by the host). */
102
+ member: (teamId: string, leaseId: string, action: BrowserMemberAction, signal: AbortSignal) => Promise<BinaryResult | null>;
103
+ }>;
104
+ export declare function createBrowser(host: BrowserHost, limits: BrowserLimits): Browser;
@@ -0,0 +1,374 @@
1
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
2
+ import { CloudBrowserLeaseNotFound, CloudBrowserUnavailable, } from "./errors.js";
3
+ import { createLeaseRegistry, } from "./lease-registry.js";
4
+ import { attachPage, clickPage, navigatePage, pdfPage, pressPage, screenshotPage, scrollPage, snapshotPage, typePage, } from "./page.js";
5
+ import { assertCloudBrowserUrlAllowed } from "./url-policy.js";
6
+ /** Module-issued lease ids; provider session ids never reach the Agent. */
7
+ const leaseIds = defineIdFamily({
8
+ family: "oyster-browser-lease",
9
+ version: 1,
10
+ kinds: { lease: { prefix: "lease" } },
11
+ });
12
+ /** One billing execution per Quick Read. */
13
+ const readIds = defineIdFamily({
14
+ family: "oyster-browser-read",
15
+ version: 1,
16
+ kinds: { read: { prefix: "read" } },
17
+ });
18
+ export function createBrowser(host, limits) {
19
+ const now = limits.now ?? Date.now;
20
+ const navigationTimeoutMs = limits.navigationTimeoutMs ?? 15_000;
21
+ const quickReadTimeoutMs = limits.quickReadTimeoutMs ?? 30_000;
22
+ const closeConfirmMs = limits.closeConfirmMs ?? 5_000;
23
+ const maxTeardownAttempts = 5;
24
+ const registry = createLeaseRegistry({
25
+ maxConcurrentPerTeam: limits.maxConcurrentPerTeam,
26
+ maxLifetimeMs: limits.maxLifetimeMs,
27
+ idleTimeoutMs: limits.idleTimeoutMs,
28
+ now,
29
+ });
30
+ const leases = new Map();
31
+ function lease(leaseId) {
32
+ const found = leases.get(leaseId);
33
+ if (found === undefined)
34
+ throw new CloudBrowserLeaseNotFound({ leaseId });
35
+ return found;
36
+ }
37
+ function quickReads() {
38
+ if (host.quickReads === null)
39
+ throw new CloudBrowserUnavailable({ reason: "unconfigured" });
40
+ return host.quickReads;
41
+ }
42
+ const allowed = (url, signal) => assertCloudBrowserUrlAllowed(url, { signal });
43
+ /**
44
+ * Provider sessions this module still owes a confirmed teardown. A lease
45
+ * leaves the Agent's view and its bill closes at close or reap; the
46
+ * provider session stays here until the provider confirms it ended.
47
+ */
48
+ const teardowns = new Map();
49
+ /**
50
+ * Delete one provider session by id; confirm a "closing" ack before calling
51
+ * it closed. Callers have already closed any CDP client of it.
52
+ */
53
+ async function tearDown(sessionId, teamId, signal) {
54
+ const keep = () => {
55
+ const pending = teardowns.get(sessionId);
56
+ const attempts = (pending?.attempts ?? 0) + 1;
57
+ if (attempts > maxTeardownAttempts) {
58
+ teardowns.delete(sessionId);
59
+ // The provider keep-alive (the idle timeout) is the last backstop.
60
+ host.report({
61
+ kind: "teardown_abandoned",
62
+ teamId,
63
+ executionId: sessionId,
64
+ cause: null,
65
+ });
66
+ return;
67
+ }
68
+ teardowns.set(sessionId, { teamId, attempts });
69
+ };
70
+ let acknowledged;
71
+ try {
72
+ acknowledged = await host.sessions.release(sessionId, signal);
73
+ }
74
+ catch (error) {
75
+ keep();
76
+ throw error;
77
+ }
78
+ if (acknowledged === "closing")
79
+ try {
80
+ await host.sessions.confirmClosed(sessionId, Date.now() + closeConfirmMs);
81
+ }
82
+ catch {
83
+ keep();
84
+ return "closing";
85
+ }
86
+ teardowns.delete(sessionId);
87
+ return "closed";
88
+ }
89
+ /** Retry every owed teardown once; bounded by `maxTeardownAttempts` each. */
90
+ async function retryTeardowns(signal) {
91
+ // Map iteration tolerates the deletes and in-place updates tearDown makes.
92
+ for (const [sessionId, { teamId }] of teardowns)
93
+ await tearDown(sessionId, teamId, signal).catch((cause) => {
94
+ host.report({
95
+ kind: "teardown_failed",
96
+ teamId,
97
+ executionId: sessionId,
98
+ cause,
99
+ });
100
+ });
101
+ }
102
+ /**
103
+ * End a lease the registry already released: it leaves the Agent's view,
104
+ * its real elapsed time is settled even when the provider refuses the
105
+ * delete, and the provider session stays owed in `teardowns` until
106
+ * confirmed. A refused delete on close rethrows its typed error.
107
+ */
108
+ async function end(owner, leaseId, openedAt, signal, failure) {
109
+ const open = leases.get(leaseId);
110
+ leases.delete(leaseId);
111
+ try {
112
+ if (open === undefined)
113
+ return "closed";
114
+ open.client.close();
115
+ return await tearDown(open.sessionId, owner.teamId, signal);
116
+ }
117
+ catch (cause) {
118
+ if (failure === "close")
119
+ throw cause;
120
+ host.report({
121
+ kind: "reap_close_failed",
122
+ teamId: owner.teamId,
123
+ executionId: leaseId,
124
+ cause,
125
+ });
126
+ return "closing";
127
+ }
128
+ finally {
129
+ await host
130
+ .meter(owner)
131
+ .settle({
132
+ executionId: leaseId,
133
+ elapsedMs: now() - openedAt,
134
+ maxMs: limits.maxLifetimeMs,
135
+ })
136
+ .catch((cause) => {
137
+ host.report({
138
+ kind: failure === "reap" ? "reap_settle_failed" : "settle_failed",
139
+ teamId: owner.teamId,
140
+ executionId: leaseId,
141
+ cause,
142
+ });
143
+ });
144
+ }
145
+ }
146
+ /**
147
+ * One billed Quick Read: hold, call the provider under the module's own
148
+ * deadline, then start and settle the browser time the provider reports
149
+ * (the measured call time when it reports none). A failed read releases.
150
+ */
151
+ async function quickRead(owner, read, signal) {
152
+ const reads = quickReads();
153
+ const executionId = readIds.read.generate();
154
+ const meter = host.meter(owner);
155
+ await meter.hold({ executionId, maxMs: quickReadTimeoutMs });
156
+ const startedAt = now();
157
+ let provided;
158
+ try {
159
+ provided = await read(reads, AbortSignal.any([signal, AbortSignal.timeout(quickReadTimeoutMs)]));
160
+ }
161
+ catch (error) {
162
+ await meter.release({ executionId }).catch((cause) => {
163
+ host.report({
164
+ kind: "read_release_failed",
165
+ teamId: owner.teamId,
166
+ executionId,
167
+ cause,
168
+ });
169
+ });
170
+ throw error;
171
+ }
172
+ await meter.start({ executionId });
173
+ await meter.settle({
174
+ executionId,
175
+ elapsedMs: provided.browserMs ?? now() - startedAt,
176
+ maxMs: quickReadTimeoutMs,
177
+ });
178
+ return provided.result;
179
+ }
180
+ async function reap(signal) {
181
+ for (const leaseId of registry.expired()) {
182
+ const record = registry.get(leaseId);
183
+ registry.release(leaseId);
184
+ if (record === undefined)
185
+ continue;
186
+ await end(record, leaseId, record.openedAt, signal, "reap");
187
+ }
188
+ }
189
+ async function open(owner, rawUrl, signal) {
190
+ // Egress first: the provider never sees a URL the policy refused.
191
+ const url = rawUrl === null ? null : await allowed(rawUrl, signal);
192
+ const leaseId = leaseIds.lease.generate();
193
+ // The slot is reserved before any await, so concurrent opens count.
194
+ registry.reserve(leaseId, owner);
195
+ let acquired;
196
+ try {
197
+ acquired = await acquireLease(owner, leaseId, signal);
198
+ }
199
+ catch (error) {
200
+ registry.abandon(leaseId);
201
+ throw error;
202
+ }
203
+ const { page, openedAt } = acquired;
204
+ registry.register(leaseId, owner, openedAt);
205
+ if (url === null)
206
+ return { leaseId, url: null };
207
+ try {
208
+ const location = await navigatePage(page, url, navigationTimeoutMs);
209
+ return { leaseId, url: location.url };
210
+ }
211
+ catch (error) {
212
+ // A lease whose first page never loaded is not left running or unbilled.
213
+ registry.release(leaseId);
214
+ await end(owner, leaseId, openedAt, signal, "reap");
215
+ throw error;
216
+ }
217
+ }
218
+ /** Hold, acquire, attach and start one lease's provider session. */
219
+ async function acquireLease(owner, leaseId, signal) {
220
+ const meter = host.meter(owner);
221
+ await meter.hold({ executionId: leaseId, maxMs: limits.maxLifetimeMs });
222
+ const cleanup = (cause) => {
223
+ host.report({
224
+ kind: "open_cleanup_failed",
225
+ teamId: owner.teamId,
226
+ executionId: leaseId,
227
+ cause,
228
+ });
229
+ };
230
+ // The provider bills from the session request, so the lease's billed
231
+ // time starts here, before create, not once it is attached.
232
+ const openedAt = now();
233
+ let sessionId;
234
+ try {
235
+ sessionId = await host.sessions.create({ keepAliveMs: limits.idleTimeoutMs }, signal);
236
+ }
237
+ catch (error) {
238
+ await meter.release({ executionId: leaseId }).catch(cleanup);
239
+ throw error;
240
+ }
241
+ // From here the provider session exists: every failure hands its id to
242
+ // the teardown owner, which retries until the provider confirms it.
243
+ const abandon = async (client) => {
244
+ client?.close();
245
+ await tearDown(sessionId, owner.teamId, signal).catch(cleanup);
246
+ };
247
+ let client;
248
+ let page;
249
+ try {
250
+ client = await host.sessions.connect(sessionId, AbortSignal.any([signal, AbortSignal.timeout(navigationTimeoutMs)]));
251
+ }
252
+ catch (error) {
253
+ await abandon(null);
254
+ await meter.release({ executionId: leaseId }).catch(cleanup);
255
+ throw error;
256
+ }
257
+ try {
258
+ page = await attachPage(client);
259
+ }
260
+ catch (error) {
261
+ await abandon(client);
262
+ await meter.release({ executionId: leaseId }).catch(cleanup);
263
+ throw error;
264
+ }
265
+ try {
266
+ await meter.start({ executionId: leaseId });
267
+ }
268
+ catch (error) {
269
+ // The meter released its own hold; only the provider session remains.
270
+ await abandon(client);
271
+ throw error;
272
+ }
273
+ leases.set(leaseId, { sessionId, client, page });
274
+ return { page, openedAt };
275
+ }
276
+ async function driven(owner, op, signal) {
277
+ if (op.kind === "close") {
278
+ const record = registry.get(op.leaseId);
279
+ registry.touch(op.leaseId, owner);
280
+ const openedAt = record?.openedAt ?? now();
281
+ registry.release(op.leaseId);
282
+ const provider = await end(owner, op.leaseId, openedAt, signal, "close");
283
+ return { leaseId: op.leaseId, closed: true, provider };
284
+ }
285
+ registry.touch(op.leaseId, owner);
286
+ const { page } = lease(op.leaseId);
287
+ switch (op.kind) {
288
+ case "navigate": {
289
+ const url = await allowed(op.url, signal);
290
+ const location = await navigatePage(page, url, navigationTimeoutMs);
291
+ return {
292
+ leaseId: op.leaseId,
293
+ url: location.url,
294
+ status: null,
295
+ title: location.title,
296
+ };
297
+ }
298
+ case "snapshot":
299
+ return snapshotPage(page, op.leaseId, signal);
300
+ case "click":
301
+ await clickPage(page, op.x, op.y, signal);
302
+ return { leaseId: op.leaseId };
303
+ case "type":
304
+ await typePage(page, op.text, signal);
305
+ return { leaseId: op.leaseId };
306
+ case "press":
307
+ await pressPage(page, op.key, signal);
308
+ return { leaseId: op.leaseId };
309
+ case "scroll":
310
+ await scrollPage(page, op.deltaX, op.deltaY, signal);
311
+ return { leaseId: op.leaseId };
312
+ case "screenshot":
313
+ return screenshotPage(page, op.leaseId, signal);
314
+ case "pdf":
315
+ return pdfPage(page, op.leaseId, signal);
316
+ }
317
+ }
318
+ return {
319
+ quickReads: host.quickReads !== null,
320
+ call: async (owner, op, signal) => {
321
+ await retryTeardowns(signal);
322
+ await reap(signal);
323
+ switch (op.kind) {
324
+ case "markdown": {
325
+ const url = await allowed(op.input.url, signal);
326
+ return quickRead(owner, (reads, bounded) => reads.markdown(url, op.input.maxChars, bounded), signal);
327
+ }
328
+ case "links": {
329
+ const url = await allowed(op.url, signal);
330
+ return quickRead(owner, (reads, bounded) => reads.links(url, bounded), signal);
331
+ }
332
+ case "scrape": {
333
+ const url = await allowed(op.input.url, signal);
334
+ return quickRead(owner, (reads, bounded) => reads.scrape(url, op.input.selectors, bounded), signal);
335
+ }
336
+ case "extract": {
337
+ const url = await allowed(op.input.url, signal);
338
+ return quickRead(owner, (reads, bounded) => reads.extract(url, op.input.prompt, bounded), signal);
339
+ }
340
+ case "open":
341
+ return open(owner, op.url, signal);
342
+ default:
343
+ return driven(owner, op, signal);
344
+ }
345
+ },
346
+ handoff: (owner, leaseId) => {
347
+ registry.touch(leaseId, owner);
348
+ registry.pause(leaseId);
349
+ },
350
+ member: async (teamId, leaseId, action, signal) => {
351
+ registry.touchAsMember(leaseId, teamId);
352
+ if (action.kind === "resume") {
353
+ registry.resume(leaseId, teamId);
354
+ return null;
355
+ }
356
+ if (registry.get(leaseId)?.paused !== true)
357
+ throw new CloudBrowserLeaseNotFound({ leaseId });
358
+ const { page } = lease(leaseId);
359
+ switch (action.kind) {
360
+ case "screenshot":
361
+ return screenshotPage(page, leaseId, signal);
362
+ case "click":
363
+ await clickPage(page, action.x, action.y, signal);
364
+ return null;
365
+ case "type":
366
+ await typePage(page, action.text, signal);
367
+ return null;
368
+ case "scroll":
369
+ await scrollPage(page, action.deltaX, action.deltaY, signal);
370
+ return null;
371
+ }
372
+ },
373
+ };
374
+ }
@@ -0,0 +1,76 @@
1
+ import { type CapabilitySource } from "@fungi.computer/mycelium";
2
+ import { type CloudBrowserOperation, type CloudBrowserOperationResult } from "./operations.js";
3
+ import type { LeaseOwner } from "./lease-registry.js";
4
+ /**
5
+ * The `cloudBrowser` Mycelium module: a work browser for reading and driving
6
+ * public pages. To show the member something, use the Session browser
7
+ * tools. Quick Reads (`markdown`/`links`/`scrape`/`extract`) need no open
8
+ * session; `open`..`close` share one driven session by `leaseId`.
9
+ * `screenshot`/`pdf` save to a named Computer path instead of returning
10
+ * bytes, and `handoff` asks a Team member to drive the page for a login or
11
+ * captcha, through the existing `handoff` card, never a CDP URL.
12
+ */
13
+ /**
14
+ * The one source of truth for every `cloudBrowser` description: the module
15
+ * itself and each operation. `cloud-browser-descriptions.test.ts` imports
16
+ * this and asserts every entry stays under 1000 characters (the acceptance
17
+ * criterion in `issues/retained-features-v1/02-*.md`); the guard right below
18
+ * repeats that check at import time so a regression fails fast either way.
19
+ */
20
+ export declare const CLOUD_BROWSER_DESCRIPTIONS: Readonly<{
21
+ readonly module: string;
22
+ readonly markdown: "Render a JavaScript page and return it as markdown, truncated to maxChars (default 20000).";
23
+ readonly links: "Render a page and return up to 500 links found on it.";
24
+ readonly scrape: "Render a page and return matched elements for up to 10 CSS selectors, bounded to 64 KiB.";
25
+ readonly extract: "Render a page and extract structured data matching a prompt.";
26
+ readonly open: "Open a driven browser session, optionally navigating to a URL, and return its leaseId.";
27
+ readonly navigate: "Navigate an open session's page to a URL.";
28
+ readonly snapshot: "Read an open session's page as a bounded accessibility-tree text summary.";
29
+ readonly click: "Click at a viewport pixel coordinate in an open session.";
30
+ readonly type: "Insert text at the current focus in an open session.";
31
+ readonly press: "Press one key (e.g. \"Enter\", \"Tab\") in an open session.";
32
+ readonly scroll: "Scroll an open session's page by a pixel delta.";
33
+ readonly screenshot: "Capture a PNG screenshot of an open session's page and save it to a Computer path; returns the saved byte count, never the bytes.";
34
+ readonly pdf: "Render an open session's page to PDF and save it to a Computer path; returns the saved byte count, never the bytes.";
35
+ readonly handoff: "Ask a Team member to take over an open session for a login or captcha; a card appears in the conversation and the run continues once they finish.";
36
+ readonly close: "End a driven session and release its lease.";
37
+ }>;
38
+ /**
39
+ * The host's door for screenshot/PDF bytes: a named path on a Computer the
40
+ * Agent is attached to. The model only ever receives the saved byte count.
41
+ */
42
+ export type SaveBrowserFile = (input: Readonly<{
43
+ computerId: string;
44
+ path: string;
45
+ bytes: Uint8Array;
46
+ contentType: string;
47
+ }>) => Promise<Readonly<{
48
+ computerId: string;
49
+ path: string;
50
+ bytes: number;
51
+ }>>;
52
+ /** The agent-facing slice of `Browser`, possibly behind a host RPC hop. */
53
+ export type AgentBrowserDoor = Readonly<{
54
+ /** One agent operation, bound to the Session's lease owner. */
55
+ call: (owner: LeaseOwner, op: CloudBrowserOperation, signal: AbortSignal) => Promise<CloudBrowserOperationResult>;
56
+ /** Pause the owner's lease for a Team member. */
57
+ handoff: (owner: LeaseOwner, leaseId: string) => Promise<void>;
58
+ }>;
59
+ /**
60
+ * Compose the `cloudBrowser` source for one Session, on either host. `browser`
61
+ * is the module's lease door (in-process, or the Hub's egress RPC); `saveFile`
62
+ * is the host's Computer bytes writer and `requestHandoff` its "needs you"
63
+ * card. A host without one passes `null` and the matching operations are not
64
+ * offered. A lease needs a Session identity, so a session-free host (e.g. the
65
+ * MCP execute path) gets no module at all.
66
+ */
67
+ export declare function browserCapabilitySource(deps: Readonly<{
68
+ identity: () => Readonly<{
69
+ teamId: string;
70
+ agentId: string;
71
+ }> | null;
72
+ browser: AgentBrowserDoor;
73
+ quickReads: boolean;
74
+ saveFile: SaveBrowserFile | null;
75
+ requestHandoff: ((sessionId: string, reason: string) => void) | null;
76
+ }>): CapabilitySource;