@@ -2135,6 +2135,141 @@ describe("registerPolicyDoctorChecks", () => {
21352135 ) ;
21362136 } ) ;
21372137
2138+ it ( "accepts sandbox-scoped tool denies for read-only agent workspace policy" , async ( ) => {
2139+ const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
2140+ const cfg = {
2141+ ...cfgWithPolicy ( ) ,
2142+ tools : {
2143+ sandbox : { tools : { deny : [ "group:runtime" , "group:fs" ] } } ,
2144+ } ,
2145+ agents : {
2146+ defaults : {
2147+ sandbox : { mode : "all" , workspaceAccess : "ro" } ,
2148+ } ,
2149+ list : [
2150+ {
2151+ id : "locked" ,
2152+ sandbox : { workspaceAccess : "none" } ,
2153+ tools : { sandbox : { tools : { deny : [ "group:runtime" , "group:fs" ] } } } ,
2154+ } ,
2155+ ] ,
2156+ } ,
2157+ } as unknown as OpenClawConfig ;
2158+ await fs . writeFile ( configPath , "{}" , "utf-8" ) ;
2159+ await fs . writeFile (
2160+ join ( workspaceDir , "policy.jsonc" ) ,
2161+ JSON . stringify ( {
2162+ agents : {
2163+ workspace : {
2164+ allowedAccess : [ "none" , "ro" ] ,
2165+ denyTools : [ "exec" , "process" , "write" , "edit" , "apply_patch" ] ,
2166+ } ,
2167+ } ,
2168+ } ) ,
2169+ "utf-8" ,
2170+ ) ;
2171+
2172+ registerPolicyDoctorChecks ( ) ;
2173+ const result = await runDoctorLintChecks ( ctx ( configPath , cfg ) ) ;
2174+ const evidence = collectPolicyEvidence ( cfg as unknown as Record < string , unknown > ) ;
2175+
2176+ expect ( evidence . agentWorkspace ) . toEqual (
2177+ expect . arrayContaining ( [
2178+ expect . objectContaining ( {
2179+ id : "agents-defaults-tool-exec" ,
2180+ denied : true ,
2181+ source : "oc://openclaw.config/tools/sandbox/tools/deny" ,
2182+ } ) ,
2183+ expect . objectContaining ( {
2184+ id : "locked-tool-apply_patch" ,
2185+ denied : true ,
2186+ source : "oc://openclaw.config/agents/list/#0/tools/sandbox/tools/deny" ,
2187+ } ) ,
2188+ ] ) ,
2189+ ) ;
2190+ expect ( result . findings ) . toEqual ( [ ] ) ;
2191+ } ) ;
2192+
2193+ it ( "accepts runtime tool deny globs for agent workspace policy" , async ( ) => {
2194+ const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
2195+ const cfg = {
2196+ ...cfgWithPolicy ( ) ,
2197+ tools : {
2198+ deny : [ "e*" ] ,
2199+ } ,
2200+ agents : {
2201+ defaults : {
2202+ sandbox : { mode : "all" , workspaceAccess : "ro" } ,
2203+ } ,
2204+ } ,
2205+ } as unknown as OpenClawConfig ;
2206+ await fs . writeFile ( configPath , "{}" , "utf-8" ) ;
2207+ await fs . writeFile (
2208+ join ( workspaceDir , "policy.jsonc" ) ,
2209+ JSON . stringify ( {
2210+ agents : {
2211+ workspace : {
2212+ allowedAccess : [ "ro" ] ,
2213+ denyTools : [ "exec" ] ,
2214+ } ,
2215+ } ,
2216+ } ) ,
2217+ "utf-8" ,
2218+ ) ;
2219+
2220+ registerPolicyDoctorChecks ( ) ;
2221+ const result = await runDoctorLintChecks ( ctx ( configPath , cfg ) ) ;
2222+
2223+ expect ( result . findings ) . toEqual ( [ ] ) ;
2224+ } ) ;
2225+
2226+ it ( "reports sandbox tool deny overrides outside policy" , async ( ) => {
2227+ const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
2228+ const cfg = {
2229+ ...cfgWithPolicy ( ) ,
2230+ tools : {
2231+ sandbox : { tools : { deny : [ "exec" ] } } ,
2232+ } ,
2233+ agents : {
2234+ defaults : {
2235+ sandbox : { mode : "all" , workspaceAccess : "ro" } ,
2236+ } ,
2237+ list : [
2238+ {
2239+ id : "locked" ,
2240+ sandbox : { workspaceAccess : "none" } ,
2241+ tools : { sandbox : { tools : { deny : [ "group:fs" ] } } } ,
2242+ } ,
2243+ ] ,
2244+ } ,
2245+ } as unknown as OpenClawConfig ;
2246+ await fs . writeFile ( configPath , "{}" , "utf-8" ) ;
2247+ await fs . writeFile (
2248+ join ( workspaceDir , "policy.jsonc" ) ,
2249+ JSON . stringify ( {
2250+ agents : {
2251+ workspace : {
2252+ allowedAccess : [ "none" , "ro" ] ,
2253+ denyTools : [ "exec" ] ,
2254+ } ,
2255+ } ,
2256+ } ) ,
2257+ "utf-8" ,
2258+ ) ;
2259+
2260+ registerPolicyDoctorChecks ( ) ;
2261+ const result = await runDoctorLintChecks ( ctx ( configPath , cfg ) ) ;
2262+
2263+ expect ( result . findings ) . toEqual ( [
2264+ expect . objectContaining ( {
2265+ checkId : "policy/agents-tool-not-denied" ,
2266+ message : "agent 'locked' does not deny required tool 'exec'." ,
2267+ ocPath : "oc://openclaw.config/agents/list/#0/tools/deny" ,
2268+ requirement : "oc://policy.jsonc/agents/workspace/denyTools" ,
2269+ } ) ,
2270+ ] ) ;
2271+ } ) ;
2272+
21382273 it ( "accepts read-only agent workspace policy with group denies" , async ( ) => {
21392274 const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
21402275 const cfg = {
@@ -2174,6 +2309,54 @@ describe("registerPolicyDoctorChecks", () => {
21742309 expect ( result . findings ) . toEqual ( [ ] ) ;
21752310 } ) ;
21762311
2312+ it ( "reports read-only workspace policy when sandbox mode skips the main session" , async ( ) => {
2313+ const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
2314+ const cfg = {
2315+ ...cfgWithPolicy ( ) ,
2316+ tools : {
2317+ sandbox : { tools : { deny : [ "exec" ] } } ,
2318+ } ,
2319+ agents : {
2320+ defaults : {
2321+ sandbox : { mode : "non-main" , workspaceAccess : "ro" } ,
2322+ } ,
2323+ } ,
2324+ } as unknown as OpenClawConfig ;
2325+ await fs . writeFile ( configPath , "{}" , "utf-8" ) ;
2326+ await fs . writeFile (
2327+ join ( workspaceDir , "policy.jsonc" ) ,
2328+ JSON . stringify ( {
2329+ agents : {
2330+ workspace : {
2331+ allowedAccess : [ "ro" ] ,
2332+ denyTools : [ "exec" ] ,
2333+ } ,
2334+ } ,
2335+ } ) ,
2336+ "utf-8" ,
2337+ ) ;
2338+
2339+ registerPolicyDoctorChecks ( ) ;
2340+ const result = await runDoctorLintChecks ( ctx ( configPath , cfg ) ) ;
2341+
2342+ expect ( result . findings ) . toEqual (
2343+ expect . arrayContaining ( [
2344+ expect . objectContaining ( {
2345+ checkId : "policy/agents-workspace-access-denied" ,
2346+ message : "agents.defaults sandbox mode 'non-main' is not allowed by policy." ,
2347+ ocPath : "oc://openclaw.config/agents/defaults/sandbox/mode" ,
2348+ requirement : "oc://policy.jsonc/agents/workspace/allowedAccess" ,
2349+ } ) ,
2350+ expect . objectContaining ( {
2351+ checkId : "policy/agents-tool-not-denied" ,
2352+ message : "agents.defaults does not deny required tool 'exec'." ,
2353+ ocPath : "oc://openclaw.config/tools/deny" ,
2354+ requirement : "oc://policy.jsonc/agents/workspace/denyTools" ,
2355+ } ) ,
2356+ ] ) ,
2357+ ) ;
2358+ } ) ;
2359+
21772360 it ( "reports read-only workspace policy when sandbox mode is disabled" , async ( ) => {
21782361 const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
21792362 const cfg = {
0 commit comments