/** * Inspector hook called from `MitmHandlerBase` (F3) on every intercepted * AgentBridge request. Centralises buffer push/update so handlers do not need * to know the inspector internals. * * Contract: see `_orchestration/master-plan-group-A.md` §3.11. */ import { randomUUID } from "node:crypto"; import type { IncomingMessage } from "node:http"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; import { maskSecret } from "../maskSecrets.ts"; import { sanitizeHeaders } from "../sanitizeHeaders.ts"; import type { AgentId } from "../types.ts"; import { globalTrafficBuffer } from "./buffer.ts"; import type { InterceptedRequest } from "./types.ts"; import { isCustomHost } from "@/lib/db/inspectorCustomHosts"; export interface RecordRequestStartOpts { req: IncomingMessage; body: Buffer; agentId: AgentId; mappedModel: string; sourceModel?: string | null; sessionId?: string; } export interface RecordRequestCompleteOpts { status: number; responseHeaders: Record; responseBody: string | null; responseSize: number; proxyLatencyMs: number; upstreamLatencyMs: number; } /** * Build the initial buffer entry and push it. Returned object is mutable by * design — handlers call `recordRequestComplete()` (or `recordRequestError`) * with the same reference once the upstream call resolves. */ export async function recordRequestStart( opts: RecordRequestStartOpts ): Promise { const requestBody = opts.body.length > 0 ? maskSecret(opts.body.toString("utf8")) : null; // Determine whether this request originates from a custom-host intercept // (Mode 2 / Custom Hosts) or a standard agent-bridge intercept (Mode 1). // // Both modes reach this hook via the same MITM server path: custom hosts are // added to inspector_custom_hosts by the Mode 2 UI and are spoofed to // 127.0.0.1 by /etc/hosts entries, so they arrive here just like agent // targets. The DB lookup below is the cheapest reliable way to distinguish // them without touching server.cjs — it costs one SQLite read per request. // // If the host resolves as a custom-host entry, source="custom-host" and // agent is left undefined so the "Custom" profile filter matches correctly. const host = opts.req.headers.host ?? ""; const customHost = isCustomHost(host); const intercepted: InterceptedRequest = { id: randomUUID(), source: customHost ? "custom-host" : "agent-bridge", agent: customHost ? undefined : opts.agentId, timestamp: new Date().toISOString(), method: opts.req.method ?? "GET", host, path: opts.req.url ?? "/", requestHeaders: sanitizeHeaders(opts.req.headers), requestBody, requestSize: opts.body.length, responseHeaders: {}, responseBody: null, responseSize: 0, status: "in-flight", sourceModel: opts.sourceModel ?? null, mappedModel: opts.mappedModel, }; if (opts.sessionId) intercepted.sessionId = opts.sessionId; // Best-effort process attribution (Linux only; no-op elsewhere). The proxy's // inbound socket remotePort is the client process's local ephemeral port, // which is what appears in that process's /proc/net/tcp local_address. Never // blocks capture — any failure leaves pid/processName unset. (Gap 1.) try { const { attributeProcess } = await import("./processAttribution.ts"); const remotePort = opts.req.socket?.remotePort; if (typeof remotePort === "number") { const info = attributeProcess(remotePort); if (info) { intercepted.pid = info.pid; intercepted.processName = info.processName; } } } catch { // attribution is best-effort — never block capture } globalTrafficBuffer.push(intercepted); return intercepted; } /** * Finalise the buffer entry with the upstream response data. Latencies are * stored as-given and combined into `totalLatencyMs`. */ export function recordRequestComplete( intercepted: InterceptedRequest, opts: RecordRequestCompleteOpts ): void { intercepted.status = opts.status; intercepted.responseHeaders = opts.responseHeaders; intercepted.responseBody = opts.responseBody != null ? maskSecret(opts.responseBody) : null; intercepted.responseSize = opts.responseSize; intercepted.proxyLatencyMs = opts.proxyLatencyMs; intercepted.upstreamLatencyMs = opts.upstreamLatencyMs; intercepted.totalLatencyMs = opts.proxyLatencyMs + opts.upstreamLatencyMs; globalTrafficBuffer.update(intercepted.id, intercepted); } /** * Mark the buffer entry as failed. Error messages are sanitized so stack * traces or absolute paths cannot leak to dashboards/exports (Hard Rule #12). */ export function recordRequestError( intercepted: InterceptedRequest, err: unknown ): void { intercepted.status = "error"; intercepted.error = sanitizeErrorMessage(err); globalTrafficBuffer.update(intercepted.id, intercepted); }