fix: redact gh auth output in code queue

This commit is contained in:
Codex
2026-05-23 04:22:42 +00:00
parent 5d5d38b9a1
commit 3b554e6154
7 changed files with 186 additions and 13 deletions
@@ -0,0 +1,40 @@
const ghAuthStatusLinePatterns = [
/^\s*(?:[\u2713\u2714]\s*)?Logged in to\s+\S+\s+account\s+\S+\s+\([^)]+\)\s*$/iu,
/^\s*(?:[\u2713\u2714]\s*)?Token:\s+\S+\s*$/iu,
/^\s*(?:[\u2713\u2714]\s*)?Token scopes?:\s+.*$/iu,
/^\s*(?:-\s*)?Token(?:\s+(?:scopes?|source|preview|value))?\s*[:=]\s*\S+.*$/iu,
] as const;
const tokenLikePatterns = [
/\bgh[pousr]_[A-Za-z0-9_]{6,}\b/gu,
/\bgithub_pat_[A-Za-z0-9_]{6,}\b/gu,
/\b(?:sk|xoxb|xoxp|AKIA)[A-Za-z0-9_=-]{8,}\b/gu,
/\b(?:token|secret|password|passwd|authorization|cookie|api[_-]?key)\s*[:=]\s*[^,\s]+/giu,
/\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/giu,
/https?:\/\/[^/\s]+:[^@\s]+@[^/\s]+/giu,
] as const;
export const ghAuthStatusWrapperHint = "Use bun scripts/cli.ts gh auth status --repo pikasTech/unidesk for structured redacted GitHub auth diagnostics.";
export function sanitizeTaskOutputText(text: string): string {
let redactionsApplied = 0;
let ghAuthStatusRedactions = 0;
const lines = String(text || "").split(/\r?\n/u).map((line) => {
if (ghAuthStatusLinePatterns.some((pattern) => pattern.test(line))) {
redactionsApplied += 1;
ghAuthStatusRedactions += 1;
return "[redacted gh auth status line]";
}
let next = line;
for (const pattern of tokenLikePatterns) {
next = next.replace(pattern, () => {
redactionsApplied += 1;
return "<redacted>";
});
}
return next;
});
const sanitized = lines.join("\n");
if (redactionsApplied === 0 || ghAuthStatusRedactions === 0 || sanitized.includes(ghAuthStatusWrapperHint)) return sanitized;
return `${sanitized}\n${ghAuthStatusWrapperHint}`;
}
@@ -5,6 +5,7 @@ import { codeAgentPortForModel, codeAgentPortInfo, codeExecutionModeInfo, codeEx
import { claudeQqNotificationOutboxStats, notificationTargetConfigured, notificationTargetLabel } from "./notifications";
import { executionModeOptions, executionProviderOptions } from "./provider-runtime";
import { taskFullOutput } from "./task-output";
import { sanitizeTaskOutputText } from "./output-redaction";
import { applyOaTraceStatsToTaskJson, taskScopeId, type OaTraceStats, type TraceStatsFallback } from "./oa-events";
import { buildExecutionDiagnostics, schedulerHeartbeatStaleMs } from "./execution-diagnostics";
import { buildCompactTaskTranscript, buildTaskTranscript, cachedPreviewTranscript, fullTranscript, prefixPreview, safePreview, statsDaysFromUrl, taskForCompactMetaResponse, taskForMetaResponse, taskListStepCount, taskStatisticsSummary, taskTiming, timestampMs } from "./task-view";
@@ -167,13 +168,14 @@ function outputChunkResponse(task: QueueTask, url: URL): Response {
const fullOutput = taskFullOutput(task);
const page = ctx().pageBySeq(fullOutput, url, limit);
const output = page.chunk.map((item) => {
const truncated = !fullText && item.text.length > maxTextChars;
const text = sanitizeTaskOutputText(item.text);
const truncated = !fullText && text.length > maxTextChars;
return {
...item,
text: truncated ? item.text.slice(0, maxTextChars) : item.text,
textChars: item.text.length,
text: truncated ? text.slice(0, maxTextChars) : text,
textChars: text.length,
textTruncated: truncated,
omittedChars: truncated ? item.text.length - maxTextChars : 0,
omittedChars: truncated ? text.length - maxTextChars : 0,
};
});
return ctx().jsonResponse({
@@ -3,6 +3,7 @@
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
import { resolve } from "node:path";
import type { ArchivedLiveOutput, JsonValue, LiveOutput, OutputChannel, QueueTask, RuntimeConfig } from "./types";
import { sanitizeTaskOutputText } from "./output-redaction";
export interface TaskOutputContext {
config: Pick<RuntimeConfig, "maxInMemoryOutputRecords" | "outputArchiveDir">;
@@ -187,7 +188,8 @@ function outputArchiveSignature(task: QueueTask): string {
}
function appendOutput(task: QueueTask, channel: OutputChannel, text: string, method?: string, itemId?: string, append = false): LiveOutput | null {
if (text.length === 0) return null;
const safeText = sanitizeTaskOutputText(text);
if (safeText.length === 0) return null;
try {
ensureTaskOutputArchiveSeeded(task);
} catch (error) {
@@ -196,14 +198,14 @@ function appendOutput(task: QueueTask, channel: OutputChannel, text: string, met
const last = task.output[task.output.length - 1];
let output: LiveOutput;
let archiveOp: ArchivedLiveOutput["op"] = "set";
let archiveText = text;
let archiveText = safeText;
if (append && last !== undefined && last.channel === channel && last.itemId === itemId && last.method === method && last.text.length < 24_000) {
last.text += text;
last.text += safeText;
last.at = ctx().nowIso();
output = last;
archiveOp = "append";
} else {
output = { seq: ctx().allocateSeq(), at: ctx().nowIso(), channel, text, method, itemId };
output = { seq: ctx().allocateSeq(), at: ctx().nowIso(), channel, text: safeText, method, itemId };
task.output.push(output);
}
appendOutputArchive(task, output, archiveOp, archiveText);
@@ -20,6 +20,7 @@ import type {
} from "./types";
import { codeAgentPortForModel, codeAgentPortInfo, codeExecutionModeInfo, extractRecord } from "./code-agent/common";
import { currentTaskPromptMarker, resolvedReferenceContextTitle, stripCodeQueueEnvironmentHint, userPromptForDisplay } from "./prompts";
import { sanitizeTaskOutputText } from "./output-redaction";
import { outputArchiveSignature, taskFullOutput } from "./task-output";
import { retryPrompt } from "./judge";
import { readOaTraceStepsForTask, type OaTraceStepSummary } from "./oa-events";
@@ -118,7 +119,7 @@ function prefixPreview(value: string, max = 900): string {
}
function linePreview(text: string, maxLines: number, maxChars: number): { text: string; omittedLines: number } {
const clean = text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd();
const clean = sanitizeTaskOutputText(text).replace(/\u001b\[[0-9;]*m/gu, "").trimEnd();
if (clean.length === 0) return { text: "", omittedLines: 0 };
const lines = clean.split(/\r?\n/u);
const kept: string[] = [];
@@ -132,7 +133,7 @@ function linePreview(text: string, maxLines: number, maxChars: number): { text:
}
function completeTraceText(text: string): { text: string; omittedLines: number } {
return { text: text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), omittedLines: 0 };
return { text: sanitizeTaskOutputText(text).replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), omittedLines: 0 };
}
function editedOutputPreview(text: string): { text: string; omittedLines: number } {
@@ -1324,8 +1325,11 @@ function cachedPreviewTranscript(task: QueueTask): TranscriptLine[] {
}
function outputForResponse(task: QueueTask, includeRaw: boolean): LiveOutput[] {
if (includeRaw) return taskFullOutput(task);
return task.output.slice(-80).map((item) => ({ ...item, text: safePreview(item.text, 4000) }));
const output = includeRaw ? taskFullOutput(task) : task.output.slice(-80);
return output.map((item) => ({
...item,
text: includeRaw ? sanitizeTaskOutputText(item.text) : safePreview(sanitizeTaskOutputText(item.text), 4000),
}));
}
function attemptForResponse(attempt: AttemptSummary, full = false): JsonValue {