diff --git a/docs/cli/policy.md b/docs/cli/policy.md index 0f073b0e2aab..87709291c4c5 100644 --- a/docs/cli/policy.md +++ b/docs/cli/policy.md @@ -196,11 +196,11 @@ Cross-cutting notes not obvious from the rule tables below: - Secret and auth-profile evidence records provider/source posture and SecretRef metadata only, never raw values. Policy does not read or attest per-agent credential stores such as `auth-profiles.json`. -- Data-handling evidence is config-level posture only (redaction mode, - telemetry capture toggle, session maintenance mode, transcript-indexing - setting). It does not inspect logs, telemetry exports, transcripts, or - memory files, and a clean result does not prove that no personal data or - secrets exist in them. +- Data-handling evidence is config-level posture (telemetry capture toggle, + session maintenance mode, transcript-indexing setting) plus the always-on log + redaction invariant. It does not inspect logs, telemetry exports, + transcripts, or memory files, and a clean result does not prove that no + personal data or secrets exist in them. - Routing probes reuse OpenClaw's runtime binding resolver. Routing evidence records only the probe id, resolved agent, match kind, and redacted binding metadata. It never records peer, account, guild, team, or role identifiers. @@ -411,7 +411,7 @@ allowlist such as `["all"]`. | Policy field | Observed state | Use when | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| `dataHandling.sensitiveLogging.requireRedaction` | Sensitive log redaction, which is always on | Set to `true` to record the requirement; OpenClaw always satisfies it. | +| `dataHandling.sensitiveLogging.requireRedaction` | Runtime invariant `oc://openclaw.invariant/logging/redaction` | Set to `true` to record the requirement; OpenClaw always satisfies it. | | `dataHandling.telemetry.denyContentCapture` | `diagnostics.otel.captureContent` | Set to `true` to reject telemetry content capture. | | `dataHandling.retention.requireSessionMaintenance` | `session.maintenance.mode` | Set to `true` to require effective session maintenance mode `enforce`. | | `dataHandling.memory.denySessionTranscriptIndexing` | `memory.qmd.sessions.enabled`, `memory.search.experimental.sessionMemory`, and per-agent overrides | Set to `true` to reject session transcript indexing into memory. | @@ -845,7 +845,6 @@ the interval. | `policy/sandbox-container-runtime-socket-mount` | A container-backed sandbox or browser mount exposes the container runtime socket. | | `policy/sandbox-container-unconfined-profile` | Container sandbox profile is unconfined when policy denies it. | | `policy/sandbox-browser-cdp-source-range-missing` | Sandbox browser CDP source range is missing when policy requires one. | -| `policy/data-handling-redaction-disabled` | Sensitive logging redaction is disabled when policy requires it. | | `policy/data-handling-telemetry-content-capture` | Telemetry content capture is enabled when policy denies it. | | `policy/data-handling-session-retention-not-enforced` | Session retention maintenance is not enforced when policy requires it. | | `policy/data-handling-session-transcript-memory-enabled` | Session transcript memory indexing is enabled when policy denies it. | @@ -1013,8 +1012,13 @@ Scoped elevated-tools repairs are detect-only. Scoped data-handling repairs are also skipped when the finding reports shared telemetry config, because changing the shared setting would affect more than the scoped policy target. -`dataHandling.sensitiveLogging.requireRedaction` has no repair. Sensitive log -redaction is unconditional, so the check only validates the policy declaration. +`dataHandling.sensitiveLogging.requireRedaction` has no check and no repair. +Sensitive log redaction is unconditional in OpenClaw, so nothing can report it +as disabled. The key stays a supported policy rule: `openclaw policy` validates +its shape, `openclaw policy compare` still requires a candidate policy to be at +least as strict as the baseline for it, and `openclaw policy check` records the +runtime invariant `oc://openclaw.invariant/logging/redaction` in the +`dataHandling` evidence and attestation as proof the requirement is satisfied. Scoped required-deny repairs are skipped when the finding reports inherited root `tools.deny`, because adding the required tool to root config would affect diff --git a/extensions/policy/src/doctor/check-ids.ts b/extensions/policy/src/doctor/check-ids.ts index d8e3e5a48363..1a46d7c2d2c6 100644 --- a/extensions/policy/src/doctor/check-ids.ts +++ b/extensions/policy/src/doctor/check-ids.ts @@ -47,7 +47,6 @@ export const CHECK_IDS = { policySandboxContainerRuntimeSocketMount: "policy/sandbox-container-runtime-socket-mount", policySandboxContainerUnconfinedProfile: "policy/sandbox-container-unconfined-profile", policySandboxBrowserCdpSourceRangeMissing: "policy/sandbox-browser-cdp-source-range-missing", - policyDataHandlingRedactionDisabled: "policy/data-handling-redaction-disabled", policyDataHandlingTelemetryContentCapture: "policy/data-handling-telemetry-content-capture", policyDataHandlingSessionRetentionNotEnforced: "policy/data-handling-session-retention-not-enforced", @@ -120,7 +119,6 @@ export const POLICY_CHECK_IDS = [ CHECK_IDS.policySandboxContainerRuntimeSocketMount, CHECK_IDS.policySandboxContainerUnconfinedProfile, CHECK_IDS.policySandboxBrowserCdpSourceRangeMissing, - CHECK_IDS.policyDataHandlingRedactionDisabled, CHECK_IDS.policyDataHandlingTelemetryContentCapture, CHECK_IDS.policyDataHandlingSessionRetentionNotEnforced, CHECK_IDS.policyDataHandlingSessionTranscriptMemory, diff --git a/extensions/policy/src/doctor/data-auth-findings.ts b/extensions/policy/src/doctor/data-auth-findings.ts index 52e52097de42..bf860dd08a70 100644 --- a/extensions/policy/src/doctor/data-auth-findings.ts +++ b/extensions/policy/src/doctor/data-auth-findings.ts @@ -110,21 +110,9 @@ function dataHandlingFindingsForRule( return []; } const findings: HealthFinding[] = []; - if (readPolicyBoolean(dataHandling, ["sensitiveLogging", "requireRedaction"]) === true) { - findings.push( - ...dataHandlingEntries(evidence, "sensitiveLoggingRedaction") - .filter(evidenceFilter) - .filter((entry) => entry.value !== true) - .map((entry) => - dataHandlingFinding(entry, { - checkId: CHECK_IDS.policyDataHandlingRedactionDisabled, - message: "Sensitive logging redaction is disabled.", - requirement: `oc://${policyDocName}/${requirementBase}/sensitiveLogging/requireRedaction`, - fixHint: "Sensitive log redaction is always enabled; update policy after review.", - }), - ), - ); - } + // dataHandling.sensitiveLogging.requireRedaction has no check here on purpose: redaction is + // an unconditional runtime invariant (src/logging/redact.ts), so policy state records it as + // satisfied evidence (oc://openclaw.invariant/logging/redaction) instead of a finding. if (readPolicyBoolean(dataHandling, ["telemetry", "denyContentCapture"]) === true) { findings.push( ...dataHandlingEntries(evidence, "telemetryContentCapture") diff --git a/extensions/policy/src/doctor/fix-metadata.ts b/extensions/policy/src/doctor/fix-metadata.ts index 10d06c74b9d1..10c5f8e78eb7 100644 --- a/extensions/policy/src/doctor/fix-metadata.ts +++ b/extensions/policy/src/doctor/fix-metadata.ts @@ -1,7 +1,7 @@ // Policy doctor fix metadata classifies findings before patch builders exist. import { CHECK_IDS, POLICY_CHECK_IDS } from "./check-ids.js"; -type PolicyFixClass = "automatic" | "reviewRequired" | "manual" | "validateOnly" | "unsupported"; +type PolicyFixClass = "automatic" | "reviewRequired" | "manual" | "unsupported"; type PolicyFixMetadata = { readonly checkId: (typeof POLICY_CHECK_IDS)[number]; @@ -327,15 +327,6 @@ const POLICY_FIX_METADATA = [ configTargets: ["agents.sandbox.browser"], }, ), - // Sensitive log redaction is an unconditional runtime invariant (src/logging/redact.ts), - // so this requirement has no config target to patch. Adding one would emit a retired - // config key that strict validation rejects. - m( - CHECK_IDS.policyDataHandlingRedactionDisabled, - "validateOnly", - "Update the policy requirement; sensitive log redaction is always enabled.", - { policyPath: ["dataHandling", "sensitiveLogging", "requireRedaction"] }, - ), m( CHECK_IDS.policyDataHandlingTelemetryContentCapture, "automatic", diff --git a/extensions/policy/src/doctor/metadata.test.ts b/extensions/policy/src/doctor/metadata.test.ts index 4e5620d84e18..6741d912a9d5 100644 --- a/extensions/policy/src/doctor/metadata.test.ts +++ b/extensions/policy/src/doctor/metadata.test.ts @@ -1,5 +1,6 @@ // Policy doctor metadata tests cover rule metadata. import { describe, expect, it } from "vitest"; +import { scanPolicyDataHandling } from "../policy-state-data.js"; import { CHECK_IDS, POLICY_CHECK_IDS } from "./check-ids.js"; import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js"; import { POLICY_RULE_METADATA, type PolicyRuleMetadata } from "./metadata.js"; @@ -207,10 +208,6 @@ describe("policy doctor metadata", () => { .get("unsupported") ?.map((rule) => rule.checkId) .toSorted(), - validateOnly: grouped - .get("validateOnly") - ?.map((rule) => rule.checkId) - .toSorted(), }).toEqual({ automatic: [ "policy/agents-tool-not-denied", @@ -285,7 +282,33 @@ describe("policy doctor metadata", () => { "policy/tools-profile-unapproved", ], unsupported: ["policy/sandbox-container-posture-unobservable"], - validateOnly: ["policy/data-handling-redaction-disabled"], }); }); + + it("declares how every policy rule is enforced", () => { + const rules = POLICY_RULE_METADATA as readonly PolicyRuleMetadata[]; + // Every rule names either its doctor checks or the invariant that satisfies it, never + // both and never neither, so an accepted policy key cannot enforce nothing in silence. + expect( + rules + .filter((rule) => rule.checkIds.length > 0 === (rule.satisfiedByInvariant !== undefined)) + .map((rule) => rule.policyPath.join(".")), + ).toEqual([]); + const invariantSources = new Set(scanPolicyDataHandling({}).map((entry) => entry.source)); + expect( + rules + .filter((rule) => rule.satisfiedByInvariant !== undefined) + .map((rule) => [ + rule.policyPath.join("."), + rule.satisfiedByInvariant, + invariantSources.has(rule.satisfiedByInvariant ?? ""), + ]), + ).toEqual([ + [ + "dataHandling.sensitiveLogging.requireRedaction", + "oc://openclaw.invariant/logging/redaction", + true, + ], + ]); + }); }); diff --git a/extensions/policy/src/doctor/metadata.ts b/extensions/policy/src/doctor/metadata.ts index b896575532dc..cf25d401666f 100644 --- a/extensions/policy/src/doctor/metadata.ts +++ b/extensions/policy/src/doctor/metadata.ts @@ -24,6 +24,12 @@ export type PolicyRuleMetadata = { | "string" | "string-list"; readonly checkIds: readonly (typeof POLICY_CHECK_IDS)[number][]; + /** + * Evidence source of the runtime invariant that satisfies this rule unconditionally. + * Set only when `checkIds` is empty, so a rule can never enforce nothing without saying + * why; `metadata.test.ts` asserts the pairing and that policy state emits the source. + */ + readonly satisfiedByInvariant?: string; readonly emptyList?: PolicyEmptyListSemantics; readonly allowedValues?: readonly string[]; readonly caseSensitive?: boolean; @@ -339,10 +345,14 @@ export const POLICY_RULE_METADATA = [ scopeSelectors: ["channelIds"], }, { + // Redaction is unconditional in src/logging/redact.ts, so no doctor check can fail for + // this rule. The key stays a policy contract: `openclaw policy compare` still enforces + // baseline strictness, and policy state records the invariant below as satisfied. policyPath: ["dataHandling", "sensitiveLogging", "requireRedaction"], strictness: "requires-true", valueType: "boolean", - checkIds: [CHECK_IDS.policyDataHandlingRedactionDisabled], + checkIds: [], + satisfiedByInvariant: "oc://openclaw.invariant/logging/redaction", }, { policyPath: ["dataHandling", "telemetry", "denyContentCapture"], diff --git a/extensions/policy/src/doctor/register.base.test-utils.ts b/extensions/policy/src/doctor/register.base.test-utils.ts index 74e1004d396d..57f2d9cb8bc9 100644 --- a/extensions/policy/src/doctor/register.base.test-utils.ts +++ b/extensions/policy/src/doctor/register.base.test-utils.ts @@ -301,7 +301,6 @@ describe("registerPolicyDoctorChecks", () => { "policy/sandbox-container-runtime-socket-mount", "policy/sandbox-container-unconfined-profile", "policy/sandbox-browser-cdp-source-range-missing", - "policy/data-handling-redaction-disabled", "policy/data-handling-telemetry-content-capture", "policy/data-handling-session-retention-not-enforced", "policy/data-handling-session-transcript-memory-enabled", @@ -1562,7 +1561,6 @@ describe("registerPolicyDoctorChecks", () => { remote: { allow: false }, }, dataHandling: { - sensitiveLogging: { requireRedaction: true }, telemetry: { denyContentCapture: true }, }, }), diff --git a/extensions/policy/src/doctor/scopes/data-auth.ts b/extensions/policy/src/doctor/scopes/data-auth.ts index cfc51f6ce03c..866e6480f23a 100644 --- a/extensions/policy/src/doctor/scopes/data-auth.ts +++ b/extensions/policy/src/doctor/scopes/data-auth.ts @@ -7,18 +7,6 @@ import type { PolicyDoctorCheckDeps } from "../types.js"; export function createPolicyDataAuthChecks(deps: PolicyDoctorCheckDeps): readonly HealthCheck[] { const { evaluatePolicy, findingsForCheck } = deps; - const policyDataHandlingRedactionDisabledCheck: HealthCheck = { - id: CHECK_IDS.policyDataHandlingRedactionDisabled, - kind: "plugin", - description: "Sensitive logging redaction remains enabled when policy requires it.", - source: "policy", - async detect(ctx) { - return findingsForCheck( - await evaluatePolicy(ctx), - CHECK_IDS.policyDataHandlingRedactionDisabled, - ); - }, - }; const policyDataHandlingTelemetryContentCaptureCheck: HealthCheck = { id: CHECK_IDS.policyDataHandlingTelemetryContentCapture, kind: "plugin", @@ -118,7 +106,6 @@ export function createPolicyDataAuthChecks(deps: PolicyDoctorCheckDeps): readonl }; return [ - policyDataHandlingRedactionDisabledCheck, policyDataHandlingTelemetryContentCaptureCheck, policyDataHandlingSessionRetentionNotEnforcedCheck, policyDataHandlingSessionTranscriptMemoryCheck, diff --git a/extensions/policy/src/policy-state-data.ts b/extensions/policy/src/policy-state-data.ts index 1f050bd14bcc..2d56e133fcee 100644 --- a/extensions/policy/src/policy-state-data.ts +++ b/extensions/policy/src/policy-state-data.ts @@ -52,6 +52,9 @@ export function scanPolicyDataHandling( cfg: Record, ): readonly PolicyDataHandlingEvidence[] { const entries: PolicyDataHandlingEvidence[] = []; + // Redaction has no config surface: src/logging/redact.ts always redacts. This invariant + // record is how dataHandling.sensitiveLogging.requireRedaction reports as satisfied in + // `openclaw policy check` evidence and the attestation, since no doctor check can fail. entries.push({ id: "logging-redaction", kind: "sensitiveLoggingRedaction", diff --git a/scripts/lib/policy-config-coverage.jsonc b/scripts/lib/policy-config-coverage.jsonc index ac84cbedaf7e..b2eb4938cfe4 100644 --- a/scripts/lib/policy-config-coverage.jsonc +++ b/scripts/lib/policy-config-coverage.jsonc @@ -29,7 +29,6 @@ "gateway.nodes.commands.deny.*", "gateway.tailscale.mode", "gateway.tailscale.preserveFunnel", - "logging.redactSensitive", "memory.qmd.sessions.enabled", "mcp.servers.*.command", "mcp.servers.*.transport", @@ -174,14 +173,6 @@ "policy": "ingress.session.requireDmScope", "reason": "Policy observes direct-message session isolation scope.", }, - { - "pattern": "logging.redactSensitive", - "status": "observed", - "area": "dataHandling", - "policy": "dataHandling.sensitiveLogging.requireRedaction", - "reason": "Policy observes sensitive log redaction posture.", - "allowNoSchemaPath": true, - }, { "pattern": "diagnostics.otel.captureContent", "status": "observed",