mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
policy: repair automatic narrowing findings (#99690)
This commit is contained in:
+18
-4
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
type RepairPatch = {
|
||||
readonly config: OpenClawConfig;
|
||||
readonly changes: readonly string[];
|
||||
readonly warnings?: readonly string[];
|
||||
};
|
||||
|
||||
const AUTOMATIC_REPAIR_CHECK_IDS = new Set<PolicyCheckId>([
|
||||
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<HealthRepairResult> {
|
||||
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);
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user