@fungi.computer/oyster 0.3.2 → 0.4.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 (86) hide show
  1. package/dist/agent-computer-source.d.ts +42 -0
  2. package/dist/agent-computer-source.js +88 -0
  3. package/dist/agent-computers.d.ts +104 -0
  4. package/dist/agent-computers.js +227 -0
  5. package/dist/agents.d.ts +10 -0
  6. package/dist/agents.js +33 -0
  7. package/dist/app-builds.d.ts +1 -1
  8. package/dist/app-builds.js +16 -6
  9. package/dist/boot-capability.d.ts +2 -1
  10. package/dist/boot-capability.js +7 -5
  11. package/dist/browser/boot.d.ts +2 -2
  12. package/dist/browser/boot.js +10 -3
  13. package/dist/browser/browser.d.ts +1 -1
  14. package/dist/browser/browser.js +2 -1
  15. package/dist/browser/cdp-client.d.ts +2 -0
  16. package/dist/browser/errors.d.ts +2 -2
  17. package/dist/browser/errors.js +2 -2
  18. package/dist/browser/index.d.ts +3 -2
  19. package/dist/browser/index.js +3 -2
  20. package/dist/browser/page.js +11 -4
  21. package/dist/browser/sessions.d.ts +7 -4
  22. package/dist/browser/sessions.js +159 -0
  23. package/dist/browser/url-policy.d.ts +4 -2
  24. package/dist/browser/url-policy.js +4 -2
  25. package/dist/build/build-jobs.d.ts +3 -3
  26. package/dist/build/build-jobs.js +10 -5
  27. package/dist/build/candidate-cli.js +5 -3
  28. package/dist/build/candidate-cli.mjs +223 -5
  29. package/dist/build/candidate-diagnostic.d.ts +2 -0
  30. package/dist/build/candidate-diagnostic.js +16 -0
  31. package/dist/computer-control-port.d.ts +26 -0
  32. package/dist/computer-control-port.js +1 -0
  33. package/dist/host.d.ts +3 -0
  34. package/dist/machine-link/delivery.d.ts +1 -1
  35. package/dist/machine-link/delivery.js +2 -3
  36. package/dist/machine-link/host.d.ts +37 -6
  37. package/dist/machine-link/host.js +209 -14
  38. package/dist/machine-link/link.d.ts +13 -1
  39. package/dist/machine-link/link.js +20 -7
  40. package/dist/mcp.d.ts +144 -10
  41. package/dist/mcp.js +130 -13
  42. package/dist/page/computer-handles.d.ts +3 -3
  43. package/dist/page-script.js +1 -1
  44. package/dist/standalone/agent-computers.d.ts +29 -0
  45. package/dist/standalone/agent-computers.js +101 -0
  46. package/dist/standalone/agent-mail.d.ts +41 -18
  47. package/dist/standalone/agent-model-choice.d.ts +29 -0
  48. package/dist/standalone/agent-model-choice.js +72 -0
  49. package/dist/standalone/agents.d.ts +44 -4
  50. package/dist/standalone/agents.js +502 -46
  51. package/dist/standalone/app-set.d.ts +8 -1
  52. package/dist/standalone/app-set.js +46 -31
  53. package/dist/standalone/computer-control.d.ts +4 -0
  54. package/dist/standalone/computer-control.js +62 -0
  55. package/dist/standalone/computer-fleet.d.ts +1 -0
  56. package/dist/standalone/computer-fleet.js +1 -1
  57. package/dist/standalone/computer.d.ts +21 -2
  58. package/dist/standalone/computer.js +12 -0
  59. package/dist/standalone/install-apps-bin.d.ts +2 -0
  60. package/dist/standalone/install-apps-bin.js +3 -0
  61. package/dist/standalone/install-apps-cli.d.ts +3 -1
  62. package/dist/standalone/install-apps-cli.js +27 -17
  63. package/dist/standalone/machine-link.d.ts +10 -0
  64. package/dist/standalone/machine-link.js +6 -0
  65. package/dist/standalone/registry.d.ts +10 -1
  66. package/dist/standalone/registry.js +9 -0
  67. package/dist/standalone/worker.js +12 -0
  68. package/dist/standalone/workflow-binding.d.ts +2 -2
  69. package/dist/standalone/workflow-host.d.ts +11 -11
  70. package/dist/standalone/workflows.d.ts +22 -22
  71. package/dist/storage-frame.d.ts +3 -3
  72. package/dist/workers-git.d.ts +2 -0
  73. package/dist/workers-git.js +2 -0
  74. package/dist/workers.d.ts +8 -8
  75. package/dist/workers.js +9 -1
  76. package/docs/api/agents.md +66 -0
  77. package/docs/api/apps.md +104 -104
  78. package/docs/api/build-contract.md +35 -35
  79. package/docs/api/build.md +12 -12
  80. package/docs/api/host.md +61 -61
  81. package/docs/api/mcp.md +127 -13
  82. package/docs/api/root-data.md +1 -1
  83. package/docs/api/tenancy.md +1 -1
  84. package/docs/api/workers-git.md +3 -2
  85. package/docs/api/workers.md +81 -83
  86. package/package.json +26 -18
