@@ -1739,6 +1739,136 @@ describe("registerPolicyDoctorChecks", () => {
17391739 } ) ;
17401740 } ) ;
17411741
1742+ it ( "dry-runs required tool deny repairs without mutating config" , async ( ) => {
1743+ const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
1744+ const cfg = {
1745+ ...cfgWithPolicy ( { workspaceRepairs : true } ) ,
1746+ tools : { deny : [ "read" ] } ,
1747+ } as unknown as OpenClawConfig ;
1748+ await fs . writeFile ( configPath , "{}" , "utf-8" ) ;
1749+ await fs . writeFile (
1750+ join ( workspaceDir , "policy.jsonc" ) ,
1751+ JSON . stringify ( { tools : { denyTools : [ "exec" , "write" ] } } ) ,
1752+ "utf-8" ,
1753+ ) ;
1754+
1755+ const result = await runPolicyRepairCheck ( "policy/tools-required-deny-missing" , {
1756+ ...repairCtx ( configPath , cfg ) ,
1757+ dryRun : true ,
1758+ } ) ;
1759+
1760+ expect ( result . findings ) . toEqual (
1761+ expect . arrayContaining ( [
1762+ expect . objectContaining ( {
1763+ checkId : "policy/tools-required-deny-missing" ,
1764+ message : "global tools config does not deny required tool 'exec'." ,
1765+ ocPath : "oc://openclaw.config/tools/deny" ,
1766+ } ) ,
1767+ expect . objectContaining ( {
1768+ checkId : "policy/tools-required-deny-missing" ,
1769+ message : "global tools config does not deny required tool 'write'." ,
1770+ ocPath : "oc://openclaw.config/tools/deny" ,
1771+ } ) ,
1772+ ] ) ,
1773+ ) ;
1774+ expect ( result . status ) . toBe ( "repaired" ) ;
1775+ expect ( result . changes ) . toEqual ( [
1776+ "Added exec to tools.deny for policy conformance." ,
1777+ "Added write to tools.deny for policy conformance." ,
1778+ ] ) ;
1779+ expect ( result . config . tools ?. deny ) . toEqual ( [ "read" , "exec" , "write" ] ) ;
1780+ expect ( cfg . tools ?. deny ) . toEqual ( [ "read" ] ) ;
1781+ } ) ;
1782+
1783+ it ( "repairs required agent workspace deny tool findings" , async ( ) => {
1784+ const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
1785+ const cfg = {
1786+ ...cfgWithPolicy ( { workspaceRepairs : true } ) ,
1787+ agents : {
1788+ list : [
1789+ {
1790+ id : "reviewer" ,
1791+ tools : { deny : [ "exec" ] } ,
1792+ } ,
1793+ ] ,
1794+ } ,
1795+ } as unknown as OpenClawConfig ;
1796+ await fs . writeFile ( configPath , "{}" , "utf-8" ) ;
1797+ await fs . writeFile (
1798+ join ( workspaceDir , "policy.jsonc" ) ,
1799+ JSON . stringify ( {
1800+ scopes : {
1801+ reviewer : {
1802+ agentIds : [ "reviewer" ] ,
1803+ agents : { workspace : { denyTools : [ "exec" , "write" , "edit" ] } } ,
1804+ } ,
1805+ } ,
1806+ } ) ,
1807+ "utf-8" ,
1808+ ) ;
1809+
1810+ const result = await runPolicyRepairCheck (
1811+ "policy/agents-tool-not-denied" ,
1812+ repairCtx ( configPath , cfg ) ,
1813+ ) ;
1814+
1815+ expect ( result . status ) . toBe ( "repaired" ) ;
1816+ expect ( result . changes ) . toEqual ( [
1817+ "Added edit to agents.list[0].tools.deny for policy conformance." ,
1818+ "Added write to agents.list[0].tools.deny for policy conformance." ,
1819+ ] ) ;
1820+ expect ( result . remainingFindings ) . toEqual ( [ ] ) ;
1821+ expect ( result . config . agents ?. list ?. [ 0 ] ) . toMatchObject ( {
1822+ id : "reviewer" ,
1823+ tools : { deny : [ "exec" , "edit" , "write" ] } ,
1824+ } ) ;
1825+ } ) ;
1826+
1827+ it ( "skips scoped required deny repairs that would mutate root tools deny" , async ( ) => {
1828+ const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
1829+ const cfg = {
1830+ ...cfgWithPolicy ( { workspaceRepairs : true } ) ,
1831+ tools : { deny : [ "exec" ] } ,
1832+ agents : {
1833+ list : [ { id : "reviewer" } ] ,
1834+ } ,
1835+ } as unknown as OpenClawConfig ;
1836+ await fs . writeFile ( configPath , "{}" , "utf-8" ) ;
1837+ await fs . writeFile (
1838+ join ( workspaceDir , "policy.jsonc" ) ,
1839+ JSON . stringify ( {
1840+ scopes : {
1841+ reviewer : {
1842+ agentIds : [ "reviewer" ] ,
1843+ tools : { denyTools : [ "exec" , "write" ] } ,
1844+ } ,
1845+ } ,
1846+ } ) ,
1847+ "utf-8" ,
1848+ ) ;
1849+
1850+ const result = await runPolicyRepairCheck (
1851+ "policy/tools-required-deny-missing" ,
1852+ repairCtx ( configPath , cfg ) ,
1853+ ) ;
1854+
1855+ expect ( result . status ) . toBe ( "skipped" ) ;
1856+ expect ( result . reason ) . toBe ( "policy automatic repair had no config changes to apply" ) ;
1857+ expect ( result . changes ) . toEqual ( [ ] ) ;
1858+ expect ( result . warnings ) . toEqual ( [
1859+ "Skipped scoped deny repair for write. The finding reports inherited root tools.deny, so changing it would affect more than the scoped policy target." ,
1860+ ] ) ;
1861+ expect ( result . config . tools ?. deny ) . toEqual ( [ "exec" ] ) ;
1862+ expect ( result . config . agents ?. list ?. [ 0 ] ) . toEqual ( { id : "reviewer" } ) ;
1863+ expect ( result . remainingFindings ) . toEqual ( [
1864+ expect . objectContaining ( {
1865+ checkId : "policy/tools-required-deny-missing" ,
1866+ ocPath : "oc://openclaw.config/tools/deny" ,
1867+ requirement : "oc://policy.jsonc/scopes/reviewer/tools/denyTools" ,
1868+ } ) ,
1869+ ] ) ;
1870+ } ) ;
1871+
17421872 it ( "skips scoped data-handling repairs that would mutate shared config" , async ( ) => {
17431873 const configPath = join ( workspaceDir , "openclaw.jsonc" ) ;
17441874 const cfg = {
0 commit comments