Files
openclaw/src/infra/exec-approval-command-display.test.ts
pashpashpash 6ce1058296 Wire diagnostics through the core chat command (#72936)
* feat: wire codex diagnostics feedback

* fix: harden codex diagnostics hints

* fix: neutralize codex diagnostics output

* fix: tighten codex diagnostics safeguards

* fix: bound codex diagnostics feedback output

* fix: tighten codex diagnostics throttling

* fix: confirm codex diagnostics uploads

* docs: clarify codex diagnostics add-on

* fix: route diagnostics through core command

* fix: tighten diagnostics authorization

* fix: pin diagnostics to bundled codex command

* fix: limit owner status in plugin commands

* fix: scope diagnostics confirmations

* fix: scope codex diagnostics cooldowns

* fix: harden codex diagnostics ownership scopes

* fix: harden diagnostics command trust and display

* fix: keep diagnostics command trust internal

* fix: clarify diagnostics exec boundary

* fix: consume codex diagnostics confirmations atomically

* test: include codex diagnostics binding metadata

* test: use string codex binding timestamps

* fix: keep reserved command trust host-only

* fix: harden diagnostics trust and resume hints

* wire diagnostics through exec approval

* fix: keep diagnostics tests aligned with bundled root trust

* fix telegram diagnostics owner auth

* route trajectory exports through exec approval

* fix trajectory exec command encoding

* fix telegram group owner auth

* fix export trajectory approval hardening

* fix pairing command owner bootstrap

* fix telegram owner exec approvals

* fix: make diagnostics approval flow pasteable

* fix: route native sensitive command followups

* fix: invoke diagnostics exports with current cli

* fix: refresh exec approval protocol models

* fix: list codex diagnostics from thread bindings

* fix: fold codex diagnostics into exec approval

* fix: preserve diagnostics approval line breaks

* docs: clarify diagnostics codex workflow
2026-04-29 07:40:37 +09:00

243 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
resolveExecApprovalCommandDisplay,
sanitizeExecApprovalDisplayText,
sanitizeExecApprovalWarningText,
} from "./exec-approval-command-display.js";
describe("sanitizeExecApprovalDisplayText", () => {
it.each([
["echo hi\u200Bthere", "echo hi\\u{200B}there"],
["date\u3164\uFFA0\u115F\u1160가", "date\\u{3164}\\u{FFA0}\\u{115F}\\u{1160}가"],
["echo safe\n\rcurl https://example.test", "echo safe\\u{A}\\u{D}curl https://example.test"],
["echo ok\u2028curl https://example.test", "echo ok\\u{2028}curl https://example.test"],
["echo ok\u2029curl https://example.test", "echo ok\\u{2029}curl https://example.test"],
])("sanitizes exec approval display text for %j", (input, expected) => {
expect(sanitizeExecApprovalDisplayText(input)).toBe(expected);
});
it("redacts bearer tokens embedded in commands", () => {
const cmd =
'curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.sig" https://api.example.com';
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.sig");
expect(result).toContain("curl");
expect(result).toContain("https://api.example.com");
});
it("redacts API keys in environment variable assignments", () => {
const cmd = 'API_SECRET="sk-abc123456789012345678" python script.py';
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("sk-abc123456789012345678");
expect(result).toContain("python script.py");
});
it("redacts GitHub personal access tokens", () => {
const cmd = "git clone https://ghp_1234567890abcdefghij1234567890abcdef@github.com/user/repo";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("ghp_1234567890abcdefghij1234567890abcdef");
expect(result).toContain("git clone");
});
it("masks the full token when a zero-width character is spliced into the middle", () => {
const cmd = "echo sk-abc123\u200B456789012345678 remainder";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("sk-abc123");
expect(result).not.toContain("456789012345678");
expect(result).toContain("echo ");
expect(result).toContain("remainder");
});
it("masks the full token when NBSP (Zs) is spliced into the middle", () => {
const cmd = "echo sk-abc123\u00A0456789012345678 remainder";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("sk-abc123");
expect(result).not.toContain("456789012345678");
expect(result).toContain("echo ");
expect(result).toContain("remainder");
});
it("masks the full token when narrow no-break space is spliced into the middle", () => {
const cmd = "echo sk-abc123\u202F456789012345678 remainder";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("sk-abc123");
expect(result).not.toContain("456789012345678");
expect(result).toContain("remainder");
});
it("keeps newline boundaries visible as escape markers even when bypass is detected", () => {
// Stripping invisibles lets the stripped-view greedy-match across the original newline
// boundaries, so the trailing `line3` gets absorbed into the union mask alongside the
// secret. The important guarantees are: (1) the secret is not visible, and (2) the
// newlines that existed in the original are still visible as `\u{A}` escapes so the
// operator is not misled about multi-line structure.
const cmd = "line1\necho sk-abc123\u00A0456789012345678\nline3";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("sk-abc123");
expect(result).not.toContain("456789012345678");
expect(result).toContain("line1");
expect(result).toContain("\\u{A}");
});
it("detects bypass even when raw and stripped redactions happen to produce the same normalized length", () => {
// Raw masks the 16-char prefix `sk-abc1234567890` as the fixed literal `***` while the
// trailing 8 chars past the zero-width stay visible. The stripped view masks the full
// 24-char token as `sk-abc…5678`. Both normalized outputs are the same length (11 chars),
// so a length-based bypass check would falsely return the raw view and leak the tail.
const cmd = "sk-abc1234567890\u200B12345678";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("12345678");
expect(result).not.toContain("1234567890");
});
it("does not leak bearer tokens when bypass is triggered by a separate spliced secret", () => {
// Bearer+NBSP is caught by the raw view (NBSP matches \s in non-u JS regex) but stripping
// removes NBSP, turning `Bearer<jwt>` into a pattern the bearer regex no longer matches.
// A separate spliced-invisible token triggers bypass detection, and the union-mask output
// must cover both the bearer span (from raw) and the spliced sk- span (from stripped).
const cmd =
'curl -H "Authorization: Bearer\u00A0eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.sig" https://api.example.com; echo sk-abc123\u200B456789012345678';
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.sig");
expect(result).not.toContain("456789012345678");
expect(result).toContain("https://api.example.com");
});
it("keeps PEM private-key context visible when raw redaction already covers the key (not a bypass)", () => {
const cmd =
"echo -----BEGIN RSA PRIVATE KEY-----\nABCDEF0123456789abcdef\n-----END RSA PRIVATE KEY----- > key.pem";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("ABCDEF0123456789abcdef");
expect(result).toContain("BEGIN RSA PRIVATE KEY");
expect(result).toContain("END RSA PRIVATE KEY");
expect(result).toContain("> key.pem");
});
it("truncates the redacted output (not the raw input) so large commands are bounded", () => {
const padding = "x".repeat(20 * 1024);
const result = sanitizeExecApprovalDisplayText(padding);
expect(result.length).toBeLessThan(padding.length);
expect(result).toContain("[truncated]");
});
it("refuses to display commands above the hard input cap", () => {
const huge = "x".repeat(300 * 1024);
const result = sanitizeExecApprovalDisplayText(huge);
expect(result).toContain("exceeds display size limit");
expect(result.length).toBeLessThan(1024);
});
it("redacts tokens at the tail of long inputs instead of truncating them below pattern length", () => {
// Pad with non-token content, then append a secret at the end. Truncating BEFORE redaction
// would split the token below the pattern's minimum length and leak the prefix. With
// redaction first, the full token is masked before any size-based truncation runs.
const padding = "a ".repeat(10 * 1024);
const cmd = padding + "ghp_1234567890abcdefghij1234567890abcdef";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("ghp_1234567890abcdefghij1234567890abcdef");
expect(result).not.toContain("ghp_1234567890");
});
it("escapes astral-plane invisible characters (e.g. U+E0061 tag characters)", () => {
const cmd = "echo hi\u{E0061}there";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).toContain("\\u{E0061}");
expect(result).not.toMatch(/hi[\uDB40\uDC61]there/u);
});
it("masks a secret spliced with an astral-plane invisible character", () => {
// U+E0061 is a Cf (format) code point in the supplementary plane. Iterating the input by
// UTF-16 code unit would see two surrogate halves, neither of which matches \p{Cf}, so
// the splice would survive stripping and the stripped-view redaction would miss the
// full token. Code-point iteration strips it correctly and bypass detection fires.
const cmd = "echo sk-abc123\u{E0061}456789012345678 remainder";
const result = sanitizeExecApprovalDisplayText(cmd);
expect(result).not.toContain("sk-abc123");
expect(result).not.toContain("456789012345678");
expect(result).toContain("remainder");
});
});
describe("sanitizeExecApprovalWarningText", () => {
it("keeps approval warning prose line breaks readable", () => {
const warning =
"Diagnostics can include sensitive local logs.\n\nOpenAI Codex harness:\nApproving diagnostics will also send Codex feedback.";
expect(sanitizeExecApprovalWarningText(warning)).toBe(warning);
});
it("normalizes escaped line separators while still escaping hidden spoofing characters", () => {
const warning = "Line one\r\nLine two\u2028Line three\u200B";
expect(sanitizeExecApprovalWarningText(warning)).toBe(
"Line one\nLine two\nLine three\\u{200B}",
);
});
it("redacts secrets in warning prose without escaping newlines", () => {
const warning = "Token:\nsk-abc123456789012345678";
const result = sanitizeExecApprovalWarningText(warning);
expect(result).toContain("Token:\n");
expect(result).not.toContain("sk-abc123456789012345678");
expect(result).not.toContain("\\u{A}");
});
});
describe("resolveExecApprovalCommandDisplay", () => {
it.each([
{
name: "prefers explicit command fields and drops identical previews after trimming",
input: {
command: "echo hi",
commandPreview: " echo hi ",
host: "gateway" as const,
},
expected: {
commandText: "echo hi",
commandPreview: null,
},
},
{
name: "falls back to node systemRunPlan values and sanitizes preview text",
input: {
command: "",
host: "node" as const,
systemRunPlan: {
argv: ["python3", "-c", "print(1)"],
cwd: null,
commandText: 'python3 -c "print(1)"',
commandPreview: "print\u200B(1)",
agentId: null,
sessionKey: null,
},
},
expected: {
commandText: 'python3 -c "print(1)"',
commandPreview: "print\\u{200B}(1)",
},
},
{
name: "ignores systemRunPlan fallback for non-node hosts",
input: {
command: "",
host: "sandbox" as const,
systemRunPlan: {
argv: ["echo", "hi"],
cwd: null,
commandText: "echo hi",
commandPreview: "echo hi",
agentId: null,
sessionKey: null,
},
},
expected: {
commandText: "",
commandPreview: null,
},
},
])("$name", ({ input, expected }) => {
expect(resolveExecApprovalCommandDisplay(input)).toEqual(expected);
});
});