Skip to content

Commit 1874315

Browse files
feat(sparsekernel): wire browser broker client
1 parent a8ffb97 commit 1874315

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

docs/architecture/browser-broker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The v0 daemon exposes broker endpoints to acquire, release, and list browser con
1313

1414
V0 also has a concrete local-browser attachment point: callers may register and probe an existing loopback CDP endpoint such as `http://127.0.0.1:9222`. The broker validates that CDP endpoints are loopback-only, probes `/json/version`, stores the endpoint on the trust-zone pool, and records audit events. It still does not return raw CDP or Playwright handles to agents.
1515

16-
The `@openclaw/sparsekernel-browser-broker` adapter materializes the ledger lease into a real CDP browser context with `Target.createBrowserContext`, creates a target for the task/session, captures screenshots through `Page.captureScreenshot`, and writes screenshots/downloads through the SparseKernel artifact API. Download capture uses CDP download events and the content-addressed artifact store rather than exposing arbitrary download paths to agents.
16+
The `@openclaw/sparsekernel-browser-broker` adapter materializes the ledger lease into a real CDP browser context with `Target.createBrowserContext`, creates a target for the task/session, captures screenshots through `Page.captureScreenshot`, and writes screenshots/downloads through the SparseKernel artifact API. Callers can construct it directly with a kernel-shaped client or use `createSparseKernelCdpBrowserBroker` to wire it to `sparsekerneld`. Download capture uses CDP download events and the content-addressed artifact store rather than exposing arbitrary download paths to agents.
1717

1818
The current daemon does not launch or supervise a Playwright browser process yet. The next backend should:
1919

packages/browser-broker/src/index.test.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import type {
77
SparseKernelCreateArtifactInput,
88
} from "../../sparsekernel-client/src/index.js";
99
import type { CdpTransport, SparseKernelBrowserKernelClient } from "./index.js";
10-
import { normalizeLoopbackCdpEndpoint, SparseKernelCdpBrowserBroker } from "./index.js";
10+
import {
11+
createSparseKernelCdpBrowserBroker,
12+
normalizeLoopbackCdpEndpoint,
13+
SparseKernelCdpBrowserBroker,
14+
} from "./index.js";
1115

1216
class FakeKernel implements SparseKernelBrowserKernelClient {
1317
readonly artifactInputs: SparseKernelCreateArtifactInput[] = [];
@@ -264,4 +268,77 @@ describe("@openclaw/sparsekernel-browser-broker", () => {
264268
retention_policy: "durable",
265269
});
266270
});
271+
272+
it("constructs from the SparseKernel daemon client", async () => {
273+
const calls: Array<{ url: string; body?: unknown }> = [];
274+
const transport = new FakeCdpTransport();
275+
const broker = createSparseKernelCdpBrowserBroker({
276+
baseUrl: "http://127.0.0.1:8765",
277+
fetchImpl: async (input, init) => {
278+
const url = input.toString();
279+
const body =
280+
typeof init?.body === "string" ? (JSON.parse(init.body) as Record<string, string>) : {};
281+
calls.push({ url, body });
282+
if (url.endsWith("/browser/pools/probe")) {
283+
return Response.json({
284+
endpoint: body.cdp_endpoint,
285+
reachable: true,
286+
status_code: 200,
287+
});
288+
}
289+
if (url.endsWith("/browser/contexts/acquire")) {
290+
return Response.json({
291+
id: "browser_ctx_client",
292+
pool_id: "browser_pool_public_web",
293+
profile_mode: "ephemeral",
294+
status: "active",
295+
created_at: "2026-04-27T00:00:00Z",
296+
});
297+
}
298+
if (url.endsWith("/json/version")) {
299+
return Response.json({
300+
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/browser/test",
301+
});
302+
}
303+
if (url.endsWith("/artifacts/create")) {
304+
return Response.json({
305+
id: "artifact_client",
306+
sha256: "sha_client",
307+
size_bytes: Buffer.from(body.content_base64, "base64").length,
308+
storage_ref: "sha256/aa/bb/sha_client",
309+
mime_type: body.mime_type,
310+
retention_policy: body.retention_policy,
311+
created_at: "2026-04-27T00:00:00Z",
312+
});
313+
}
314+
if (url.endsWith("/browser/contexts/release")) {
315+
return Response.json({ released: true });
316+
}
317+
return Response.json({ error: "not found" }, { status: 404 });
318+
},
319+
transportFactory: async () => transport,
320+
});
321+
322+
const context = await broker.acquireContext({
323+
trust_zone_id: "public_web",
324+
cdp_endpoint: "http://127.0.0.1:9222",
325+
});
326+
const screenshot = await broker.captureScreenshotArtifact(context.ledger_context.id);
327+
await expect(broker.releaseContext(context.ledger_context.id)).resolves.toBe(true);
328+
329+
expect(screenshot.artifact.id).toBe("artifact_client");
330+
expect(calls.map((call) => new URL(call.url).pathname)).toEqual(
331+
expect.arrayContaining([
332+
"/browser/pools/probe",
333+
"/browser/contexts/acquire",
334+
"/json/version",
335+
"/artifacts/create",
336+
"/browser/contexts/release",
337+
]),
338+
);
339+
expect(calls.find((call) => call.url.endsWith("/artifacts/create"))?.body).toMatchObject({
340+
content_base64: Buffer.from("pixels").toString("base64"),
341+
mime_type: "image/png",
342+
});
343+
});
267344
});

packages/browser-broker/src/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import WebSocket, { type RawData } from "ws";
5-
import type {
5+
import {
6+
SparseKernelClient,
7+
type SparseKernelClientOptions,
8+
type SparseKernelAcquireBrowserContextInput,
9+
type SparseKernelArtifact,
10+
type SparseKernelArtifactSubject,
11+
type SparseKernelBrowserContext,
12+
type SparseKernelBrowserEndpointProbe,
13+
type SparseKernelCreateArtifactInput,
14+
} from "../../sparsekernel-client/src/index.js";
15+
16+
export type {
617
SparseKernelAcquireBrowserContextInput,
718
SparseKernelArtifact,
819
SparseKernelArtifactSubject,
@@ -38,6 +49,10 @@ export type SparseKernelBrowserBrokerOptions = {
3849
transportFactory?: CdpTransportFactory;
3950
};
4051

52+
export type SparseKernelCdpBrowserBrokerFactoryOptions = SparseKernelClientOptions & {
53+
transportFactory?: CdpTransportFactory;
54+
};
55+
4156
export type AcquireMaterializedBrowserContextInput = {
4257
agent_id?: string | null;
4358
session_id?: string | null;
@@ -81,6 +96,17 @@ export type BrowserArtifactResult = {
8196
source_url?: string;
8297
};
8398

99+
export function createSparseKernelCdpBrowserBroker(
100+
options: SparseKernelCdpBrowserBrokerFactoryOptions = {},
101+
): SparseKernelCdpBrowserBroker {
102+
const client = new SparseKernelClient(options);
103+
return new SparseKernelCdpBrowserBroker({
104+
kernel: client,
105+
fetchImpl: options.fetchImpl,
106+
transportFactory: options.transportFactory,
107+
});
108+
}
109+
84110
type CdpResponseWaiter = {
85111
resolve: (value: unknown) => void;
86112
reject: (error: Error) => void;

0 commit comments

Comments
 (0)