diff --git a/CHANGELOG.md b/CHANGELOG.md index 6767eb7039ae..d6fb979742b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai - Dependencies: refresh workspace pins and move the WhatsApp plugin from `@whiskeysockets/baileys` to `baileys` while keeping the `7.0.0-rc10` runtime. - Plugin SDK: add bundled-plugin session actions, `sendSessionAttachment`, and Cron-backed `scheduleSessionTurn`/tag cleanup under the grouped session namespace. Replaces #75578/#75581/#75588 and part of #73384/#74483. Thanks @100yenadmin. - Plugin SDK/media-understanding: add `extractStructuredWithModel(...)` plus the optional provider-side `extractStructured(...)` seam so trusted plugins can run bounded image-first structured extraction with optional supplemental text context through provider-owned runtimes such as Codex. +- Exec approvals: add `tools.exec.commandHighlighting` so parser-derived command highlighting in approval prompts can be enabled globally or per agent. (#79348) Thanks @jesse-merhi. ### Fixes diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 5bf77b7a49bf..764d9cb011b7 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -2800c9a6377fd02cb55bbf2577d63f2acc7193ed89be6fcffd32439893eaabc1 config-baseline.json -ebbf966b41d99cd17282553f7882d1b826782599cbca6719b6f2c25b0e6b235d config-baseline.core.json +17b1ca7d3087be090b456b125d1e380b552e5bb4359132751f1a55590aba8fad config-baseline.json +3325af3a6292959bb38166e9136c638dce5d2093d2339076742890848088a972 config-baseline.core.json 222d0338d6ed290870cac70cdf5e390bc1bb60c4462e46f847003bafe25c5a6e config-baseline.channel.json 18f71e9d4a62fe68fbd5bf18d5833a4e380fc705ad641769e1cf05794286344c config-baseline.plugin.json diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index 11233b791b13..6e70c76c0516 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -131,6 +131,7 @@ Controls elevated exec access outside the sandbox: cleanupMs: 1800000, notifyOnExit: true, notifyOnExitEmptySuccess: false, + commandHighlighting: false, applyPatch: { enabled: false, allowModels: ["gpt-5.5"], diff --git a/docs/tools/exec-approvals.md b/docs/tools/exec-approvals.md index c68e9eb37d76..8e23a32fc3c0 100644 --- a/docs/tools/exec-approvals.md +++ b/docs/tools/exec-approvals.md @@ -166,6 +166,20 @@ In strict mode these commands still need explicit approval, and `allow-always` does not persist new allowlist entries for them automatically. +### `tools.exec.commandHighlighting` + + + Controls only presentation in exec approval prompts. When enabled, + OpenClaw may attach parser-derived command spans so Web approval + prompts can highlight command tokens. Set it to `true` to enable + command text highlighting. + + +This setting does **not** change `security`, `ask`, allowlist matching, +strict inline-eval behavior, approval forwarding, or command execution. +It can be set globally under `tools.exec.commandHighlighting` or per +agent under `agents.list[].tools.exec.commandHighlighting`. + ## YOLO mode (no-approval) If you want host exec to run without approval prompts, you must open diff --git a/docs/tools/exec.md b/docs/tools/exec.md index c86bada78a4c..711d4ab1b370 100644 --- a/docs/tools/exec.md +++ b/docs/tools/exec.md @@ -109,6 +109,7 @@ Notes: - In `security=full` plus `ask=off` mode, host exec follows the configured policy directly; there is no extra heuristic command-obfuscation prefilter or script-preflight rejection layer. - `tools.exec.node` (default: unset) - `tools.exec.strictInlineEval` (default: false): when true, inline interpreter eval forms such as `python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`, `lua -e`, and `osascript -e` always require explicit approval. `allow-always` can still persist benign interpreter/script invocations, but inline-eval forms still prompt each time. +- `tools.exec.commandHighlighting` (default: false): when true, approval prompts can highlight parser-derived command spans in the command text. Set to `true` globally or per agent to enable command text highlighting without changing exec approval policy. - `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs (gateway + sandbox only). - `tools.exec.safeBins`: stdin-only safe binaries that can run without explicit allowlist entries. For behavior details, see [Safe bins](/tools/exec-approvals-advanced#safe-bins-stdin-only). - `tools.exec.safeBinTrustedDirs`: additional explicit directories trusted for `safeBins` path checks. `PATH` entries are never auto-trusted. Built-in defaults are `/bin` and `/usr/bin`. diff --git a/src/agents/bash-tools.exec-approval-request.test.ts b/src/agents/bash-tools.exec-approval-request.test.ts index 419acf7802e1..e6a2468cbc05 100644 --- a/src/agents/bash-tools.exec-approval-request.test.ts +++ b/src/agents/bash-tools.exec-approval-request.test.ts @@ -250,6 +250,7 @@ describe("requestExecApprovalDecision", () => { await registerExecApprovalRequestForHost({ approvalId: "approval-id", command: 'ls | grep "stuff" | python -c \'print("hi")\'', + commandHighlighting: true, workdir: "/tmp/project", host: "node", security: "allowlist", @@ -264,6 +265,47 @@ describe("requestExecApprovalDecision", () => { expect(payload?.commandSpans).toContainEqual({ startIndex: 20, endIndex: 26 }); }); + it("does not generate command spans by default", async () => { + vi.mocked(callGatewayTool).mockResolvedValue({ id: "approval-id", expiresAtMs: 1234 }); + + await registerExecApprovalRequestForHost({ + approvalId: "approval-id", + command: 'ls | grep "stuff" | python -c \'print("hi")\'', + workdir: "/tmp/project", + host: "node", + security: "allowlist", + ask: "always", + }); + + expect(commandExplainerMock.explainShellCommand).not.toHaveBeenCalled(); + expect(commandExplainerMock.formatCommandSpans).not.toHaveBeenCalled(); + const payload = vi.mocked(callGatewayTool).mock.calls[0]?.[2] as + | { commandSpans?: unknown } + | undefined; + expect(payload?.commandSpans).toBeUndefined(); + }); + + it("does not generate command spans when command highlighting is disabled", async () => { + vi.mocked(callGatewayTool).mockResolvedValue({ id: "approval-id", expiresAtMs: 1234 }); + + await registerExecApprovalRequestForHost({ + approvalId: "approval-id", + command: 'ls | grep "stuff" | python -c \'print("hi")\'', + commandHighlighting: false, + workdir: "/tmp/project", + host: "node", + security: "allowlist", + ask: "always", + }); + + expect(commandExplainerMock.explainShellCommand).not.toHaveBeenCalled(); + expect(commandExplainerMock.formatCommandSpans).not.toHaveBeenCalled(); + const payload = vi.mocked(callGatewayTool).mock.calls[0]?.[2] as + | { commandSpans?: unknown } + | undefined; + expect(payload?.commandSpans).toBeUndefined(); + }); + it("uses system run plan command text for host approval explanations", async () => { vi.mocked(callGatewayTool).mockResolvedValue({ id: "approval-id", expiresAtMs: 1234 }); @@ -276,6 +318,7 @@ describe("requestExecApprovalDecision", () => { agentId: null, sessionKey: null, }, + commandHighlighting: true, workdir: "/tmp/project", host: "node", security: "allowlist", @@ -362,6 +405,7 @@ describe("requestExecApprovalDecision", () => { approvalId: "approval-id", command: "echo hi", commandSpans: [{ startIndex: 0, endIndex: 4 }], + commandHighlighting: true, workdir: "/tmp/project", host: "node", security: "allowlist", diff --git a/src/agents/bash-tools.exec-approval-request.ts b/src/agents/bash-tools.exec-approval-request.ts index 520c0d89652c..eece585f9149 100644 --- a/src/agents/bash-tools.exec-approval-request.ts +++ b/src/agents/bash-tools.exec-approval-request.ts @@ -185,6 +185,7 @@ type HostExecApprovalParams = { ask: ExecAsk; warningText?: string; commandSpans?: ExecApprovalCommandSpan[]; + commandHighlighting?: boolean; agentId?: string; resolvedPath?: string; sessionKey?: string; @@ -269,10 +270,12 @@ async function buildHostApprovalDecisionParams( params: HostExecApprovalParams, ): Promise { const commandSpans = - params.commandSpans ?? - (shouldSkipGeneratedCommandSpans(params) - ? undefined - : await resolveCommandSpans(params.command ?? params.systemRunPlan?.commandText)); + params.commandHighlighting === true + ? (params.commandSpans ?? + (shouldSkipGeneratedCommandSpans(params) + ? undefined + : await resolveCommandSpans(params.command ?? params.systemRunPlan?.commandText))) + : undefined; return { id: params.approvalId, command: params.command, diff --git a/src/agents/bash-tools.exec-host-gateway.ts b/src/agents/bash-tools.exec-host-gateway.ts index 3562ae09f85e..ff360c7ae009 100644 --- a/src/agents/bash-tools.exec-host-gateway.ts +++ b/src/agents/bash-tools.exec-host-gateway.ts @@ -60,6 +60,7 @@ export type ProcessGatewayAllowlistParams = { safeBins: Set; safeBinProfiles: Readonly>; strictInlineEval?: boolean; + commandHighlighting?: boolean; trigger?: string; agentId?: string; sessionKey?: string; @@ -373,6 +374,7 @@ export async function processGatewayAllowlist( host: "gateway", security: hostSecurity, ask: hostAsk, + commandHighlighting: params.commandHighlighting, warningText: params.warnings.join("\n").trim() || undefined, ...buildExecApprovalRequesterContext({ agentId: params.agentId, diff --git a/src/agents/bash-tools.exec-host-node.ts b/src/agents/bash-tools.exec-host-node.ts index 479eb2a1bec1..d61ca66500da 100644 --- a/src/agents/bash-tools.exec-host-node.ts +++ b/src/agents/bash-tools.exec-host-node.ts @@ -92,6 +92,7 @@ export async function executeNodeHostCommand( nodeId: target.nodeId, security: hostSecurity, ask: hostAsk, + commandHighlighting: params.commandHighlighting, ...buildExecApprovalRequesterContext({ agentId: prepared.agentId, sessionKey: prepared.sessionKey, diff --git a/src/agents/bash-tools.exec-host-node.types.ts b/src/agents/bash-tools.exec-host-node.types.ts index eaac6a428024..c44fd00167c8 100644 --- a/src/agents/bash-tools.exec-host-node.types.ts +++ b/src/agents/bash-tools.exec-host-node.types.ts @@ -19,6 +19,7 @@ export type ExecuteNodeHostCommandParams = { security: ExecSecurity; ask: ExecAsk; strictInlineEval?: boolean; + commandHighlighting?: boolean; timeoutSec?: number; defaultTimeoutSec: number; approvalRunningNoticeMs: number; diff --git a/src/agents/bash-tools.exec-types.ts b/src/agents/bash-tools.exec-types.ts index 9da0e83244aa..a540a1cbdf68 100644 --- a/src/agents/bash-tools.exec-types.ts +++ b/src/agents/bash-tools.exec-types.ts @@ -14,6 +14,7 @@ export type ExecToolDefaults = { pathPrepend?: string[]; safeBins?: string[]; strictInlineEval?: boolean; + commandHighlighting?: boolean; safeBinTrustedDirs?: string[]; safeBinProfiles?: Record; agentId?: string; diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index c80a702b5a84..ce2cb125ca15 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -1486,6 +1486,7 @@ export function createExecTool( security, ask, strictInlineEval: defaults?.strictInlineEval, + commandHighlighting: defaults?.commandHighlighting, trigger: defaults?.trigger, timeoutSec: params.timeout, defaultTimeoutSec, @@ -1515,6 +1516,7 @@ export function createExecTool( safeBins, safeBinProfiles, strictInlineEval: defaults?.strictInlineEval, + commandHighlighting: defaults?.commandHighlighting, trigger: defaults?.trigger, agentId, sessionKey: defaults?.sessionKey, diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index a2eca39a7049..c4b410440126 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -1,6 +1,7 @@ import { createCodingTools, createReadTool } from "@earendil-works/pi-coding-agent"; import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js"; import { HEARTBEAT_RESPONSE_TOOL_NAME } from "../auto-reply/heartbeat-tool-response.js"; +import { resolveExecCommandHighlighting } from "../config/exec-command-highlighting.js"; import type { ModelCompatConfig } from "../config/types.models.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js"; @@ -274,6 +275,10 @@ function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) { pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend, safeBins: agentExec?.safeBins ?? globalExec?.safeBins, strictInlineEval: agentExec?.strictInlineEval ?? globalExec?.strictInlineEval, + commandHighlighting: resolveExecCommandHighlighting({ + config: cfg, + agentId: params.agentId, + }), safeBinTrustedDirs: agentExec?.safeBinTrustedDirs ?? globalExec?.safeBinTrustedDirs, safeBinProfiles: resolveMergedSafeBinProfileFixtures({ global: globalExec, @@ -678,6 +683,7 @@ export function createOpenClawCodingTools(options?: { pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend, safeBins: options?.exec?.safeBins ?? execConfig.safeBins, strictInlineEval: options?.exec?.strictInlineEval ?? execConfig.strictInlineEval, + commandHighlighting: options?.exec?.commandHighlighting ?? execConfig.commandHighlighting, safeBinTrustedDirs: options?.exec?.safeBinTrustedDirs ?? execConfig.safeBinTrustedDirs, safeBinProfiles: options?.exec?.safeBinProfiles ?? execConfig.safeBinProfiles, agentId, diff --git a/src/config/exec-command-highlighting.ts b/src/config/exec-command-highlighting.ts new file mode 100644 index 000000000000..11c68fd18057 --- /dev/null +++ b/src/config/exec-command-highlighting.ts @@ -0,0 +1,16 @@ +import { normalizeAgentId } from "../routing/session-key.js"; +import type { OpenClawConfig } from "./types.openclaw.js"; + +export function resolveExecCommandHighlighting(params: { + config?: OpenClawConfig | null; + agentId?: string | null; +}): boolean { + const config = params.config ?? {}; + const globalValue = config.tools?.exec?.commandHighlighting; + const agentId = params.agentId ? normalizeAgentId(params.agentId) : null; + const agentValue = agentId + ? config.agents?.list?.find((entry) => normalizeAgentId(entry.id) === agentId)?.tools?.exec + ?.commandHighlighting + : undefined; + return agentValue ?? globalValue ?? false; +} diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 03d8681db45b..9e74a2a7c9c8 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -500,6 +500,7 @@ const TOOLS_HOOKS_TARGET_KEYS = [ "tools.byProvider", "tools.exec.approvalRunningNoticeMs", "tools.exec.strictInlineEval", + "tools.exec.commandHighlighting", "tools.links.enabled", "tools.links.maxLinks", "tools.links.models", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 9a319bd45e92..2b89d3f4f87c 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -700,6 +700,8 @@ export const FIELD_HELP: Record = { "Allow stdin-only safe binaries to run without explicit allowlist entries.", "tools.exec.strictInlineEval": "Require explicit approval for interpreter inline-eval forms such as `python -c`, `node -e`, `ruby -e`, or `osascript -e`. Prevents silent allowlist reuse and downgrades allow-always to ask-each-time for those forms.", + "tools.exec.commandHighlighting": + "Show parser-derived command highlights in exec approval prompts (default: false). Enable this to render highlighted command text without changing exec approval policy.", "tools.exec.safeBinTrustedDirs": "Additional explicit directories trusted for safe-bin path checks (PATH entries are never auto-trusted).", "tools.exec.safeBinProfiles": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 570fba87aa2c..4af051319ecf 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -259,6 +259,7 @@ export const FIELD_LABELS: Record = { "tools.exec.pathPrepend": "Exec PATH Prepend", "tools.exec.safeBins": "Exec Safe Bins", "tools.exec.strictInlineEval": "Require Inline-Eval Approval", + "tools.exec.commandHighlighting": "Exec Command Highlighting", "tools.exec.safeBinTrustedDirs": "Exec Safe Bin Trusted Dirs", "tools.exec.safeBinProfiles": "Exec Safe Bin Profiles", approvals: "Approvals", diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index d5005cf5b82b..16f319aca381 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -329,6 +329,31 @@ describe("config schema", () => { }); }); + it("accepts exec command highlighting config in global and agent scopes", () => { + const tools = ToolsSchema.parse({ + exec: { + commandHighlighting: false, + }, + }); + expect(tools?.exec?.commandHighlighting).toBe(false); + + const config = OpenClawSchema.parse({ + agents: { + list: [ + { + id: "main", + tools: { + exec: { + commandHighlighting: false, + }, + }, + }, + ], + }, + }); + expect(config.agents?.list?.[0]?.tools?.exec?.commandHighlighting).toBe(false); + }); + it("accepts experimental tool flags in the runtime zod schema", () => { const parsed = ToolsSchema.parse({ experimental: { diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index 0d4ca008b6a3..8003b6e75010 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -284,6 +284,8 @@ export type ExecToolConfig = { * Prevents silent allowlist reuse and allow-always persistence for those forms. */ strictInlineEval?: boolean; + /** Render parser-derived command highlights in exec approval prompts (default: false). */ + commandHighlighting?: boolean; /** Extra explicit directories trusted for safeBins path checks (never derived from PATH). */ safeBinTrustedDirs?: string[]; /** Optional custom safe-bin profiles for entries in tools.exec.safeBins. */ diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index ee2872cd447a..30f24725bcf3 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -471,6 +471,7 @@ const ToolExecBaseShape = { pathPrepend: z.array(z.string()).optional(), safeBins: z.array(z.string()).optional(), strictInlineEval: z.boolean().optional(), + commandHighlighting: z.boolean().optional(), safeBinTrustedDirs: z.array(z.string()).optional(), safeBinProfiles: z.record(z.string(), ToolExecSafeBinProfileSchema).optional(), backgroundMs: z.number().int().positive().optional(), diff --git a/src/gateway/server-methods/exec-approval.ts b/src/gateway/server-methods/exec-approval.ts index 64b86b870819..2512c6887391 100644 --- a/src/gateway/server-methods/exec-approval.ts +++ b/src/gateway/server-methods/exec-approval.ts @@ -1,3 +1,4 @@ +import { resolveExecCommandHighlighting } from "../../config/exec-command-highlighting.js"; import { resolveCommandAnalysisSummaryForDisplay } from "../../infra/command-analysis/explain.js"; import { resolveExecApprovalCommandDisplay, @@ -241,6 +242,12 @@ export function createExecApprovalHandlers( } const envBinding = buildSystemRunApprovalEnvBinding(p.env); const warningText = normalizeOptionalString(p.warningText); + const runtimeConfig = + typeof context.getRuntimeConfig === "function" ? context.getRuntimeConfig() : {}; + const commandHighlighting = resolveExecCommandHighlighting({ + config: runtimeConfig, + agentId: effectiveAgentId, + }); const commandAnalysis = resolveCommandAnalysisSummaryForDisplay({ host, commandText: effectiveCommandText, @@ -250,7 +257,7 @@ export function createExecApprovalHandlers( }); const sanitizedCommandText = sanitizeExecApprovalDisplayText(effectiveCommandText); const commandSpans = - sanitizedCommandText === effectiveCommandText + commandHighlighting && sanitizedCommandText === effectiveCommandText ? normalizeCommandSpans(p.commandSpans, sanitizedCommandText.length) : undefined; const systemRunBinding = diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index 62b1bb998511..395383bd511c 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { emitAgentEvent } from "../../infra/agent-events.js"; import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js"; import { @@ -861,12 +862,13 @@ describe("exec approval handlers", () => { }); } - function createExecApprovalFixture() { + function createExecApprovalFixture(opts?: { config?: OpenClawConfig }) { const manager = new ExecApprovalManager(); const handlers = createExecApprovalHandlers(manager); const broadcasts: Array<{ event: string; payload: unknown }> = []; const respond = vi.fn(); const context = { + getRuntimeConfig: () => opts?.config ?? {}, broadcast: (event: string, payload: unknown) => { broadcasts.push({ event, payload }); }, @@ -1476,7 +1478,9 @@ describe("exec approval handlers", () => { }); it("preserves command analysis and normalizes command spans", async () => { - const { handlers, broadcasts, respond, context } = createExecApprovalFixture(); + const { handlers, broadcasts, respond, context } = createExecApprovalFixture({ + config: { tools: { exec: { commandHighlighting: true } } }, + }); await requestExecApproval({ handlers, respond, @@ -1503,8 +1507,56 @@ describe("exec approval handlers", () => { ]); }); - it("drops command spans when command display sanitization changes offsets", async () => { + it("drops command spans by default", async () => { const { handlers, broadcasts, respond, context } = createExecApprovalFixture(); + await requestExecApproval({ + handlers, + respond, + context, + params: { + timeoutMs: 10, + command: "ls | python -c 'print(1)'", + commandSpans: [ + { startIndex: 0, endIndex: 2 }, + { startIndex: 5, endIndex: 11 }, + ], + }, + }); + const { request } = getRequestedExecApprovalPayload(broadcasts); + expect(request["commandAnalysis"]).toEqual( + expect.objectContaining({ commandCount: 1, nestedCommandCount: 0 }), + ); + expect(request["commandSpans"]).toBeUndefined(); + }); + + it("drops command spans when command highlighting is disabled", async () => { + const { handlers, broadcasts, respond, context } = createExecApprovalFixture({ + config: { tools: { exec: { commandHighlighting: false } } }, + }); + await requestExecApproval({ + handlers, + respond, + context, + params: { + timeoutMs: 10, + command: "ls | python -c 'print(1)'", + commandSpans: [ + { startIndex: 0, endIndex: 2 }, + { startIndex: 5, endIndex: 11 }, + ], + }, + }); + const { request } = getRequestedExecApprovalPayload(broadcasts); + expect(request["commandAnalysis"]).toEqual( + expect.objectContaining({ commandCount: 1, nestedCommandCount: 0 }), + ); + expect(request["commandSpans"]).toBeUndefined(); + }); + + it("drops command spans when command display sanitization changes offsets", async () => { + const { handlers, broadcasts, respond, context } = createExecApprovalFixture({ + config: { tools: { exec: { commandHighlighting: true } } }, + }); await requestExecApproval({ handlers, respond,