@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.
- package/dist/agent-computer-source.d.ts +42 -0
- package/dist/agent-computer-source.js +88 -0
- package/dist/agent-computers.d.ts +104 -0
- package/dist/agent-computers.js +227 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.js +33 -0
- package/dist/app-builds.d.ts +1 -1
- package/dist/app-builds.js +16 -6
- package/dist/boot-capability.d.ts +2 -1
- package/dist/boot-capability.js +7 -5
- package/dist/browser/boot.d.ts +2 -2
- package/dist/browser/boot.js +10 -3
- package/dist/browser/browser.d.ts +1 -1
- package/dist/browser/browser.js +2 -1
- package/dist/browser/cdp-client.d.ts +2 -0
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +2 -2
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +3 -2
- package/dist/browser/page.js +11 -4
- package/dist/browser/sessions.d.ts +7 -4
- package/dist/browser/sessions.js +159 -0
- package/dist/browser/url-policy.d.ts +4 -2
- package/dist/browser/url-policy.js +4 -2
- package/dist/build/build-jobs.d.ts +3 -3
- package/dist/build/build-jobs.js +10 -5
- package/dist/build/candidate-cli.js +5 -3
- package/dist/build/candidate-cli.mjs +223 -5
- package/dist/build/candidate-diagnostic.d.ts +2 -0
- package/dist/build/candidate-diagnostic.js +16 -0
- package/dist/computer-control-port.d.ts +26 -0
- package/dist/computer-control-port.js +1 -0
- package/dist/host.d.ts +3 -0
- package/dist/machine-link/delivery.d.ts +1 -1
- package/dist/machine-link/delivery.js +2 -3
- package/dist/machine-link/host.d.ts +37 -6
- package/dist/machine-link/host.js +209 -14
- package/dist/machine-link/link.d.ts +13 -1
- package/dist/machine-link/link.js +20 -7
- package/dist/mcp.d.ts +144 -10
- package/dist/mcp.js +130 -13
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page-script.js +1 -1
- package/dist/standalone/agent-computers.d.ts +29 -0
- package/dist/standalone/agent-computers.js +101 -0
- package/dist/standalone/agent-mail.d.ts +41 -18
- package/dist/standalone/agent-model-choice.d.ts +29 -0
- package/dist/standalone/agent-model-choice.js +72 -0
- package/dist/standalone/agents.d.ts +44 -4
- package/dist/standalone/agents.js +502 -46
- package/dist/standalone/app-set.d.ts +8 -1
- package/dist/standalone/app-set.js +46 -31
- package/dist/standalone/computer-control.d.ts +4 -0
- package/dist/standalone/computer-control.js +62 -0
- package/dist/standalone/computer-fleet.d.ts +1 -0
- package/dist/standalone/computer-fleet.js +1 -1
- package/dist/standalone/computer.d.ts +21 -2
- package/dist/standalone/computer.js +12 -0
- package/dist/standalone/install-apps-bin.d.ts +2 -0
- package/dist/standalone/install-apps-bin.js +3 -0
- package/dist/standalone/install-apps-cli.d.ts +3 -1
- package/dist/standalone/install-apps-cli.js +27 -17
- package/dist/standalone/machine-link.d.ts +10 -0
- package/dist/standalone/machine-link.js +6 -0
- package/dist/standalone/registry.d.ts +10 -1
- package/dist/standalone/registry.js +9 -0
- package/dist/standalone/worker.js +12 -0
- package/dist/standalone/workflow-binding.d.ts +2 -2
- package/dist/standalone/workflow-host.d.ts +11 -11
- package/dist/standalone/workflows.d.ts +22 -22
- package/dist/storage-frame.d.ts +3 -3
- package/dist/workers-git.d.ts +2 -0
- package/dist/workers-git.js +2 -0
- package/dist/workers.d.ts +8 -8
- package/dist/workers.js +9 -1
- package/docs/api/agents.md +66 -0
- package/docs/api/apps.md +104 -104
- package/docs/api/build-contract.md +35 -35
- package/docs/api/build.md +12 -12
- package/docs/api/host.md +61 -61
- package/docs/api/mcp.md +127 -13
- package/docs/api/root-data.md +1 -1
- package/docs/api/tenancy.md +1 -1
- package/docs/api/workers-git.md +3 -2
- package/docs/api/workers.md +81 -83
- package/package.json +26 -18
package/dist/browser/boot.d.ts
CHANGED
|
@@ -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.
|
|
12
|
-
*
|
|
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
|
package/dist/browser/boot.js
CHANGED
|
@@ -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.
|
|
16
|
-
*
|
|
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({
|
|
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
|
-
/**
|
|
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;
|
package/dist/browser/browser.js
CHANGED
|
@@ -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
|
-
//
|
|
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;
|
package/dist/browser/errors.d.ts
CHANGED
|
@@ -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
|
-
* (
|
|
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
|
|
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;
|
package/dist/browser/errors.js
CHANGED
|
@@ -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
|
-
* (
|
|
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
|
|
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. */
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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
|
|
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";
|
package/dist/browser/index.js
CHANGED
|
@@ -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
|
|
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";
|
package/dist/browser/page.js
CHANGED
|
@@ -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({
|
|
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"
|
|
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",
|
|
38
|
-
|
|
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
|
-
/**
|
|
18
|
+
/** Browser Run idle keep-alive (10s–20min); Chromium has no equivalent. */
|
|
18
19
|
keepAliveMs: number;
|
|
19
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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;
|
package/dist/browser/sessions.js
CHANGED
|
@@ -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
|
|
16
|
-
*
|
|
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
|
|
14
|
-
*
|
|
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;
|
package/dist/build/build-jobs.js
CHANGED
|
@@ -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([
|
|
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
|
-
:
|
|
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
|
|
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
|
}
|