@@ -8,8 +8,8 @@ import type { BrowserSessions } from "./sessions.js";
8
8
  * (`BrowserSessions`), never a meter, so a boot check is never billed to a
9
9
  * Team, and the agent tool never receives it. It accepts no URL: it builds
10
10
  * `<origin>/_boot/<releaseId>/index.html` itself from the host's configured
11
- * origin. Its dedicated provider session is pinned to that host by the
12
- * provider guardrail (a host-level backstop). The browser's request gate
11
+ * origin. Browser Run also pins that host through a provider guardrail;
12
+ * Chromium contexts have no provider host allowlist. The browser's request gate
13
13
  * fails every request whose exact origin (scheme, host, port) differs, on
14
14
  * the page and on every frame it admits, before that frame runs. The
15
15
  * capability token is added by that gate, and only to exact-origin
@@ -12,8 +12,8 @@ import { attachPage, navigatePage, pageLocation } from "./page.js";
12
12
  * (`BrowserSessions`), never a meter, so a boot check is never billed to a
13
13
  * Team, and the agent tool never receives it. It accepts no URL: it builds
14
14
  * `<origin>/_boot/<releaseId>/index.html` itself from the host's configured
15
- * origin. Its dedicated provider session is pinned to that host by the
16
- * provider guardrail (a host-level backstop). The browser's request gate
15
+ * origin. Browser Run also pins that host through a provider guardrail;
16
+ * Chromium contexts have no provider host allowlist. The browser's request gate
17
17
  * fails every request whose exact origin (scheme, host, port) differs, on
18
18
  * the page and on every frame it admits, before that frame runs. The
19
19
  * capability token is added by that gate, and only to exact-origin
@@ -102,7 +102,11 @@ const responseSchema = z.object({
102
102
  });
103
103
  const loadingFailedSchema = z.object({ requestId: z.string() });
104
104
  const createdSchema = z.object({
105
- targetInfo: z.object({ targetId: z.string(), type: z.string() }),
105
+ targetInfo: z.object({
106
+ targetId: z.string(),
107
+ type: z.string(),
108
+ browserContextId: z.string().optional(),
109
+ }),
106
110
  });
