# 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` implementation and routes, with compatibility helpers in core | `/api/v1/admin/privacy-retention/policies/{scope}` and `/explain`; capability `policy.privacyRetention` | 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. |
| Definition governance | `govoplan-policy` | capability `policy.definitionGovernance` | Resolves view, edit, run/start, reuse, derive, and automate for system, tenant, group, and user Dataflow/Workflow definitions. |

## 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`
- `<scope_type>:<url-encoded-scope-id>`

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.

The retention implementation lives in `govoplan-policy`
(`govoplan_policy.backend.retention`). Core keeps
`govoplan_core.privacy.retention` only as a compatibility facade for older
imports. Effective/scoped retention behavior dispatches through the
`policy.privacyRetention` capability; core does not import policy implementation
code as a hidden fallback when the module is disabled or no runtime is active.
New backend code should import policy-owned retention behavior from
`govoplan-policy` or request the capability, not add new implementation logic
to core.

The retention API DTOs live in `govoplan_core.privacy.schemas`.
`PrivacyRetentionPolicyItem`, `PrivacyRetentionPolicyPatchItem`,
`RETENTION_POLICY_FIELD_KEYS`, and `default_allow_lower_level_limits()` are
platform contracts because admin, access compatibility, and policy routes expose
the same stable retention payload shape. The policy engine's internal
`PrivacyRetentionPolicy` and `PrivacyRetentionPolicyPatch` models stay in
`govoplan-policy`, because they carry implementation validators and merge
behavior that are not generic API contracts.

Tenant administration DTOs remain owned by `govoplan-tenancy`; access keeps
matching compatibility DTOs only for its legacy admin surface. Admin overview
responses remain module-local because the same counters are exposed from
different menu contexts and are not yet a separately versioned platform API.

## Definition Governance

Dataflow and Workflow submit a `DefinitionGovernanceRequest` using only stable
scope, principal, status, definition-kind, and limit fields. Policy returns a
standard `PolicyDecision`. System definitions may be inherited as read-only;
group and user definitions are visible only in matching contexts. Templates
may be viewed and derived but never run or automated. A derived definition
passes its pinned ancestor limits back through the request context, and Policy
applies those limits as ceilings rather than defaults that can be broadened.

When the capability is absent, modules must not silently emulate cross-scope
inheritance. Their conservative fallback is limited to local tenant
definitions and disables reuse, derivation, and automation.

## 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.
