Skip to content

Commit 9c95df1

Browse files
committed
fix(tui): redact tool details before middle truncation
Apply redactToolDetail() to command and generic string text before middle truncation so credential-like suffixes are masked while full flag/key context is still available. Previously, truncation could remove the --flag prefix while preserving the raw secret at the tail, causing redaction patterns to miss the value. Add regression tests for sk- prefixed tokens in commands and ghp_ tokens in generic string details. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent fbb559f commit 9c95df1

3 files changed

Lines changed: 40 additions & 6 deletions

File tree

src/agents/tool-display-common.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
normalizeOptionalString,
44
} from "@openclaw/normalization-core/string-coerce";
55
import { parseStrictFiniteNumber } from "../infra/parse-finite-number.js";
6+
import { redactToolDetail } from "../logging/redact.js";
67
import { resolveExecDetail, type ToolDetailMode } from "./tool-display-exec.js";
78
import { asRecord } from "./tool-display-record.js";
89

@@ -116,10 +117,11 @@ function coerceDisplayValue(
116117
if (!trimmed) {
117118
return undefined;
118119
}
119-
const firstLine = normalizeOptionalString(trimmed.split(/\r?\n/)[0]) ?? "";
120-
if (!firstLine) {
120+
const rawLine = normalizeOptionalString(trimmed.split(/\r?\n/)[0]) ?? "";
121+
if (!rawLine) {
121122
return undefined;
122123
}
124+
const firstLine = redactToolDetail(rawLine);
123125
if (firstLine.length > maxStringChars) {
124126
const half = Math.floor((maxStringChars - 1) / 2);
125127
return `${firstLine.slice(0, half)}${firstLine.slice(-(maxStringChars - 1 - half))}`;

src/agents/tool-display-exec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { redactToolDetail } from "../logging/redact.js";
12
import {
23
binaryName,
34
firstPositional,
@@ -426,10 +427,12 @@ function isGenericSummary(summary: string): boolean {
426427
}
427428

428429
function compactRawCommand(raw: string, maxLength = 120): string {
429-
const oneLine = raw
430-
.replace(/\s*\n\s*/g, " ")
431-
.replace(/\s{2,}/g, " ")
432-
.trim();
430+
const oneLine = redactToolDetail(
431+
raw
432+
.replace(/\s*\n\s*/g, " ")
433+
.replace(/\s{2,}/g, " ")
434+
.trim(),
435+
);
433436
if (oneLine.length <= maxLength) {
434437
return oneLine;
435438
}

src/agents/tool-display.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,17 @@ describe("compactRawCommand middle truncation", () => {
521521
const result = resolveExecDetail({ command: "/opt/custom/bin/my-tool --version" });
522522
expect(result).toBe("/opt/custom/bin/my-tool --version");
523523
});
524+
525+
it("redacts credential-like tails before middle truncation", () => {
526+
// The --token flag and its value sit in the middle of a long command.
527+
// Without redaction-before-truncation, middle truncation could cut out
528+
// the --token flag context but preserve the raw secret at the tail.
529+
const longCommand =
530+
"/opt/custom/bin/deploy --region us-east-1 --token sk-proj-ABCDEFGHIJKLMNOP1234567890abcdefghij --output /data/results/deploy-output.json";
531+
const result = resolveExecDetail({ command: longCommand });
532+
// The sk- prefixed token must be redacted (masked) before truncation
533+
expect(result).not.toContain("ABCDEFGHIJKLMNOP1234567890abcdefghij");
534+
});
524535
});
525536

526537
describe("coerceDisplayValue middle truncation", () => {
@@ -553,4 +564,22 @@ describe("coerceDisplayValue middle truncation", () => {
553564
expect(detail).toBe("short-task-name");
554565
expect(detail).not.toContain("…");
555566
});
567+
568+
it("redacts credential-like values in long generic string details", () => {
569+
// A long string whose tail contains a GitHub PAT. Without
570+
// redaction-before-truncation, middle truncation could preserve
571+
// the raw token at the tail after its prefix context is cut.
572+
const longValue =
573+
"Deploying service to production cluster with auth ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop and " +
574+
"x".repeat(200) +
575+
" final-step";
576+
const detail = formatToolDetail(
577+
resolveToolDisplay({
578+
name: "sessions_spawn",
579+
args: { task: longValue },
580+
}),
581+
);
582+
// The ghp_ token must be redacted before truncation
583+
expect(detail).not.toContain("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop");
584+
});
556585
});

0 commit comments

Comments
 (0)