11/** Tests system.run allowlist planning, output truncation, and argv resolution. */
2+ import { spawn } from "node:child_process" ;
3+ import fs from "node:fs" ;
4+ import os from "node:os" ;
5+ import path from "node:path" ;
26import { describe , expect , it } from "vitest" ;
3- import { resolveExecApprovalsFromFile } from "../infra/exec-approvals.js" ;
7+ import { resolveExecApprovalsFromFile , type ExecCommandSegment } from "../infra/exec-approvals.js" ;
48import { planShellAuthorization } from "../infra/exec-authorization-plan.js" ;
59import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js" ;
6- import { resolveSystemRunExecArgv } from "./invoke-system-run-allowlist.js" ;
10+ import {
11+ evaluateSystemRunAllowlist ,
12+ resolveSystemRunExecArgv ,
13+ } from "./invoke-system-run-allowlist.js" ;
714
815function resolveAllowlistApprovals ( ) {
916 return resolveExecApprovalsFromFile ( {
@@ -18,7 +25,193 @@ function resolveAllowlistApprovals() {
1825 } ) ;
1926}
2027
28+ function resolveWindowsShellExecArgv ( segment : ExecCommandSegment ) {
29+ return resolveSystemRunExecArgv ( {
30+ plannedAllowlistArgv : undefined ,
31+ argv : [ "powershell.exe" , "-Command" , "safe --version" ] ,
32+ security : "allowlist" ,
33+ approvals : resolveAllowlistApprovals ( ) ,
34+ safeBins : new Set ( ) ,
35+ safeBinProfiles : { } ,
36+ trustedSafeBinDirs : new Set ( ) ,
37+ skillBins : [ ] ,
38+ autoAllowSkills : false ,
39+ isWindows : true ,
40+ policy : {
41+ approvedByAsk : false ,
42+ analysisOk : true ,
43+ allowlistSatisfied : true ,
44+ } ,
45+ shellCommand : "safe --version" ,
46+ segments : [ segment ] ,
47+ segmentSatisfiedBy : [ "allowlist" ] ,
48+ authorizationPlan : undefined ,
49+ cwd : "C:\\workspace" ,
50+ env : undefined ,
51+ } ) ;
52+ }
53+
54+ function runExecutable ( params : {
55+ argv : string [ ] ;
56+ cwd : string ;
57+ env : NodeJS . ProcessEnv ;
58+ } ) : Promise < { exitCode : number | null ; stdout : string } > {
59+ return new Promise ( ( resolve , reject ) => {
60+ const child = spawn ( params . argv [ 0 ] , params . argv . slice ( 1 ) , {
61+ cwd : params . cwd ,
62+ env : params . env ,
63+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
64+ windowsHide : true ,
65+ } ) ;
66+ const stdout : Buffer [ ] = [ ] ;
67+ child . stdout . on ( "data" , ( chunk : Buffer ) => stdout . push ( chunk ) ) ;
68+ child . once ( "error" , reject ) ;
69+ child . once ( "close" , ( exitCode ) => {
70+ resolve ( { exitCode, stdout : Buffer . concat ( stdout ) . toString ( "utf8" ) } ) ;
71+ } ) ;
72+ } ) ;
73+ }
74+
2175describe ( "resolveSystemRunExecArgv" , ( ) => {
76+ it ( "pins Windows shell execution to the resolved allowlisted executable" , async ( ) => {
77+ const trustedExecutable = "C:\\trusted-bin\\safe.exe" ;
78+ const result = await resolveWindowsShellExecArgv ( {
79+ raw : "safe --version" ,
80+ argv : [ "safe" , "--version" ] ,
81+ resolution : {
82+ execution : {
83+ rawExecutable : "safe" ,
84+ resolvedPath : trustedExecutable ,
85+ executableName : "safe.exe" ,
86+ } ,
87+ policy : {
88+ rawExecutable : "safe" ,
89+ resolvedPath : trustedExecutable ,
90+ executableName : "safe.exe" ,
91+ } ,
92+ } ,
93+ } ) ;
94+
95+ expect ( result ) . toEqual ( [ trustedExecutable , "--version" ] ) ;
96+ } ) ;
97+
98+ it ( "preserves unresolved Windows shell argv authorized by a bare wildcard" , async ( ) => {
99+ const result = await resolveWindowsShellExecArgv ( {
100+ raw : "safe --version" ,
101+ argv : [ "safe" , "--version" ] ,
102+ resolution : null ,
103+ } ) ;
104+
105+ expect ( result ) . toEqual ( [ "safe" , "--version" ] ) ;
106+ } ) ;
107+
108+ it ( "fails closed when the Windows shell execution plan is blocked" , async ( ) => {
109+ const result = await resolveWindowsShellExecArgv ( {
110+ raw : "safe --version" ,
111+ argv : [ "safe" , "--version" ] ,
112+ resolution : {
113+ policyBlocked : true ,
114+ execution : {
115+ rawExecutable : "safe" ,
116+ executableName : "safe" ,
117+ } ,
118+ policy : {
119+ rawExecutable : "safe" ,
120+ executableName : "safe" ,
121+ } ,
122+ } ,
123+ } ) ;
124+
125+ expect ( result ) . toBeNull ( ) ;
126+ } ) ;
127+
128+ it . runIf ( process . platform === "win32" ) (
129+ "executes the allowlisted path instead of a workspace shadow executable" ,
130+ async ( ) => {
131+ const fixtureRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-windows-shadow-" ) ) ;
132+ const trustedBin = path . join ( fixtureRoot , "trusted-bin" ) ;
133+ const workspace = path . join ( fixtureRoot , "workspace" ) ;
134+ fs . mkdirSync ( trustedBin ) ;
135+ fs . mkdirSync ( workspace ) ;
136+ const trustedExecutable = path . join ( trustedBin , "safe.exe" ) ;
137+ const shadowExecutable = path . join ( workspace , "safe.exe" ) ;
138+ const systemRoot = process . env . SystemRoot ?? process . env . WINDIR ?? "C:\\Windows" ;
139+ fs . copyFileSync ( path . join ( systemRoot , "System32" , "cmd.exe" ) , trustedExecutable ) ;
140+ fs . copyFileSync ( path . join ( systemRoot , "System32" , "where.exe" ) , shadowExecutable ) ;
141+ const env = {
142+ ...process . env ,
143+ PATH : `${ trustedBin } ${ path . delimiter } ${ process . env . PATH ?? "" } ` ,
144+ PATHEXT : process . env . PATHEXT ?? ".COM;.EXE;.BAT;.CMD" ,
145+ } ;
146+ const shellCommand = "safe /d /s /c echo TRUSTED_EXECUTABLE" ;
147+ const commandTail = [ "/d" , "/s" , "/c" , "echo" , "TRUSTED_EXECUTABLE" ] ;
148+
149+ try {
150+ const bareResult = await runExecutable ( {
151+ argv : [ "safe" , ...commandTail ] ,
152+ cwd : workspace ,
153+ env,
154+ } ) ;
155+ expect ( bareResult . exitCode ) . not . toBe ( 0 ) ;
156+ expect ( bareResult . stdout ) . not . toContain ( "TRUSTED_EXECUTABLE" ) ;
157+
158+ const approvals = resolveExecApprovalsFromFile ( {
159+ file : {
160+ version : 1 ,
161+ defaults : { security : "allowlist" , ask : "off" , askFallback : "deny" } ,
162+ agents : { main : { allowlist : [ { pattern : trustedExecutable } ] } } ,
163+ } ,
164+ } ) ;
165+ const analysis = await evaluateSystemRunAllowlist ( {
166+ shellCommand,
167+ argv : [ "powershell.exe" , "-Command" , shellCommand ] ,
168+ approvals,
169+ security : "allowlist" ,
170+ safeBins : new Set ( ) ,
171+ safeBinProfiles : { } ,
172+ trustedSafeBinDirs : new Set ( ) ,
173+ cwd : workspace ,
174+ env,
175+ skillBins : [ ] ,
176+ autoAllowSkills : false ,
177+ } ) ;
178+ expect ( analysis . analysisOk ) . toBe ( true ) ;
179+ expect ( analysis . allowlistSatisfied ) . toBe ( true ) ;
180+
181+ const execArgv = await resolveSystemRunExecArgv ( {
182+ plannedAllowlistArgv : undefined ,
183+ argv : [ "powershell.exe" , "-Command" , shellCommand ] ,
184+ security : "allowlist" ,
185+ approvals,
186+ safeBins : new Set ( ) ,
187+ safeBinProfiles : { } ,
188+ trustedSafeBinDirs : new Set ( ) ,
189+ skillBins : [ ] ,
190+ autoAllowSkills : false ,
191+ isWindows : true ,
192+ policy : {
193+ approvedByAsk : false ,
194+ analysisOk : analysis . analysisOk ,
195+ allowlistSatisfied : analysis . allowlistSatisfied ,
196+ } ,
197+ shellCommand,
198+ segments : analysis . segments ,
199+ segmentSatisfiedBy : analysis . segmentSatisfiedBy ,
200+ authorizationPlan : analysis . authorizationPlan ,
201+ cwd : workspace ,
202+ env,
203+ } ) ;
204+ expect ( execArgv ?. [ 0 ] ) . toBe ( fs . realpathSync ( trustedExecutable ) ) ;
205+
206+ const fixedResult = await runExecutable ( { argv : execArgv ?? [ ] , cwd : workspace , env } ) ;
207+ expect ( fixedResult . exitCode ) . toBe ( 0 ) ;
208+ expect ( fixedResult . stdout ) . toContain ( "TRUSTED_EXECUTABLE" ) ;
209+ } finally {
210+ fs . rmSync ( fixtureRoot , { recursive : true , force : true } ) ;
211+ }
212+ } ,
213+ ) ;
214+
22215 it . runIf ( process . platform !== "win32" ) (
23216 "fails closed when shell rewriting has no authorization plan" ,
24217 async ( ) => {
0 commit comments