Skip to content

feat(middleware): resolve per-target CDP capability profiles for agent sessions - #448

Open
V3RON wants to merge 1 commit into
mainfrom
claude/lynx-cdp-domain-support-epbhlg
Open

feat(middleware): resolve per-target CDP capability profiles for agent sessions#448
V3RON wants to merge 1 commit into
mainfrom
claude/lynx-cdp-domain-support-epbhlg

Conversation

@V3RON

@V3RON V3RON commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Description

Rozenite for Agents now knows which built-in domains the connected target can actually back, instead of advertising all five on every session.

  • Each session resolves a capability profile from its target's platform. Unsupported tools are never registered, so list-tools is honest.

  • Unavailable domains stay visible in rozenite agent domains, which gains an availability column (supported / degraded / unsupported) and a fallback — both default fields, not --verbose ones.

  • Calling an unsupported tool now fails during resolution with the reason and a next step, before anything reaches the wire:

    Tool "listRequests" is not supported on this lynx target. Lynx registers no CDP
    Network domain, so every Network.* command returns "Not implemented" (-32601).
    Use the `@rozenite/network-activity-plugin` domain instead.
    
  • On a Lynx target that means console, plugin domains, app tools and memory.takeHeapSnapshot are supported; memory is degraded (allocation sampling removed); network, react and performance are unavailable with reasons.

Three fixes to the CDP command channel apply to every platform:

  • A device error names the method it refused (UnsupportedCdpMethodError for -32601) instead of arriving as a stringified error object.
  • Waits on a device event are bounded, so a capture whose completion event never arrives fails with a diagnosis rather than hanging forever.
  • stopTrace refuses to finalise a zero-event trace artifact instead of reporting a successful capture of nothing.

Agent docs gain a platforms frontmatter field, platform-availability sections on network, react, performance and memory, and a new lynx orientation doc.

Related Issue

Closes #447

Context

The five built-in domains were designed against React Native's CDP surface. Lynx's is different, verified against LynxDevToolNG::RegisterAgents (the domains a Lynx device answers at all) and primjs's src/inspector/protocols.cc (the method table behind the domains Lynx forwards into the JS engine): no Network domain is registered, no React DevTools backend exists, Tracing is Perfetto-based rather than Chrome-based, and HeapProfiler implements snapshots but not sampling.

The design follows from the fact that "unsupported" is three behaviours on the wire, and only one is loud: an unregistered domain answers -32601; an unknown method inside an engine-backed domain answers with an empty success, so the call resolves and the awaited event never comes; and a same-named-different-protocol domain succeeds while producing the wrong thing. The latter two cannot be detected from a response, which is why this is resolved from a profile before the call rather than translated from an error after it.

Some decisions worth a reviewer's attention:

  • The table is static, not probed. A probe costs round trips at session start and cannot see the empty-success case anyway. The one axis that genuinely varies at runtime is the V8-vs-PrimJS backend, which already has a free signal (only V8 emits a real Runtime.executionContextCreated); that is left as a future refinement rather than a foundation.
  • Absence means supported. React Native's profile is empty and its behaviour is unchanged; nobody maintains an allowlist that grows with every new tool.
  • Unavailable domains are still listed. Dropping network outright would make rozenite agent network listRequests fail with Unknown domain "network". Did you mean…?, which reads to an agent as a typo to correct rather than a platform limit to route around.
  • Both new response fields are optional. An older CLI against a newer server ignores them; a newer CLI against an older server reads undefined and treats every domain as supported — exactly the previous behaviour. No lockstep upgrade.
  • Filtering is a wrapper, not a branch per service. The four domain services stay ignorant of platforms; withCapabilityFilter returns the original service untouched when nothing is filtered. callTool is guarded as well as getTools, since the call-tool route reaches tools by exact name.
  • Platform comes from two places. The dev server's platform config is the default; @rozenite/lynx-dev additionally stamps rozenite: { platform: 'lynx' } on each /json/list page, which metro-discovery carries onto MetroTarget and which wins per target. Metro never emits that key and every other consumer ignores unknown ones, so it is additive.

Two gaps I'd rather name than leave implicit:

  • The profile drift guard covers memory and network — the domains whose services construct cheaply in a test. react and performance have no tool-level entries today so nothing is currently unguarded, but adding one there would not be caught.
  • A Lynx-only domain over UITree / Template / Lynx's own Performance and Memory is now a profile entry away, but it is new surface area rather than a fix and stayed out of scope.

Testing

Automated, from the repository root after git fetch origin main:

  • pnpm checks:affected — typecheck, lint and format across 102 tasks, all passing.
  • pnpm test:affected — 63/63 tasks passing.
  • pnpm release:plan — confirms the version plan.

New tests in packages/middleware/src/__tests__/agent-capabilities.test.ts cover the React Native profile declaring no gaps, a drift guard asserting the Lynx table names only tools the services actually expose, whole-domain versus tool-level gap reporting, filtering on both getTools and callTool, and the untouched-service fast path.

Three existing tests changed deliberately: the two agent-command-output domain-listing assertions carry the new default columns (their mocks now include a degraded and an unsupported domain so the columns are exercised), and skills-registry counts the new lynx doc.

Not manually verified against a physical Lynx device — the Lynx behaviour encoded in the profile is sourced from the upstream C++ registration and dispatch tables cited above rather than from a live session.


Generated by Claude Code

…t sessions

Rozenite's five built-in agent domains were designed against React
Native's CDP surface, and Lynx exposes a different one: no `Network`
domain at all, no React DevTools backend, and a Perfetto-based `Tracing`
domain that shares Chrome's method names but not its protocol. Every
session advertised all five regardless of target, so an agent could only
discover the gap by calling — and two of the three ways it fails are
silent, because a device answers an unknown method with an empty success
just as readily as with `-32601`.

Each session now resolves a capability profile from its target's
platform. Unsupported tools are never registered; unavailable domains
stay listed with an availability, a reason and a fallback, so the gap is
legible rather than looking like a typo; and calling one fails during
resolution with that explanation instead of a protocol error.

Three fixes to the CDP command channel are platform-agnostic: a device
error now names the method it refused, waits on device events are
bounded, and `stopTrace` refuses to finalise an empty trace artifact
instead of reporting a successful capture of nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pj8YXsJh4mMsJ1HpJEr7B5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rozenite for Agents advertises built-in domains that a Lynx target cannot back

2 participants