Skip to content

Commit f625052

Browse files
committed
feat: make exec command highlighting optional
1 parent 3775532 commit f625052

22 files changed

Lines changed: 194 additions & 10 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
5858
- Dependencies: refresh workspace pins and move the WhatsApp plugin from `@whiskeysockets/baileys` to `baileys` while keeping the `7.0.0-rc10` runtime.
5959
- 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.
6060
- 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.
61+
- 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.
6162

6263
### Fixes
6364

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
2800c9a6377fd02cb55bbf2577d63f2acc7193ed89be6fcffd32439893eaabc1 config-baseline.json
2-
ebbf966b41d99cd17282553f7882d1b826782599cbca6719b6f2c25b0e6b235d config-baseline.core.json
1+
17b1ca7d3087be090b456b125d1e380b552e5bb4359132751f1a55590aba8fad config-baseline.json
2+
3325af3a6292959bb38166e9136c638dce5d2093d2339076742890848088a972 config-baseline.core.json
33
222d0338d6ed290870cac70cdf5e390bc1bb60c4462e46f847003bafe25c5a6e config-baseline.channel.json
44
18f71e9d4a62fe68fbd5bf18d5833a4e380fc705ad641769e1cf05794286344c config-baseline.plugin.json

docs/gateway/config-tools.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Controls elevated exec access outside the sandbox:
131131
cleanupMs: 1800000,
132132
notifyOnExit: true,
133133
notifyOnExitEmptySuccess: false,
134+
commandHighlighting: false,
134135
applyPatch: {
135136
enabled: false,
136137
allowModels: ["gpt-5.5"],

docs/tools/exec-approvals.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,20 @@ In strict mode these commands still need explicit approval, and
166166
`allow-always` does not persist new allowlist entries for them
167167
automatically.
168168

169+
### `tools.exec.commandHighlighting`
170+
171+
<ParamField path="commandHighlighting" type="boolean" default="false">
172+
Controls only presentation in exec approval prompts. When enabled,
173+
OpenClaw may attach parser-derived command spans so Web approval
174+
prompts can highlight command tokens. Set it to `true` to enable
175+
command text highlighting.
176+
</ParamField>
177+
178+
This setting does **not** change `security`, `ask`, allowlist matching,
179+
strict inline-eval behavior, approval forwarding, or command execution.
180+
It can be set globally under `tools.exec.commandHighlighting` or per
181+
agent under `agents.list[].tools.exec.commandHighlighting`.
182+
169183
## YOLO mode (no-approval)
170184

171185
If you want host exec to run without approval prompts, you must open

docs/tools/exec.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Notes:
109109
- 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.
110110
- `tools.exec.node` (default: unset)
111111
- `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.
112+
- `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.
112113
- `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs (gateway + sandbox only).
113114
- `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).
114115
- `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`.

