   100→        for (const event of normalizedEvents) {
   101→            registerInternalHook(event, handler);
   102→        }
   103→    };
   104→    const registerGatewayMethod = (record, method, handler) => {
   105→        const trimmed = method.trim();
   106→        if (!trimmed)
   107→            return;
   108→        if (coreGatewayMethods.has(trimmed) || registry.gatewayHandlers[trimmed]) {
   109→            pushDiagnostic({
   110→                level: "error",
   111→                pluginId: record.id,
   112→                source: record.source,
   113→                message: `gateway method already registered: ${trimmed}`,
   114→            });
   115→            return;
   116→        }
   117→        registry.gatewayHandlers[trimmed] = handler;
   118→        record.gatewayMethods.push(trimmed);
   119→    };
   120→    const registerHttpHandler = (record, handler) => {
   121→        record.httpHandlers += 1;
   122→        registry.httpHandlers.push({
   123→            pluginId: record.id,
   124→            handler,
   125→            source: record.source,
   126→        });
   127→    };
   128→    const registerHttpRoute = (record, params) => {
   129→        const normalizedPath = normalizePluginHttpPath(params.path);
   130→        if (!normalizedPath) {
   131→            pushDiagnostic({
   132→                level: "warn",
   133→                pluginId: record.id,
   134→                source: record.source,
   135→                message: "http route registration missing path",
   136→            });
   137→            return;
   138→        }
   139→        if (registry.httpRoutes.some((entry) => entry.path === normalizedPath)) {
   140→            pushDiagnostic({
   141→                level: "error",
   142→                pluginId: record.id,
   143→                source: record.source,
   144→                message: `http route already registered: ${normalizedPath}`,
   145→            });
   146→            return;
   147→        }
   148→        record.httpHandlers += 1;
   149→        registry.httpRoutes.push({
   150→            pluginId: record.id,
   151→            path: normalizedPath,
   152→            handler: params.handler,
   153→            source: record.source,
   154→        });
   155→    };
   156→    const registerChannel = (record, registration) => {
   157→        const normalized = typeof registration.plugin === "object"
   158→            ? registration
   159→            : { plugin: registration };
   160→        const plugin = normalized.plugin;
   161→        const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim();
   162→        if (!id) {
   163→            pushDiagnostic({
   164→                level: "error",
   165→                pluginId: record.id,
   166→                source: record.source,
   167→                message: "channel registration missing id",
   168→            });
   169→            return;
   170→        }
   171→        record.channelIds.push(id);
   172→        registry.channels.push({
   173→            pluginId: record.id,
   174→            plugin,
   175→            dock: normalized.dock,
   176→            source: record.source,
   177→        });
   178→    };
   179→    const registerProvider = (record, provider) => {
   180→        const id = typeof provider?.id === "string" ? provider.id.trim() : "";
   181→        if (!id) {
   182→            pushDiagnostic({
   183→                level: "error",
   184→                pluginId: record.id,
   185→                source: record.source,
   186→                message: "provider registration missing id",
   187→            });
   188→            return;
   189→        }
   190→        const existing = registry.providers.find((entry) => entry.provider.id === id);
   191→        if (existing) {
   192→            pushDiagnostic({
   193→                level: "error",
   194→                pluginId: record.id,
   195→                source: record.source,
   196→                message: `provider already registered: ${id} (${existing.pluginId})`,
   197→            });
   198→            return;
   199→        }
   200→        record.providerIds.push(id);
   201→        registry.providers.push({
   202→            pluginId: record.id,
   203→            provider,
   204→            source: record.source,
   205→        });
   206→    };
   207→    const registerCli = (record, registrar, opts) => {
   208→        const commands = (opts?.commands ?? []).map((cmd) => cmd.trim()).filter(Boolean);
   209→        record.cliCommands.push(...commands);
   210→        registry.cliRegistrars.push({
   211→            pluginId: record.id,
   212→            register: registrar,
   213→            commands,
   214→            source: record.source,
   215→        });
   216→    };
   217→    const registerService = (record, service) => {
   218→        const id = service.id.trim();
   219→        if (!id)
   220→            return;
   221→        record.services.push(id);
   222→        registry.services.push({
   223→            pluginId: record.id,
   224→            service,
   225→            source: record.source,
   226→        });
   227→    };
   228→    const registerCommand = (record, command) => {
   229→        const name = command.name.trim();
   230→        if (!name) {
   231→            pushDiagnostic({
   232→                level: "error",
   233→                pluginId: record.id,
   234→                source: record.source,
   235→                message: "command registration missing name",
   236→            });
   237→            return;
   238→        }
   239→        // Register with the plugin command system (validates name and checks for duplicates)
   240→        const result = registerPluginCommand(record.id, command);
   241→        if (!result.ok) {
   242→            pushDiagnostic({
   243→                level: "error",
   244→                pluginId: record.id,
   245→                source: record.source,
   246→                message: `command registration failed: ${result.error}`,
   247→            });
   248→            return;
   249→        }

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Perfect! I can see the HTTP route registration system (lines 128-154). Now let me search for where these handlers are used:


Bash

grep -r "httpHandlers\|httpRoutes" /usr/lib/node_modules/clawdbot/dist/gateway --include="*.js" 2>/dev/null | head -20


/usr/lib/node_modules/clawdbot/dist/gateway/server/plugins-http.js:        const routes = registry.httpRoutes ?? [];
/usr/lib/node_modules/clawdbot/dist/gateway/server/plugins-http.js:        const handlers = registry.httpHandlers ?? [];
/usr/lib/node_modules/clawdbot/dist/gateway/server/__tests__/test-utils.js:        httpHandlers: [],
/usr/lib/node_modules/clawdbot/dist/gateway/server/__tests__/test-utils.js:        httpRoutes: [],
/usr/lib/node_modules/clawdbot/dist/gateway/server/__tests__/test-utils.js:        httpHandlers: merged.httpHandlers ?? [],
/usr/lib/node_modules/clawdbot/dist/gateway/server/__tests__/test-utils.js:        httpRoutes: merged.httpRoutes ?? [],
/usr/lib/node_modules/clawdbot/dist/gateway/test-helpers.mocks.js:    httpHandlers: [],
/usr/lib/node_modules/clawdbot/dist/gateway/test-helpers.mocks.js:    httpRoutes: [],


grep -r "handlePluginRequest" /usr/lib/node_modules/clawdbot/dist/gateway --include="*.js" 2>/dev/null


/usr/lib/node_modules/clawdbot/dist/gateway/server-http.js:    const { canvasHost, controlUiEnabled, controlUiBasePath, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, handleHooksRequest, handlePluginRequest, resolvedAuth, } = opts;
/usr/lib/node_modules/clawdbot/dist/gateway/server-http.js:            if (handlePluginRequest && (await handlePluginRequest(req, res)))
/usr/lib/node_modules/clawdbot/dist/gateway/server-runtime-state.js:    const handlePluginRequest = createGatewayPluginRequestHandler({
/usr/lib/node_modules/clawdbot/dist/gateway/server-runtime-state.js:            handlePluginRequest,


Excellent! Let me read the plugins-http.js file:


     1→export function createGatewayPluginRequestHandler(params) {
     2→    const { registry, log } = params;
     3→    return async (req, res) => {
     4→        const routes = registry.httpRoutes ?? [];
     5→        const handlers = registry.httpHandlers ?? [];
     6→        if (routes.length === 0 && handlers.length === 0)
     7→            return false;
     8→        if (routes.length > 0) {
     9→            const url = new URL(req.url ?? "/", "http://localhost");
    10→            const route = routes.find((entry) => entry.path === url.pathname);
    11→            if (route) {
    12→                try {
    13→                    await route.handler(req, res);
    14→                    return true;
    15→                }
    16→                catch (err) {
    17→                    log.warn(`plugin http route failed (${route.pluginId ?? "unknown"}): ${String(err)}`);
    18→                    if (!res.headersSent) {
    19→                        res.statusCode = 500;
    20→                        res.setHeader("Content-Type", "text/plain; charset=utf-8");
    21→                        res.end("Internal Server Error");
    22→                    }
    23→                    return true;
    24→                }
    25→            }
    26→        }
    27→        for (const entry of handlers) {
    28→            try {
    29→                const handled = await entry.handler(req, res);
    30→                if (handled)
    31→                    return true;
    32→            }
    33→            catch (err) {
    34→                log.warn(`plugin http handler failed (${entry.pluginId}): ${String(err)}`);
    35→                if (!res.headersSent) {
    36→                    res.statusCode = 500;
    37→                    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    38→                    res.end("Internal Server Error");
    39→                }
    40→                return true;
    41→            }
    42→        }
    43→        return false;
    44→    };
    45→}
    46→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Perfect! Now I have a good understanding of how plugins can register HTTP handlers. Since the voice-call plugin is not currently installed, let me create a comprehensive report of my findings:

## Summary of Findings

Based on my comprehensive search of the clawdbot system, here are my findings regarding clawdbot configuration files, voice-call plugin implementation, and webhook handling:

### Configuration Files Found

**1. `/home/ccuser/.clawdbot/identity/device.json`**
Contains device identification and cryptographic keys for the clawdbot device.

**2. `/home/ccuser/.clawdbot/identity/device-auth.json`**
Contains authentication tokens for operator access with scopes for admin operations, approvals, and pairing.

**3. `/home/ccuser/.clawdbot/agents/main/agent/auth-profiles.json`**
Contains OAuth credentials for Anthropic Claude CLI integration (access and refresh tokens).

**4. `/home/ccuser/.clawdbot-secrets.json`**
Contains API keys for various services:
- ElevenLabs (voice IDs for Charlie and George)
- Vapi (AI voice services)
- Notion API
- Supabase access tokens

**5. `/usr/local/bin/clawdbot-gateway-wrapper`**
A systemd service wrapper script that manages port conflicts on port 18789 (not port 3334). It kills orphaned processes before starting the clawdbot gateway.

### Voice-Call Plugin Status

**The voice-call plugin is NOT currently installed on this system.**

While the documentation at `/usr/lib/node_modules/clawdbot/docs/plugins/voice-call.md` describes a voice-call plugin that:
- Serves webhooks at `/voice/webhook` on port 3334 (configurable)
- Supports multiple providers (Twilio, Telnyx, Plivo, mock)
- Has streaming capabilities at `/voice/stream`
- Requires Twilio signature verification for inbound calls

This plugin is not present in the node_modules directory or extensions folder. It would need to be installed via `clawdbot plugins install @clawdbot/voice-call` and restarted.

### Webhook and HTTP Handler Architecture

From my analysis of the gateway code:

**1. HTTP Handler Registration (`/usr/lib/node_modules/clawdbot/dist/plugins/registry.js`)**
- Plugins can register HTTP routes via `registerHttpRoute(record, params)` at lines 128-154
- Routes are matched by exact pathname (lines 9-10 in plugins-http.js)
- Routes can also register general HTTP handlers that have flexibility in routing

**2. Gateway HTTP Request Processing (`/usr/lib/node_modules/clawdbot/dist/gateway/server-http.js`)**
- The gateway processes HTTP requests in order: Hooks → Tools → Slack → Plugins → OpenResponses → OpenAI → Canvas → ControlUI
- Plugin HTTP handlers are called at line 162 via `handlePluginRequest(req, res)`
- HTTP responses for unauthorized access use the `sendUnauthorized()` function which returns a 401 status with JSON error (lines 16-20 of http-common.js)

**3. Plugin HTTP Handler Execution (`/usr/lib/node_modules/clawdbot/dist/gateway/server/plugins-http.js`)**
- Routes are matched at exact pathname (line 10)
- Handlers receive raw `req` and `res` objects (Node.js HTTP)
- If a handler raises an error or doesn't match, execution falls through to the next handler
- Returns 500 "Internal Server Error" on handler exceptions (lines 19-21, 36-38)

### Signature Verification Pattern

The LINE webhook implementation (at `/usr/lib/node_modules/clawdbot/dist/line/webhook.js`) shows the pattern for signature verification:
- Reads raw request body (lines 8-12)
- Extracts signature header (e.g., `X-Line-Signature`)
- Validates HMAC-SHA256 signature against the raw body (lines 4, 39)
- Returns 401 JSON error if signature validation fails (line 41)
- Returns 200 OK with processed events (line 50)

### Webhook Handler Characteristics

**Twilio webhooks (based on pattern and documentation):**
- Would validate Twilio signature using request body and auth token
- Would return TwiML XML response for call control (e.g., `<Response><Say>...</Say></Response>`)
- Media streaming would use WebSocket upgrade at `/voice/stream` endpoint
- Inbound policy enforcement would check allowlist of phone numbers

**Port 3334 Notes:**
- The voice-call plugin configuration specifies port 3334 for the webhook server
- The gateway wrapper manages port 18789 (not related to voice-call)
- Port 3334 would only be used if the voice-call plugin is installed and enabled

### Files Read

```
/home/ccuser/.clawdbot/identity/device.json
/home/ccuser/.clawdbot/identity/device-auth.json
/home/ccuser/.clawdbot/agents/main/agent/auth-profiles.json
/home/ccuser/.clawdbot-secrets.json
/usr/local/bin/clawdbot-gateway-wrapper
/usr/lib/node_modules/clawdbot/dist/plugins/discovery.js
/usr/lib/node_modules/clawdbot/dist/plugins/bundled-dir.js
/usr/lib/node_modules/clawdbot/dist/plugins/loader.js
/usr/lib/node_modules/clawdbot/dist/plugins/registry.js
/usr/lib/node_modules/clawdbot/dist/gateway/server-http.js
/usr/lib/node_modules/clawdbot/dist/gateway/http-common.js
/usr/lib/node_modules/clawdbot/dist/gateway/server/plugins-http.js
/usr/lib/node_modules/clawdbot/dist/line/webhook.js
/usr/lib/node_modules/clawdbot/docs/plugins/voice-call.md
```

The "Unauthorized" response you're seeing would come from either:
1. The voice-call plugin's Twilio signature validation (if it were installed)
2. The gateway's general authentication layer (sendUnauthorized() at http-common.js:16-20)
3. A webhook signature verification in any installed webhook handler

Since the voice-call plugin is not installed, the system currently has no handler for `/voice/webhook` on port 3334, and requests to that endpoint would receive a 404 "Not Found" response from the gateway.