Soundorv0.13.0

DevTools protocol

Transports, identity, clients, methods, events, and diagnostic limits.

Source: packages/devtools-protocol/.

Soundor Debug Protocol v2: browser-safe types and Zod validation schemas for native UI developer tools. It depends on no runtime, engine, renderer or browser UI. The local server lives in the soundor CLI and runs only during supported, enabled development sessions.

Session and transports

The CLI creates one server per development session, binding 127.0.0.1 only. Browser HTTP, browser WebSockets and native IPC share its actual selected port. An implicit 4545 advances on collision; an explicitly configured port fails with an actionable error. Sessions do not discover one another by port scanning.

ctx.devtools gives a supported runtime endpoint, sessionId, token and descriptorPath. The JSON descriptor is created exclusively in a unique directory under the platform's user temporary directory, with mode 0600 (directory 0700 on Unix), and removed on shutdown. Windows inherits the private user temporary directory's access permissions rather than a shared project's. Never log its contents or bake it into an artifact. A launcher can give its child only the descriptor path through an environment variable, or a future adapter can use another protected means to deliver the same credentials. Direct CLI launch is not part of the protocol contract.

Native HTTP requests require Authorization: Bearer <token> and X-Soundor-Session: <sessionId>; requests carrying browser Origin headers are rejected. All requests validate Host to resist DNS rebinding. Native operations are intended for a worker; tree access and mutations must be dispatched to the engine's owning thread. Never issue blocking IPC on UI or audio threads.

  1. POST /runtime/register with {version:1, sessionId, name, runtime, generation:1, capabilities, processId?}. Receive {version:1, targetId, targetToken}. Each registration gets a fresh unpredictable instance ID. Instance capabilities must be a subset of the runtime's static declaration.
  2. Further requests also require X-Soundor-Target and X-Soundor-Target-Token. This prevents one registered instance from replying to another's commands, even within the same session.
  3. GET /runtime/poll returns {commands:[...]} immediately when there is work or after up to 20 seconds. Keep one poll outstanding, off the owning thread.
  4. POST /runtime/messages accepts one response or typed runtime event.
  5. POST /runtime/disconnect unregisters the instance. Idle/crashed targets expire after 60 seconds. Reconnect by registration; old target IDs remain invalid. After a server restart obtain the new designated session descriptor.

Commands include request (with the sending client), cancel, subscribe (the union of browser event subscriptions), and clientGone, which drops that client's state (see clients). Reset every client's state (input, highlights, pickers, profiling) independently when transport is lost, without waiting for another poll. Drop cancelled and expired work before it reaches the owning thread. Responses to cancelled requests are stale. Runtime request envelopes additionally carry the server-stamped generation; validate it against the owning world even for commands without node refs. Use runtimeCommandSchema rather than the browser-only requestSchema to validate native incoming commands. Cancelling a long poll disconnects that target; re-register explicitly after transport loss.

Browser connections

Navigating to the dashboard grants no access to debugging data. Authorize it with the browserKey from the protected session descriptor, or set devtools.open:true: automatic opening uses a separate short-lived, one-use ticket in a URL fragment. The page removes the fragment and exchanges it via POST /browser/auth for a session-specific HttpOnly, SameSite=Strict cookie. Neither keys nor ticket URLs are printed in logs. Only the credential-free URL is shown. A ticket expires after 60 seconds and cannot be reused; the native authentication token cannot authorize a browser.

Manual authorization reads a key from the descriptor; there is no server-side filesystem-reading endpoint. Other local processes cannot obtain a browser cookie merely by fetching the dashboard. Access depends on descriptor filesystem permissions, including the user's Windows temporary-directory ACLs. Cross-origin dashboard and authentication requests are rejected. /browser upgrades only with that cookie and an exact Origin matching this session's localhost or 127.0.0.1 URL. Credentials never go in WebSocket URLs. Cookies are unique to each session, so different projects on localhost do not share authentication.

The server sends {type:'hello',version:1,sessionId,clientId,project}. A browser must answer {type:'hello',version:1} before issuing requests. Incompatible clients are closed. Server WebSocket ping/pong drives heartbeat expiry.

Requests, responses and identity

import type { Request, Response } from '@soundor/devtools-protocol';

const request: Request<'UI.getNode'> = {
  type: 'request',
  id: 'browser-request-1',
  targetId: 'instance-id',
  method: 'UI.getNode',
  params: { node: { generation: 1, id: 'engine-node-id' } },
};
// Response<'UI.getNode'> contains a DebugNode or a structured DebugError.

