fix: redact commander approval reasons
This commit is contained in:
@@ -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("<reason:redacted>");
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return shown.join(" ");
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
|
||||
@@ -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>"), "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`);
|
||||
|
||||
@@ -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 "<redacted>";
|
||||
});
|
||||
}
|
||||
return { text, redactionsApplied };
|
||||
}
|
||||
|
||||
function commanderHelp(): Record<string, unknown> {
|
||||
return {
|
||||
command: "commander",
|
||||
@@ -270,7 +287,8 @@ function commanderApprovalRequest(args: string[]): Record<string, unknown> {
|
||||
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<string, unknown> {
|
||||
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-<stable-id>",
|
||||
action,
|
||||
taskId,
|
||||
reason,
|
||||
reason: reason.text,
|
||||
status: "draft",
|
||||
approvedBy: null,
|
||||
approvedAt: null,
|
||||
|
||||
Reference in New Issue
Block a user