Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: 🏗️ Set up workspace
uses: ./.github/actions/setup-workspace

- name: 🔨 Build packages
run: pnpm build

- name: ✅ Validate packages
run: pnpm check:package

Expand Down
19 changes: 18 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ reinjection.
The notification controller is headless. The optional Shadow DOM renderer supplies the default
light/dark theme, typed CSS-variable customization and runtime theme updates without rebuilding
controls. Hosts choose branding, placement and approval callbacks. A host using Devframe
notifications uses its message API instead of injecting the default renderer.
notifications uses its message API instead of injecting the default renderer. The panel host owns
each shared Devframe notification. Viewing tabs own matching local command registrations, keeping
approval in the clicked tab while avoiding duplicate messages across connected tabs.

### Automation providers

Expand Down Expand Up @@ -377,3 +379,18 @@ not parse the message.
Unit tests cover protocol behavior. Repository integration tests cover authenticated transports,
browser clients, extension helpers, package consumers, and example compositions. Embedding hosts
remain responsible for end-to-end validation of their approval policy and platform adapter.

## Native page WebMCP

The optional Chrome adapter owns native WebMCP discovery, document-bound references and invocation
completion. The kernel authorizes the transport-neutral `Bridge.listWebMcpTools` and
`Bridge.invokeWebMcpTools` operations, reserving native `WebMCP.*` commands and events. Broker lease
demand owns domain activation; the extension serializes activation and catalogue refresh against
the current main document. There is no injected JavaScript fallback or iframe aggregation.

MCP exposes two stable tools with principal-owned target references. Page registrations remain
result data. Listing requires inspect access; invocation requires an exclusive interact lease even
when the page advertises a read-only annotation. Installation discovery policy never grants or
revokes invocation authority. Cancellation and lost results after dispatch report unknown outcomes
without replay. Large results retain the existing lease-owned artifact mechanism; semantic artifact
reads stay bound to the tool session. See [WebMCP](docs/webmcp.md).
3 changes: 2 additions & 1 deletion docs/benchmarks/2026-09-09-aligned.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ Estimated payload tokens below are serialized tool argument and response charact

The exact hub-ui 0.9.10 patch from Devframe PR #375 is exercised in the installed embedded and standalone browser bundles. Both hosts pass deletion, full reconciliation, changed descriptions, unchanged publications, manual dismissal and intentional resurfacing. Eight page-script tests cover direct Accept/default Review, stable message registrations, count changes, serialized updates and creation/expiry/disposal races. The MCP package's 67 unit regressions also pass.

Workspace patches do not propagate to consumers of published CDB packages. See [patch provenance and removal conditions](../../patches/README.md).
The benchmark used the Devframe versions pinned by that revision. CDB no longer carries the
temporary Devframe patches after the fixes shipped upstream.

## Follow-up lifecycle matrix

Expand Down
44 changes: 44 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Defining adapter configuration

Each configurable adapter exports a `define…` helper beside its factory. Helpers return the original
object, preserve callback types and literal values, and check configuration without opening
connections, installing listeners, reading stores, or starting timers. Factories still accept plain
options and apply the same checks when the helper is omitted.

```ts
import { createChromeProvider, defineProvider } from '@dvcol/cdb-extension/chrome';

const definition = defineProvider({
maximumLevel: 'interact',
connect: connectProvider,
authorizeApproval,
});

const provider = createChromeProvider(definition);
provider.start();
```

`connectProvider` and `authorizeApproval` remain host callbacks. Defining configuration never calls
them. Mutable runtime dependencies, regular expressions, and callback closures are retained by
reference. Helpers do not serialize options or allocate default resources. Environment-dependent
checks, such as availability of Chrome, belong to runtime construction or connection.

| Entry | Helpers |
| --- | --- |
| Core | `defineTargetBroker`, `defineEmbeddedBridge`, `defineAgentConnection`, `defineClientConnection`, `defineGrantRequests`, `defineLogicalSession` |
| Broker | `defineBroker` |
| Extension `/chrome` | `defineProvider` |
| Extension | `definePublisher`, `defineSelectedTab`, `defineTabLifecycle`, `defineTabScope`, `defineApproval`, `defineApprovalSender`, `defineRecovery`, `defineBootstrap`, `defineOfferRelay`, `definePairingStore`, `definePageRequest` |
| Extension `/notifications` | `defineNotifications`, `defineNotificationRenderer` |
| WebSocket `/browser` | `defineAgentWebSocket`, `defineClientWebSocket`, `defineBrowserClient` |
| WebSocket `/node` | `defineWebSocketBridge`, `defineStandaloneWebSocket`, `defineClientWebSocket`, `defineNodeClient`, `defineStandaloneHost`, `defineArtifactEndpoint`, `defineArtifactReader`, `defineArtifactStore` |
| Birpc `/node` | `defineBridge` |
| Devframe | `defineService`, `definePanel` |
| Devframe `/connection` | `defineConnection` |
| Devframe `/client` | `defineClientSession`, `defineProvider` |
| Devframe `/panel`, `/page-script` | `definePanelMount`, `definePage` respectively |
| MCP | `defineTools`, `defineHttp`, `defineStdio` |
| Playwright automation | `defineProvider` |