Every response repeats the browser request ID and target ID. The dispatcher rewrites request IDs to unpredictable transport IDs before routing to a target; the native response repeats that transport ID. Send either result or error:{code,message}, never both. Schemas validate both requests and results. Errors distinguish authentication, protocol mismatch, missing capabilities, stale nodes, disconnects, cancellation, timeouts and overload.

Node refs pair an opaque string ID with a positive generation, scoped by the request's target ID. Never expose a pointer. Node IDs must never be reused within a generation; removed IDs return STALE_NODE. On host replacement, send Runtime.generationChanged with a strictly increasing generation. The server invalidates pending work; the runtime drops every client's debug state with the old host. Snapshots and node results from another generation are rejected.

Methods and events

MethodsBehavior
Runtime.getTargetsCurrent registered instance metadata
Events.subscribeReplace this browser's event subscription for a target
UI.getTree / getNodeBounded actual engine snapshots, not a DOM tree
UI.getLayout / getStyles / getAccessibilityAvailable engine properties
UI.highlightNode / clearHighlight / setPickerEnabledThis client's highlight and picker
Animation.getActiveActual animation metadata where supported
Diagnostics.getMetrics / setRecordingRead measured counters; start or stop the recording
UI.hitTestThe deepest node at a point and its ancestors
Input.dispatchPointer / dispatchWheelThis client's synthetic pointer and wheel input
Input.dispatchKey / typeText / focusThis client's synthetic keys, text, and focus
Capture.screenshotA PNG of the rendered view, as a capture resource

Targets negotiate inspector, accessibility, animations, performance, input and capture. input is never implied by inspector: a runtime advertises it only where it dispatches synthetic input through its engine. Unsupported operations return MISSING_CAPABILITY. Omit unsupported properties rather than filling them with inferred values. Layout, base/computed styles, presentation and accessibility are distinct JSON property groups.

Typed runtime events are UI.treeChanged (revision invalidation; subscribers may request a new snapshot), UI.nodeSelected, UI.pickerChanged, Runtime.generationChanged, Console.message, Animation.changed and Diagnostics.metrics. Notifications are sent only to subscribers. Native producers must coalesce geometry and revision notifications and enable expensive instrumentation only when requested. Runtime.targetsChanged is a server event delivered to every tab. A runtime event with a client field (a pick, a picker turning on or off) is about that client's own state: the server sends it to that client alone, without the field. UI.treeChanged.kind distinguishes hierarchy invalidation from property changes; optional nodes metadata patches avoid repeated whole-tree serialization for text/name/role changes. metadataIncomplete signals that a patch exceeded its budget; a client can offer an explicit snapshot refresh. Geometry changes can refresh selected details independently of the hierarchy.

The CLI reuses its source-mapped logging pipeline. Since the current shared log file lacks instance IDs, these messages have scope:'session' and no target ID. Instance-specific runtime log events carry a target ID and follow subscriptions.

Animation and performance data

Animation.getActive returns DebugAnimation[]: generation-scoped targets, animated property names and actual presentation values, playback generation/state, engine elapsed time, duration, delay, iterations, and supported easing, spring or keyframe metadata. durationScope distinguishes an iteration from a compiled composition's whole program. A null duration/iteration count means unbounded. Compositions report their actual participating targets; unavailable child phase metadata is omitted. Snapshot limits are 256 animations, 64 targets per animation, and a transport budget. targetsTruncated and snapshotTruncated report limits.

Animation.changed carries bounded changes (ID, node, playback generation, state) and truncated. JUCE captures these from the real timeline only while subscribed, including replacement, pause/resume, completion and cancellation. Running animations also invalidate presentation inspection at up to 10 Hz. Lifecycle queues retain at most 128 changes; the browser retains 100 entries.

Diagnostics.setRecording({enabled}) starts or stops the instance's one recording, for every client. Diagnostics.getMetrics returns DebugMetrics; Diagnostics.metrics sends the same typed shape with only the newest frame sample, coalesced to 10 Hz. Full retrieval includes up to 300 samples. Capture starts/stops explicitly, preserves the stopped recording, stops once every client that started it disconnected, and resets on runtime replacement or transport loss. Idle recording reports tick work without fabricating frames when the application does not render.

Frame fields are optional when a runtime cannot measure them; clients omit unsupported fields rather than display guessed zeroes. Frame times measure CPU work around normal frame production and composition, including GPU command submission but not GPU execution. rasterMs measures native node paint calls, excluding the debug overlay; layoutMs reuses actual Yoga work timing. Counters come from existing frame and compositor statistics: root frame layers, raster work, GPU layers/readbacks, uploads and draw calls. phases reports measured UI tick dispatch phases over its windowMs, including timers/async work, parameter dispatch, promise jobs, animation callbacks and native animation work. These are not total Hermes execution time. tickIntervalMs is the measured UI callback interval; JUCE's delayedTicks counts intervals over its reported tickDelayThresholdMs of 25 ms against the 60 Hz timer, not dropped GPU frames. GPU execution time is unavailable. Expensive raster and frame timing clocks run only during recording, on the owning UI thread.

