Add workflow baseline orchestration contracts
This commit is contained in:
@@ -10,6 +10,9 @@ from govoplan_core.core.module_management import ModuleManagementError, REQUIRED
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.runtime import configure_runtime
|
||||
from govoplan_core.core.workflows import (
|
||||
workflow_definition_contribution_provider,
|
||||
)
|
||||
from govoplan_core.server.route_validation import validate_router_can_mount
|
||||
|
||||
|
||||
@@ -116,6 +119,8 @@ class ModuleLifecycleManager:
|
||||
if hook is not None:
|
||||
hook(self.context)
|
||||
|
||||
self.reconcile_workflow_definitions()
|
||||
|
||||
if self._app is not None:
|
||||
self._app.openapi_schema = None
|
||||
|
||||
@@ -127,6 +132,24 @@ class ModuleLifecycleManager:
|
||||
migrations_applied=migrate,
|
||||
)
|
||||
|
||||
def reconcile_workflow_definitions(self) -> Mapping[str, object]:
|
||||
if not any(
|
||||
manifest.workflow_definitions for manifest in self.registry.manifests()
|
||||
):
|
||||
return {"skipped": True, "reason": "no_contributions"}
|
||||
provider = workflow_definition_contribution_provider(self.registry)
|
||||
if provider is None:
|
||||
return {"skipped": True, "reason": "provider_unavailable"}
|
||||
|
||||
from govoplan_core.db.session import get_database
|
||||
|
||||
with get_database().session() as session:
|
||||
result = provider.reconcile(session)
|
||||
session.commit()
|
||||
if self._app is not None:
|
||||
self._app.state.govoplan_workflow_reconciliation = dict(result)
|
||||
return dict(result)
|
||||
|
||||
def _mount_module_router(self, module_id: str) -> bool:
|
||||
if module_id in self._mounted_modules:
|
||||
return False
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Literal, Protocol, runtime_checkable
|
||||
|
||||
@@ -79,6 +79,7 @@ class OrganizationFunctionRef:
|
||||
delegable: bool = False
|
||||
act_in_place_allowed: bool = False
|
||||
status: OrganizationStatus = "active"
|
||||
settings: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -102,6 +103,7 @@ class OrganizationFunctionTypeRef:
|
||||
delegable: bool = False
|
||||
act_in_place_allowed: bool = False
|
||||
status: OrganizationStatus = "active"
|
||||
settings: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
||||
@@ -26,11 +26,25 @@ ViewGovernanceAction = Literal[
|
||||
"derive",
|
||||
"workflow_activate",
|
||||
]
|
||||
FunctionAssignmentChangeKind = Literal["request", "grant"]
|
||||
FunctionAssignmentGovernanceAction = Literal[
|
||||
"submit",
|
||||
"approve_holder",
|
||||
"approve_authority",
|
||||
"accept_recipient",
|
||||
"request_changes",
|
||||
"respond",
|
||||
"reject",
|
||||
"withdraw",
|
||||
"recover",
|
||||
"apply",
|
||||
]
|
||||
|
||||
CAPABILITY_POLICY_PRIVACY_RETENTION = "policy.privacyRetention"
|
||||
CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY = "policy.schedulingParticipantPrivacy"
|
||||
CAPABILITY_POLICY_DEFINITION_GOVERNANCE = "policy.definitionGovernance"
|
||||
CAPABILITY_POLICY_VIEW_GOVERNANCE = "policy.viewGovernance"
|
||||
CAPABILITY_POLICY_FUNCTION_ASSIGNMENT_GOVERNANCE = "policy.functionAssignmentGovernance"
|
||||
|
||||
POLICY_SCOPE_TYPES: tuple[PolicyScopeType, ...] = (
|
||||
"system",
|
||||
@@ -168,6 +182,83 @@ class PolicyDecision:
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FunctionAssignmentGovernanceRequest:
|
||||
tenant_id: str
|
||||
kind: FunctionAssignmentChangeKind
|
||||
action: FunctionAssignmentGovernanceAction
|
||||
function_id: str
|
||||
actor: PrincipalRef
|
||||
candidate_identity_id: str
|
||||
candidate_account_id: str | None = None
|
||||
current_state: str = "draft"
|
||||
function_settings: Mapping[str, Any] = field(default_factory=dict)
|
||||
context: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FunctionAssignmentGovernanceDecision:
|
||||
allowed: bool
|
||||
reason: str | None = None
|
||||
profile: str = "unavailable"
|
||||
required_steps: tuple[str, ...] = ()
|
||||
authority_function_id: str | None = None
|
||||
evidence_required: bool = False
|
||||
recipient_acceptance_required: bool = False
|
||||
separation_of_duties: bool = True
|
||||
quorum: int = 1
|
||||
maximum_validity_days: int | None = None
|
||||
request_expiry_hours: int = 336
|
||||
source_path: tuple[PolicySourceStep, ...] = ()
|
||||
requirements: tuple[str, ...] = ()
|
||||
details: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"allowed": self.allowed,
|
||||
"reason": self.reason,
|
||||
"profile": self.profile,
|
||||
"required_steps": list(self.required_steps),
|
||||
"authority_function_id": self.authority_function_id,
|
||||
"evidence_required": self.evidence_required,
|
||||
"recipient_acceptance_required": (self.recipient_acceptance_required),
|
||||
"separation_of_duties": self.separation_of_duties,
|
||||
"quorum": self.quorum,
|
||||
"maximum_validity_days": self.maximum_validity_days,
|
||||
"request_expiry_hours": self.request_expiry_hours,
|
||||
"source_path": [step.to_dict() for step in self.source_path],
|
||||
"requirements": list(self.requirements),
|
||||
"details": dict(self.details),
|
||||
}
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class FunctionAssignmentGovernancePolicy(Protocol):
|
||||
def resolve_function_assignment_action(
|
||||
self,
|
||||
session: object | None = None,
|
||||
*,
|
||||
request: FunctionAssignmentGovernanceRequest,
|
||||
) -> FunctionAssignmentGovernanceDecision: ...
|
||||
|
||||
|
||||
def function_assignment_governance_policy(
|
||||
registry: object | None,
|
||||
) -> FunctionAssignmentGovernancePolicy | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not registry.has_capability(CAPABILITY_POLICY_FUNCTION_ASSIGNMENT_GOVERNANCE)
|
||||
):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_POLICY_FUNCTION_ASSIGNMENT_GOVERNANCE)
|
||||
return (
|
||||
capability
|
||||
if isinstance(capability, FunctionAssignmentGovernancePolicy)
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DefinitionScopeRef:
|
||||
scope_type: DefinitionScopeType
|
||||
|
||||
@@ -234,6 +234,9 @@ class PlatformRegistry:
|
||||
def has_capability(self, name: str) -> bool:
|
||||
return name in self._capability_factories
|
||||
|
||||
def capability_names(self) -> tuple[str, ...]:
|
||||
return tuple(sorted(self._capability_factories))
|
||||
|
||||
def capability(self, name: str) -> object | None:
|
||||
if name in self._capabilities:
|
||||
return self._capabilities[name]
|
||||
|
||||
@@ -9,9 +9,8 @@ from typing import Protocol, runtime_checkable
|
||||
|
||||
|
||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER = "workflow.runtimeWorker"
|
||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS = (
|
||||
"workflow.definitionContributions"
|
||||
)
|
||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS = "workflow.definitionContributions"
|
||||
CAPABILITY_WORKFLOW_ORCHESTRATION = "workflow.orchestration"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -45,6 +44,42 @@ class WorkflowDefinitionContribution:
|
||||
content_hash: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WorkflowStandardStartRequest:
|
||||
tenant_id: str
|
||||
origin_module_id: str
|
||||
definition_key: str
|
||||
idempotency_key: str
|
||||
input: Mapping[str, object] = field(default_factory=dict)
|
||||
actor_id: str | None = None
|
||||
correlation_id: str | None = None
|
||||
start_origin: str = "user"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WorkflowCurrentStepResolution:
|
||||
action: str
|
||||
expected_step_id: str | None = None
|
||||
actor_id: str | None = None
|
||||
output: Mapping[str, object] = field(default_factory=dict)
|
||||
evidence: tuple[str, ...] = ()
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WorkflowInstanceRef:
|
||||
id: str
|
||||
tenant_id: str
|
||||
definition_id: str
|
||||
definition_revision_id: str
|
||||
definition_revision: int
|
||||
definition_hash: str
|
||||
status: str
|
||||
current_step_id: str | None = None
|
||||
current_node_id: str | None = None
|
||||
replayed: bool = False
|
||||
|
||||
|
||||
def workflow_definition_contribution_hash(
|
||||
contribution: WorkflowDefinitionContribution,
|
||||
) -> str:
|
||||
@@ -106,6 +141,35 @@ class WorkflowDefinitionContributionProvider(Protocol):
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class WorkflowOrchestrationProvider(Protocol):
|
||||
def start_standard(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: WorkflowStandardStartRequest,
|
||||
) -> WorkflowInstanceRef: ...
|
||||
|
||||
def resolve_current_step(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
instance_id: str,
|
||||
resolution: WorkflowCurrentStepResolution,
|
||||
) -> WorkflowInstanceRef: ...
|
||||
|
||||
def get_instance(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
instance_id: str,
|
||||
) -> WorkflowInstanceRef: ...
|
||||
|
||||
|
||||
def workflow_runtime_worker(
|
||||
registry: object | None,
|
||||
) -> WorkflowRuntimeWorker | None:
|
||||
@@ -144,13 +208,32 @@ def workflow_definition_contribution_provider(
|
||||
)
|
||||
|
||||
|
||||
def workflow_orchestration_provider(
|
||||
registry: object | None,
|
||||
) -> WorkflowOrchestrationProvider | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not registry.has_capability(CAPABILITY_WORKFLOW_ORCHESTRATION)
|
||||
):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_WORKFLOW_ORCHESTRATION)
|
||||
return capability if isinstance(capability, WorkflowOrchestrationProvider) else None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS",
|
||||
"CAPABILITY_WORKFLOW_ORCHESTRATION",
|
||||
"CAPABILITY_WORKFLOW_RUNTIME_WORKER",
|
||||
"WorkflowCurrentStepResolution",
|
||||
"WorkflowDefinitionContribution",
|
||||
"WorkflowDefinitionContributionProvider",
|
||||
"WorkflowInstanceRef",
|
||||
"WorkflowOrchestrationProvider",
|
||||
"WorkflowRuntimeWorker",
|
||||
"WorkflowStandardStartRequest",
|
||||
"workflow_definition_contribution_hash",
|
||||
"workflow_definition_contribution_provider",
|
||||
"workflow_orchestration_provider",
|
||||
"workflow_runtime_worker",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user