107
111
  const attachedSchema = z.object({
108
112
  sessionId: z.string(),
@@ -355,6 +359,9 @@ function observe(client, recorder, pinned, target, children, pages) {
355
359
  const created = createdSchema.safeParse(params);
356
360
  if (!created.success || created.data.targetInfo.type !== "page")
357
361
  return;
362
+ if (client.browserContextId !== undefined &&
363
+ created.data.targetInfo.browserContextId !== client.browserContextId)
364
+ return;
358
365
  const { targetId } = created.data.targetInfo;
359
366
  if (!pages.sealed)
360
367
  pages.known.add(targetId);
@@ -63,7 +63,7 @@ export type BrowserLimits = Readonly<{
63
63
  maxConcurrentPerTeam: number;
64
64
  /** A lease older than this is reaped even if active. */
65
65
  maxLifetimeMs: number;
66
- /** A lease idle this long is reaped; also the provider keep-alive. */
66
+ /** Reaped on the next operation; Browser Run also uses it as keep-alive. */
67
67
  idleTimeoutMs: number;
68
68
  /** Per-navigation load deadline. */
69
69
  navigationTimeoutMs?: number;
@@ -56,7 +56,8 @@ export function createBrowser(host, limits) {
56
56
  const attempts = (pending?.attempts ?? 0) + 1;
57
57
  if (attempts > maxTeardownAttempts) {
58
58
  teardowns.delete(sessionId);
59
- // The provider keep-alive (the idle timeout) is the last backstop.
59
+ // Browser Run has an idle backstop. Chromium does not: a context
60
+ // abandoned here persists until Chromium restarts.
60
61
  host.report({
61
62
  kind: "teardown_abandoned",
62
63
  teamId,
@@ -34,6 +34,8 @@ export type HttpFetch = (url: string, init?: RequestInit) => Promise<Response>;
34
34
  */
35
35
  export declare function openCdpSocket(fetcher: HttpFetch, url: string, headers: Readonly<Record<string, string>>, signal: AbortSignal): Promise<CdpSocket>;
36
36
  export type CdpClient = Readonly<{
37
+ /** A Chromium context-scoped host supplies this; Browser Run leaves it absent. */
38
+ browserContextId?: string;
37
39
  /** One command/response round trip, optionally flattened into `sessionId`. */
38
40
  call: (method: string, params: Readonly<Record<string, Json>>, options?: Readonly<{
39
41
  sessionId?: string;
@@ -3,7 +3,7 @@ declare const CloudBrowserUrlDenied_base: new <A extends Record<string, any> = {
3
3
  } & Readonly<A>;
4
4
  /**
5
5
  * The cloud-browser backend's closed, typed failure channel. Every backend
6
- * (today only Cloudflare Browser Run) throws exclusively from this union;
6
+ * (Cloudflare Browser Run or on-box Chromium) throws from this union;
7
7
  * `agent-cloud-browser-source.ts` is the one place that maps it onto
8
8
  * Mycelium's `CapabilityFailed`, so the model only ever sees a bounded,
9
9
  * owner-admitted message (never a raw provider error body or stack).
@@ -17,7 +17,7 @@ export declare class CloudBrowserUrlDenied extends CloudBrowserUrlDenied_base<{
17
17
  declare const CloudBrowserUpstreamError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
18
18
  readonly _tag: "CloudBrowserUpstreamError";
19
19
  } & Readonly<A>;
20
- /** The provider (Cloudflare Browser Run) answered with a failure. */
20
+ /** The configured provider answered with a failure or is unreachable. */
21
21
  export declare class CloudBrowserUpstreamError extends CloudBrowserUpstreamError_base<{
22
22
  readonly op: string;
23
23
  readonly status: number | null;
@@ -1,7 +1,7 @@
1
1
  import * as Data from "effect/Data";
2
2
  /**
3
3
  * The cloud-browser backend's closed, typed failure channel. Every backend
4
- * (today only Cloudflare Browser Run) throws exclusively from this union;
4
+ * (Cloudflare Browser Run or on-box Chromium) throws from this union;
5
5
  * `agent-cloud-browser-source.ts` is the one place that maps it onto
6
6
  * Mycelium's `CapabilityFailed`, so the model only ever sees a bounded,
7
7
  * owner-admitted message (never a raw provider error body or stack).
@@ -9,7 +9,7 @@ import * as Data from "effect/Data";
9
9
  /** The target URL failed policy before any provider call was made. */
10
10
  export class CloudBrowserUrlDenied extends Data.TaggedError("CloudBrowserUrlDenied") {
11
11
  }
12
- /** The provider (Cloudflare Browser Run) answered with a failure. */
12
+ /** The configured provider answered with a failure or is unreachable. */
13
13
  export class CloudBrowserUpstreamError extends Data.TaggedError("CloudBrowserUpstreamError") {
14
14
  }
15
15
  /** A call did not complete within its deadline. */
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
3
3
  * standalone Oyster. A host builds the port from a provider (`browserRunSessions`
4
- * or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
4
+ * or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
5
+ * Quick Reads when it has them), its
5
6
  * required metering owner and an event sink; `createBrowser` owns lease
6
7
  * ownership, egress policy and billing order; `browserCapabilitySource` is
7
8
  * the agent's `cloudBrowser` tool. Raw CDP stays internal to this directory.
@@ -13,4 +14,4 @@ export type { LeaseOwner } from "./lease-registry.js";
13
14
  export { cloudBrowserBounds, extractInputSchema, extractResultSchema, linksResultSchema, markdownInputSchema, markdownResultSchema, navigateResultSchema, openResultSchema, scrapeInputSchema, scrapeResultSchema, snapshotResultSchema, type BinaryResult, type CloudBrowserAck, type CloudBrowserClosed, type CloudBrowserOperation, type CloudBrowserOperationResult, type ExtractInput, type ExtractResult, type LinksResult, type MarkdownInput, type MarkdownResult, type NavigateResult, type OpenResult, type ScrapeInput, type ScrapeResult, type SnapshotResult, } from "./operations.js";
14
15
  export type { CdpSocket, HttpFetch } from "./cdp-client.js";
15
16
  export { browserRunQuickReads, type BrowserRunQuickReadsConfig, type QuickRead, type QuickReads, } from "./quick-reads.js";
16
- export { bindingSessions, browserRunSessions, type BrowserBinding, type BrowserSessions, type CdpConnect, } from "./sessions.js";
17
+ export { bindingSessions, browserRunSessions, onboxBrowserSessions, type BrowserBinding, type BrowserSessions, type CdpConnect, } from "./sessions.js";
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
3
3
  * standalone Oyster. A host builds the port from a provider (`browserRunSessions`
4
- * or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
4
+ * or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
5
+ * Quick Reads when it has them), its
5
6
  * required metering owner and an event sink; `createBrowser` owns lease
6
7
  * ownership, egress policy and billing order; `browserCapabilitySource` is
7
8
  * the agent's `cloudBrowser` tool. Raw CDP stays internal to this directory.
@@ -11,4 +12,4 @@ export { browserCapabilitySource, CLOUD_BROWSER_DESCRIPTIONS, } from "./capabili
11
12
  export { CloudBrowserCapacityExceeded, CloudBrowserCreditDenied, CloudBrowserLeaseNotFound, CloudBrowserLeasePaused, CloudBrowserTimeout, CloudBrowserUnavailable, CloudBrowserUpstreamError, CloudBrowserUrlDenied, } from "./errors.js";
12
13
  export { cloudBrowserBounds, extractInputSchema, extractResultSchema, linksResultSchema, markdownInputSchema, markdownResultSchema, navigateResultSchema, openResultSchema, scrapeInputSchema, scrapeResultSchema, snapshotResultSchema, } from "./operations.js";
13
14
  export { browserRunQuickReads, } from "./quick-reads.js";
14
- export { bindingSessions, browserRunSessions, } from "./sessions.js";
15
+ export { bindingSessions, browserRunSessions, onboxBrowserSessions, } from "./sessions.js";
@@ -8,7 +8,11 @@ import { cloudBrowserBounds, } from "./operations.js";
8
8
  * The session-create response shape was confirmed by the live workerd smoke
9
9
  * on 2026-10-03; see `test/browser/cdp-client.test.ts`.
10
10
  */
11
- const targetSchema = z.object({ targetId: z.string(), type: z.string() });
11
+ const targetSchema = z.object({
12
+ targetId: z.string(),
13
+ type: z.string(),
14
+ browserContextId: z.string().optional(),
15
+ });
12
16
  const getTargetsResultSchema = z.object({ targetInfos: z.array(targetSchema) });
13
17
  const attachResultSchema = z.object({ sessionId: z.string() });
14
18
  const navigateResultSchema = z.object({
@@ -31,11 +35,14 @@ const capturedDataSchema = z.object({ data: z.string() });
31
35
  /** Attach to the session's first page target (or a new blank one). */
32
36
  export async function attachPage(client) {
33
37
  const targets = getTargetsResultSchema.parse(await client.call("Target.getTargets", {}));
34
- const targetId = targets.targetInfos.find((target) => target.type === "page")?.targetId ??
38
+ const targetId = targets.targetInfos.find((target) => target.type === "page" &&
39
+ (client.browserContextId === undefined ||
40
+ target.browserContextId === client.browserContextId))?.targetId ??
35
41
  z
36
42
  .object({ targetId: z.string() })
37
- .parse(await client.call("Target.createTarget", { url: "about:blank" }))
38
- .targetId;
43
+ .parse(await client.call("Target.createTarget", client.browserContextId === undefined
44
+ ? { url: "about:blank" }
45
+ : { url: "about:blank", browserContextId: client.browserContextId })).targetId;
39
46
  const { sessionId: cdpSessionId } = attachResultSchema.parse(await client.call("Target.attachToTarget", { targetId, flatten: true }));
40
47
  await client.call("Page.enable", {}, { sessionId: cdpSessionId });
41
48
  await client.call("Accessibility.enable", {}, { sessionId: cdpSessionId });
@@ -11,16 +11,17 @@ import { type CdpClient, type CdpSocket, type HttpFetch } from "./cdp-client.js"
11
11
  * - Binding: `binding.fetch("http://fake.host/v1/devtools/…")`, the endpoints
12
12
  * a Workers Browser Run binding answers.
13
13
  *
14
+ * Chromium on staging uses browser contexts rather than Browser Run endpoints.
14
15
  * Provider session ids stay inside the module; agent leases use their own ids.
15
16
  */
16
17
  export type AcquireOptions = Readonly<{
17
- /** Provider idle keep-alive, clamped to Browser Run's 10s–20min range. */
18
+ /** Browser Run idle keep-alive (10s–20min); Chromium has no equivalent. */
18
19
  keepAliveMs: number;
19
- /** Provider guardrail: the only hosts this session may load. */
20
+ /** Browser Run provider guardrail; Chromium relies on its caller's URL gate. */
20
21
  allowedDomains?: readonly string[];
21
22
  }>;
22
23
  /**
23
- * The provider half of the host port. Built only by the two functions below.
24
+ * The provider half of the host port.
24
25
  * Each step is separate so `createBrowser` owns a provider session id from the
25
26
  * moment it exists, including when its CDP connect then fails.
26
27
  */
@@ -31,7 +32,7 @@ export type BrowserSessions = Readonly<{
31
32
  connect: (sessionId: string, signal: AbortSignal) => Promise<CdpClient>;
32
33
  /** Ask the provider to delete a session; 404 is already gone. @internal */
33
34
  release: (sessionId: string, signal: AbortSignal) => Promise<"closed" | "closing">;
34
- /** Reads the provider's session record until it reports an end time. @internal */
35
+ /** Confirm provider closure (Chromium checks context absence). @internal */
35
36
  confirmClosed: (sessionId: string, deadlineAt: number) => Promise<void>;
36
37
  }>;
37
38
  /** A Workers BROWSER binding (`Fetcher`), reduced to the call this module makes. */
@@ -50,3 +51,5 @@ export declare function browserRunSessions(config: Readonly<{
50
51
  }>): BrowserSessions;
51
52
  /** A standalone Oyster's own Workers BROWSER binding. */
52
53
  export declare function bindingSessions(binding: BrowserBinding): BrowserSessions;
54
+ /** Staging uses Chromium's native CDP browser contexts; no session store. */
55
+ export declare function onboxBrowserSessions(origin: string, fetcher?: HttpFetch): BrowserSessions;
@@ -116,3 +116,162 @@ export function bindingSessions(binding) {
116
116
  connect: (url, headers, signal) => openCdpSocket(fetcher, url, headers, signal),
117
117
  });
118
118
  }
119
+ /** Staging uses Chromium's native CDP browser contexts; no session store. */
120
+ export function onboxBrowserSessions(origin, fetcher = (url, init) => fetch(url, init)) {
121
+ const base = new URL(origin);
122
+ if (base.protocol !== "http:" ||
123
+ base.hostname !== "127.0.0.1" ||
124
+ base.username ||
125
+ base.password ||
126
+ base.pathname !== "/" ||
127
+ base.search ||
128
+ base.hash)
129
+ throw new TypeError("On-box browser origin must be bare IPv4 loopback");
130
+ const browserEndpointSchema = z.object({ webSocketDebuggerUrl: z.url() });
131
+ const contextsSchema = z.object({ browserContextIds: z.array(z.string()) });
132
+ const createdContextSchema = z.object({
133
+ browserContextId: z.string().min(1),
134
+ });
135
+ async function connectBrowser(signal) {
136
+ // Chromium's native discovery endpoint is not a Botanical session server.
137
+ let discovery;
138
+ try {
139
+ discovery = await fetcher(new URL("/json/version", base).href, {
140
+ signal,
141
+ });
142
+ }
143
+ catch (cause) {
144
+ if (signal.aborted)
145
+ throw cause;
146
+ throw new CloudBrowserUpstreamError({
147
+ op: "onbox.connect",
148
+ status: null,
149
+ message: "On-box Chromium is unavailable",
150
+ });
151
+ }
152
+ if (!discovery.ok)
153
+ throw new CloudBrowserUpstreamError({
154
+ op: "onbox.connect",
155
+ status: discovery.status,
156
+ message: "On-box Chromium is unavailable",
157
+ });
158
+ let body;
159
+ try {
160
+ body = await discovery.json();
161
+ }
162
+ catch (cause) {
163
+ if (signal.aborted)
164
+ throw cause;
165
+ throw new CloudBrowserUpstreamError({
166
+ op: "onbox.connect",
167
+ status: null,
168
+ message: "On-box browser returned an unreadable CDP endpoint",
169
+ });
170
+ }
171
+ const discovered = browserEndpointSchema.safeParse(body);
172
+ if (!discovered.success)
173
+ throw new CloudBrowserUpstreamError({
174
+ op: "onbox.connect",
175
+ status: null,
176
+ message: "On-box browser returned an unreadable CDP endpoint",
177
+ });
178
+ const { webSocketDebuggerUrl } = discovered.data;
179
+ const endpoint = new URL(webSocketDebuggerUrl);
180
+ if (endpoint.protocol !== "ws:" ||
181
+ endpoint.hostname !== "127.0.0.1" ||
182
+ endpoint.port !== base.port ||
183
+ !/^\/devtools\/browser\/[^/]+$/u.test(endpoint.pathname) ||
184
+ endpoint.username ||
185
+ endpoint.password ||
186
+ endpoint.search ||
187
+ endpoint.hash)
188
+ throw new CloudBrowserUpstreamError({
189
+ op: "onbox.connect",
190
+ status: null,
191
+ message: "On-box browser returned an invalid CDP endpoint",
192
+ });
193
+ return createCdpClient(await openCdpSocket(fetcher, endpoint.href.replace(/^ws:/u, "http:"), {}, signal));
194
+ }
195
+ async function contexts(client, signal) {
196
+ const result = contextsSchema.safeParse(await client.call("Target.getBrowserContexts", {}, { signal }));
197
+ if (!result.success)
198
+ throw new CloudBrowserUpstreamError({
199
+ op: "onbox.contexts",
200
+ status: null,
201
+ message: "On-box browser returned unreadable contexts",
202
+ });
203
+ return result.data.browserContextIds;
204
+ }
205
+ return {
206
+ create: async (_options, signal) => {
207
+ // Chromium has no idle keep-alive or allowedDomains create guardrail:
208
+ // Oyster's Agent URL policy and the boot page request gate remain active.
209
+ // A Chromium restart ends every context; no restart recovery is claimed.
210
+ const client = await connectBrowser(signal);
211
+ try {
212
+ const created = createdContextSchema.safeParse(await client.call("Target.createBrowserContext", {}, { signal }));
213
+ if (!created.success)
214
+ throw new CloudBrowserUpstreamError({
215
+ op: "onbox.create",
216
+ status: null,
217
+ message: "On-box browser returned an unreadable context",
218
+ });
219
+ const { browserContextId } = created.data;
220
+ try {
221
+ await client.call("Target.createTarget", { url: "about:blank", browserContextId }, { signal });
222
+ return browserContextId;
223
+ }
224
+ catch (error) {
225
+ await client.call("Target.disposeBrowserContext", { browserContextId }, { signal });
226
+ throw error;
227
+ }
228
+ }
229
+ finally {
230
+ client.close();
231
+ }
232
+ },
233
+ connect: async (id, signal) => {
234
+ const client = await connectBrowser(signal);
235
+ try {
236
+ if (!(await contexts(client, signal)).includes(id))
237
+ throw new CloudBrowserUpstreamError({
238
+ op: "onbox.connect",
239
+ status: null,
240
+ message: "On-box browser context is gone",
241
+ });
242
+ return { ...client, browserContextId: id };
243
+ }
244
+ catch (error) {
245
+ client.close();
246
+ throw error;
247
+ }
248
+ },
249
+ release: async (id, signal) => {
250
+ const client = await connectBrowser(signal);
251
+ try {
252
+ if ((await contexts(client, signal)).includes(id))
253
+ await client.call("Target.disposeBrowserContext", { browserContextId: id }, { signal });
254
+ return "closed";
255
+ }
256
+ finally {
257
+ client.close();
258
+ }
259
+ },
260
+ confirmClosed: async (id, deadlineAt) => {
261
+ const signal = AbortSignal.timeout(Math.max(1, deadlineAt - Date.now()));
262
+ const client = await connectBrowser(signal);
263
+ try {
264
+ if (!(await contexts(client, signal)).includes(id))
265
+ return;
266
+ }
267
+ finally {
268
+ client.close();
269
+ }
270
+ throw new CloudBrowserUpstreamError({
271
+ op: "onbox.close.confirm",
272
+ status: null,
273
+ message: "On-box browser did not confirm closure",
274
+ });
275
+ },
276
+ };
277
+ }
@@ -12,7 +12,9 @@ export type UrlCheckOptions = Readonly<{
12
12
  * `169.254.169.254`, a private range like `10/8`, the box's own address, and
13
13
  * a rebinding hostname (one whose DNS answer is private) are all refused
14
14
  * here, before Cloudflare ever sees the URL. The name is resolved once; the
15
- * caller never connects to it directly — Cloudflare does, so this is a policy
16
- * gate, not a proxy.
15
+ * caller does not connect directly: Browser Run does in production, Chromium
16
+ * does on staging. This check is not network confinement: a page can redirect,
17
+ * load subresources or resolve the host differently after this check. Staging
18
+ * must isolate Chromium's network egress before enabling Agent leases.
17
19
  */
18
20
  export declare function assertCloudBrowserUrlAllowed(raw: string, options: UrlCheckOptions): Promise<URL>;
@@ -10,8 +10,10 @@ import { CloudBrowserUrlDenied } from "./errors.js";
10
10
  * `169.254.169.254`, a private range like `10/8`, the box's own address, and
11
11
  * a rebinding hostname (one whose DNS answer is private) are all refused
12
12
  * here, before Cloudflare ever sees the URL. The name is resolved once; the
13
- * caller never connects to it directly — Cloudflare does, so this is a policy
14
- * gate, not a proxy.
13
+ * caller does not connect directly: Browser Run does in production, Chromium
14
+ * does on staging. This check is not network confinement: a page can redirect,
15
+ * load subresources or resolve the host differently after this check. Staging
16
+ * must isolate Chromium's network egress before enabling Agent leases.
15
17
  */
16
18
  export async function assertCloudBrowserUrlAllowed(raw, options) {
17
19
  let url;
@@ -11,7 +11,7 @@ declare const jobSchema: Schema.Struct<{
11
11
  readonly expiresAt: Schema.Number;
12
12
  readonly remote: Schema.NonEmptyString;
13
13
  readonly reportUrl: Schema.NonEmptyString;
14
- readonly status: Schema.Literals<readonly ["running", "succeeded", "failed", "revoked"]>;
14
+ readonly status: Schema.Literals<readonly ["running", "succeeded", "failed", "interrupted", "revoked"]>;
15
15
  readonly artifact: Schema.NullOr<Schema.Struct<{
16
16
  readonly digest: Schema.NonEmptyString;
17
17
  readonly bytes: Schema.Number;
@@ -87,7 +87,7 @@ export declare function createTeamBuildPublisher(options: Readonly<{
87
87
  readonly expiresAt: number;
88
88
  readonly remote: string;
89
89
  readonly reportUrl: string;
90
- readonly status: "revoked" | "running" | "failed" | "succeeded";
90
+ readonly status: "revoked" | "running" | "failed" | "succeeded" | "interrupted";
91
91
  readonly artifact: {
92
92
  readonly digest: string;
93
93
  readonly bytes: number;
@@ -103,7 +103,7 @@ export declare function createTeamBuildPublisher(options: Readonly<{
103
103
  readonly expiresAt: number;
104
104
  readonly remote: string;
105
105
  readonly reportUrl: string;
106
- readonly status: "revoked" | "running" | "failed" | "succeeded";
106
+ readonly status: "revoked" | "running" | "failed" | "succeeded" | "interrupted";
107
107
  readonly artifact: {
108
108
  readonly digest: string;
109
109
  readonly bytes: number;
@@ -53,6 +53,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
53
53
  import * as Schema from "effect/Schema";
54
54
  import { base64url } from "jose";
55
55
  import { workspaceBuildPolicy } from "./build-policy.js";
56
+ import { boundedDiagnostic } from "@fungi.computer/bird-dog/effect";
56
57
  /** The secret-free Team job; no provider token enters publication. */
57
58
  const jobSchema = Schema.Struct({
58
59
  jobId: Schema.NonEmptyString,
@@ -63,7 +64,13 @@ const jobSchema = Schema.Struct({
63
64
  expiresAt: Schema.Number,
64
65
  remote: Schema.NonEmptyString,
65
66
  reportUrl: Schema.NonEmptyString,
66
- status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
67
+ status: Schema.Literals([
68
+ "running",
69
+ "succeeded",
70
+ "failed",
71
+ "interrupted",
72
+ "revoked",
73
+ ]),
67
74
  artifact: Schema.NullOr(Schema.Struct({ digest: Schema.NonEmptyString, bytes: Schema.Number })),
68
75
  detail: Schema.NullOr(Schema.String),
69
76
  });
@@ -148,10 +155,8 @@ export function createTeamBuildPublisher(options) {
148
155
  ? "succeeded"
149
156
  : "failed", exitCode === 0
150
157
  ? "Build exited without reporting status"
151
- : "Build exited with " +
152
- String(exitCode) +
153
- ": " +
154
- (result.output?.stderr ?? "").slice(-1_000)));
158
+ : (boundedDiagnostic(result.output?.stderr ?? "") ??
159
+ `Build exited with ${String(exitCode)}`)));
155
160
  await build.deliverProjection();
156
161
  return completed;
157
162
  }
@@ -1,17 +1,21 @@
1
1
  import * as Schema from "effect/Schema";
2
2
  import { releaseSchema } from "@fungi.computer/puffball/effect";
3
3
  import { readFile } from "node:fs/promises";
4
+ import { candidateFailureDiagnostic } from "./candidate-diagnostic.js";
4
5
  import { candidateManifestObjects, teamAppCandidateManifestSchema, } from "../build-contract.js";
5
6
  import { buildTeamCandidate, teamCandidateConfigurationSchema, teamCandidateLayout, TeamCandidateBuildError, } from "./candidate-builder.js";
6
7
  /** Objects in flight at once; each is read from disk only while it uploads. */
7
8
  const uploadConcurrency = 4;
8
9
  /** Attempts per object when its connection drops before a response. */
9
10
  const uploadAttempts = 3;
10
- /** Report a failed build to the host's build output: its kind, never its text. */
11
+ /** Report bounded diagnostics to the host's build output. */
11
12
  function reportFailure(cause) {
13
+ const diagnostic = candidateFailureDiagnostic(cause);
14
+ process.stderr.write(`Team App source/build failed: ${diagnostic}\n`);
12
15
  process.stderr.write(`${JSON.stringify({
13
16
  source: "oyster-candidate-builder",
14
17
  failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
18
+ diagnostic,
15
19
  })}\n`);
16
20
  }
17
21
  const [configuration, repositoryPath, outputPath] = process.argv.slice(2);
@@ -113,7 +117,5 @@ try {
113
117
  }
114
118
  catch (cause) {
115
119
  reportFailure(cause);
116
- // Customer source and provider messages can contain secrets; only expose the stage.
117
- process.stderr.write("Team App source/build failed\n");
118
120
  process.exitCode = 1;
119
121
  }