Property inspection is currently read-only. The reconciler writes complete base styles while native animations own presentation groups; there is no separate, reconciliation-aware temporary override layer. Direct mutation would risk replacing application-owned state, so the protocol has no property-edit command. Node source navigation also requires new renderer metadata: createInstance currently stores only native type/props/context, without reliable component/file/line information. Existing source maps continue to map console error stacks. Future development instrumentation can attach explicit source metadata during node creation; tools must not infer it from arbitrary native nodes or stacks.

Captures

Capture.screenshot {node?} (capability capture) renders the view with the engine's renderer and encodes it as PNG. Captures never travel as messages:

  1. The runtime uploads the PNG to POST /runtime/captures/<transport id> (Content-Type: image/png, target credentials), then answers the request with {width, height, scale, format: 'png', bytes}. The server accepts one PNG upload per pending capture request of that target, of at most MAX_CAPTURE_BYTES (32 MiB); others get 409, 400 or 401.
  2. When the answer matches the upload, the server keeps the bytes under a new unguessable ID and answers the client {id, width, height, scale, format, bytes} (screenshotSchema). An answer without its upload becomes CAPTURE_UNAVAILABLE.
  3. Clients read GET /browser/captures/<id> with the session cookie (from a trusted Origin when one is sent) and may DELETE it. Runtime credentials cannot read captures.

A session keeps MAX_CAPTURES (8) for CAPTURE_TTL_MS (60 s) at most, makes at most 2 at once (BUSY beyond), and drops a client's captures when it disconnects and a target's when it goes. Runtimes refuse views over MAX_CAPTURE_SIDE (8192) pixels a side or MAX_CAPTURE_PIXELS (16 M) with CAPTURE_UNAVAILABLE.

Clients and bounds

A target has no owner. Every signed-in client can read, highlight, pick, record and send input, as several mice and keyboards share one computer. The server adds the sending client's ID (client) to every request it queues for a runtime, and the runtime keeps each client's state apart: its synthetic pointer (an ID of its own, from 32000) and held keys, its highlight (the view draws every client's), and its picker (a pick goes to every client that was picking). Focus is the view's, shared. A runtime keeps state for at most 64 clients (BUSY beyond). The one performance recording is shared: any client starts or stops it.

When a client disconnects or misses its heartbeat, the server sends each runtime {type:'clientGone',client}: the runtime ends what that client's input held (a pointercancel, keyups), removes its highlight and picker, and stops the recording once no client that started it remains. Commands run in the order they arrive; read-only operations may overlap.

Messages are limited to 1 MiB; trees to 5000 nodes (with truncated reported), sessions to 64 targets and 32 browser connections, pending requests to 64 and native command queues to 128. Polls deliver at most 32 commands. Slow browser connections exceeding their bounded outgoing buffer are disconnected. Commands time out after 5 seconds. A browser can send {type:'cancel',id}. Shutdown invalidates pending requests, subscriptions, transports and the descriptor.

Soundor Driver is a client of this same protocol and browser transport for scripts, the CLI and coding agents. Audio processing is not part of the protocol.

Input

Input is synthetic platform input, in the view's logical pixels, dispatched by the engine's own hit testing, capture, propagation and default actions. It never calls application handlers directly.

  • Input.dispatchPointer {action, x?, y?, button?, modifiers?}: move, down, up (at the last position unless given), click (move, press and release in one command, so no press outlives it) and cancel (a pointercancel, no click). button is left, middle or right.
  • Input.dispatchWheel {x, y, deltaX, deltaY, modifiers?}: pixels, positive down and right.
  • Input.dispatchKey {action: down|up|press, key, modifiers?}: KeyboardEvent.key values. A printable key also types its text when the UI leaves the press unhandled and neither Control nor Meta is held.
  • Input.typeText {text}: up to 1024 characters, a key press each.
  • Input.focus {node}: NOT_ACTIONABLE for a node that cannot take focus.

Results are {handled, node}: whether the UI handled the input, and the node under the pointer (or focused, for keys). Keys held with dispatchKey add their modifiers to that client's later input. Each client's synthetic pointer has its own pointer ID, apart from real pointers and other clients'; an instance releases whatever a client holds (a pointercancel for a pressed button, keyup for held keys) on clientGone, and forgets it on reload. UI.hitTest {x, y} returns {node, path} (the deepest node and its ancestors' IDs from a root) and needs only inspector.

License

MIT

On this page