From 1ce91f36298a0a67af0c4e50c450bc6398f351d9 Mon Sep 17 00:00:00 2001 From: Christian Oliveira Date: Wed, 10 Jun 2026 21:21:52 -0300 Subject: [PATCH 1/2] fix(core): never compact read-only config-inspection command output Compacting the output of read-only configuration-inspection commands can silently drop config keys, so agents act on truncated data without knowing anything was removed - strictly worse than spending the tokens. This extends the file-content inspection bypass so these outputs pass through verbatim: - plutil plist dumps (plutil -p, plutil -convert ... -o -) - read-only config CLIs (openclaw config get) - ssh-wrapped inspection commands (ssh host 'cat file'), including ssh option/value parsing to locate the remote command The verbatim bypass in reduceExecution now applies to every detected inspection command instead of only those that classified to generic/fallback, and plutil dumps are exempted from the large-document summary. Detection is fail-open: anything not positively identified as a read-only inspection command keeps today's compaction behavior. Co-Authored-By: Claude Fable 5 --- src/core/command-identity.ts | 94 ++++++++++++++++++++++++++- src/core/reduce-inspection-summary.ts | 6 +- src/core/reduce.ts | 2 +- test/core/command.test.ts | 22 +++++++ test/core/reduce.test.ts | 47 ++++++++++++++ 5 files changed, 167 insertions(+), 4 deletions(-) diff --git a/src/core/command-identity.ts b/src/core/command-identity.ts index a329d65b..c6e93974 100644 --- a/src/core/command-identity.ts +++ b/src/core/command-identity.ts @@ -8,6 +8,37 @@ import { stripLeadingCdPrefix, tokenizeCommand } from "./command-shell.js"; const FILE_CONTENT_INSPECTION_COMMANDS = new Set(["cat", "sed", "head", "tail", "nl", "bat", "batcat", "jq", "yq"]); const REPO_INVENTORY_COMMANDS = new Set(["find", "fd", "fdfind", "ls", "tree"]); +// Read-only configuration-inspection CLIs whose output agents rely on verbatim. +// Compacting these can silently drop config keys and make agents act on wrong data. +const READ_ONLY_CONFIG_INSPECTION_COMMAND_PATTERNS = [ + /(?:^|[;&|]\s*|\s)openclaw\s+config\s+get(?:\s|$)/u, +]; + +// ssh options that consume a separate value argument (per ssh(1)); needed to +// find where the destination ends and the remote command begins. +const SSH_OPTIONS_WITH_VALUES = new Set([ + "-b", + "-c", + "-D", + "-E", + "-e", + "-F", + "-I", + "-i", + "-J", + "-L", + "-l", + "-m", + "-O", + "-o", + "-p", + "-Q", + "-R", + "-S", + "-W", + "-w", +]); + function getNormalizedArgv(input: Pick): string[] { if (input.argv?.length) { return input.argv; @@ -110,12 +141,58 @@ function isGitShowFileContentArgv(argv: string[]): boolean { return false; } +function isPlutilFileContentArgv(argv: string[]): boolean { + if (getCommandName(argv) !== "plutil") { + return false; + } + if (argv.includes("-p")) { + return true; + } + const outputIndex = argv.indexOf("-o"); + return outputIndex !== -1 && argv[outputIndex + 1] === "-"; +} + +function isReadOnlyConfigInspectionCommand(command: string | undefined): boolean { + return typeof command === "string" + && READ_ONLY_CONFIG_INSPECTION_COMMAND_PATTERNS.some((pattern) => pattern.test(command)); +} + +function getSshRemoteCommand(argv: string[]): string | null { + if (getCommandName(argv) !== "ssh") { + return null; + } + + for (let index = 1; index < argv.length; index += 1) { + const arg = argv[index]; + if (!arg) { + continue; + } + if (arg === "--") { + continue; + } + if (SSH_OPTIONS_WITH_VALUES.has(arg)) { + index += 1; + continue; + } + if (arg.startsWith("-")) { + continue; + } + + const remoteCommand = argv.slice(index + 1).join(" ").trim(); + return remoteCommand || null; + } + + return null; +} + export function isFileContentInspectionArgv(argv: string[]): boolean { const argv0 = getCommandName(argv); if (!argv0) { return false; } - return FILE_CONTENT_INSPECTION_COMMANDS.has(argv0) || isGitShowFileContentArgv(argv); + return FILE_CONTENT_INSPECTION_COMMANDS.has(argv0) + || isGitShowFileContentArgv(argv) + || isPlutilFileContentArgv(argv); } function isGhApiContentsDecodeCommand(command: string | undefined): boolean { @@ -211,9 +288,22 @@ function getInspectionArgv(input: Pick): return sourceCommand ? tokenizeCommand(sourceCommand) : []; } +export function isPlutilFileContentInspectionCommand(input: Pick): boolean { + return isPlutilFileContentArgv(getInspectionArgv(input)); +} + export function isFileContentInspectionCommand(input: Pick): boolean { + const candidates = deriveCommandMatchCandidates(input); return isFileContentInspectionArgv(getInspectionArgv(input)) - || deriveCommandMatchCandidates(input).some((candidate) => isGhApiContentsDecodeCommand(candidate.command)); + || candidates.some((candidate) => isGhApiContentsDecodeCommand(candidate.command)) + || candidates.some((candidate) => isReadOnlyConfigInspectionCommand(candidate.command)) + || candidates.some((candidate) => { + const remoteCommand = getSshRemoteCommand(candidate.argv); + return remoteCommand !== null + && (isReadOnlyConfigInspectionCommand(remoteCommand) + || isFileContentInspectionArgv(getInspectionArgv({ command: remoteCommand })) + || isGhApiContentsDecodeCommand(remoteCommand)); + }); } export function isRepositoryInspectionCommand(input: Pick): boolean { diff --git a/src/core/reduce-inspection-summary.ts b/src/core/reduce-inspection-summary.ts index 51a1a7d2..998f8546 100644 --- a/src/core/reduce-inspection-summary.ts +++ b/src/core/reduce-inspection-summary.ts @@ -1,4 +1,4 @@ -import { isFileContentInspectionCommand } from "./command-identity.js"; +import { isFileContentInspectionCommand, isPlutilFileContentInspectionCommand } from "./command-identity.js"; import { createCompactionMetadata, mergeCompactionMetadata, type CompactionMetadata } from "./compaction-metadata.js"; import { clipMiddleWithHash, parseJsonValue } from "./reduce-utils.js"; import { countTextChars, headTail, normalizeLines, stripAnsi, trimEmptyEdges } from "./text.js"; @@ -140,6 +140,10 @@ export function buildInspectionSummary(input: ToolExecutionInput, rawText: strin : null; } + if (isPlutilFileContentInspectionCommand(input)) { + return null; + } + const rawChars = countTextChars(rawText); const lines = trimEmptyEdges(normalizeLines(stripAnsi(rawText))); if (!isFileContentInspectionCommand(input) || !isLargeDocumentOutput(lines, rawChars)) { diff --git a/src/core/reduce.ts b/src/core/reduce.ts index 4adbb7a3..0ac723b0 100644 --- a/src/core/reduce.ts +++ b/src/core/reduce.ts @@ -452,7 +452,7 @@ export async function reduceExecutionWithRules( }; } - if (classification.matchedReducer === "generic/fallback" && isFileContentInspectionCommand(normalizedInput)) { + if (isFileContentInspectionCommand(normalizedInput)) { if (!opts.store && opts.recordStats) { await storeArtifactMetadata( { diff --git a/test/core/command.test.ts b/test/core/command.test.ts index cdeef352..bad83a4d 100644 --- a/test/core/command.test.ts +++ b/test/core/command.test.ts @@ -420,6 +420,12 @@ describe("isFileContentInspectionCommand", () => { { label: "clustered shell wrapper", command: "bash -ec 'cat README.md'" }, { label: "git show blob", command: "git show HEAD:src/core/reduce.ts" }, { label: "gh contents decode", command: "gh api repos/gumadeiras/tokenjuice/contents/src/core/reduce.ts --jq .content | base64 -d" }, + { label: "plutil print", command: "plutil -p /Library/LaunchDaemons/com.example.daemon.plist" }, + { label: "plutil convert to stdout", command: "plutil -convert json -o - settings.plist" }, + { label: "read-only config get", command: "openclaw config get agents.defaults" }, + { label: "ssh-wrapped cat", command: "ssh build-host 'cat /etc/hosts'" }, + { label: "ssh-wrapped plutil with ssh options", command: "ssh -p 2222 -i ~/.ssh/id_ed25519 build-host 'plutil -p /Library/LaunchDaemons/com.example.daemon.plist'" }, + { label: "ssh-wrapped read-only config get", command: "ssh build-host 'openclaw config get gateway'" }, ])("detects $label as file inspection from command text", ({ command }) => { expect(isFileContentInspectionCommand({ command })).toBe(true); }); @@ -443,6 +449,22 @@ describe("isFileContentInspectionCommand", () => { it("does not treat git show commit summaries as file inspection", () => { expect(isFileContentInspectionCommand({ command: "git show HEAD --stat" })).toBe(false); }); + + it("does not treat plutil in-place conversions as file inspection", () => { + expect(isFileContentInspectionCommand({ command: "plutil -convert binary1 settings.plist" })).toBe(false); + }); + + it("does not treat config writes as file inspection", () => { + expect(isFileContentInspectionCommand({ command: "openclaw config set agents.defaults.model test" })).toBe(false); + }); + + it("does not treat ssh remote mutations as file inspection", () => { + expect(isFileContentInspectionCommand({ command: "ssh build-host 'rm -rf /tmp/scratch'" })).toBe(false); + }); + + it("does not treat ssh without a remote command as file inspection", () => { + expect(isFileContentInspectionCommand({ command: "ssh build-host" })).toBe(false); + }); }); describe("isRepositoryInspectionCommand", () => { diff --git a/test/core/reduce.test.ts b/test/core/reduce.test.ts index c38d2fef..bb508abb 100644 --- a/test/core/reduce.test.ts +++ b/test/core/reduce.test.ts @@ -416,6 +416,53 @@ describe("reduceExecution", () => { expect(result.stats.ratio).toBe(1); }); + it("keeps plutil plist dumps verbatim", async () => { + const rawText = [ + "{", + ...Array.from({ length: 40 }, (_, index) => ` "Key${index + 1}" => "value ${index + 1}"`), + "}", + ].join("\n"); + + const result = await reduceExecution({ + toolName: "exec", + command: "plutil -p /Library/LaunchDaemons/com.example.daemon.plist", + argv: ["plutil", "-p", "/Library/LaunchDaemons/com.example.daemon.plist"], + stdout: rawText, + exitCode: 0, + }); + + expect(result.inlineText).toBe(rawText); + expect(result.stats.ratio).toBe(1); + }); + + it("keeps read-only config inspection output verbatim", async () => { + const rawText = Array.from({ length: 30 }, (_, index) => `setting-${index + 1}: value-${index + 1}`).join("\n"); + + const result = await reduceExecution({ + toolName: "exec", + command: "openclaw config get agents.defaults", + stdout: rawText, + exitCode: 0, + }); + + expect(result.inlineText).toBe(rawText); + expect(result.stats.ratio).toBe(1); + }); + + it("keeps ssh-wrapped file inspection output verbatim", async () => { + const rawText = Array.from({ length: 30 }, (_, index) => `host-line ${index + 1}`).join("\n"); + + const result = await reduceExecution({ + toolName: "exec", + command: "ssh build-host 'cat /var/log/app.log'", + stdout: rawText, + exitCode: 0, + }); + + expect(result.inlineText).toBe(rawText); + expect(result.stats.ratio).toBe(1); + }); + it("still compacts filesystem inventory commands through their dedicated reducers", async () => { const result = await reduceExecution({ toolName: "exec", From 786419dc66e51a4f90d6bcfffd6daad1e474309a Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:30:46 +0800 Subject: [PATCH 2/2] fix(core): bypass summaries for exact config reads --- src/core/command-identity.ts | 66 +++++++++++++++++---------- src/core/command.ts | 1 + src/core/reduce-inspection-summary.ts | 6 +-- src/core/reduce.ts | 8 ++-- test/core/command.test.ts | 47 ++++++++++++------- test/core/reduce.test.ts | 20 ++++++-- 6 files changed, 97 insertions(+), 51 deletions(-) diff --git a/src/core/command-identity.ts b/src/core/command-identity.ts index c6e93974..808c59a9 100644 --- a/src/core/command-identity.ts +++ b/src/core/command-identity.ts @@ -2,21 +2,16 @@ import { basename } from "node:path"; import type { ToolExecutionInput } from "../types.js"; -import { deriveCommandMatchCandidates, getSourcePriority, type CommandMatchCandidate } from "./command-match.js"; -import { stripLeadingCdPrefix, tokenizeCommand } from "./command-shell.js"; +import { deriveCommandMatchCandidates, getSourcePriority, type CommandMatchCandidate, unwrapShellRunner } from "./command-match.js"; +import { hasSequentialShellCommands, isCompoundShellCommand, stripLeadingCdPrefix, tokenizeCommand } from "./command-shell.js"; const FILE_CONTENT_INSPECTION_COMMANDS = new Set(["cat", "sed", "head", "tail", "nl", "bat", "batcat", "jq", "yq"]); const REPO_INVENTORY_COMMANDS = new Set(["find", "fd", "fdfind", "ls", "tree"]); -// Read-only configuration-inspection CLIs whose output agents rely on verbatim. -// Compacting these can silently drop config keys and make agents act on wrong data. -const READ_ONLY_CONFIG_INSPECTION_COMMAND_PATTERNS = [ - /(?:^|[;&|]\s*|\s)openclaw\s+config\s+get(?:\s|$)/u, -]; - // ssh options that consume a separate value argument (per ssh(1)); needed to // find where the destination ends and the remote command begins. const SSH_OPTIONS_WITH_VALUES = new Set([ + "-B", "-b", "-c", "-D", @@ -31,6 +26,7 @@ const SSH_OPTIONS_WITH_VALUES = new Set([ "-m", "-O", "-o", + "-P", "-p", "-Q", "-R", @@ -152,9 +148,10 @@ function isPlutilFileContentArgv(argv: string[]): boolean { return outputIndex !== -1 && argv[outputIndex + 1] === "-"; } -function isReadOnlyConfigInspectionCommand(command: string | undefined): boolean { - return typeof command === "string" - && READ_ONLY_CONFIG_INSPECTION_COMMAND_PATTERNS.some((pattern) => pattern.test(command)); +function isReadOnlyConfigInspectionArgv(argv: string[]): boolean { + return getCommandName(argv) === "openclaw" + && argv[1] === "config" + && argv[2] === "get"; } function getSshRemoteCommand(argv: string[]): string | null { @@ -190,9 +187,7 @@ export function isFileContentInspectionArgv(argv: string[]): boolean { if (!argv0) { return false; } - return FILE_CONTENT_INSPECTION_COMMANDS.has(argv0) - || isGitShowFileContentArgv(argv) - || isPlutilFileContentArgv(argv); + return FILE_CONTENT_INSPECTION_COMMANDS.has(argv0) || isGitShowFileContentArgv(argv); } function isGhApiContentsDecodeCommand(command: string | undefined): boolean { @@ -288,21 +283,46 @@ function getInspectionArgv(input: Pick): return sourceCommand ? tokenizeCommand(sourceCommand) : []; } -export function isPlutilFileContentInspectionCommand(input: Pick): boolean { - return isPlutilFileContentArgv(getInspectionArgv(input)); +export function isFileContentInspectionCommand(input: Pick): boolean { + return isFileContentInspectionArgv(getInspectionArgv(input)) + || deriveCommandMatchCandidates(input).some((candidate) => isGhApiContentsDecodeCommand(candidate.command)); } -export function isFileContentInspectionCommand(input: Pick): boolean { +function isVerbatimRemoteInspectionCommand(command: string): boolean { + const effectiveCommand = unwrapShellRunner({ command }) ?? command; + const isSingleGhContentsDecode = isGhApiContentsDecodeCommand(effectiveCommand) + && !hasSequentialShellCommands(effectiveCommand) + && /^[^|]+\|\s*base64\s+(?:-[dD]\b|--decode\b)\s*$/u.test(effectiveCommand.trim()); + if (isSingleGhContentsDecode) { + return true; + } + if ( + isCompoundShellCommand(stripLeadingCdPrefix(command)) + || isCompoundShellCommand(effectiveCommand) + ) { + return false; + } + + const argv = getInspectionArgv({ command: effectiveCommand }); + return isPlutilFileContentArgv(argv) + || isReadOnlyConfigInspectionArgv(argv) + || isFileContentInspectionArgv(argv); +} + +export function isVerbatimConfigInspectionCommand(input: Pick): boolean { + if (input.command && isCompoundShellCommand(stripLeadingCdPrefix(input.command))) { + return false; + } + const candidates = deriveCommandMatchCandidates(input); - return isFileContentInspectionArgv(getInspectionArgv(input)) - || candidates.some((candidate) => isGhApiContentsDecodeCommand(candidate.command)) - || candidates.some((candidate) => isReadOnlyConfigInspectionCommand(candidate.command)) + return candidates.some((candidate) => ( + isPlutilFileContentArgv(candidate.argv) + || isReadOnlyConfigInspectionArgv(candidate.argv) + )) || candidates.some((candidate) => { const remoteCommand = getSshRemoteCommand(candidate.argv); return remoteCommand !== null - && (isReadOnlyConfigInspectionCommand(remoteCommand) - || isFileContentInspectionArgv(getInspectionArgv({ command: remoteCommand })) - || isGhApiContentsDecodeCommand(remoteCommand)); + && isVerbatimRemoteInspectionCommand(remoteCommand); }); } diff --git a/src/core/command.ts b/src/core/command.ts index 8700eeca..8bfd2d11 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -22,6 +22,7 @@ export { getGitSubcommand, isFileContentInspectionCommand, isRepositoryInspectionCommand, + isVerbatimConfigInspectionCommand, normalizeCommandSignature, normalizeEffectiveCommandSignature, } from "./command-identity.js"; diff --git a/src/core/reduce-inspection-summary.ts b/src/core/reduce-inspection-summary.ts index 998f8546..51a1a7d2 100644 --- a/src/core/reduce-inspection-summary.ts +++ b/src/core/reduce-inspection-summary.ts @@ -1,4 +1,4 @@ -import { isFileContentInspectionCommand, isPlutilFileContentInspectionCommand } from "./command-identity.js"; +import { isFileContentInspectionCommand } from "./command-identity.js"; import { createCompactionMetadata, mergeCompactionMetadata, type CompactionMetadata } from "./compaction-metadata.js"; import { clipMiddleWithHash, parseJsonValue } from "./reduce-utils.js"; import { countTextChars, headTail, normalizeLines, stripAnsi, trimEmptyEdges } from "./text.js"; @@ -140,10 +140,6 @@ export function buildInspectionSummary(input: ToolExecutionInput, rawText: strin : null; } - if (isPlutilFileContentInspectionCommand(input)) { - return null; - } - const rawChars = countTextChars(rawText); const lines = trimEmptyEdges(normalizeLines(stripAnsi(rawText))); if (!isFileContentInspectionCommand(input) || !isLargeDocumentOutput(lines, rawChars)) { diff --git a/src/core/reduce.ts b/src/core/reduce.ts index 0ac723b0..46d8f6a8 100644 --- a/src/core/reduce.ts +++ b/src/core/reduce.ts @@ -1,7 +1,7 @@ import { loadRules } from "./rules.js"; import { hasMultipleSubstantiveShellCommands } from "./command-match.js"; import { classifyExecution, resolveRuleMatch } from "./classify.js"; -import { isFileContentInspectionCommand } from "./command-identity.js"; +import { isFileContentInspectionCommand, isVerbatimConfigInspectionCommand } from "./command-identity.js"; import { normalizeExecutionInput } from "./execution-input.js"; import { clampTextMiddleWithMetadata, clampTextWithMetadata, countTextChars, dedupeAdjacent, headTail, normalizeLines, pluralize, stripAnsi, trimEmptyEdges } from "./text.js"; import { storeArtifact, storeArtifactMetadata } from "./artifacts.js"; @@ -354,7 +354,9 @@ export async function reduceExecutionWithRules( } : undefined; - if (opts.raw) { + const requiresVerbatimOutput = !multipleSubstantiveCommands + && isVerbatimConfigInspectionCommand(input); + if (opts.raw || requiresVerbatimOutput) { const rawRef = opts.store ? await storeArtifact( { @@ -452,7 +454,7 @@ export async function reduceExecutionWithRules( }; } - if (isFileContentInspectionCommand(normalizedInput)) { + if (classification.matchedReducer === "generic/fallback" && isFileContentInspectionCommand(normalizedInput)) { if (!opts.store && opts.recordStats) { await storeArtifactMetadata( { diff --git a/test/core/command.test.ts b/test/core/command.test.ts index bad83a4d..962a7a3e 100644 --- a/test/core/command.test.ts +++ b/test/core/command.test.ts @@ -7,6 +7,7 @@ import { hasSequentialShellCommands, isFileContentInspectionCommand, isRepositoryInspectionCommand, + isVerbatimConfigInspectionCommand, normalizeCommandSignature, normalizeEffectiveCommandSignature, normalizeExecutionInput, @@ -420,12 +421,6 @@ describe("isFileContentInspectionCommand", () => { { label: "clustered shell wrapper", command: "bash -ec 'cat README.md'" }, { label: "git show blob", command: "git show HEAD:src/core/reduce.ts" }, { label: "gh contents decode", command: "gh api repos/gumadeiras/tokenjuice/contents/src/core/reduce.ts --jq .content | base64 -d" }, - { label: "plutil print", command: "plutil -p /Library/LaunchDaemons/com.example.daemon.plist" }, - { label: "plutil convert to stdout", command: "plutil -convert json -o - settings.plist" }, - { label: "read-only config get", command: "openclaw config get agents.defaults" }, - { label: "ssh-wrapped cat", command: "ssh build-host 'cat /etc/hosts'" }, - { label: "ssh-wrapped plutil with ssh options", command: "ssh -p 2222 -i ~/.ssh/id_ed25519 build-host 'plutil -p /Library/LaunchDaemons/com.example.daemon.plist'" }, - { label: "ssh-wrapped read-only config get", command: "ssh build-host 'openclaw config get gateway'" }, ])("detects $label as file inspection from command text", ({ command }) => { expect(isFileContentInspectionCommand({ command })).toBe(true); }); @@ -450,20 +445,38 @@ describe("isFileContentInspectionCommand", () => { expect(isFileContentInspectionCommand({ command: "git show HEAD --stat" })).toBe(false); }); - it("does not treat plutil in-place conversions as file inspection", () => { - expect(isFileContentInspectionCommand({ command: "plutil -convert binary1 settings.plist" })).toBe(false); - }); - - it("does not treat config writes as file inspection", () => { - expect(isFileContentInspectionCommand({ command: "openclaw config set agents.defaults.model test" })).toBe(false); - }); +}); - it("does not treat ssh remote mutations as file inspection", () => { - expect(isFileContentInspectionCommand({ command: "ssh build-host 'rm -rf /tmp/scratch'" })).toBe(false); +describe("isVerbatimConfigInspectionCommand", () => { + it.each([ + { label: "plutil print", command: "plutil -p /Library/LaunchDaemons/com.example.daemon.plist" }, + { label: "plutil convert to stdout", command: "plutil -convert json -o - settings.plist" }, + { label: "read-only config get", command: "openclaw config get agents.defaults" }, + { label: "ssh-wrapped cat", command: "ssh build-host 'cat /etc/hosts'" }, + { label: "ssh-wrapped cat with compression", command: "ssh -C build-host 'cat /etc/hosts'" }, + { label: "ssh-wrapped cat with cipher", command: "ssh -c aes128-ctr build-host 'cat /etc/hosts'" }, + { label: "ssh-wrapped cat with bind interface", command: "ssh -B en0 build-host 'cat /etc/hosts'" }, + { label: "ssh-wrapped cat with tag", command: "ssh -P audit build-host 'cat /etc/hosts'" }, + { label: "ssh-wrapped shell runner", command: "ssh build-host \"bash -lc 'cat /etc/hosts'\"" }, + { label: "ssh-wrapped plutil with ssh options", command: "ssh -p 2222 -i ~/.ssh/id_ed25519 build-host 'plutil -p /Library/LaunchDaemons/com.example.daemon.plist'" }, + { label: "ssh-wrapped read-only config get", command: "ssh build-host 'openclaw config get gateway'" }, + { label: "ssh-wrapped gh contents decode", command: "ssh build-host 'gh api repos/o/r/contents/file --jq .content | base64 --decode'" }, + ])("detects $label as a verbatim config inspection", ({ command }) => { + expect(isVerbatimConfigInspectionCommand({ command })).toBe(true); }); - it("does not treat ssh without a remote command as file inspection", () => { - expect(isFileContentInspectionCommand({ command: "ssh build-host" })).toBe(false); + it.each([ + "plutil -convert binary1 settings.plist", + "openclaw config set agents.defaults.model test", + "ssh build-host 'rm -rf /tmp/scratch'", + "ssh build-host", + "ssh build-host 'cat /etc/hosts && pytest -q'", + "ssh build-host \"bash -lc 'cat /etc/hosts; pytest -q'\"", + "ssh build-host 'gh api repos/o/r/contents/file --jq .content | base64 --decode; pytest -q'", + "bash -lc 'openclaw config get gateway' && pytest -q", + "ssh build-host \"bash -lc 'cat /etc/hosts' && pytest -q\"", + ])("does not treat `%s` as a verbatim config inspection", (command) => { + expect(isVerbatimConfigInspectionCommand({ command })).toBe(false); }); }); diff --git a/test/core/reduce.test.ts b/test/core/reduce.test.ts index bb508abb..ef1497e9 100644 --- a/test/core/reduce.test.ts +++ b/test/core/reduce.test.ts @@ -419,7 +419,7 @@ describe("reduceExecution", () => { it("keeps plutil plist dumps verbatim", async () => { const rawText = [ "{", - ...Array.from({ length: 40 }, (_, index) => ` "Key${index + 1}" => "value ${index + 1}"`), + ...Array.from({ length: 80 }, (_, index) => ` "Key${index + 1}" => "${"value ".repeat(12)}${index + 1}"`), "}", ].join("\n"); @@ -436,7 +436,7 @@ describe("reduceExecution", () => { }); it("keeps read-only config inspection output verbatim", async () => { - const rawText = Array.from({ length: 30 }, (_, index) => `setting-${index + 1}: value-${index + 1}`).join("\n"); + const rawText = Array.from({ length: 80 }, (_, index) => `setting-${index + 1}: ${"value ".repeat(12)}${index + 1}`).join("\n"); const result = await reduceExecution({ toolName: "exec", @@ -450,7 +450,7 @@ describe("reduceExecution", () => { }); it("keeps ssh-wrapped file inspection output verbatim", async () => { - const rawText = Array.from({ length: 30 }, (_, index) => `host-line ${index + 1}`).join("\n"); + const rawText = Array.from({ length: 80 }, (_, index) => `host-line ${index + 1} ${"value ".repeat(12)}`).join("\n"); const result = await reduceExecution({ toolName: "exec", @@ -463,6 +463,20 @@ describe("reduceExecution", () => { expect(result.stats.ratio).toBe(1); }); + it("keeps ssh-wrapped gh contents decode output verbatim", async () => { + const rawText = Array.from({ length: 80 }, (_, index) => `file-line ${index + 1} ${"value ".repeat(12)}`).join("\n"); + + const result = await reduceExecution({ + toolName: "exec", + command: "ssh build-host 'gh api repos/o/r/contents/file --jq .content | base64 --decode'", + stdout: rawText, + exitCode: 0, + }); + + expect(result.inlineText).toBe(rawText); + expect(result.stats.ratio).toBe(1); + }); + it("still compacts filesystem inventory commands through their dedicated reducers", async () => { const result = await reduceExecution({ toolName: "exec",