diff --git a/docs/cli/policy.md b/docs/cli/policy.md index 6fa06eca0b4f..931085bbc93a 100644 --- a/docs/cli/policy.md +++ b/docs/cli/policy.md @@ -937,10 +937,24 @@ Example findings: `workspaceRepairs` is explicitly enabled; otherwise checks report what they would repair and leave settings unchanged. -Currently, repair can disable channels that are enabled in OpenClaw config but -denied by `channels.denyRules`. Enable `workspaceRepairs` only after the -policy file has been reviewed, since a valid deny rule can turn off a -configured channel: +In this version, repair can disable channels denied by `channels.denyRules` and +apply the automatic narrowing repairs listed below. Enable `workspaceRepairs` +only after the policy file has been reviewed, because a valid rule can change +workspace config: + +- set `tools.elevated.enabled=false` when a global policy forbids elevated tools +- set insecure `gateway.controlUi.*` toggles to `false` +- set `gateway.mode=local` when policy denies remote gateway mode +- set `logging.redactSensitive=tools` when policy requires sensitive logging + redaction +- set `diagnostics.otel.captureContent=false`, or + `diagnostics.otel.captureContent.enabled=false` for object-form telemetry + capture settings, when policy denies telemetry content capture + +Scoped elevated-tools repairs are detect-only. Scoped data-handling repairs are +also skipped when the finding reports shared logging or telemetry config, +because changing the shared setting would affect more than the scoped policy +target. ```jsonc { diff --git a/extensions/policy/src/doctor/automatic-repairs.ts b/extensions/policy/src/doctor/automatic-repairs.ts new file mode 100644 index 000000000000..bffb06b51e34 --- /dev/null +++ b/extensions/policy/src/doctor/automatic-repairs.ts @@ -0,0 +1,257 @@ +// Policy automatic repairs apply only deterministic narrowing config changes. +import type { + HealthFinding, + HealthRepairContext, + HealthRepairResult, + OpenClawConfig, +} from "openclaw/plugin-sdk/health"; +import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js"; +import { CHECK_IDS, type POLICY_CHECK_IDS } from "./metadata.js"; + +type PolicyCheckId = (typeof POLICY_CHECK_IDS)[number]; +type ConfigRecord = Record; +type RepairPatch = { + readonly config: OpenClawConfig; + readonly changes: readonly string[]; + readonly warnings?: readonly string[]; +}; + +const AUTOMATIC_REPAIR_CHECK_IDS = new Set([ + CHECK_IDS.policyToolsElevatedEnabled, + CHECK_IDS.policyGatewayControlUiInsecure, + CHECK_IDS.policyGatewayRemoteEnabled, + CHECK_IDS.policyDataHandlingRedactionDisabled, + CHECK_IDS.policyDataHandlingTelemetryContentCapture, +]); + +export function repairPolicyAutomaticNarrower( + ctx: HealthRepairContext, + findings: readonly HealthFinding[], + checkId: PolicyCheckId, +): Promise { + if (!workspaceRepairsEnabled(ctx)) { + return Promise.resolve(workspaceRepairsDisabledResult()); + } + if (!AUTOMATIC_REPAIR_CHECK_IDS.has(checkId)) { + return Promise.resolve({ + status: "skipped", + reason: "policy finding is not an automatic narrowing repair", + changes: [], + }); + } + if ( + findings.length === 0 || + findings.some( + (finding) => + finding.checkId !== checkId || + POLICY_FIX_METADATA_BY_CHECK_ID.get(finding.checkId)?.fixClass !== "automatic", + ) + ) { + return Promise.resolve({ + status: "skipped", + reason: "policy finding is not classified as automatic", + changes: [], + }); + } + + const patch = applyAutomaticPatch(ctx.cfg, findings, checkId); + if (patch.changes.length === 0) { + return Promise.resolve({ + status: "skipped", + reason: "policy automatic repair had no config changes to apply", + changes: [], + ...(patch.warnings !== undefined ? { warnings: patch.warnings } : {}), + }); + } + return Promise.resolve({ + status: "repaired", + config: patch.config, + changes: patch.changes, + ...(patch.warnings !== undefined ? { warnings: patch.warnings } : {}), + }); +} + +function applyAutomaticPatch( + cfg: OpenClawConfig, + findings: readonly HealthFinding[], + checkId: PolicyCheckId, +): RepairPatch { + switch (checkId) { + case CHECK_IDS.policyToolsElevatedEnabled: + if (hasScopedPolicyRequirement(findings)) { + return skippedUnsafeScopedRepair( + cfg, + "Skipped scoped tools repair. Scoped elevated-tools policy findings are detect-only because automatic repair cannot safely choose between shared and agent-local config targets.", + ); + } + return disableElevatedTools(cfg, findings); + case CHECK_IDS.policyGatewayControlUiInsecure: + return disableInsecureControlUi(cfg, findings); + case CHECK_IDS.policyGatewayRemoteEnabled: + return disableRemoteGatewayMode(cfg, findings); + case CHECK_IDS.policyDataHandlingRedactionDisabled: + if (hasScopedPolicyRequirement(findings)) { + return skippedUnsafeScopedRepair( + cfg, + "Skipped scoped data-handling repair. The finding reports shared logging config, so changing it would affect more than the scoped policy target.", + ); + } + return enableSensitiveLoggingRedaction(cfg); + case CHECK_IDS.policyDataHandlingTelemetryContentCapture: + if (hasScopedPolicyRequirement(findings)) { + return skippedUnsafeScopedRepair( + cfg, + "Skipped scoped data-handling repair. The finding reports shared telemetry config, so changing it would affect more than the scoped policy target.", + ); + } + return disableTelemetryContentCapture(cfg); + default: + return { config: cfg, changes: [] }; + } +} + +function disableElevatedTools( + cfg: OpenClawConfig, + findings: readonly HealthFinding[], +): RepairPatch { + if ( + !findings.some((finding) => finding.ocPath === "oc://openclaw.config/tools/elevated/enabled") + ) { + return { config: cfg, changes: [] }; + } + const next = cloneConfig(cfg); + const tools = ensureRecord(next, "tools"); + const elevated = ensureRecord(tools, "elevated"); + if (elevated.enabled === false) { + return { config: cfg, changes: [] }; + } + elevated.enabled = false; + return { + config: next as OpenClawConfig, + changes: ["Set tools.elevated.enabled=false for policy conformance."], + }; +} + +function disableInsecureControlUi( + cfg: OpenClawConfig, + findings: readonly HealthFinding[], +): RepairPatch { + const next = cloneConfig(cfg); + const gateway = ensureRecord(next, "gateway"); + const controlUi = ensureRecord(gateway, "controlUi"); + const changes: string[] = []; + const fields = [ + ["allowInsecureAuth", "oc://openclaw.config/gateway/controlUi/allowInsecureAuth"], + [ + "dangerouslyDisableDeviceAuth", + "oc://openclaw.config/gateway/controlUi/dangerouslyDisableDeviceAuth", + ], + [ + "dangerouslyAllowHostHeaderOriginFallback", + "oc://openclaw.config/gateway/controlUi/dangerouslyAllowHostHeaderOriginFallback", + ], + ] as const; + const findingPaths = new Set(findings.map((finding) => finding.ocPath)); + for (const [field, ocPath] of fields) { + if (findingPaths.has(ocPath) && controlUi[field] !== false) { + controlUi[field] = false; + changes.push(`Set gateway.controlUi.${field}=false for policy conformance.`); + } + } + return changes.length > 0 + ? { config: next as OpenClawConfig, changes } + : { config: cfg, changes }; +} + +function disableRemoteGatewayMode( + cfg: OpenClawConfig, + findings: readonly HealthFinding[], +): RepairPatch { + if (!findings.some((finding) => finding.ocPath === "oc://openclaw.config/gateway/mode")) { + return { config: cfg, changes: [] }; + } + const next = cloneConfig(cfg); + const gateway = ensureRecord(next, "gateway"); + const changes: string[] = []; + if (gateway.mode === "remote") { + gateway.mode = "local"; + changes.push("Set gateway.mode=local for policy conformance."); + } + return changes.length > 0 + ? { config: next as OpenClawConfig, changes } + : { config: cfg, changes }; +} + +function enableSensitiveLoggingRedaction(cfg: OpenClawConfig): RepairPatch { + const next = cloneConfig(cfg); + const logging = ensureRecord(next, "logging"); + if (logging.redactSensitive !== "off") { + return { config: cfg, changes: [] }; + } + logging.redactSensitive = "tools"; + return { + config: next as OpenClawConfig, + changes: ["Set logging.redactSensitive=tools for policy conformance."], + }; +} + +function disableTelemetryContentCapture(cfg: OpenClawConfig): RepairPatch { + const next = cloneConfig(cfg); + const diagnostics = ensureRecord(next, "diagnostics"); + const otel = ensureRecord(diagnostics, "otel"); + if (otel.captureContent === false) { + return { config: cfg, changes: [] }; + } + otel.captureContent = false; + return { + config: next as OpenClawConfig, + changes: ["Set diagnostics.otel.captureContent=false for policy conformance."], + }; +} + +function cloneConfig(cfg: OpenClawConfig): ConfigRecord { + return structuredClone(cfg) as ConfigRecord; +} + +function workspaceRepairsEnabled(ctx: HealthRepairContext): boolean { + const plugins = isRecord(ctx.cfg.plugins) ? ctx.cfg.plugins : {}; + const entries = isRecord(plugins.entries) ? plugins.entries : {}; + const policy = isRecord(entries.policy) ? entries.policy : {}; + const config = isRecord(policy.config) ? policy.config : {}; + return config.workspaceRepairs === true; +} + +function workspaceRepairsDisabledResult(): HealthRepairResult { + const warning = + "Skipped policy config repair. Enable plugins.entries.policy.config.workspaceRepairs to let doctor --fix edit workspace policy config."; + return { + status: "skipped", + reason: "workspace repairs are disabled", + changes: [], + warnings: [warning], + }; +} + +function hasScopedPolicyRequirement(findings: readonly HealthFinding[]): boolean { + return findings.some((finding) => finding.requirement?.includes("/scopes/") === true); +} + +function skippedUnsafeScopedRepair(cfg: OpenClawConfig, warning: string): RepairPatch { + return { config: cfg, changes: [], warnings: [warning] }; +} + +function ensureRecord(parent: ConfigRecord, key: string): ConfigRecord { + const current = parent[key]; + if (isRecord(current)) { + const copy = { ...current }; + parent[key] = copy; + return copy; + } + const next: ConfigRecord = {}; + parent[key] = next; + return next; +} + +function isRecord(value: unknown): value is ConfigRecord { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/extensions/policy/src/doctor/register.test.ts b/extensions/policy/src/doctor/register.test.ts index dc7064ff3150..e320ff894a60 100644 --- a/extensions/policy/src/doctor/register.test.ts +++ b/extensions/policy/src/doctor/register.test.ts @@ -100,6 +100,20 @@ async function runDeniedChannelRepair(repairCheckCtx: HealthRepairContext) { return { ...result, config, remainingFindings }; } +async function runPolicyRepairCheck(checkId: string, repairCheckCtx: HealthRepairContext) { + resetPolicyDoctorChecksForTest(); + const check = registerChecks().find((entry) => entry.id === checkId); + if (check?.detect === undefined || check.repair === undefined) { + throw new Error(`${checkId} repair check was not registered`); + } + const findings = await check.detect(repairCheckCtx); + const result = await check.repair(repairCheckCtx, findings); + const config = result.config ?? repairCheckCtx.cfg; + const remainingFindings = + repairCheckCtx.dryRun === true ? [] : await check.detect({ ...repairCheckCtx, cfg: config }); + return { ...result, findings, config, remainingFindings }; +} + describe("registerPolicyDoctorChecks", () => { beforeEach(async () => { clearHealthChecksForTest(); @@ -1507,6 +1521,278 @@ describe("registerPolicyDoctorChecks", () => { expect(result.config.channels?.telegram).toEqual({ enabled: true }); }); + it("dry-runs automatic policy narrowing repairs without mutating config", async () => { + const configPath = join(workspaceDir, "openclaw.jsonc"); + const cfg = { + ...cfgWithPolicy({ workspaceRepairs: true }), + tools: { elevated: { enabled: true } }, + } as OpenClawConfig; + await fs.writeFile(configPath, "{}", "utf-8"); + await fs.writeFile( + join(workspaceDir, "policy.jsonc"), + JSON.stringify({ tools: { elevated: { allow: false } } }), + "utf-8", + ); + + const result = await runPolicyRepairCheck("policy/tools-elevated-enabled", { + ...repairCtx(configPath, cfg), + dryRun: true, + }); + + expect(result.status).toBe("repaired"); + expect(result.changes).toEqual(["Set tools.elevated.enabled=false for policy conformance."]); + expect(result.config.tools?.elevated?.enabled).toBe(false); + expect(cfg.tools?.elevated?.enabled).toBe(true); + }); + + it("does not repair automatic policy narrowing config without workspace repair opt-in", async () => { + const configPath = join(workspaceDir, "openclaw.jsonc"); + const cfg = { + ...cfgWithPolicy(), + tools: { elevated: { enabled: true } }, + } as OpenClawConfig; + await fs.writeFile(configPath, "{}", "utf-8"); + await fs.writeFile( + join(workspaceDir, "policy.jsonc"), + JSON.stringify({ tools: { elevated: { allow: false } } }), + "utf-8", + ); + + const result = await runPolicyRepairCheck( + "policy/tools-elevated-enabled", + repairCtx(configPath, cfg), + ); + + expect(result.status).toBe("skipped"); + expect(result.reason).toBe("workspace repairs are disabled"); + expect(result.changes).toEqual([]); + expect(result.warnings).toEqual([ + "Skipped policy config repair. Enable plugins.entries.policy.config.workspaceRepairs to let doctor --fix edit workspace policy config.", + ]); + expect(result.config.tools?.elevated?.enabled).toBe(true); + }); + + it("does not over-apply scoped elevated policy findings globally", async () => { + const configPath = join(workspaceDir, "openclaw.jsonc"); + const cfg = { + ...cfgWithPolicy({ workspaceRepairs: true }), + agents: { + list: [ + { + id: "reviewer", + tools: { elevated: { enabled: true } }, + }, + ], + }, + } as unknown as OpenClawConfig; + await fs.writeFile(configPath, "{}", "utf-8"); + await fs.writeFile( + join(workspaceDir, "policy.jsonc"), + JSON.stringify({ + scopes: { + reviewer: { + agentIds: ["reviewer"], + tools: { elevated: { allow: false } }, + }, + }, + }), + "utf-8", + ); + + const result = await runPolicyRepairCheck( + "policy/tools-elevated-enabled", + repairCtx(configPath, cfg), + ); + + expect(result.status).toBe("skipped"); + expect(result.reason).toBe("policy automatic repair had no config changes to apply"); + expect(result.config).not.toHaveProperty("tools.elevated.enabled"); + expect(result.config.agents?.list?.[0]).toMatchObject({ + id: "reviewer", + tools: { elevated: { enabled: true } }, + }); + }); + + it("skips scoped elevated repairs that inherit shared global tools config", async () => { + const configPath = join(workspaceDir, "openclaw.jsonc"); + const cfg = { + ...cfgWithPolicy({ workspaceRepairs: true }), + tools: { elevated: { enabled: true } }, + agents: { + list: [{ id: "reviewer" }], + }, + } as unknown as OpenClawConfig; + await fs.writeFile(configPath, "{}", "utf-8"); + await fs.writeFile( + join(workspaceDir, "policy.jsonc"), + JSON.stringify({ + scopes: { + reviewer: { + agentIds: ["reviewer"], + tools: { elevated: { allow: false } }, + }, + }, + }), + "utf-8", + ); + + const result = await runPolicyRepairCheck( + "policy/tools-elevated-enabled", + repairCtx(configPath, cfg), + ); + + expect(result.status).toBe("skipped"); + expect(result.reason).toBe("policy automatic repair had no config changes to apply"); + expect(result.changes).toEqual([]); + expect(result.warnings).toEqual([ + "Skipped scoped tools repair. Scoped elevated-tools policy findings are detect-only because automatic repair cannot safely choose between shared and agent-local config targets.", + ]); + expect(result.config.tools?.elevated?.enabled).toBe(true); + }); + + it("repairs automatic policy narrowing config findings", async () => { + const configPath = join(workspaceDir, "openclaw.jsonc"); + const cfg = { + ...cfgWithPolicy({ workspaceRepairs: true }), + tools: { elevated: { enabled: true } }, + gateway: { + mode: "remote", + remote: { enabled: true, url: "wss://remote.example.test:18789" }, + controlUi: { + allowInsecureAuth: true, + dangerouslyDisableDeviceAuth: true, + dangerouslyAllowHostHeaderOriginFallback: true, + }, + }, + logging: { redactSensitive: "off" }, + diagnostics: { otel: { enabled: true, captureContent: { enabled: true, toolInputs: true } } }, + } as unknown as OpenClawConfig; + await fs.writeFile(configPath, "{}", "utf-8"); + await fs.writeFile( + join(workspaceDir, "policy.jsonc"), + JSON.stringify({ + tools: { elevated: { allow: false } }, + gateway: { + controlUi: { allowInsecure: false }, + remote: { allow: false }, + }, + dataHandling: { + sensitiveLogging: { requireRedaction: true }, + telemetry: { denyContentCapture: true }, + }, + }), + "utf-8", + ); + + const elevated = await runPolicyRepairCheck( + "policy/tools-elevated-enabled", + repairCtx(configPath, cfg), + ); + const controlUi = await runPolicyRepairCheck( + "policy/gateway-control-ui-insecure", + repairCtx(configPath, elevated.config), + ); + const remote = await runPolicyRepairCheck( + "policy/gateway-remote-enabled", + repairCtx(configPath, controlUi.config), + ); + const redaction = await runPolicyRepairCheck( + "policy/data-handling-redaction-disabled", + repairCtx(configPath, remote.config), + ); + const telemetry = await runPolicyRepairCheck( + "policy/data-handling-telemetry-content-capture", + repairCtx(configPath, redaction.config), + ); + + expect([ + ...elevated.changes, + ...controlUi.changes, + ...remote.changes, + ...redaction.changes, + ...telemetry.changes, + ]).toEqual([ + "Set tools.elevated.enabled=false for policy conformance.", + "Set gateway.controlUi.allowInsecureAuth=false for policy conformance.", + "Set gateway.controlUi.dangerouslyDisableDeviceAuth=false for policy conformance.", + "Set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=false for policy conformance.", + "Set gateway.mode=local for policy conformance.", + "Set logging.redactSensitive=tools for policy conformance.", + "Set diagnostics.otel.captureContent=false for policy conformance.", + ]); + expect(telemetry.remainingFindings).toEqual([]); + expect(telemetry.config).toMatchObject({ + tools: { elevated: { enabled: false } }, + gateway: { + mode: "local", + remote: { + enabled: true, + }, + controlUi: { + allowInsecureAuth: false, + dangerouslyDisableDeviceAuth: false, + dangerouslyAllowHostHeaderOriginFallback: false, + }, + }, + logging: { redactSensitive: "tools" }, + diagnostics: { otel: { captureContent: false } }, + }); + }); + + it("skips scoped data-handling repairs that would mutate shared config", async () => { + const configPath = join(workspaceDir, "openclaw.jsonc"); + const cfg = { + ...cfgWithPolicy({ workspaceRepairs: true }), + logging: { redactSensitive: "off" }, + agents: { + list: [{ id: "reviewer" }], + }, + } as unknown as OpenClawConfig; + await fs.writeFile(configPath, "{}", "utf-8"); + await fs.writeFile( + join(workspaceDir, "policy.jsonc"), + JSON.stringify({ + scopes: { + reviewer: { + agentIds: ["reviewer"], + dataHandling: { + sensitiveLogging: { requireRedaction: true }, + }, + }, + }, + }), + "utf-8", + ); + + const result = await runPolicyRepairCheck( + "policy/data-handling-redaction-disabled", + repairCtx(configPath, cfg), + ); + + expect(result.status).toBe("skipped"); + expect(result.reason).toBe("policy automatic repair had no config changes to apply"); + expect(result.changes).toEqual([]); + expect(result.warnings).toEqual([ + "Skipped scoped data-handling repair. The finding reports shared logging config, so changing it would affect more than the scoped policy target.", + ]); + expect(result.config.logging?.redactSensitive).toBe("off"); + expect(result.remainingFindings).toEqual([ + expect.objectContaining({ + checkId: "policy/data-handling-redaction-disabled", + requirement: + "oc://policy.jsonc/scopes/reviewer/dataHandling/sensitiveLogging/requireRedaction", + }), + ]); + }); + + it("does not register repair for review-required policy findings", () => { + const check = registerChecks().find( + (entry) => entry.id === "policy/gateway-http-url-fetch-unrestricted", + ); + + expect("repair" in (check ?? {})).toBe(false); + }); + it("does not report denied providers for disabled channels", async () => { const configPath = join(workspaceDir, "openclaw.jsonc"); const cfg = { diff --git a/extensions/policy/src/doctor/scopes/data-auth.ts b/extensions/policy/src/doctor/scopes/data-auth.ts index b28cd42cda86..52987e71adbf 100644 --- a/extensions/policy/src/doctor/scopes/data-auth.ts +++ b/extensions/policy/src/doctor/scopes/data-auth.ts @@ -1,5 +1,6 @@ // Policy doctor health-check factories for one policy scope. import type { HealthCheck } from "openclaw/plugin-sdk/health"; +import { repairPolicyAutomaticNarrower } from "../automatic-repairs.js"; import { CHECK_IDS } from "../metadata.js"; import type { PolicyDoctorCheckDeps } from "../types.js"; @@ -17,6 +18,13 @@ export function createPolicyDataAuthChecks(deps: PolicyDoctorCheckDeps): readonl CHECK_IDS.policyDataHandlingRedactionDisabled, ); }, + repair(ctx, findings) { + return repairPolicyAutomaticNarrower( + ctx, + findings, + CHECK_IDS.policyDataHandlingRedactionDisabled, + ); + }, }; const policyDataHandlingTelemetryContentCaptureCheck: HealthCheck = { id: CHECK_IDS.policyDataHandlingTelemetryContentCapture, @@ -29,6 +37,13 @@ export function createPolicyDataAuthChecks(deps: PolicyDoctorCheckDeps): readonl CHECK_IDS.policyDataHandlingTelemetryContentCapture, ); }, + repair(ctx, findings) { + return repairPolicyAutomaticNarrower( + ctx, + findings, + CHECK_IDS.policyDataHandlingTelemetryContentCapture, + ); + }, }; const policyDataHandlingSessionRetentionNotEnforcedCheck: HealthCheck = { id: CHECK_IDS.policyDataHandlingSessionRetentionNotEnforced, diff --git a/extensions/policy/src/doctor/scopes/gateway.ts b/extensions/policy/src/doctor/scopes/gateway.ts index 009fca60b176..b949b1d7f93d 100644 --- a/extensions/policy/src/doctor/scopes/gateway.ts +++ b/extensions/policy/src/doctor/scopes/gateway.ts @@ -1,6 +1,7 @@ // Policy doctor checks and findings for gateway exposure policy. import type { HealthCheck, HealthFinding } from "openclaw/plugin-sdk/health"; import type { PolicyEvidence } from "../../policy-state.js"; +import { repairPolicyAutomaticNarrower } from "../automatic-repairs.js"; import { CHECK_IDS } from "../metadata.js"; import type { PolicyDoctorCheckDeps } from "../types.js"; import { readPolicyBoolean, readStringList } from "../utils.js"; @@ -43,6 +44,9 @@ export function createPolicyGatewayChecks(deps: PolicyDoctorCheckDeps): readonly async detect(ctx) { return findingsForCheck(await evaluatePolicy(ctx), CHECK_IDS.policyGatewayControlUiInsecure); }, + repair(ctx, findings) { + return repairPolicyAutomaticNarrower(ctx, findings, CHECK_IDS.policyGatewayControlUiInsecure); + }, }; const policyGatewayTailscaleFunnelCheck: HealthCheck = { id: CHECK_IDS.policyGatewayTailscaleFunnel, @@ -61,6 +65,9 @@ export function createPolicyGatewayChecks(deps: PolicyDoctorCheckDeps): readonly async detect(ctx) { return findingsForCheck(await evaluatePolicy(ctx), CHECK_IDS.policyGatewayRemoteEnabled); }, + repair(ctx, findings) { + return repairPolicyAutomaticNarrower(ctx, findings, CHECK_IDS.policyGatewayRemoteEnabled); + }, }; const policyGatewayHttpEndpointEnabledCheck: HealthCheck = { id: CHECK_IDS.policyGatewayHttpEndpointEnabled, diff --git a/extensions/policy/src/doctor/scopes/tools.ts b/extensions/policy/src/doctor/scopes/tools.ts index 4504addf9bda..4f1bc668dcd6 100644 --- a/extensions/policy/src/doctor/scopes/tools.ts +++ b/extensions/policy/src/doctor/scopes/tools.ts @@ -1,5 +1,6 @@ // Policy doctor health-check factories for one policy scope. import type { HealthCheck } from "openclaw/plugin-sdk/health"; +import { repairPolicyAutomaticNarrower } from "../automatic-repairs.js"; import { CHECK_IDS } from "../metadata.js"; import type { PolicyDoctorCheckDeps } from "../types.js"; @@ -86,6 +87,9 @@ export function createPolicyAgentToolChecks(deps: PolicyDoctorCheckDeps): readon async detect(ctx) { return findingsForCheck(await evaluatePolicy(ctx), CHECK_IDS.policyToolsElevatedEnabled); }, + repair(ctx, findings) { + return repairPolicyAutomaticNarrower(ctx, findings, CHECK_IDS.policyToolsElevatedEnabled); + }, }; const policyToolsAlsoAllowMissingCheck: HealthCheck = { id: CHECK_IDS.policyToolsAlsoAllowMissing,