forked from EduCraft/curriculum-project-hub
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
const MAX_LOGGED_CHARS = 8_000;
|
|
const MAX_LOGGED_LINES = 20;
|
|
const MAX_LINE_CHARS = 2_000;
|
|
const MAX_PENDING_LINE_CHARS = 16_000;
|
|
|
|
interface DiagnosticLogger {
|
|
warn(bindings: Readonly<Record<string, unknown>>, message: string): void;
|
|
}
|
|
|
|
export interface AgentSdkStderrSink {
|
|
readonly write: (data: string) => void;
|
|
readonly flush: () => void;
|
|
}
|
|
|
|
/**
|
|
* Turn the SDK subprocess stderr stream into bounded, run-correlated logs.
|
|
* Complete lines are buffered so a credential split across chunks is still
|
|
* removed before anything reaches the logger.
|
|
*/
|
|
export function createAgentSdkStderrSink(input: {
|
|
readonly logger: DiagnosticLogger;
|
|
readonly runId: string;
|
|
readonly projectId: string;
|
|
readonly sensitiveValues: readonly (string | undefined)[];
|
|
}): AgentSdkStderrSink {
|
|
const secrets = [...new Set(input.sensitiveValues.filter((value): value is string => value !== undefined && value !== ""))]
|
|
.sort((left, right) => right.length - left.length);
|
|
let pending = "";
|
|
let droppingOversizedLine = false;
|
|
let remainingChars = MAX_LOGGED_CHARS;
|
|
let remainingLines = MAX_LOGGED_LINES;
|
|
let limitReported = false;
|
|
|
|
const reportLimit = (): void => {
|
|
if (limitReported) return;
|
|
limitReported = true;
|
|
input.logger.warn(
|
|
{ runId: input.runId, projectId: input.projectId },
|
|
"agent SDK stderr log limit reached; further diagnostics omitted",
|
|
);
|
|
};
|
|
|
|
const emit = (line: string): void => {
|
|
if (line.trim() === "") return;
|
|
if (remainingChars <= 0 || remainingLines <= 0) {
|
|
reportLimit();
|
|
return;
|
|
}
|
|
const redacted = redactDiagnostic(line, secrets);
|
|
const allowed = Math.min(MAX_LINE_CHARS, remainingChars);
|
|
const diagnostic = boundedDiagnostic(redacted, allowed);
|
|
remainingChars -= diagnostic.length;
|
|
remainingLines--;
|
|
input.logger.warn(
|
|
{ runId: input.runId, projectId: input.projectId, sdkStderr: diagnostic },
|
|
"agent SDK stderr",
|
|
);
|
|
if (redacted.length > allowed || remainingChars <= 0 || remainingLines <= 0) reportLimit();
|
|
};
|
|
|
|
const append = (fragment: string): void => {
|
|
if (fragment === "" || droppingOversizedLine) return;
|
|
if (pending.length + fragment.length > MAX_PENDING_LINE_CHARS) {
|
|
pending = "";
|
|
droppingOversizedLine = true;
|
|
emit("[oversized stderr line omitted]");
|
|
return;
|
|
}
|
|
pending += fragment;
|
|
};
|
|
|
|
const write = (data: string): void => {
|
|
let lineStart = 0;
|
|
for (let index = 0; index < data.length; index++) {
|
|
const character = data[index];
|
|
if (character !== "\n" && character !== "\r") continue;
|
|
append(data.slice(lineStart, index));
|
|
if (!droppingOversizedLine) emit(pending);
|
|
pending = "";
|
|
droppingOversizedLine = false;
|
|
if (character === "\r" && data[index + 1] === "\n") index++;
|
|
lineStart = index + 1;
|
|
}
|
|
append(data.slice(lineStart));
|
|
};
|
|
|
|
const flush = (): void => {
|
|
if (droppingOversizedLine) {
|
|
droppingOversizedLine = false;
|
|
} else {
|
|
emit(pending);
|
|
}
|
|
pending = "";
|
|
};
|
|
|
|
return { write, flush };
|
|
}
|
|
|
|
function boundedDiagnostic(value: string, limit: number): string {
|
|
if (value.length <= limit) return value;
|
|
const marker = "…[truncated]";
|
|
if (limit <= marker.length) return marker.slice(0, limit);
|
|
return `${value.slice(0, limit - marker.length)}${marker}`;
|
|
}
|
|
|
|
function redactDiagnostic(value: string, secrets: readonly string[]): string {
|
|
let redacted = value;
|
|
for (const secret of secrets) redacted = redacted.replaceAll(secret, "[REDACTED]");
|
|
return redacted
|
|
.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "")
|
|
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ")
|
|
.replace(/\b(postgres(?:ql)?):\/\/([^:\s/@]+):([^@\s]+)@/gi, "$1://$2:[REDACTED]@")
|
|
.replace(/\b((?:api[_-]?key|token|secret|password)\s*[:=]\s*)\S+/gi, "$1[REDACTED]")
|
|
.trim();
|
|
}
|