Execute approved exec-approval commands as a resolved, shell-free argv - #799
Conversation
Exec-approvals must run an approved command exactly as it was evaluated, with no shell between the policy decision and process creation. The local runner only had the shell-wrapped path, which re-tokenizes the command with its own quoting. CommandRequest.Argv (optional) selects direct mode: FileName = Argv[0] and the remaining arguments go through ProcessStartInfo.ArgumentList with UseShellExecute=false, so they reach the process verbatim. The decision is extracted into PlanExecution (internal) so it is unit-testable without spawning a process; a null Argv leaves the legacy shell-wrapped path unchanged. Direct mode fails closed on a malformed payload: an empty argv, a non-absolute executable (which Windows would otherwise guess from PATH/cwd), or a batch script (.bat/.cmd require cmd.exe, which re-parses arguments) are rejected rather than degraded to a shell. An invalid argv returns an error result instead of crashing the host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…esult An allow decision now carries the command to execute: the validated argument vector, the sanitized environment, the working directory, and the timeout. argv[0] is the executable resolved to an absolute path, never the raw command token, so it cannot be re-resolved against PATH or the working directory at execution time. If the executable cannot be resolved, the allow fails closed. The payload takes defensive copies of argv and env and rejects an empty argv. The result type enforces the invariant that an allow always carries a payload and a deny never does. An end-to-end test confirms the payload emitted on allow is directly executable by the runner with no shell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Codex review: needs maintainer review before merge. Reviewed June 21, 2026, 2:52 PM ET / 18:52 UTC. Summary Reproducibility: not applicable. as a PR feature/security-hardening review rather than a standalone bug report. The relevant check is source review plus the provided runtime screenshot showing approved, payload, and received argv bytes. Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the staged direct-argv payload and host-runner support after maintainer security review confirms the boundary, while keeping production V2 routing and sandbox argv transport as explicit follow-up work. Do we have a high-confidence way to reproduce the issue? Not applicable as a PR feature/security-hardening review rather than a standalone bug report. The relevant check is source review plus the provided runtime screenshot showing approved, payload, and received argv bytes. Is this the best way to solve the issue? Yes. Carrying a resolved argv payload and executing it via ProcessStartInfo.ArgumentList is the narrowest maintainable way to avoid shell re-tokenization, with sandboxed direct argv failing closed until transport support exists. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 5d1c65bb6ff8. Label changesLabel justifications:
Evidence reviewedWhat I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
… tail When a transparent env wrapper prefixes a command (e.g. ["env", "git", "status"]), the resolver strips it and resolves the inner executable. BuildApprovedExecution was copying identity.Command[1..] — the original request tail — so the payload executed a different argv than the one that was approved and evaluated. Now uses ExecEnvInvocationUnwrapper.UnwrapForResolution to derive the same effective argv the resolver used, replacing only argv[0] with the resolved absolute path. Regression test added for the env-wrapper case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
UnwrapForResolution is designed for executable resolution and strips all
env forms, including modified ones (VAR=val assignments, flags). Using it
to build the execution payload would silently drop those modifiers, running
the process in a different environment than the one that was approved.
BuildApprovedExecution now checks HasModifiers before unwrapping: if the
env invocation carries assignments or flags, it returns null so the caller
fails closed rather than executing with altered semantics. Transparent
wrappers (plain env without modifiers) are unaffected.
Validation: ./dotnet test OpenClaw.Shared.Tests — 2324 passing, 0 failed.
./dotnet test OpenClaw.Tray.Tests — 1088 passing, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
If BuildApprovedExecution fails closed (unresolved executable or modified env wrapper), the previous ordering wrote to the allowlist store before discovering the failure. A denied execution could leave a persisted allowlist entry behind. Build the payload first. Only write side effects when the payload is valid. Matches the pre-approved allow path, which already had the correct order. Regression: AllowAlways with a modified env wrapper produces InternalError and leaves the store file unchanged. Validation: dotnet build openclaw-windows-node.slnx — succeeded, 0 errors dotnet test OpenClaw.Shared.Tests — 2325 passing, 0 failed dotnet test OpenClaw.Tray.Tests — 1088 passing, 0 failed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
A command can resolve to a .bat or .cmd, which cannot run without cmd.exe — cmd.exe re-parses arguments and breaks the verbatim-argv guarantee. The direct-argv runner already rejects these, but the coordinator emitted the payload first and wrote approval state before the runner could reject it. BuildApprovedExecution now rejects batch scripts up front, so the fail-closed result is reached before any allowlist write, consistent with the unresolved-executable and modified-env-wrapper guards. Validation: dotnet build openclaw-windows-node.slnx — succeeded, 0 errors dotnet test OpenClaw.Shared.Tests — 2328 passing, 0 failed dotnet test OpenClaw.Tray.Tests — 1088 passing, 0 failed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
MxcCommandRunner serializes only the legacy command/shell/args fields into the sandbox request; it has no transport for CommandRequest.Argv. An approved direct-argv command reaching the sandbox would be silently serialized as the legacy fields and run something other than what was approved. Block it: when sandboxing is available and enabled and the request carries a non-null Argv, fail closed rather than degrade to the legacy fields. The host-fallback branches (sandbox unavailable or disabled) are unchanged — the host runner does honor Argv — so only the sandbox serialization path is gated. Validation: dotnet build openclaw-windows-node.slnx — succeeded, 0 errors dotnet test OpenClaw.Shared.Tests — 2330 passing, 0 failed dotnet test OpenClaw.Tray.Tests — 1088 passing, 0 failed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
The modifier guard only checked the outer env wrapper, so a nested form such as `env env FOO=bar node` passed: the outer level has no modifier, and the resolver then unwraps every level down to `node`, dropping the inner FOO=bar. The payload would run without the approved environment. Add AnyWrapperHasModifiers, which walks the same unwrap chain the resolver uses and fails closed if any level carries assignments or flags. Transparent nested wrappers (`env env git status`) still emit a payload. Validation: dotnet build openclaw-windows-node.slnx — succeeded, 0 errors dotnet test OpenClaw.Shared.Tests — 2333 passing, 0 failed dotnet test OpenClaw.Tray.Tests — 1088 passing, 0 failed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Clamp approved execution timeouts, expose immutable payload collections, and add a canonical CommandRequest mapper so future wiring preserves argv, cwd, timeout, and env exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
The exec-approvals V2 path could decide to allow a command but had no way to run
it: the allow result carried no payload, and the local runner could only execute
through a shell that re-tokenizes the command. This change makes an approved
command executable exactly as it was evaluated.
argument vector with its executable resolved to an absolute path, the sanitized
environment, the working directory, and the timeout. argv[0] is the resolved
path, never the raw command token, so the executable cannot be re-resolved
against PATH or the working directory at execution time. If the executable
cannot be resolved, the allow fails closed.
the process is launched with ProcessStartInfo.ArgumentList and
UseShellExecute=false, so the approved arguments reach the process verbatim
with no shell re-parsing them. The legacy shell-wrapped path is unchanged when
no argv is supplied.
envwrapper (e.g.["env", "git", "status"]), the payload is built from the effective argv the resolver evaluated,not the original request tail, so the approved and executed command identity
match exactly.
Safety
The payload builder fails closed before any approval state is written, so a
command that cannot be represented faithfully never leaves an allowlist entry
or usage record behind. It rejects:
envwrapper carrying modifiers (VAR=val assignments or flags such as-ior
--unset), whose environment semantics a direct-argv payload cannot carry,including nested forms such as
env env FOO=bar node, whose modifier the resolverwould otherwise strip. Transparent
envwrappers (including nested) are unwrapped safely.The direct-argv runner enforces the same guards independently:
from PATH/cwd), or a batch script are rejected rather than degraded to a shell.
back to the shell.
name and the argument count are logged.
between approval and execution.
The sandboxed runner has no argv transport yet, so it fails closed rather than
degrade silently: when sandboxing is available and enabled and a request carries
a direct argv, it is blocked instead of being serialized as the legacy command
fields. When sandboxing is unavailable or disabled, the request routes to the
host runner, which does honor argv.
Scope
This adds the capability to execute an approved command faithfully. It does not
enable the new path in production: wiring the coordinator behind its feature flag,
and a faithful argv transport for the sandboxed runner, are separate changes. The
new path stays unreachable until those land, and the sandbox fails closed in the
interim.
Proof
A standalone program approves a command carrying deliberately hostile arguments
(spaces, an empty string, an embedded quote, a trailing backslash, shell
metacharacters, a tab, and Unicode with an emoji), runs it through the real
coordinator and the real runner, and has the launched child process dump the argv
it actually received. Each argument is shown with its character length and its
exact UTF-8 bytes in hex, so a space, a tab, and an empty string are
distinguishable rather than ambiguous on screen. No pass/fail verdict is printed
by the program: the three blocks are compared by reading the bytes.
arguments unchanged).
APPROVED and RECEIVED match byte for byte; PAYLOAD is the same arguments with the
resolved executable as argv[0].
Proof output below:
Testing
Local validation after latest changes:
Coverage includes: launch-planning decision (direct-argv vs legacy, verbatim
argument preservation, fail-closed guards), payload builder (resolved path becomes
argv[0]; unresolved executable, batch script, and modified env wrapper each produce
no payload), empty-argv guard, sanitized environment in payload, end-to-end
coordinator→runner handoff without shell, regression for transparent env wrapper,
no allowlist persistence when the payload fails closed, and the sandboxed runner
failing closed on a direct-argv request while the host fallback still honors argv.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com