fix: redact gh auth output in code queue
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user