/** * The fixed observability surface for the sandbox duplex transport. * * This module owns the one closed contract for every duplex telemetry sink: the * span names, the counter names, the one event name, the dimension keys, and the * enum values. Each name and value is a literal constant here, so the surface * never drifts and a test asserts the exact set. * * The module also owns the provider allowlist. The `provider` dimension carries * one approved public value, `daytona`. Any other plugin key maps to the constant * `other`. The map runs inside this module before every sink, so a raw plugin key * never reaches a span attribute, a counter label, or an event field. * * The module stays free of `@opentelemetry/api` and of the database. The host * injects a {@link DuplexObservabilityRecorder}; the default is a no-op recorder, so * the whole surface stays inert until the host binds a real recorder. Every * recorder call sits inside an error swallow, so a telemetry failure never breaks * the request path. */ /** The span for one duplex channel-open attempt. */ export const DUPLEX_SPAN_CHANNEL_OPEN = "sandbox.duplex.channel_open"; /** The span for one duplex request. It carries the request latency. */ export const DUPLEX_SPAN_REQUEST = "sandbox.duplex.request"; /** The one duplex transport event. The host emits it at each transport boundary. */ export const DUPLEX_TRANSPORT_EVENT = "sandbox.duplex.transport"; /** The guarded counter for one successful channel open. */ export const DUPLEX_COUNTER_CHANNEL_OPEN_TOTAL = "sandbox_duplex_channel_open_total"; /** The guarded counter for one fallback to the file bridge. */ export const DUPLEX_COUNTER_FALLBACK_TOTAL = "sandbox_duplex_fallback_total"; /** The guarded counter for one terminal channel loss. */ export const DUPLEX_COUNTER_LOSS_TOTAL = "sandbox_duplex_loss_total"; /** The guarded counter for one leaked provider session on teardown. */ export const DUPLEX_COUNTER_SESSION_LEAK_TOTAL = "sandbox_duplex_session_leak_total"; /** * The closed dimension-key set. Every span attribute, counter label, and event * field uses only these keys. A test asserts the exact set, so a new key never * reaches a sink by accident. */ export const DUPLEX_DIMENSION_KEYS = [ "provider", "transport", "outcome", "fallback_reason", "loss_class", "loss_reason", ] as const; /** One dimension key from the closed set. */ export type DuplexDimensionKey = (typeof DUPLEX_DIMENSION_KEYS)[number]; /** * The transport a record is about. `duplex` names the retired bespoke frame * protocol; `http2` names the Node HTTP/2 session over the sandbox channel; * `file` names the queue-file bridge. */ export type DuplexTransportValue = "duplex" | "http2" | "file"; /** The outcome of a record. */ export type DuplexOutcomeValue = "ok" | "error"; /** * The reason the host selected the file bridge instead of the duplex transport. * The open-failure stage is split, so a reader groups a failed open by the exact * stage: the process-scoped route ceiling was full (`route_busy`), the entrypoint * sync failed (`entrypoint_sync_failed`), the broker construction failed * (`broker_construction_failed`), or the channel open failed (`channel_open_failed`). * The `preface_missing` reason names a missing or an invalid HTTP/2 client * connection preface inside the bounded readiness buffer: the host found no * valid preface after the accepted READY line, aborted the HTTP/2 open, and * moved the run to `queue_v1` one time. */ export type DuplexFallbackReason = | "gate_off" | "capability_absent" | "route_busy" | "entrypoint_sync_failed" | "broker_construction_failed" | "channel_open_failed" | "ready_invalid" | "ready_nonce_mismatch" | "ready_timeout" | "contaminated" | "preface_missing"; /** The class of a terminal loss, relative to the first request dispatch. */ export type DuplexLossClass = "pre_dispatch" | "post_dispatch"; /** * The closed, typed reason for a terminal channel loss. The host maps every loss * cause to one of these values before any sink reads it. The set covers the * loss-detection modes the transport names: a gateway stdin end of file, a * provider process exit, a heartbeat timeout, and an RPC failure. It adds * `write_error` for a rejected host-to-sandbox write, `transport_closed` for a * reason-less provider transport close with no exit data, and `other` for an * unknown cause. The host maps an unknown cause or any caught provider text to * `other`, so no raw provider text reaches a sink. */ export const DUPLEX_LOSS_REASONS = [ "stdin_eof", "provider_exit", "heartbeat_timeout", "rpc_failure", "write_error", "transport_closed", "other", ] as const; /** One typed loss reason from the closed set. */ export type DuplexLossReason = (typeof DUPLEX_LOSS_REASONS)[number]; /** The host-owned closed loss-reason set. It backs {@link normalizeDuplexLossReason}. */ const LOSS_REASONS: ReadonlySet = new Set(DUPLEX_LOSS_REASONS); /** * Map a raw loss-cause value to the closed {@link DuplexLossReason} set. Return * the value when the closed set holds it. Return `other` for any other value or a * missing value, so a raw provider string never reaches a sink. */ export function normalizeDuplexLossReason(value: string | null | undefined): DuplexLossReason { return typeof value === "string" && LOSS_REASONS.has(value) ? (value as DuplexLossReason) : "other"; } /** * The closed set of HTTP/2 session and stream event names the host maps to a * loss reason. Each name spells one distinct signal the `http2_v1` transport * can observe: a session-level protocol fault, a peer GOAWAY, a stalled PING * watchdog, a rejected host-to-sandbox write, a reason-less transport close, * or the pseudo-terminal channel process exit. */ export const HTTP2_TELEMETRY_EVENT_NAMES = [ "session_error", "session_goaway", "session_stall", "write_error", "transport_closed", "channel_exit", ] as const; /** One event name from the closed HTTP/2 event set. */ export type Http2TelemetryEventName = (typeof HTTP2_TELEMETRY_EVENT_NAMES)[number]; /** The host-owned closed HTTP/2 event-name set. It backs {@link mapHttp2EventToDuplexLossReason}. */ const HTTP2_EVENT_NAMES: ReadonlySet = new Set(HTTP2_TELEMETRY_EVENT_NAMES); /** * The map from one closed HTTP/2 event name to the existing, closed * {@link DuplexLossReason} taxonomy. The map reuses that one taxonomy instead * of a second, HTTP/2-only reason list, so the closed-set pattern stays one * set of values across every transport. */ const HTTP2_EVENT_TO_LOSS_REASON: Readonly> = { session_error: "rpc_failure", session_goaway: "transport_closed", session_stall: "heartbeat_timeout", write_error: "write_error", transport_closed: "transport_closed", channel_exit: "provider_exit", }; /** * Map one HTTP/2 session or stream event name to the closed * {@link DuplexLossReason} taxonomy (accepted security fix 7). Return the * mapped reason when the closed event-name set holds the value. Return * `other` for any other value or a missing value, so an unknown event name or * a raw provider string never reaches a sink. */ export function mapHttp2EventToDuplexLossReason( event: string | null | undefined, ): DuplexLossReason { return typeof event === "string" && HTTP2_EVENT_NAMES.has(event) ? HTTP2_EVENT_TO_LOSS_REASON[event as Http2TelemetryEventName] : "other"; } /** The one approved public provider value. */ export const DUPLEX_APPROVED_PROVIDER = "daytona"; /** The constant for any provider key outside the allowlist. */ export const DUPLEX_PROVIDER_OTHER = "other"; /** The `provider` dimension value after the allowlist map. */ export type DuplexProviderValue = typeof DUPLEX_APPROVED_PROVIDER | typeof DUPLEX_PROVIDER_OTHER; /** The host-owned closed provider allowlist. It holds one approved public value. */ const APPROVED_PROVIDERS: ReadonlySet = new Set([DUPLEX_APPROVED_PROVIDER]); /** * Map a raw provider key to the closed `provider` dimension value. Return the key * when the allowlist holds it. Return `other` for any other value, so a raw * plugin key never reaches a sink. A missing key also maps to `other`. */ export function normalizeDuplexProvider(key: string | null | undefined): DuplexProviderValue { return typeof key === "string" && APPROVED_PROVIDERS.has(key) ? (key as DuplexProviderValue) : DUPLEX_PROVIDER_OTHER; } /** * The dimension bag a sink reads. Only the closed keys appear. The optional keys * are present only when the record defines them, so a span or a counter never * carries an empty dimension. */ export interface DuplexObservabilityDimensions { provider: DuplexProviderValue; transport: DuplexTransportValue; outcome?: DuplexOutcomeValue; fallback_reason?: DuplexFallbackReason; loss_class?: DuplexLossClass; loss_reason?: DuplexLossReason; } /** One span record the host records. The request span carries a latency. */ export interface DuplexObservabilitySpanRecord { name: string; dimensions: DuplexObservabilityDimensions; /** The request latency in milliseconds. Only the request span sets it. */ latencyMs?: number; } /** One counter increment the host records. */ export interface DuplexObservabilityCounterRecord { metric: string; dimensions: DuplexObservabilityDimensions; } /** One event the host emits. */ export interface DuplexObservabilityEventRecord { name: string; dimensions: DuplexObservabilityDimensions; } /** * The injected low-level recorder. The host binds it to the existing telemetry * pipeline: the span to the OTel tracer, the counter to the guarded counter * store, and the event to the run-events bridge. The default is a no-op recorder. * The recorder receives only already-mapped dimensions, so the raw provider key * never reaches it. */ export interface DuplexObservabilityRecorder { recordSpan(record: DuplexObservabilitySpanRecord): void; incrementCounter(record: DuplexObservabilityCounterRecord): void; emitEvent(record: DuplexObservabilityEventRecord): void; } /** A no-op recorder. Every method does nothing, so the surface stays inert. */ export const NOOP_DUPLEX_OBSERVABILITY_RECORDER: DuplexObservabilityRecorder = { recordSpan() {}, incrementCounter() {}, emitEvent() {}, }; /** One channel-open attempt. The caller reports exactly one terminal. */ export interface DuplexChannelOpenAttempt { /** * The channel opened and readiness passed. Record the channel-open span with * the `ok` outcome, increment the channel-open counter, and emit the transport * event for the duplex transport. */ ready(): void; /** * The channel did not open or readiness failed. Record the channel-open span * with the `error` outcome, increment the fallback counter, and emit the * transport event for the file bridge. */ fallback(reason: DuplexFallbackReason): void; } /** * The bound telemetry facade the call sites use. It carries the normalized * provider, so a call site never passes a raw key. It maps each semantic event to * the fixed names and dimensions, then calls the recorder inside an error swallow. */ export interface DuplexObservability { /** Begin a channel-open attempt. The caller reports `ready` or `fallback`. */ startChannelOpen(): DuplexChannelOpenAttempt; /** * Record a fallback to the file bridge with no channel-open attempt. Use it for * `gate_off` and `capability_absent`, where the host opens no channel. */ recordFallback(reason: DuplexFallbackReason): void; /** Record one duplex request span with its latency and outcome. */ recordRequest(record: { latencyMs: number; outcome: DuplexOutcomeValue }): void; /** * Record one terminal channel loss. The caller passes the loss class and the * typed, closed loss reason. The loss counter and the transport loss event carry * both dimensions. No raw provider text rides either sink. */ recordLoss(lossClass: DuplexLossClass, lossReason: DuplexLossReason): void; /** Record one leaked provider session on teardown. */ recordSessionLeak(): void; } /** The options for {@link createDuplexObservability}. */ export interface DuplexObservabilityOptions { /** The injected recorder. The default is the no-op recorder. */ recorder?: DuplexObservabilityRecorder | null; /** The raw provider key. The facade maps it through the allowlist one time. */ providerKey?: string | null; /** * The transport value the facade stamps on every non-file record (a channel * open, a request, a loss, a session leak). The default is `duplex`, so an * existing caller that names no transport sees no change. The host passes * `http2` for the `http2_v1` path. */ transport?: DuplexTransportValue; } /** * Build the bound telemetry facade. The facade normalizes the provider key one * time, then reuses the mapped value for every record. Every recorder call sits * inside a `try/catch`, so a throwing recorder never breaks the request path. A * missing recorder yields a facade whose methods do nothing. */ export function createDuplexObservability(options: DuplexObservabilityOptions = {}): DuplexObservability { const recorder = options.recorder ?? NOOP_DUPLEX_OBSERVABILITY_RECORDER; const provider = normalizeDuplexProvider(options.providerKey); const transport: DuplexTransportValue = options.transport ?? "duplex"; const safeSpan = (record: DuplexObservabilitySpanRecord): void => { try { recorder.recordSpan(record); } catch { // Observability must not break the request path. } }; const safeCounter = (record: DuplexObservabilityCounterRecord): void => { try { recorder.incrementCounter(record); } catch { // Observability must not break the request path. } }; const safeEvent = (record: DuplexObservabilityEventRecord): void => { try { recorder.emitEvent(record); } catch { // Observability must not break the request path. } }; const recordFallback = (reason: DuplexFallbackReason): void => { const dimensions: DuplexObservabilityDimensions = { provider, transport: "file", outcome: "error", fallback_reason: reason, }; safeCounter({ metric: DUPLEX_COUNTER_FALLBACK_TOTAL, dimensions }); safeEvent({ name: DUPLEX_TRANSPORT_EVENT, dimensions }); }; return { startChannelOpen(): DuplexChannelOpenAttempt { let settled = false; return { ready(): void { if (settled) return; settled = true; const dimensions: DuplexObservabilityDimensions = { provider, transport, outcome: "ok", }; safeSpan({ name: DUPLEX_SPAN_CHANNEL_OPEN, dimensions }); safeCounter({ metric: DUPLEX_COUNTER_CHANNEL_OPEN_TOTAL, dimensions }); safeEvent({ name: DUPLEX_TRANSPORT_EVENT, dimensions }); }, fallback(reason: DuplexFallbackReason): void { if (settled) return; settled = true; // The channel-open span records the failed attempt on the duplex // transport; the counter and the event record the file-bridge fallback. // The span carries `fallback_reason` on this fallback path only, so a // reader can group the failed opens by reason. `fallback_reason` is a // closed dimension key, so no new key reaches a sink. safeSpan({ name: DUPLEX_SPAN_CHANNEL_OPEN, dimensions: { provider, transport, outcome: "error", fallback_reason: reason }, }); recordFallback(reason); }, }; }, recordFallback, recordRequest(record: { latencyMs: number; outcome: DuplexOutcomeValue }): void { safeSpan({ name: DUPLEX_SPAN_REQUEST, dimensions: { provider, transport, outcome: record.outcome }, latencyMs: record.latencyMs, }); }, recordLoss(lossClass: DuplexLossClass, lossReason: DuplexLossReason): void { const dimensions: DuplexObservabilityDimensions = { provider, transport, outcome: "error", loss_class: lossClass, loss_reason: lossReason, }; safeCounter({ metric: DUPLEX_COUNTER_LOSS_TOTAL, dimensions }); safeEvent({ name: DUPLEX_TRANSPORT_EVENT, dimensions }); }, recordSessionLeak(): void { safeCounter({ metric: DUPLEX_COUNTER_SESSION_LEAK_TOTAL, dimensions: { provider, transport, outcome: "error" }, }); }, }; }