Import aliases distinguish similarly named helpers when composing several adapters. Functions that
only wrap a live connection or platform object do not need a configuration helper.
68 changes: 68 additions & 0 deletions docs/webmcp-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# WebMCP validation handoff

Work for [issue 70](https://github.com/dvcol/chrome-debugger-bridge/issues/70) is staged in two local
CDB branches: `dvcol/define-adapters` from `main`, then `dvcol/webmcp`. The adapter configuration layer
is commit `76b9b99`. Downstream branches are both named `dvcol/cdb-webmcp`: app-frontends starts at
`88518587946` on `dvcol/cdb`, and QA Helper starts at `b86e151c` on `dvcol/cdb`.

## Current boundary

Another session owns the live downstream TNR. Do not start this change's live servers, Chromium
fixtures, extension reloads, downstream installs/builds, or full TNR until the user explicitly gives
the go-ahead. PR submission is also held because CDB PR CI starts full verification.

CDB checks performed before that boundary use the in-memory Chrome command port, broker, client
facade and MCP session. They cover main-document selection, empty/unsupported discovery, filtering,
callback failure and mutation isolation, generation/document references, exclusive invocation,
principal isolation, artifacts, cancellation and late responses. Package-scoped TypeScript checks,
lint and builds validate the public entrypoints. These are not live Chromium proof.

The dedicated app-frontends and QA Helper worktrees contain source, test and documentation changes
only. Their dependency manifests and installed packages have not been changed. They still reference
CDB 0.2.0, which does not export these new helpers. Consequently those downstream branches are not
ready for CI or release until dependency alignment is completed. No downstream tests have been run.

## Recorded pre-live checks

On 2026-09-14, 164 focused CDB tests passed: 78 core catalogue/authority/target-directory/scaffold
checks and 86 extension/MCP controller, publisher, session and semantic-tool checks. The latter
include 23 dedicated WebMCP cases. Type checks passed for core, extension, MCP, broker, Devframe,
WebSocket and Birpc, plus a scoped compilation of the new tests and native fixture. Core,
extension and MCP packages built successfully, changed TypeScript sources passed lint, and the
CDP catalogue regeneration check passed. Downstream checks and live fixtures remain unrun.

## Resume after the user's go-ahead

1. Re-read the branches and working-tree state. Confirm the other TNR has released its servers,
browser profile, extension and package outputs before touching them.
2. Build and pack the CDB stack in dependency order. For local proof, install those tarballs only in
the dedicated downstream worktrees and keep overrides local. Do not commit absolute filesystem
paths or invent a published version. Before downstream PR submission, update manifests, both
app-frontends catalogue definitions and lockfiles to the actual CDB release containing the stack.
3. Run the scoped DevKit/DevTools and QA Helper type, lint, format, quality and affected unit-test
scripts required by their guides. Validate the QA Helper discovery exclusion for DevKit's exported
`WEBMCP_TOOL_PREFIX`. Preserve installation identity, pairing database and storage keys.
4. Run the authored CDB native fixture:

```sh
pnpm exec vitest run --project extension-e2e tests/e2e/native-webmcp.test.ts
```

It uses an isolated Chromium profile, enables experimental web platform features, and registers
a real tool on a trustworthy loopback page. It exercises the public MCP/client/broker/extension
path, confirms iframe exclusion and a visible native side effect, then checks stale references
and an empty catalogue after reload. It has only been statically checked so far. A missing native
`document.modelContext` fails the test explicitly; record the Chrome version and flags.
5. Through DevKit MCP in the intended Chrome environment, request inspect access and list page tools.
Verify QA Helper and application tools appear while `dev_square_` tools are hidden. Invocation at
inspect must fail. At interact, invoke a known page tool and verify its visible effect. Check
direct invocation of a hidden name, cancellation, navigation, authority renewal, another
principal's isolation, and an artifact-sized result. Never replay an unknown invocation outcome.
6. Run the full CDB validation gates and downstream TNR inventories, including the public Devframe
path, local shell and staging. When full local CDB gates are explicitly authorized, constrain
Turbo and Vitest concurrency to one and use `NODE_OPTIONS=--max-old-space-size=4096`. Keep latency
measurements separate from builds or other browser runs. Do not use DevKit's default registry
port for temporary fixtures.
7. Review the diffs, submit the configuration/WebMCP PR stack, then submit downstream PRs against
their intended CDB branches. Include actual test evidence and remaining browser limitations;
do not label the live behavior verified until the preceding steps pass.
124 changes: 124 additions & 0 deletions docs/webmcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Native page WebMCP

CDB exposes the approved page's native WebMCP registrations through two stable tools:

| MCP tool | Client method | Minimum access | Lease |
| --- | --- | --- | --- |
| `browser.list_webmcp_tools` | `listWebMcpTools` | `inspect` | Shared read |
| `browser.invoke_webmcp_tools` | `invokeWebMcpTools` | `interact` | Exclusive control |

Both target the main document. There is no frame argument, frame aggregation, injected registration
hook, or JavaScript fallback. An empty native catalogue returns `tools: []`; a browser without the
native CDP WebMCP domain returns `FEATURE_UNSUPPORTED`.

## Discovery policy

The extension installation chooses discovery policy through `defineProvider` or `definePublisher`.
Configuration is fixed for the adapter lifetime; callbacks can read changing application state and
are evaluated on each listing.

```ts
import { createChromeProvider, defineProvider } from '@dvcol/cdb-extension/chrome';

const provider = createChromeProvider(defineProvider({
connect: connectProvider,
maximumLevel: 'interact',
authorizeApproval,
webMcp: {
discovery: {
enabled: ({ url }) => new URL(url).hostname.endsWith('.example.com'),
include: [/^catalog_/u, 'search'],
exclude: [
'catalog_delete',
/^internal_/u,
({ tool }) => tool.annotations?.untrustedContent === true,
],
},
},
}));
```

`connectProvider` and `authorizeApproval` are host callbacks. Schema conditions use ordinary
JavaScript or a host's existing validator; CDB adds no schema-matching dependency. For example,
`exclude: [({ tool }) => forbiddenSchema.safeParse(tool.inputSchema).success]` can use a host-owned
schema validator.

Discovery defaults to enabled with every tool included. If `include` is present, a tool must match at
least one entry; an empty include array includes nothing. Any matching exclusion wins. Matchers may
be exact names, regular expressions, or synchronous/asynchronous callbacks. Callbacks receive the
page URL, target ID, target generation, and a tool descriptor with cloned schema and annotation data.
Stateful regular expressions do not mutate the host's `lastIndex`. A thrown or rejected callback
fails the entire listing with `WEBMCP_DISCOVERY_FAILED`; CDB never returns a partly filtered list.

These controls affect discovery only. An approved caller with `interact` access may invoke a hidden
tool by name, including when discovery is disabled. Use the provider's command authorization policy
to impose additional execution restrictions. Tool annotations are page-provided hints and never
reduce the required access level or lease mode.

## Calls and references

```json
{ "targetRef": "t1" }
```

Pass this to `browser.list_webmcp_tools`. Its result contains `documentRef`, `enabled`, and `tools`.
Each tool has `name`, `description`, `toolRef`, and any native `inputSchema` and `annotations`.
Native stack traces, backend node IDs, and frame IDs are not returned.

Invoke one tool through `browser.invoke_webmcp_tools`:

```json
{ "targetRef": "t1", "toolName": "search", "input": { "query": "coffee" } }
```

Use either `toolName` or a returned `toolRef`, never both. Names resolve in the current main document
without a preceding list. References are opaque and bound to the target generation and document;
navigation, authority renewal, or removal makes them stale. List again after `WEBMCP_TOOL_STALE`.
References confer no authority. Every call still passes the current principal's grant and lease checks.

The client facade offers the same operations through an explicit existing lease:

```ts
const result = await client.listWebMcpTools({
targetId, targetGeneration, leaseId, operationId: crypto.randomUUID(),
});
const invoked = await client.invokeWebMcpTools({
targetId, targetGeneration, leaseId: exclusiveLeaseId,
operationId: crypto.randomUUID(), toolName: 'search', input: { query: 'coffee' },
});
```

Acquire those leases for `Bridge.listWebMcpTools` and `Bridge.invokeWebMcpTools`, respectively.
Semantic MCP sessions acquire temporary leases and keep target generations internal. Raw native
`WebMCP.*` commands and events are reserved, including at `unsafe` access, so they cannot bypass
provider discovery and invocation handling.

## Results and interruption

Successful invocation returns `{ "status": "completed", "output": ... }`. Output may be any JSON
value and remains untrusted page content. Page tools are returned as data; they are not registered
as additional MCP tools.

Results above the broker's inline limit use its existing artifact store. Semantic WebMCP responses
contain `artifact`, `leaseId`, `expiresAt`, and `targetRef`. Read bounded base64 ranges with
`browser.read_artifact` and release with `browser.release_artifact`, supplying that target reference,
artifact ID, and lease ID. Releasing the artifact also releases its temporary lease. Session disposal,
client replacement, or target revocation releases retained leases. The lease can expire earlier than
the artifact descriptor; use the response's lease expiry. Another session cannot read the artifact.

Cancellation after dispatch attempts `WebMCP.cancelInvocation` and returns
`WEBMCP_OUTCOME_UNKNOWN`. Navigation, revocation, or transport loss may also leave an unknown outcome.
No automatic replay is performed: page effects may have happened before the result was lost.
A native tool error returns `CDP_COMMAND_FAILED`; a native cancellation returns `REQUEST_CANCELLED`.
The extension's maximum result size remains an outer bound for catalogues and invocation output.

## Native protocol basis

The implementation uses [`WebMCP.enable`, `invokeTool`, and `toolResponded`](https://chromedevtools.github.io/devtools-protocol/tot/WebMCP/).
Chrome's [DevTools session handler](https://github.com/chromium/chromium/blob/main/chrome/browser/devtools/chrome_devtools_session.cc)
permits the WebMCP domain for extension debugger clients. The
[Blink inspector agent](https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/inspector/inspector_web_mcp_agent.cc)
seeds existing tools during enable. The empty catalogue has no initial event; CDB uses completion of
the enable command rather than waiting for a tools-added event. Main-document selection avoids
claiming complete same-process iframe discovery. Live verification is still required against the
Chrome build used by an embedding application.
7 changes: 7 additions & 0 deletions packages/automation-playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,5 +794,12 @@ class PlaywrightAutomationProvider implements AutomationProvider {
export function createPlaywrightAutomationProvider(
options: PlaywrightAutomationProviderOptions = {},
): AutomationProvider {
defineProvider(options);
return new PlaywrightAutomationProvider(options);
}

/** Defines configuration without starting the adapter or calling runtime dependencies. */
export function defineProvider<const Definition extends PlaywrightAutomationProviderOptions>(definition: Definition): Definition {
validateTimeoutMilliseconds({ ...defaultPlaywrightAutomationTimingPolicy, ...definition.timing }.actionTimeoutMilliseconds, 'actionTimeoutMilliseconds');
return definition;
}
14 changes: 13 additions & 1 deletion packages/birpc/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
BirpcSubscriptionDescriptor,
} from './client.js';

import { connectAgentTargetBroker, connectClientTargetBroker, createTargetBroker } from '@dvcol/cdb';
import { connectAgentTargetBroker, connectClientTargetBroker, createTargetBroker, defineTargetBroker, validateTimeoutMilliseconds } from '@dvcol/cdb';
import { mountAuthenticatedWebSocketBridge } from '@dvcol/cdb-websocket/node';
import { createBirpc } from 'birpc';

Expand Down Expand Up @@ -88,6 +88,7 @@ export function mountBirpcChromeDebuggerBridge<
AgentPrincipal extends AuthenticatedPrincipal,
ClientPrincipal extends AuthenticatedPrincipal,
>(options: MountBirpcChromeDebuggerBridgeOptions<AgentPrincipal, ClientPrincipal>): MountedBirpcChromeDebuggerBridge {
defineBridge<AgentPrincipal, ClientPrincipal>(options);
const broker = options.broker ?? createTargetBroker(options);
const ownsBroker = options.broker === undefined;
const subscriptions = new Map<string, BirpcSubscriptionState>();
Expand Down Expand Up @@ -255,3 +256,14 @@ export function mountBirpcChromeDebuggerBridge<
},
};
}

/** Defines configuration without starting the adapter or calling runtime dependencies. */
export function defineBridge<AgentPrincipal extends AuthenticatedPrincipal, ClientPrincipal extends AuthenticatedPrincipal, const Definition extends MountBirpcChromeDebuggerBridgeOptions<AgentPrincipal, ClientPrincipal> = MountBirpcChromeDebuggerBridgeOptions<AgentPrincipal, ClientPrincipal>>(definition: Definition & MountBirpcChromeDebuggerBridgeOptions<AgentPrincipal, ClientPrincipal>): Definition {
if (definition.broker === undefined) defineTargetBroker(definition);
for (const path of [definition.agentPath, definition.clientPath]) {
if (!path.startsWith('/') || path.includes('?') || path.includes('#')) throw new Error('WebSocket paths must be absolute paths without query parameters or fragments');
}
if (definition.agentPath === definition.clientPath) throw new Error('Agent and client WebSocket paths must be distinct');
for (const [name, value] of Object.entries(definition.webSocketTiming ?? {})) validateTimeoutMilliseconds(value, name);
return definition;
}
Loading