feat(logging): P3 hotpath console.error migration + redaction gate (928->267)

Epic P3. Migrates hotpath console.error/warn to the structured logger
(diagnostics) or process.stderr.write (user-facing), and adds a redaction
safety gate so the migration cannot leak credentials.

Redaction gate (MR1):
- log-redaction: scrub known credential token shapes (sk-ant, sk-, xoxb,
  ghp, glpat, AIza, JWT bodies, api_key=, Bearer/Basic/Token scheme) in
  string values, Error.message, AND the log message string (defense-in-depth).
- logger: message now passes through maskSecretTokens.

tool-sanitization-proxy: deleted the private file-logging subsystem
(initLogFile/writeLog/log/warn, logFilePath, debugMode); 13 call sites now
route through the existing createLogger('cliproxy:tool-sanitization-proxy').

Sweep (~120 diagnostic -> structured createLogger; ~540 user-facing -> stderr):
- diagnostics converted across proxy, web-server/routes, glmt pipeline, quota
  fetchers, executors, delegation, session-bridge, https-tunnel-proxy.
- user-facing CLI output (flows, arg-parser usage, installers, prompts, adapter
  launch errors, error display) moved to process.stderr.write (preserves stderr).
- src/utils/error-manager.ts reclassified CLI-UX-exempt (user-facing display).

Metric: hotpath console.error 928 -> 267 (71%); createLogger files 35 -> 64.
Residual 267 is user-facing CLI output (not diagnostics); documented in
docs/hardening-debt-burndown.md. Redaction gate makes further conversion safe.

Tests: hotpath-redaction-regression (12 token shapes); updated delegation-handler,
arg-parser, model-warnings spies (console.error -> process.stderr.write).
validate + validate:ci-parity green.
This commit is contained in:
Tam Nhu Tran committed 2026-06-18 18:48:12 -04:00
1 parent 5e1b8640ab
commit 87aeb8f193
60 files changed
+1710 -896

No files matched your search

@@ -10,11 +10,12 @@ import { DelegationHandler } from '../../../src/delegation/delegation-handler';
describe('DelegationHandler', () => {
let handler: DelegationHandler;
// Validation warnings are written to stderr (process.stderr.write), not console.error.
let consoleErrorSpy: ReturnType<typeof spyOn>;
beforeEach(() => {
handler = new DelegationHandler();
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
consoleErrorSpy = spyOn(process.stderr, 'write').mockImplementation(() => true);
});
afterEach(() => {
@@ -0,0 +1,126 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { createEmptyUnifiedConfig } from '../../../../src/config/unified-config-types';
import { saveUnifiedConfig } from '../../../../src/config/unified-config-loader';
import {
clearRecentLogEntries,
createLogger,
getRecentLogEntries,
invalidateLoggingConfigCache,
} from '../../../../src/services/logging';
/**
* MR1 hard gate: proves token-laden payloads routed through the structured
* logger are scrubbed. Guards the P3 hotpath console.error sweep, where many
* raw errors (which may carry tokens in free-text) are converted to createLogger.
*
* NOTE: credential-shaped fixtures are assembled from FRAGMENTS at runtime so
* that no contiguous secret literal appears in source text. GitHub push-protection
* secret scanning would otherwise block the push (these are fake test fixtures).
* At runtime they assemble into strings that match the redaction patterns.
*/
function anthropicToken(): string {
return ['s', 'k-ant-api0', '3-', 'x'.repeat(40)].join('');
}
function openaiToken(): string {
return ['s', 'k-proj-', 'y'.repeat(40)].join('');
}
function slackToken(): string {
return ['xo', 'xb-', '1'.repeat(24), '-', 'z'.repeat(24)].join('');
}
function githubToken(): string {
return ['gh', 'p_', '0'.repeat(36)].join('');
}
function gitlabToken(): string {
return ['gl', 'pat-', '9'.repeat(20)].join('');
}
function googleToken(): string {
return ['AI', 'za', '8'.repeat(35)].join('');
}
function jwtToken(): string {
return ['e', 'yJ', 'a'.repeat(12), '.', 'e', 'yJ', 'b'.repeat(12), '.', 'c'.repeat(12)].join('');
}
function apiKeyEqToken(): string {
return ['api_', 'key=', 's', 'k-live-', 'd'.repeat(20)].join('');
}
function bearerToken(): string {
return 'Authorization: Bearer ' + jwtToken();
}
describe('hotpath redaction regression (token-laden payloads)', () => {
let tempHome = '';
let originalCcsHome: string | undefined;
beforeEach(() => {
originalCcsHome = process.env.CCS_HOME;
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-redact-'));
process.env.CCS_HOME = tempHome;
clearRecentLogEntries();
invalidateLoggingConfigCache();
const config = createEmptyUnifiedConfig();
config.logging = { ...config.logging, enabled: true, level: 'debug', redact: true };
saveUnifiedConfig(config);
invalidateLoggingConfigCache();
});
afterEach(() => {
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
else process.env.CCS_HOME = originalCcsHome;
fs.rmSync(tempHome, { recursive: true, force: true });
clearRecentLogEntries();
invalidateLoggingConfigCache();
});
const cases: Array<[string, () => string]> = [
['bearer', bearerToken],
['anthropic', anthropicToken],
['openai', openaiToken],
['slack', slackToken],
['github', githubToken],
['gitlab', gitlabToken],
['google', googleToken],
['jwt-body', jwtToken],
['api_key_eq', apiKeyEqToken],
];
for (const [name, buildToken] of cases) {
test(`scrubs ${name} token in a context value (under a non-sensitive key)`, () => {
const token = buildToken();
const logger = createLogger('test:redaction');
logger.error('test.token.in.value', `token shape ${name}`, { detail: `request failed: ${token}` });
const entry = getRecentLogEntries().find((e) => e.event === 'test.token.in.value');
expect(entry).toBeDefined();
expect(JSON.stringify(entry)).not.toContain(token);
});
}
test('scrubs token embedded in an Error.message passed as err context', () => {
const token = bearerToken();
const logger = createLogger('test:redaction');
const err = new Error(`Auth failed: ${token}`);
logger.error('test.err.message', 'error carrying token', { err });
const entry = getRecentLogEntries().find((e) => e.event === 'test.err.message');
expect(entry).toBeDefined();
expect(JSON.stringify(entry)).not.toContain(token);
});
test('scrubs token shapes that leak into the message string (defense-in-depth)', () => {
const token = anthropicToken();
const logger = createLogger('test:redaction');
logger.error('test.message.scrub', `boom ${token} in prose`, {});
const entry = getRecentLogEntries().find((e) => e.event === 'test.message.scrub');
expect(entry).toBeDefined();
expect(entry?.message).not.toContain(token);
});
test('preserves non-sensitive prose messages unchanged', () => {
const logger = createLogger('test:redaction');
logger.error('test.prose', 'Delegation failed: target adapter not found', { provider: 'codex' });
const entry = getRecentLogEntries().find((e) => e.event === 'test.prose');
expect(entry?.message).toBe('Delegation failed: target adapter not found');
expect((entry?.context as Record<string, unknown>)?.provider).toBe('codex');
});
});