# GovOPlaN Policy Contracts GovOPlaN has several policy families that are moving out of core into owning modules. The shared kernel contract keeps their decision and provenance shape consistent while each module still owns its domain rules. ## Current Policy Inventory | Policy area | Current owner | Runtime surface | Notes | | --- | --- | --- | --- | | Privacy retention | `govoplan-policy` routes with compatibility helpers in core | `/api/v1/admin/privacy-retention/policies/{scope}` and `/explain` | System, tenant, user, group, and campaign sources merge into the effective retention policy. Parent locks block lower-level widening. | | Mail profile policy | `govoplan-mail` | `/api/v1/mail/policies/{scope}` | Uses the same source-step path format for system, tenant, owner, and campaign provenance. | | RBAC/access policy | `govoplan-access` | access capabilities in `govoplan_core.core.access` | Permission decisions should use access capability contracts. Explain responses should adopt `PolicyDecision` when an API-level explanation is added. | | Governance defaults | `govoplan-admin` plus `govoplan-access` materializer | admin settings, governance template routes, access materialization capability | System governance can block tenant-local groups, roles, and API keys. | | Delegation and ownership policy | access/campaign/mail/files modules | capability checks and owner-scoped APIs | Source provenance should use this contract when policies become externally explainable. | ## Policy Decision The shared DTO lives in `govoplan_core.core.policy.PolicyDecision`. ```json { "allowed": false, "reason": "Parent retention policy locks lower-level changes.", "source_path": [ { "scope_type": "system", "scope_id": null, "path": "system", "label": "System", "applied_fields": ["allow_lower_level_limits"], "policy": {} } ], "requirements": ["raw_campaign_json_retention_days"], "details": { "blocked_fields": ["raw_campaign_json_retention_days"] } } ``` `allowed` is the effective answer for the checked action. `reason` is a stable, human-readable summary. `source_path` lists the policy sources that explain the answer. `requirements` lists machine-readable blockers or prerequisites, and `details` carries domain-specific structured context. Every source step should be concrete enough for an operator to understand the decision without knowing internal merge rules. Use real scope labels such as `System`, `Tenant`, `Owner user`, `Group`, or a campaign/profile name. Include the stable `path`, the fields applied by that step, and the local policy fragment that caused them. This lets UIs render explanations like `System: Allow > Tenant: Deny without override` without additional lookups. If a policy family cannot expose the full local fragment for security reasons, it must still include a redacted structured value that identifies the applied field and the effective allow/deny or lock state. ## Source Path Format Policy source paths are stable string identifiers for provenance steps: - `system` - `:` Supported scope types are `system`, `tenant`, `user`, `group`, and `campaign`. Examples: - `tenant:4a45b4fe-1d86-43ce-9d10-6022333f4d4b` - `campaign:campaign%2Fwith%20space` Use `policy_source_path()` and `parse_policy_source_path()` instead of building or splitting these strings manually. ## Retention Explain Endpoint `GET /api/v1/admin/privacy-retention/policies/{scope_type}/explain` returns: - `scope_type` and optional `scope_id` - `decision`, using the shared `PolicyDecision` shape - `effective_policy` - optional `parent_policy` - `effective_policy_sources` - `parent_policy_sources` - `blocked_fields` The endpoint is read-only. Enforcement remains in the existing policy write path. For lower-level scopes, `blocked_fields` is derived from the parent policy's `allow_lower_level_limits`; clients can use it to disable local controls before attempting a write. ## Frontend Contract Policy UIs must: - render effective source provenance when `effective_policy_sources` is present - display a field-level path when the source data is shown next to a specific setting, using concrete source labels and stop at the first non-overridable deny/lock - disable local field controls when the parent policy sets that field's lower-level limit to `false` - avoid sending locked fields or re-enable attempts in save payloads - show inherited values separately from local overrides The core WebUI helper `privacyRetentionParentAllowsField()` centralizes the field-lock decision used by the retention editor and its lightweight module tests.