From 0decf34760d4790a6ed79352848f2131e99fb37f Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 27 Feb 2026 13:18:37 -0800 Subject: [PATCH] fix(audit): require integer gateway window padding Enforce non-negative integer padding_ms/--window-padding-ms across gateway window helper and live baseline capture CLI, with regression coverage for invalid values. Architecture/protocol diagrams reviewed; no updates were needed for this validation-only change. --- docs/plans/state.json | 13 +++++++++++++ scripts/capture-phase0-live-baseline.ts | 7 ++----- src/audit/phase0GatewayWindow.test.ts | 10 ++++++++++ src/audit/phase0GatewayWindow.ts | 21 +++++++++++++++++---- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index 3ea385c..2a17724 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -520,6 +520,19 @@ ], "test_status": "pnpm test:run src/audit/phase0BaselineDrift.test.ts + pnpm typecheck passing" }, + "phase0-live-baseline-gateway-window-padding-integer-validation": { + "status": "completed", + "date": "2026-02-27", + "updated": "2026-02-27", + "summary": "Hardened gateway cancel-window auto-detection padding by requiring non-negative integer `padding_ms`/`--window-padding-ms` values in both helper and capture CLI, preventing silent flooring of fractional millisecond values.", + "files_modified": [ + "src/audit/phase0GatewayWindow.ts", + "src/audit/phase0GatewayWindow.test.ts", + "scripts/capture-phase0-live-baseline.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/audit/phase0GatewayWindow.test.ts + pnpm typecheck passing" + }, "phase0-instrumentation-ticket-checklist": { "status": "completed", "date": "2026-02-25", diff --git a/scripts/capture-phase0-live-baseline.ts b/scripts/capture-phase0-live-baseline.ts index ebda4b0..5d41103 100644 --- a/scripts/capture-phase0-live-baseline.ts +++ b/scripts/capture-phase0-live-baseline.ts @@ -39,7 +39,7 @@ function usage(): string { ' --backend Restrict sample to selected backends (via backend.route timeline)', ' --exclude-session-substring Exclude sessions containing any substring (default: probe)', ' --auto-gateway-cancel-window Auto-select latest gateway cancel/cancelled session window', - ' --window-padding-ms Milliseconds added before/after auto-selected window (default: 250)', + ' --window-padding-ms Milliseconds added before/after auto-selected window (default: 250)', ' --raw-identifiers Keep raw session/sender/request IDs (default: anonymized)', ' --tag Output file tag (default: current date UTC)', ' --sample-out Output JSONL sample path override', @@ -202,10 +202,7 @@ async function main(): Promise { const backendTargets = parseBackendTargets(values.backend); const excludeSessionSubstrings = parseCsv(values['exclude-session-substring']) ?? ['probe']; const autoGatewayCancelWindow = Boolean(values['auto-gateway-cancel-window']); - const windowPaddingMs = parseOptionalNumber(values['window-padding-ms'], '--window-padding-ms'); - if (windowPaddingMs !== undefined && windowPaddingMs < 0) { - throw new Error('--window-padding-ms must be greater than or equal to 0.'); - } + const windowPaddingMs = parseOptionalInteger(values['window-padding-ms'], '--window-padding-ms'); if (autoGatewayCancelWindow && (values.since || values.until)) { throw new Error('--auto-gateway-cancel-window cannot be combined with --since/--until.'); diff --git a/src/audit/phase0GatewayWindow.test.ts b/src/audit/phase0GatewayWindow.test.ts index be92ecb..a565c7b 100644 --- a/src/audit/phase0GatewayWindow.test.ts +++ b/src/audit/phase0GatewayWindow.test.ts @@ -71,4 +71,14 @@ describe('findLatestGatewayCancelWindow', () => { expect(findLatestGatewayCancelWindow(events)).toBeNull(); }); + + it('rejects invalid padding values', () => { + const events: AuditEvent[] = [ + event(1000, 'run.cancel', { session_id: 's1', source: 'gateway' }), + event(1010, 'run.state', { session_id: 's1', source: 'gateway', state: 'cancelled' }), + ]; + + expect(() => findLatestGatewayCancelWindow(events, { padding_ms: -1 })).toThrow('padding_ms'); + expect(() => findLatestGatewayCancelWindow(events, { padding_ms: 1.5 })).toThrow('padding_ms'); + }); }); diff --git a/src/audit/phase0GatewayWindow.ts b/src/audit/phase0GatewayWindow.ts index f4829d5..61c0321 100644 --- a/src/audit/phase0GatewayWindow.ts +++ b/src/audit/phase0GatewayWindow.ts @@ -36,6 +36,22 @@ function isGatewayEvent(payload: Record): boolean { return readString(payload.source) === 'gateway'; } +function normalizePaddingMs(value: number | undefined): number { + if (value === undefined) { + return 0; + } + if (!Number.isFinite(value)) { + throw new Error('padding_ms must be a finite integer greater than or equal to 0.'); + } + if (!Number.isInteger(value)) { + throw new Error('padding_ms must be an integer greater than or equal to 0.'); + } + if (value < 0) { + throw new Error('padding_ms must be greater than or equal to 0.'); + } + return value; +} + export function findLatestGatewayCancelWindow( events: AuditEvent[], options: FindGatewayCancelWindowOptions = {}, @@ -104,10 +120,7 @@ export function findLatestGatewayCancelWindow( return null; } - const padRaw = options.padding_ms ?? 0; - const paddingMs = Number.isFinite(padRaw) && padRaw > 0 - ? Math.floor(padRaw) - : 0; + const paddingMs = normalizePaddingMs(options.padding_ms); return { session_id: latest.session_id,