fix: redact commander approval reasons
This commit is contained in:
@@ -44,6 +44,18 @@ function displayCommandName(parts: string[]): string {
|
|||||||
}
|
}
|
||||||
return shown.join(" ");
|
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(" ");
|
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));
|
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);
|
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");
|
const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
|
||||||
for (const snippet of [
|
for (const snippet of [
|
||||||
"不直接重启 Code Queue backend",
|
"不直接重启 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",
|
"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",
|
"non-dry-run plan is rejected",
|
||||||
"approval request is dry-run only and rejects unsupported high-risk actions",
|
"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",
|
"reference doc states backend restart, task interrupt/cancel, and token-output prohibitions",
|
||||||
],
|
],
|
||||||
}, null, 2)}\n`);
|
}, null, 2)}\n`);
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ function isHighRiskAction(value: string): value is HighRiskAction {
|
|||||||
return highRiskActions.some((action) => action === value);
|
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> {
|
function commanderHelp(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
command: "commander",
|
command: "commander",
|
||||||
@@ -270,7 +287,8 @@ function commanderApprovalRequest(args: string[]): Record<string, unknown> {
|
|||||||
highRiskActions,
|
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;
|
const taskId = optionValue(args, "--task-id") ?? null;
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -279,20 +297,21 @@ function commanderApprovalRequest(args: string[]): Record<string, unknown> {
|
|||||||
mutation: false,
|
mutation: false,
|
||||||
action,
|
action,
|
||||||
taskId,
|
taskId,
|
||||||
reason,
|
reason: reason.text,
|
||||||
|
redactionsApplied: reason.redactionsApplied,
|
||||||
requiresExplicitUserApproval: true,
|
requiresExplicitUserApproval: true,
|
||||||
claudeqq: {
|
claudeqq: {
|
||||||
mutation: false,
|
mutation: false,
|
||||||
endpointShape: "POST /api/microservices/claudeqq/proxy/api/push/text",
|
endpointShape: "POST /api/microservices/claudeqq/proxy/api/push/text",
|
||||||
target: "configured primary user private chat",
|
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,
|
sendImplemented: false,
|
||||||
},
|
},
|
||||||
approvalRecordShape: {
|
approvalRecordShape: {
|
||||||
id: "commander-approval-<stable-id>",
|
id: "commander-approval-<stable-id>",
|
||||||
action,
|
action,
|
||||||
taskId,
|
taskId,
|
||||||
reason,
|
reason: reason.text,
|
||||||
status: "draft",
|
status: "draft",
|
||||||
approvedBy: null,
|
approvedBy: null,
|
||||||
approvedAt: null,
|
approvedAt: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user