From cf40cd5f6cabff1b741fdd325a5be62fa1e45186 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 21 May 2026 08:53:14 +0000 Subject: [PATCH] fix: redact commander approval reasons --- scripts/cli.ts | 12 +++++++++ scripts/host-codex-commander-contract-test.ts | 27 +++++++++++++++++++ scripts/src/commander.ts | 27 ++++++++++++++++--- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/scripts/cli.ts b/scripts/cli.ts index 8b94b0c7..8ccdd5af 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -44,6 +44,18 @@ function displayCommandName(parts: string[]): string { } return shown.join(" "); } + if (parts[0] === "commander" && parts[1] === "approval" && parts[2] === "request") { + const shown: string[] = []; + for (let index = 0; index < parts.length; index += 1) { + const part = parts[index] ?? ""; + shown.push(part); + if (part === "--reason") { + shown.push(""); + index += 1; + } + } + return shown.join(" "); + } return parts.join(" "); } diff --git a/scripts/host-codex-commander-contract-test.ts b/scripts/host-codex-commander-contract-test.ts index fc00c789..1d7760a9 100644 --- a/scripts/host-codex-commander-contract-test.ts +++ b/scripts/host-codex-commander-contract-test.ts @@ -96,6 +96,32 @@ assertCondition(claudeqq.sendImplemented === false, "ClaudeQQ send must not be i const invalidApproval = dataOf(runCli(["commander", "approval", "request", "--action", "read-token-file", "--dry-run"], 1)); assertCondition(invalidApproval.error === "validation-failed", "unsupported approval action must fail validation", invalidApproval); +const secretReasonResult = spawnSync("bun", [ + "scripts/cli.ts", + "commander", + "approval", + "request", + "--action", + "code-queue-backend-restart", + "--reason", + "token=ghp_1234567890abcdef", + "--dry-run", +], { + cwd: process.cwd(), + encoding: "utf8", + maxBuffer: 4 * 1024 * 1024, +}); +assertCondition(secretReasonResult.status === 0, "secret-like approval reason command should still return successfully", { + stdout: secretReasonResult.stdout, + stderr: secretReasonResult.stderr, +}); +assertCondition(!secretReasonResult.stdout.includes("ghp_1234567890abcdef"), "secret-like approval reason must be redacted from stdout", { + stdout: secretReasonResult.stdout, +}); +assertCondition(secretReasonResult.stdout.includes(""), "redacted approval reason should disclose redaction marker", { + stdout: secretReasonResult.stdout, +}); + const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8"); for (const snippet of [ "不直接重启 Code Queue backend", @@ -116,6 +142,7 @@ process.stdout.write(`${JSON.stringify({ "dry-run plan covers process discovery, SSH/PTY/stdio bridge, prompt guidance, trace summary, #20/#46 and ClaudeQQ approval", "non-dry-run plan is rejected", "approval request is dry-run only and rejects unsupported high-risk actions", + "secret-like approval reasons are redacted from stdout", "reference doc states backend restart, task interrupt/cancel, and token-output prohibitions", ], }, null, 2)}\n`); diff --git a/scripts/src/commander.ts b/scripts/src/commander.ts index be5cd535..13672a75 100644 --- a/scripts/src/commander.ts +++ b/scripts/src/commander.ts @@ -31,6 +31,23 @@ function isHighRiskAction(value: string): value is HighRiskAction { return highRiskActions.some((action) => action === value); } +function redactText(value: string): { text: string; redactionsApplied: number } { + let redactionsApplied = 0; + const patterns = [ + /\b(?:sk|ghp|github_pat|xoxb|xoxp|AKIA)[A-Za-z0-9_=-]{8,}\b/g, + /\b(?:token|secret|password|passwd|authorization|cookie|api[_-]?key)\s*[:=]\s*[^,\s]+/gi, + /\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/gi, + ]; + let text = value; + for (const pattern of patterns) { + text = text.replace(pattern, () => { + redactionsApplied += 1; + return ""; + }); + } + return { text, redactionsApplied }; +} + function commanderHelp(): Record { return { command: "commander", @@ -270,7 +287,8 @@ function commanderApprovalRequest(args: string[]): Record { highRiskActions, }; } - const reason = optionValue(args, "--reason") ?? "operator-supplied reason required before live execution"; + const rawReason = optionValue(args, "--reason") ?? "operator-supplied reason required before live execution"; + const reason = redactText(rawReason); const taskId = optionValue(args, "--task-id") ?? null; return { ok: true, @@ -279,20 +297,21 @@ function commanderApprovalRequest(args: string[]): Record { mutation: false, action, taskId, - reason, + reason: reason.text, + redactionsApplied: reason.redactionsApplied, requiresExplicitUserApproval: true, claudeqq: { mutation: false, endpointShape: "POST /api/microservices/claudeqq/proxy/api/push/text", target: "configured primary user private chat", - messageTemplate: `Approval required for ${action}. Reason: ${reason}. Reply with explicit approval id before execution.`, + messageTemplate: `Approval required for ${action}. Reason: ${reason.text}. Reply with explicit approval id before execution.`, sendImplemented: false, }, approvalRecordShape: { id: "commander-approval-", action, taskId, - reason, + reason: reason.text, status: "draft", approvedBy: null, approvedAt: null,