How to Add a New CDP Domain to Lightpanda: A Step-by-Step Guide

To add a new CDP domain to Lightpanda, create a domain module in src/cdp/domains/<Domain>.zig implementing the processMessage function, then register it in src/cdp/cdp.zig by extending the length-based switch dispatcher using the asUint helper.

Lightpanda implements the Chrome DevTools Protocol (CDP) by routing JSON-RPC commands to specialized domain modules. Adding a new CDP domain requires implementing the message processing logic and wiring it into the central dispatcher located in src/cdp/cdp.zig.

Creating the Domain Module

Start by creating a new file in src/cdp/domains/<YourDomain>.zig. This file must export a processMessage function that parses the action, validates parameters, and dispatches to the appropriate handler.

The standard pattern uses std.meta.stringToEnum to match the action name, cmd.params to deserialize JSON parameters, and cmd.sendResult to return responses:

const std = @import("std");
const log = @import("../log.zig");

pub fn processMessage(cmd: anytype) !void {
    const action = std.meta.stringToEnum(enum { echo }, cmd.input.action) orelse return error.UnknownMethod;
    switch (action) {
        .echo => {
            const params = (try cmd.params(struct {
                message: []const u8,
            })) orelse return error.InvalidParams;
            try cmd.sendResult(.{ .message = params.message }, .{});
        },
    }
}

Key implementation details from the Lightpanda source code:

  • Action parsing: Use an anonymous enum (e.g., enum { echo }) with std.meta.stringToEnum to convert the cmd.input.action string into a typed value.
  • Parameter extraction: The cmd.params method takes a struct type reflecting the expected JSON schema and returns an optional pointer.
  • Response handling: Use cmd.sendResult for success responses and cmd.sendError for protocol errors.

Registering the Domain in the Dispatcher

Edit src/cdp/cdp.zig and locate the dispatchCommand function. Lightpanda uses a compile-time optimized dispatcher that matches domain names by their byte length to avoid runtime string comparisons.

For a domain named "Example" (7 characters), extend the existing case 7 block using the asUint helper to convert the string into a u56 integer:

7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
    asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command),
    asUint(u56, "Example") => return @import("domains/example.zig").processMessage(command),
    else => {},
},

The asUint macro (defined at lines 13-15 in src/cdp/cdp.zig) enables compile-time constant generation. The integer width must match the domain length (e.g., u56 for 7 bytes, u24 for 3 bytes).

Testing the New Domain

Add unit tests in src/cdp/testing.zig to validate the domain's behavior. The testing harness provides testing.context() and assertion methods like expectSentResult:

test "cdp: Example.echo" {
    var ctx = testing.context();
    defer ctx.deinit();

    try ctx.processMessage(.{
        .id = 1,
        .method = "Example.echo",
        .params = .{ .message = "hello world" },
    });
    try ctx.expectSentResult(.{ .message = "hello world" }, .{ .id = 1, .index = 0 });
}

Run the full test suite to verify integration:

zig build test

Architectural Details

The CDP implementation in src/cdp/cdp.zig (lines 22-38) extracts the domain name from the method string ("<Domain>.<Action>") and routes commands via the dispatchCommand function. Each domain module follows a consistent contract:

  • File location: src/cdp/domains/<name>.zig
  • Entry point: processMessage(cmd: anytype) !void
  • Error handling: Return error.UnknownMethod for unrecognized actions or error.InvalidParams for malformed input

Existing domains like Runtime (src/cdp/domains/runtime.zig) and Network serve as reference implementations for the pattern.

Summary

  • Create a new domain file in src/cdp/domains/<Domain>.zig implementing processMessage with action enum parsing and parameter validation.
  • Register the domain in src/cdp/cdp.zig by adding a case to the length-based switch in dispatchCommand using the appropriate asUint width.
  • Write tests in src/cdp/testing.zig using the testing.context() harness and run zig build test to verify.
  • Document the domain's methods and parameters for downstream developers.

Frequently Asked Questions

What is the maximum length for a CDP domain name in Lightpanda?

The dispatcher supports domain names up to the maximum integer width used in the switch cases (typically u56 for 7 characters or u64 for 8 characters). For longer domain names, you would need to extend the dispatchCommand switch with a new case block using a wider integer type (e.g., u64 for 8 bytes) following the existing pattern in src/cdp/cdp.zig.

Why does Lightpanda use length-based matching instead of string comparison?

The dispatcher converts domain name bytes into integers (e.g., u56 for 7-character names) and matches them at compile time using the asUint helper. This eliminates runtime string hashing and comparison overhead, resulting in faster command dispatch with zero-allocation parsing for the domain routing logic.

How do I handle errors in a new CDP domain?

Return error.UnknownMethod when std.meta.stringToEnum fails to match the action, and error.InvalidParams when cmd.params returns null (indicating missing or malformed JSON parameters). The dispatcher automatically converts these into proper CDP error responses via cmd.sendError.

Is it required to add tests in src/cdp/testing.zig?

While not strictly enforced by the compiler, the Lightpanda repository requires comprehensive test coverage for all CDP domains. You should add tests in src/cdp/testing.zig or create a dedicated test file that imports the testing harness. Use testing.context() to simulate CDP sessions and expectSentResult to verify JSON responses.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →