policy: classify doctor fix recommendations

This commit is contained in:
Gio Della-Libera
2026-07-03 15:34:59 -07:00
parent 1efc7270cd
commit f466ebc00c
5 changed files with 597 additions and 7 deletions

View File

@@ -0,0 +1,453 @@
// Policy doctor fix metadata classifies findings before patch builders exist.
import { CHECK_IDS, POLICY_CHECK_IDS } from "./metadata.js";
export type PolicyFixClass =
| "automatic"
| "reviewRequired"
| "manual"
| "validateOnly"
| "unsupported";
export type PolicyFixMetadata = {
readonly checkId: (typeof POLICY_CHECK_IDS)[number];
readonly fixClass: PolicyFixClass;
readonly policyPath?: readonly string[];
readonly configTargets?: readonly string[];
readonly summary: string;
};
const m = (
checkId: (typeof POLICY_CHECK_IDS)[number],
fixClass: PolicyFixClass,
summary: string,
options: Omit<PolicyFixMetadata, "checkId" | "fixClass" | "summary"> = {},
): PolicyFixMetadata => ({
checkId,
fixClass,
summary,
...options,
});
export const POLICY_FIX_METADATA = [
m(CHECK_IDS.policyMissingFile, "manual", "Restore or author the approved policy artifact."),
m(CHECK_IDS.policyInvalidFile, "manual", "Repair the policy JSONC syntax or schema."),
m(
CHECK_IDS.policyHashMismatch,
"manual",
"Restore the approved artifact or update the expected hash after review.",
{ configTargets: ["plugins.entries.policy.config.expectedHash"] },
),
m(
CHECK_IDS.policyAttestationMismatch,
"manual",
"Review the current attestation and update accepted hashes after approval.",
{ configTargets: ["plugins.entries.policy.config.expectedAttestationHash"] },
),
m(
CHECK_IDS.policyDeniedChannelProvider,
"automatic",
"Disable product-managed channels matching the denied provider.",
{ policyPath: ["channels", "denyRules"], configTargets: ["channels"] },
),
m(CHECK_IDS.policyDeniedMcpServer, "reviewRequired", "Remove or disable the denied MCP server.", {
policyPath: ["mcp", "servers", "deny"],
configTargets: ["mcp.servers"],
}),
m(
CHECK_IDS.policyUnapprovedMcpServer,
"reviewRequired",
"Remove the unapproved MCP server or select an approved replacement.",
{ policyPath: ["mcp", "servers", "allow"], configTargets: ["mcp.servers"] },
),
m(
CHECK_IDS.policyDeniedModelProvider,
"reviewRequired",
"Remove the model provider or switch references to an approved provider.",
{ policyPath: ["models", "providers", "deny"], configTargets: ["models"] },
),
m(
CHECK_IDS.policyUnapprovedModelProvider,
"reviewRequired",
"Select an approved model provider.",
{ policyPath: ["models", "providers", "allow"], configTargets: ["models"] },
),
m(
CHECK_IDS.policyPrivateNetworkAccess,
"reviewRequired",
"Disable the concrete private-network access opt-in.",
{ policyPath: ["network", "privateNetwork", "allow"], configTargets: ["network"] },
),
m(
CHECK_IDS.policyIngressDmPolicyUnapproved,
"reviewRequired",
"Set channel DM policy to an allowed value.",
{ policyPath: ["ingress", "channels", "allowDmPolicies"], configTargets: ["channels"] },
),
m(
CHECK_IDS.policyIngressDmScopeUnapproved,
"reviewRequired",
"Move session DM scope to the required or stricter ordered value.",
{ policyPath: ["ingress", "session", "requireDmScope"], configTargets: ["ingress"] },
),
m(
CHECK_IDS.policyIngressOpenGroupsDenied,
"automatic",
"Disable product-managed open group ingress.",
{ policyPath: ["ingress", "channels", "denyOpenGroups"], configTargets: ["channels"] },
),
m(
CHECK_IDS.policyIngressGroupMentionRequired,
"automatic",
"Require mention in product-managed group channels.",
{ policyPath: ["ingress", "channels", "requireMentionInGroups"], configTargets: ["channels"] },
),
m(
CHECK_IDS.policyGatewayNonLoopbackBind,
"reviewRequired",
"Set gateway bind address to loopback when remote exposure is not intended.",
{
policyPath: ["gateway", "exposure", "allowNonLoopbackBind"],
configTargets: ["gateway.bind"],
},
),
m(
CHECK_IDS.policyGatewayAuthDisabled,
"manual",
"Configure token, password, or trusted-proxy auth.",
{ policyPath: ["gateway", "auth", "requireAuth"], configTargets: ["gateway.auth"] },
),
m(
CHECK_IDS.policyGatewayRateLimitMissing,
"reviewRequired",
"Add explicit gateway auth rate limits from product defaults.",
{
policyPath: ["gateway", "auth", "requireExplicitRateLimit"],
configTargets: ["gateway.auth.rateLimit"],
},
),
m(
CHECK_IDS.policyGatewayControlUiInsecure,
"automatic",
"Disable the insecure Control UI toggle.",
{ policyPath: ["gateway", "controlUi", "allowInsecure"], configTargets: ["gateway.controlUi"] },
),
m(
CHECK_IDS.policyGatewayTailscaleFunnel,
"reviewRequired",
"Disable Tailscale funnel or serve exposure.",
{ policyPath: ["gateway", "exposure", "allowTailscaleFunnel"], configTargets: ["tailscale"] },
),
m(
CHECK_IDS.policyGatewayRemoteEnabled,
"automatic",
"Disable product-managed remote gateway mode.",
{ policyPath: ["gateway", "remote", "allow"], configTargets: ["gateway.remote"] },
),
m(
CHECK_IDS.policyGatewayHttpEndpointEnabled,
"reviewRequired",
"Disable denied Gateway HTTP endpoints.",
{ policyPath: ["gateway", "http", "denyEndpoints"], configTargets: ["gateway.http"] },
),
m(
CHECK_IDS.policyGatewayHttpUrlFetchUnrestricted,
"manual",
"Add URL allowlists for each URL-fetch input.",
{ policyPath: ["gateway", "http", "requireUrlAllowlists"], configTargets: ["gateway.http"] },
),
m(
CHECK_IDS.policyGatewayNodeCommandDenied,
"reviewRequired",
"Add the command to gateway node denyCommands or update policy after review.",
{
policyPath: ["gateway", "nodes", "denyCommands"],
configTargets: ["gateway.nodes.denyCommands"],
},
),
m(
CHECK_IDS.policyAgentsWorkspaceAccessDenied,
"reviewRequired",
"Set agent workspace access to an allowed mode.",
{ policyPath: ["agents", "workspace", "allowedAccess"], configTargets: ["agents"] },
),
m(
CHECK_IDS.policyAgentsToolNotDenied,
"automatic",
"Merge required built-in workspace tool denies.",
{ policyPath: ["agents", "workspace", "denyTools"], configTargets: ["agents"] },
),
m(
CHECK_IDS.policyToolsProfileUnapproved,
"reviewRequired",
"Set the tool profile to an allowed profile.",
{ policyPath: ["tools", "profiles", "allow"], configTargets: ["tools.profile"] },
),
m(
CHECK_IDS.policyToolsFsWorkspaceOnlyRequired,
"reviewRequired",
"Set workspace-only filesystem posture when required assets remain readable.",
{
policyPath: ["tools", "fs", "requireWorkspaceOnly"],
configTargets: ["tools.fs.workspaceOnly"],
},
),
m(
CHECK_IDS.policyToolsExecSecurityUnapproved,
"reviewRequired",
"Set exec security to an allowed value.",
{ policyPath: ["tools", "exec", "allowSecurity"], configTargets: ["tools.exec.security"] },
),
m(
CHECK_IDS.policyToolsExecAskUnapproved,
"reviewRequired",
"Set exec ask mode to an allowed value.",
{ policyPath: ["tools", "exec", "requireAsk"], configTargets: ["tools.exec.ask"] },
),
m(
CHECK_IDS.policyToolsExecHostUnapproved,
"reviewRequired",
"Move exec host to an allowed host mode.",
{ policyPath: ["tools", "exec", "allowHosts"], configTargets: ["tools.exec.host"] },
),
m(CHECK_IDS.policyToolsElevatedEnabled, "automatic", "Set tools elevated mode to disabled.", {
policyPath: ["tools", "elevated", "allow"],
configTargets: ["tools.elevated.enabled"],
}),
m(
CHECK_IDS.policyToolsAlsoAllowMissing,
"reviewRequired",
"Add expected alsoAllow entries only when policy intentionally grants them.",
{ policyPath: ["tools", "alsoAllow", "expected"], configTargets: ["tools.alsoAllow"] },
),
m(
CHECK_IDS.policyToolsAlsoAllowUnexpected,
"reviewRequired",
"Remove unexpected alsoAllow entries.",
{ policyPath: ["tools", "alsoAllow", "expected"], configTargets: ["tools.alsoAllow"] },
),
m(
CHECK_IDS.policyToolsRequiredDenyMissing,
"automatic",
"Merge required built-in deny tool classes.",
{ policyPath: ["tools", "denyTools"], configTargets: ["tools.denyTools"] },
),
m(
CHECK_IDS.policySandboxModeUnapproved,
"reviewRequired",
"Set sandbox mode to an allowed value.",
{ policyPath: ["sandbox", "requireMode"], configTargets: ["sandbox.mode"] },
),
m(
CHECK_IDS.policySandboxBackendUnapproved,
"reviewRequired",
"Choose an approved sandbox backend that is installed.",
{ policyPath: ["sandbox", "allowBackends"], configTargets: ["sandbox.backend"] },
),
m(
CHECK_IDS.policySandboxContainerPostureUnobservable,
"unsupported",
"Add observable container posture evidence before patching.",
),
m(
CHECK_IDS.policySandboxContainerHostNetworkDenied,
"reviewRequired",
"Disable container host networking.",
{
policyPath: ["sandbox", "containers", "denyHostNetwork"],
configTargets: ["sandbox.containers"],
},
),
m(
CHECK_IDS.policySandboxContainerNamespaceJoinDenied,
"reviewRequired",
"Disable joining container namespaces.",
{
policyPath: ["sandbox", "containers", "denyContainerNamespaceJoin"],
configTargets: ["sandbox.containers"],
},
),
m(
CHECK_IDS.policySandboxContainerMountModeRequired,
"reviewRequired",
"Change required mounts to read-only.",
{
policyPath: ["sandbox", "containers", "requireReadOnlyMounts"],
configTargets: ["sandbox.containers"],
},
),
m(
CHECK_IDS.policySandboxContainerRuntimeSocketMount,
"reviewRequired",
"Remove container runtime socket binds.",
{
policyPath: ["sandbox", "containers", "denyContainerRuntimeSocketMounts"],
configTargets: ["sandbox.containers"],
},
),
m(
CHECK_IDS.policySandboxContainerUnconfinedProfile,
"reviewRequired",
"Remove unconfined container profiles.",
{
policyPath: ["sandbox", "containers", "denyUnconfinedProfiles"],
configTargets: ["sandbox.containers"],
},
),
m(
CHECK_IDS.policySandboxBrowserCdpSourceRangeMissing,
"manual",
"Add an explicit browser CDP source range.",
{
policyPath: ["sandbox", "browser", "requireCdpSourceRange"],
configTargets: ["agents.sandbox.browser"],
},
),
m(
CHECK_IDS.policyDataHandlingRedactionDisabled,
"automatic",
"Set sensitive logging to a redacting mode.",
{
policyPath: ["dataHandling", "sensitiveLogging", "requireRedaction"],
configTargets: ["logging.redactSensitive"],
},
),
m(
CHECK_IDS.policyDataHandlingTelemetryContentCapture,
"automatic",
"Disable telemetry content capture.",
{
policyPath: ["dataHandling", "telemetry", "denyContentCapture"],
configTargets: ["diagnostics.otel.captureContent"],
},
),
m(
CHECK_IDS.policyDataHandlingSessionRetentionNotEnforced,
"reviewRequired",
"Set session maintenance to enforced mode.",
{
policyPath: ["dataHandling", "retention", "requireSessionMaintenance"],
configTargets: ["session.maintenance.mode"],
},
),
m(
CHECK_IDS.policyDataHandlingSessionTranscriptMemory,
"reviewRequired",
"Disable transcript indexing for the affected agent scope.",
{
policyPath: ["dataHandling", "memory", "denySessionTranscriptIndexing"],
configTargets: ["memory"],
},
),
m(
CHECK_IDS.policySecretsUnmanagedProvider,
"manual",
"Migrate the secret to a managed provider.",
{ policyPath: ["secrets", "requireManagedProviders"], configTargets: ["secrets"] },
),
m(
CHECK_IDS.policySecretsDeniedProviderSource,
"reviewRequired",
"Move the secret out of the denied source.",
{ policyPath: ["secrets", "denySources"], configTargets: ["secrets"] },
),
m(
CHECK_IDS.policySecretsInsecureProvider,
"reviewRequired",
"Remove insecure provider overrides.",
{ policyPath: ["secrets", "allowInsecureProviders"], configTargets: ["secrets"] },
),
m(
CHECK_IDS.policyAuthProfileInvalidMetadata,
"manual",
"Add required provider and mode metadata to auth profiles.",
{ policyPath: ["auth", "profiles", "requireMetadata"], configTargets: ["auth.profiles"] },
),
m(
CHECK_IDS.policyAuthProfileUnapprovedMode,
"manual",
"Change auth mode and credentials through the auth owner flow.",
{ policyPath: ["auth", "profiles", "allowModes"], configTargets: ["auth.profiles"] },
),
m(
CHECK_IDS.policyExecApprovalsMissing,
"manual",
"Restore an attributable exec-approvals evidence file.",
{ policyPath: ["execApprovals", "requireFile"], configTargets: ["exec-approvals.json"] },
),
m(CHECK_IDS.policyExecApprovalsInvalid, "manual", "Repair the exec approvals evidence artifact."),
m(
CHECK_IDS.policyExecApprovalsDefaultSecurityUnapproved,
"manual",
"Update reviewed default approval evidence or policy.",
{
policyPath: ["execApprovals", "defaults", "allowSecurity"],
configTargets: ["exec-approvals.json"],
},
),
m(
CHECK_IDS.policyExecApprovalsAgentSecurityUnapproved,
"manual",
"Update reviewed agent approval evidence or policy.",
{
policyPath: ["execApprovals", "agents", "allowSecurity"],
configTargets: ["exec-approvals.json"],
},
),
m(
CHECK_IDS.policyExecApprovalsAutoAllowSkillsEnabled,
"reviewRequired",
"Disable auto-allow skills in the approval owner surface.",
{
policyPath: ["execApprovals", "agents", "allowAutoAllowSkills"],
configTargets: ["exec-approvals.json"],
},
),
m(
CHECK_IDS.policyExecApprovalsAllowlistMissing,
"manual",
"Add expected approval patterns through approval review.",
{
policyPath: ["execApprovals", "agents", "allowlist", "expected"],
configTargets: ["exec-approvals.json"],
},
),
m(
CHECK_IDS.policyExecApprovalsAllowlistUnexpected,
"manual",
"Remove unexpected approval patterns through approval review.",
{
policyPath: ["execApprovals", "agents", "allowlist", "expected"],
configTargets: ["exec-approvals.json"],
},
),
m(
CHECK_IDS.policyMissingToolRisk,
"manual",
"Add tool risk metadata in the owning tool declaration.",
{ policyPath: ["tools", "requireMetadata"], configTargets: ["tools"] },
),
m(CHECK_IDS.policyUnknownToolRisk, "manual", "Use a supported tool risk level.", {
policyPath: ["tools", "requireMetadata"],
configTargets: ["tools"],
}),
m(
CHECK_IDS.policyMissingToolSensitivity,
"manual",
"Add tool sensitivity metadata in the owning tool declaration.",
{ policyPath: ["tools", "requireMetadata"], configTargets: ["tools"] },
),
m(
CHECK_IDS.policyMissingToolOwner,
"manual",
"Add owner metadata in the owning tool declaration.",
{ policyPath: ["tools", "requireMetadata"], configTargets: ["tools"] },
),
m(CHECK_IDS.policyUnknownToolSensitivity, "manual", "Use a supported tool sensitivity token.", {
policyPath: ["tools", "requireMetadata"],
configTargets: ["tools"],
}),
] as const satisfies readonly PolicyFixMetadata[];
export const POLICY_FIX_METADATA_BY_CHECK_ID = new Map(
POLICY_FIX_METADATA.map((rule) => [rule.checkId, rule] as const),
);

View File

@@ -1,6 +1,7 @@
// Policy doctor metadata tests cover rule metadata.
import { describe, expect, it } from "vitest";
import { POLICY_RULE_METADATA, type PolicyRuleMetadata } from "./metadata.js";
import { POLICY_FIX_METADATA, POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
import { POLICY_CHECK_IDS, POLICY_RULE_METADATA, type PolicyRuleMetadata } from "./metadata.js";
describe("policy doctor metadata", () => {
it("describes strictness for agent-scoped policy fields", () => {
@@ -158,4 +159,97 @@ describe("policy doctor metadata", () => {
},
]);
});
it("classifies every policy finding for fix recommendation coverage", () => {
expect(POLICY_FIX_METADATA.map((rule) => rule.checkId)).toHaveLength(
new Set(POLICY_FIX_METADATA.map((rule) => rule.checkId)).size,
);
expect([...POLICY_FIX_METADATA_BY_CHECK_ID.keys()].toSorted()).toEqual(
[...POLICY_CHECK_IDS].toSorted(),
);
});
it("keeps policy fix class assignments explicit", () => {
const grouped = Object.groupBy(POLICY_FIX_METADATA, (rule) => rule.fixClass);
expect({
automatic: grouped.automatic?.map((rule) => rule.checkId).toSorted(),
manual: grouped.manual?.map((rule) => rule.checkId).toSorted(),
reviewRequired: grouped.reviewRequired?.map((rule) => rule.checkId).toSorted(),
unsupported: grouped.unsupported?.map((rule) => rule.checkId).toSorted(),
validateOnly: grouped.validateOnly?.map((rule) => rule.checkId).toSorted() ?? [],
}).toEqual({
automatic: [
"policy/agents-tool-not-denied",
"policy/channels-denied-provider",
"policy/data-handling-redaction-disabled",
"policy/data-handling-telemetry-content-capture",
"policy/gateway-control-ui-insecure",
"policy/gateway-remote-enabled",
"policy/ingress-group-mention-required",
"policy/ingress-open-groups-denied",
"policy/tools-elevated-enabled",
"policy/tools-required-deny-missing",
],
manual: [
"policy/attestation-hash-mismatch",
"policy/auth-profile-invalid-metadata",
"policy/auth-profile-unapproved-mode",
"policy/exec-approvals-agent-security-unapproved",
"policy/exec-approvals-allowlist-missing",
"policy/exec-approvals-allowlist-unexpected",
"policy/exec-approvals-default-security-unapproved",
"policy/exec-approvals-invalid",
"policy/exec-approvals-missing",
"policy/gateway-auth-disabled",
"policy/gateway-http-url-fetch-unrestricted",
"policy/policy-hash-mismatch",
"policy/policy-jsonc-invalid",
"policy/policy-jsonc-missing",
"policy/sandbox-browser-cdp-source-range-missing",
"policy/secrets-unmanaged-provider",
"policy/tools-missing-owner",
"policy/tools-missing-risk-level",
"policy/tools-missing-sensitivity-token",
"policy/tools-unknown-risk-level",
"policy/tools-unknown-sensitivity-token",
],
reviewRequired: [
"policy/agents-workspace-access-denied",
"policy/data-handling-session-retention-not-enforced",
"policy/data-handling-session-transcript-memory-enabled",
"policy/exec-approvals-auto-allow-skills-enabled",
"policy/gateway-http-endpoint-enabled",
"policy/gateway-node-command-denied",
"policy/gateway-non-loopback-bind",
"policy/gateway-rate-limit-missing",
"policy/gateway-tailscale-funnel",
"policy/ingress-dm-policy-unapproved",
"policy/ingress-dm-scope-unapproved",
"policy/mcp-denied-server",
"policy/mcp-unapproved-server",
"policy/models-denied-provider",
"policy/models-unapproved-provider",
"policy/network-private-access-enabled",
"policy/sandbox-backend-unapproved",
"policy/sandbox-container-host-network-denied",
"policy/sandbox-container-mount-mode-required",
"policy/sandbox-container-namespace-join-denied",
"policy/sandbox-container-runtime-socket-mount",
"policy/sandbox-container-unconfined-profile",
"policy/sandbox-mode-unapproved",
"policy/secrets-denied-provider-source",
"policy/secrets-insecure-provider",
"policy/tools-also-allow-missing",
"policy/tools-also-allow-unexpected",
"policy/tools-exec-ask-unapproved",
"policy/tools-exec-host-unapproved",
"policy/tools-exec-security-unapproved",
"policy/tools-fs-workspace-only-required",
"policy/tools-profile-unapproved",
],
unsupported: ["policy/sandbox-container-posture-unobservable"],
validateOnly: [],
});
});
});

View File

@@ -19,7 +19,11 @@ import {
scanPolicyIngress,
scanPolicyMcpServers,
} from "../policy-state.js";
import { registerPolicyDoctorChecks, resetPolicyDoctorChecksForTest } from "./register.js";
import {
evaluatePolicy,
registerPolicyDoctorChecks,
resetPolicyDoctorChecksForTest,
} from "./register.js";
let workspaceDir: string;
let originalOpenClawHome: string | undefined;
@@ -1406,8 +1410,18 @@ describe("registerPolicyDoctorChecks", () => {
target: "oc://openclaw.config/channels/telegram",
requirement: "oc://policy.jsonc/channels/denyRules/#0",
fixHint: "Telegram is not approved for this workspace.",
fixRecommendation: {
fixClass: "automatic",
policyPath: ["channels", "denyRules"],
configTargets: ["channels"],
summary: "Disable product-managed channels matching the denied provider.",
},
}),
]);
const evaluation = await evaluatePolicy(ctx(configPath, cfg));
expect(evaluation.findings[0]).toHaveProperty("fixRecommendation");
expect(evaluation.attestedFindings[0]).not.toHaveProperty("fixRecommendation");
});
it("repairs denied enabled channels by disabling them when workspace repairs are enabled", async () => {

View File

@@ -29,6 +29,7 @@ import { POLICY_TOOL_GROUPS } from "../tool-policy-conformance.js";
const loadFsPromisesModule = createLazyRuntimeModule(() => import("node:fs/promises"));
import { createPolicyDoctorChecks } from "./checks.js";
import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
import {
CHECK_IDS,
POLICY_CHECK_IDS,
@@ -175,7 +176,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
policyPath,
evidence,
expectedAttestationHash: settings.expectedAttestationHash,
findings,
findings: withPolicyFixRecommendations(findings),
attestedFindings: findings,
};
}
@@ -194,7 +195,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
policyPath,
evidence,
expectedAttestationHash: settings.expectedAttestationHash,
findings,
findings: withPolicyFixRecommendations(findings),
attestedFindings: findings,
};
}
@@ -206,7 +207,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
policyPath,
evidence,
expectedAttestationHash: settings.expectedAttestationHash,
findings,
findings: withPolicyFixRecommendations(findings),
attestedFindings: findings,
};
}
@@ -234,7 +235,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
policy: { value: policy, hash: policyHash },
evidence,
expectedAttestationHash: settings.expectedAttestationHash,
findings,
findings: withPolicyFixRecommendations(findings),
attestedFindings: findings,
};
}
@@ -343,11 +344,33 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
policy: { value: policy, hash: policyHash },
evidence,
expectedAttestationHash: settings.expectedAttestationHash,
findings,
findings: withPolicyFixRecommendations(findings),
attestedFindings: policyFindings,
};
}
function withPolicyFixRecommendations(
findings: readonly HealthFinding[],
): readonly HealthFinding[] {
return findings.map((finding) => {
const metadata = POLICY_FIX_METADATA_BY_CHECK_ID.get(
finding.checkId as (typeof POLICY_CHECK_IDS)[number],
);
if (metadata === undefined) {
return finding;
}
return {
...finding,
fixRecommendation: {
fixClass: metadata.fixClass,
...(metadata.policyPath !== undefined ? { policyPath: metadata.policyPath } : {}),
...(metadata.configTargets !== undefined ? { configTargets: metadata.configTargets } : {}),
summary: metadata.summary,
},
};
});
}
function policyParseFinding(
policyPath: string,
policyDocName: string,

View File

@@ -44,6 +44,12 @@ export interface HealthFinding {
readonly target?: string;
readonly requirement?: string;
readonly fixHint?: string;
readonly fixRecommendation?: {
readonly fixClass: "automatic" | "reviewRequired" | "manual" | "validateOnly" | "unsupported";
readonly policyPath?: readonly string[];
readonly configTargets?: readonly string[];
readonly summary: string;
};
}
export type HealthCheckMode = "doctor" | "lint" | "fix";