src/agents/bash-tools.exec-approval-request.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ describe("requestExecApprovalDecision", () => {
250250
await registerExecApprovalRequestForHost({
251251
approvalId: "approval-id",
252252
command: 'ls | grep "stuff" | python -c \'print("hi")\'',
253+
commandHighlighting: true,
253254
workdir: "/tmp/project",
254255
host: "node",
255256
security: "allowlist",
@@ -264,6 +265,47 @@ describe("requestExecApprovalDecision", () => {
264265
expect(payload?.commandSpans).toContainEqual({ startIndex: 20, endIndex: 26 });
265266
});
266267

268+
it("does not generate command spans by default", async () => {
269+
vi.mocked(callGatewayTool).mockResolvedValue({ id: "approval-id", expiresAtMs: 1234 });
270+
271+
await registerExecApprovalRequestForHost({
272+
approvalId: "approval-id",
273+
command: 'ls | grep "stuff" | python -c \'print("hi")\'',
274+
workdir: "/tmp/project",
275+
host: "node",
276+
security: "allowlist",
277+
ask: "always",
278+
});
279+
280+
expect(commandExplainerMock.explainShellCommand).not.toHaveBeenCalled();
281+
expect(commandExplainerMock.formatCommandSpans).not.toHaveBeenCalled();
282+
const payload = vi.mocked(callGatewayTool).mock.calls[0]?.[2] as
283+
| { commandSpans?: unknown }
284+
| undefined;
285+
expect(payload?.commandSpans).toBeUndefined();
286+
});
287+
288+
it("does not generate command spans when command highlighting is disabled", async () => {
289+
vi.mocked(callGatewayTool).mockResolvedValue({ id: "approval-id", expiresAtMs: 1234 });
290+
291+
await registerExecApprovalRequestForHost({
292+
approvalId: "approval-id",
293+
command: 'ls | grep "stuff" | python -c \'print("hi")\'',
294+
commandHighlighting: false,
295+
workdir: "/tmp/project",
296+
host: "node",
297+
security: "allowlist",
298+
ask: "always",
299+
});
300+
301+
expect(commandExplainerMock.explainShellCommand).not.toHaveBeenCalled();
302+
expect(commandExplainerMock.formatCommandSpans).not.toHaveBeenCalled();
303+
const payload = vi.mocked(callGatewayTool).mock.calls[0]?.[2] as
304+
| { commandSpans?: unknown }
305+
| undefined;
306+
expect(payload?.commandSpans).toBeUndefined();
307+
});
308+
267309
it("uses system run plan command text for host approval explanations", async () => {
268310
vi.mocked(callGatewayTool).mockResolvedValue({ id: "approval-id", expiresAtMs: 1234 });
269311

@@ -276,6 +318,7 @@ describe("requestExecApprovalDecision", () => {
276318
agentId: null,
277319
sessionKey: null,
278320
},
321+
commandHighlighting: true,
279322
workdir: "/tmp/project",
280323
host: "node",
281324
security: "allowlist",
@@ -362,6 +405,7 @@ describe("requestExecApprovalDecision", () => {
362405
approvalId: "approval-id",
363406
command: "echo hi",
364407
commandSpans: [{ startIndex: 0, endIndex: 4 }],
408+
commandHighlighting: true,
365409
workdir: "/tmp/project",
366410
host: "node",
367411
security: "allowlist",

src/agents/bash-tools.exec-approval-request.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ type HostExecApprovalParams = {
185185
ask: ExecAsk;
186186
warningText?: string;
187187
commandSpans?: ExecApprovalCommandSpan[];
188+
commandHighlighting?: boolean;
188189
agentId?: string;
189190
resolvedPath?: string;
190191
sessionKey?: string;
@@ -269,10 +270,12 @@ async function buildHostApprovalDecisionParams(
269270
params: HostExecApprovalParams,
270271
): Promise<RequestExecApprovalDecisionParams> {
271272
const commandSpans =
272-
params.commandSpans ??
273-
(shouldSkipGeneratedCommandSpans(params)
274-
? undefined
275-
: await resolveCommandSpans(params.command ?? params.systemRunPlan?.commandText));
273+
params.commandHighlighting === true
274+
? (params.commandSpans ??
275+
(shouldSkipGeneratedCommandSpans(params)
276+
? undefined
277+
: await resolveCommandSpans(params.command ?? params.systemRunPlan?.commandText)))
278+
: undefined;
276279
return {
277280
id: params.approvalId,
278281
command: params.command,

src/agents/bash-tools.exec-host-gateway.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type ProcessGatewayAllowlistParams = {
6060
safeBins: Set<string>;
6161
safeBinProfiles: Readonly<Record<string, SafeBinProfile>>;
6262
strictInlineEval?: boolean;
63+
commandHighlighting?: boolean;
6364
trigger?: string;
6465
agentId?: string;
6566
sessionKey?: string;
@@ -373,6 +374,7 @@ export async function processGatewayAllowlist(
373374
host: "gateway",
374375
security: hostSecurity,
375376
ask: hostAsk,
377+
commandHighlighting: params.commandHighlighting,
376378
warningText: params.warnings.join("\n").trim() || undefined,
377379
...buildExecApprovalRequesterContext({
378380
agentId: params.agentId,

src/agents/bash-tools.exec-host-node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export async function executeNodeHostCommand(
9292
nodeId: target.nodeId,
9393
security: hostSecurity,
9494
ask: hostAsk,
95+
commandHighlighting: params.commandHighlighting,
9596
...buildExecApprovalRequesterContext({
9697
agentId: prepared.agentId,
9798
sessionKey: prepared.sessionKey,

src/agents/bash-tools.exec-host-node.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type ExecuteNodeHostCommandParams = {
1919
security: ExecSecurity;
2020
ask: ExecAsk;
2121
strictInlineEval?: boolean;
22+
commandHighlighting?: boolean;
2223
timeoutSec?: number;
2324
defaultTimeoutSec: number;
2425
approvalRunningNoticeMs: number;

0 commit comments

Comments
 (0)