feat(policy): add delegation and escalation contracts
Module Package Release / publish-packages (push) Failing after 6s

This commit is contained in:
2026-08-22 03:12:30 +02:00
parent a9035c4c3b
commit 0c1358b862
6 changed files with 83 additions and 4 deletions
+17
View File
@@ -14,6 +14,7 @@ consistent while each module still owns its domain rules.
| 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. | | 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. | | 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. | | 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. |
| Function assignment governance | `govoplan-policy` | capability `policy.functionAssignmentGovernance` | Returns current review steps, delegation depth/validity ceilings, and explicit timed-escalation targets consumed by IDM. |
## Policy Decision ## Policy Decision
@@ -126,6 +127,22 @@ When the capability is absent, modules must not silently emulate cross-scope
inheritance. Their conservative fallback is limited to local tenant inheritance. Their conservative fallback is limited to local tenant
definitions and disables reuse, derivation, and automation. definitions and disables reuse, derivation, and automation.
## Function Assignment Delegation And Escalation
`FunctionAssignmentGovernanceDecision` is the versioned cross-module contract
for request/grant review. In addition to the required holder, authority, and
recipient steps, it returns `delegation_allowed`,
`maximum_delegation_depth`, `maximum_delegated_validity_days`, and typed
`FunctionAssignmentEscalationRule` entries. Each escalation entry binds one
review step to an exact target function and timeout.
The decision is a current ceiling, not durable authorization. IDM must recheck
the complete assignment-source chain and all recorded decisions before final
application. An elapsed timeout creates explicit state and evidence; it must
never be interpreted as approval or as permission to silently substitute an
approver. Missing providers, malformed rules, invalid chains, or tightened
limits fail closed with an explainable reason.
## Bounded Impact-Subject Providers ## Bounded Impact-Subject Providers
Policy impact previews discover optional subject providers through capability Policy impact previews discover optional subject providers through capability
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-core" name = "govoplan-core"
version = "0.1.28" version = "0.1.29"
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components." description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+32
View File
@@ -41,10 +41,12 @@ ViewGovernanceAction = Literal[
"workflow_activate", "workflow_activate",
] ]
FunctionAssignmentChangeKind = Literal["request", "grant"] FunctionAssignmentChangeKind = Literal["request", "grant"]
FunctionAssignmentReviewStep = Literal["holder", "authority", "recipient"]
FunctionAssignmentGovernanceAction = Literal[ FunctionAssignmentGovernanceAction = Literal[
"submit", "submit",
"approve_holder", "approve_holder",
"approve_authority", "approve_authority",
"approve_escalation",
"accept_recipient", "accept_recipient",
"request_changes", "request_changes",
"respond", "respond",
@@ -419,6 +421,20 @@ class FunctionAssignmentGovernanceRequest:
context: Mapping[str, Any] = field(default_factory=dict) context: Mapping[str, Any] = field(default_factory=dict)
@dataclass(frozen=True, slots=True)
class FunctionAssignmentEscalationRule:
step: FunctionAssignmentReviewStep
target_function_id: str
timeout_hours: int
def to_dict(self) -> dict[str, Any]:
return {
"step": self.step,
"target_function_id": self.target_function_id,
"timeout_hours": self.timeout_hours,
}
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class FunctionAssignmentGovernanceDecision: class FunctionAssignmentGovernanceDecision:
allowed: bool allowed: bool
@@ -431,6 +447,10 @@ class FunctionAssignmentGovernanceDecision:
separation_of_duties: bool = True separation_of_duties: bool = True
quorum: int = 1 quorum: int = 1
maximum_validity_days: int | None = None maximum_validity_days: int | None = None
delegation_allowed: bool = False
maximum_delegation_depth: int = 0
maximum_delegated_validity_days: int | None = None
escalation_rules: tuple[FunctionAssignmentEscalationRule, ...] = ()
request_expiry_hours: int = 336 request_expiry_hours: int = 336
source_path: tuple[PolicySourceStep, ...] = () source_path: tuple[PolicySourceStep, ...] = ()
requirements: tuple[str, ...] = () requirements: tuple[str, ...] = ()
@@ -448,12 +468,24 @@ class FunctionAssignmentGovernanceDecision:
"separation_of_duties": self.separation_of_duties, "separation_of_duties": self.separation_of_duties,
"quorum": self.quorum, "quorum": self.quorum,
"maximum_validity_days": self.maximum_validity_days, "maximum_validity_days": self.maximum_validity_days,
"delegation_allowed": self.delegation_allowed,
"maximum_delegation_depth": self.maximum_delegation_depth,
"maximum_delegated_validity_days": (
self.maximum_delegated_validity_days
),
"escalation_rules": [rule.to_dict() for rule in self.escalation_rules],
"request_expiry_hours": self.request_expiry_hours, "request_expiry_hours": self.request_expiry_hours,
"source_path": [step.to_dict() for step in self.source_path], "source_path": [step.to_dict() for step in self.source_path],
"requirements": list(self.requirements), "requirements": list(self.requirements),
"details": dict(self.details), "details": dict(self.details),
} }
def escalation_rule(
self,
step: FunctionAssignmentReviewStep,
) -> FunctionAssignmentEscalationRule | None:
return next((rule for rule in self.escalation_rules if rule.step == step), None)
@runtime_checkable @runtime_checkable
class FunctionAssignmentGovernancePolicy(Protocol): class FunctionAssignmentGovernancePolicy(Protocol):
+30
View File
@@ -10,6 +10,8 @@ from govoplan_core.core.configuration_safety import (
ui_managed_configuration_fields_requiring_approval, ui_managed_configuration_fields_requiring_approval,
) )
from govoplan_core.core.policy import ( from govoplan_core.core.policy import (
FunctionAssignmentEscalationRule,
FunctionAssignmentGovernanceDecision,
PolicyDecision, PolicyDecision,
PolicySourceStep, PolicySourceStep,
parse_policy_source_path, parse_policy_source_path,
@@ -19,6 +21,34 @@ from govoplan_core.core.policy import (
class PolicyContractTests(unittest.TestCase): class PolicyContractTests(unittest.TestCase):
def test_function_assignment_policy_serializes_delegation_and_escalation(self) -> None:
decision = FunctionAssignmentGovernanceDecision(
allowed=True,
delegation_allowed=True,
maximum_delegation_depth=2,
maximum_delegated_validity_days=30,
escalation_rules=(
FunctionAssignmentEscalationRule(
step="authority",
target_function_id="function-escalation",
timeout_hours=48,
),
),
)
payload = decision.to_dict()
self.assertEqual(2, payload["maximum_delegation_depth"])
self.assertEqual(30, payload["maximum_delegated_validity_days"])
self.assertEqual(
"function-escalation",
payload["escalation_rules"][0]["target_function_id"],
)
self.assertEqual(
"function-escalation",
decision.escalation_rule("authority").target_function_id,
)
def test_policy_source_paths_are_stable_and_round_trip(self) -> None: def test_policy_source_paths_are_stable_and_round_trip(self) -> None:
self.assertEqual(policy_source_path("system"), "system") self.assertEqual(policy_source_path("system"), "system")
self.assertEqual(policy_source_path("tenant", "tenant-1"), "tenant:tenant-1") self.assertEqual(policy_source_path("tenant", "tenant-1"), "tenant:tenant-1")
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.28", "version": "0.1.29",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.28", "version": "0.1.29",
"dependencies": { "dependencies": {
"@govoplan/access-webui": "file:../../govoplan-access/webui", "@govoplan/access-webui": "file:../../govoplan-access/webui",
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.28", "version": "0.1.29",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",