feat: add workflow engine contribution contracts
This commit is contained in:
@@ -5,7 +5,7 @@ only linear screen flows. Workflows, schedules, imports, connectors, policies,
|
|||||||
and external events all need to request governed actions without bypassing the
|
and external events all need to request governed actions without bypassing the
|
||||||
same safety rules that apply to human users.
|
same safety rules that apply to human users.
|
||||||
|
|
||||||
The first implementation should live in `govoplan-workflow` and core contracts.
|
The first implementation lives in `govoplan-workflow-engine` and Core contracts.
|
||||||
Create a separate `govoplan-automation` module only if action planning,
|
Create a separate `govoplan-automation` module only if action planning,
|
||||||
schedulers, rule execution, or cross-module automation become too broad for
|
schedulers, rule execution, or cross-module automation become too broad for
|
||||||
workflow ownership.
|
workflow ownership.
|
||||||
@@ -122,7 +122,7 @@ recovery decisions, and workflow advancement.
|
|||||||
## Boundary
|
## Boundary
|
||||||
|
|
||||||
Core may own stable DTOs, registry contracts, and generic audit/event hooks.
|
Core may own stable DTOs, registry contracts, and generic audit/event hooks.
|
||||||
`govoplan-workflow` should own the first runner because workflow is the first
|
`govoplan-workflow-engine` owns the first runner because workflow is the first
|
||||||
module that coordinates cross-module process actions.
|
module that coordinates cross-module process actions.
|
||||||
|
|
||||||
Domain modules own their own action providers. For example, templates own
|
Domain modules own their own action providers. For example, templates own
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ release evidence.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `REDIS_URL` | `redis://redis:6379/0` | Celery broker/result backend when async workers are enabled. |
|
| `REDIS_URL` | `redis://redis:6379/0` | Celery broker/result backend when async workers are enabled. |
|
||||||
| `CELERY_ENABLED` | `false` | Local/dev can send synchronously. Production campaign delivery should run workers and set this to `true`. |
|
| `CELERY_ENABLED` | `false` | Local/dev can send synchronously. Production campaign delivery should run workers and set this to `true`. |
|
||||||
| `CELERY_QUEUES` | `send_email,append_sent,notifications,calendar,dataflow,events,default` | Queue list expected by worker/process manager definitions. The `events` queue drains transactional platform events; `dataflow` drains trigger deliveries and schedules. |
|
| `CELERY_QUEUES` | `send_email,append_sent,notifications,calendar,dataflow,workflow,events,default` | Queue list expected by worker/process manager definitions. The `events` queue drains transactional platform events; `dataflow` drains trigger deliveries and schedules; `workflow` reconciles Workflow Engine instances and module standards. |
|
||||||
| `PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS` | `8` | Failed durable consumer deliveries are quarantined after this many attempts. |
|
| `PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS` | `8` | Failed durable consumer deliveries are quarantined after this many attempts. |
|
||||||
| `PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS` | `90` | Successful event envelopes older than this are removed by the daily retention task. Quarantined evidence is retained. |
|
| `PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS` | `90` | Successful event envelopes older than this are removed by the daily retention task. Quarantined evidence is retained. |
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,16 @@ Other stable runtime capabilities currently include:
|
|||||||
- `calendar.outbox` and `calendar.scheduling`
|
- `calendar.outbox` and `calendar.scheduling`
|
||||||
- `poll.scheduling`
|
- `poll.scheduling`
|
||||||
- `notifications.dispatch`
|
- `notifications.dispatch`
|
||||||
|
- `workflow.definitionContributions` and `workflow.runtimeWorker`
|
||||||
|
|
||||||
|
Modules contribute reusable process baselines through
|
||||||
|
`ModuleManifest.workflow_definitions`. Each contribution pins its origin module
|
||||||
|
and version, stable key, schema and content hash, native graph/BPMN content,
|
||||||
|
governance ceilings, execution mode, and required capabilities/interfaces.
|
||||||
|
`govoplan-workflow-engine` reconciles these declarations idempotently. A module
|
||||||
|
upgrade appends a baseline revision without replacing the active revision or
|
||||||
|
mutating a local override; the optional `govoplan-workflow` package supplies
|
||||||
|
the comparison, derivation, and reset UI.
|
||||||
|
|
||||||
### Named Interface Contracts
|
### Named Interface Contracts
|
||||||
|
|
||||||
@@ -889,7 +899,7 @@ First slice:
|
|||||||
- `govoplan-files` owns file-backed governed locations and uploaded/stored file
|
- `govoplan-files` owns file-backed governed locations and uploaded/stored file
|
||||||
evidence.
|
evidence.
|
||||||
- `govoplan-reporting` owns report/data views and scheduled outputs.
|
- `govoplan-reporting` owns report/data views and scheduled outputs.
|
||||||
- `govoplan-workflow` owns process state, approvals, scheduling of process
|
- `govoplan-workflow-engine` owns process state, approvals, scheduling of process
|
||||||
steps, and human review.
|
steps, and human review.
|
||||||
|
|
||||||
Future `govoplan-datasources` is justified when GovOPlaN needs a broad source
|
Future `govoplan-datasources` is justified when GovOPlaN needs a broad source
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ Current tag-only module repositories:
|
|||||||
- `govoplan-search`
|
- `govoplan-search`
|
||||||
- `govoplan-tasks`
|
- `govoplan-tasks`
|
||||||
- `govoplan-templates`
|
- `govoplan-templates`
|
||||||
- `govoplan-workflow`
|
- `govoplan-workflow-engine` (headless definitions, migrations, and execution)
|
||||||
|
- `govoplan-workflow` (optional authoring and inspection WebUI)
|
||||||
- `govoplan-xoev`
|
- `govoplan-xoev`
|
||||||
- `govoplan-xrechnung`
|
- `govoplan-xrechnung`
|
||||||
- `govoplan-xta-osci`
|
- `govoplan-xta-osci`
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ ENABLED_MODULES=tenancy,organizations,identity,access,admin,dashboard,policy,aud
|
|||||||
|
|
||||||
CELERY_ENABLED=true
|
CELERY_ENABLED=true
|
||||||
REDIS_URL=redis://127.0.0.1:6379/0
|
REDIS_URL=redis://127.0.0.1:6379/0
|
||||||
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default
|
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,workflow,events,default
|
||||||
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
||||||
PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS=8
|
PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS=8
|
||||||
PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
||||||
@@ -446,7 +446,7 @@ DATABASE_URL=postgresql+psycopg://govoplan:govoplan-dev@127.0.0.1:55433/govoplan
|
|||||||
GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan
|
GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan
|
||||||
REDIS_URL=redis://127.0.0.1:56379/0
|
REDIS_URL=redis://127.0.0.1:56379/0
|
||||||
CELERY_ENABLED=true
|
CELERY_ENABLED=true
|
||||||
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default
|
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,workflow,events,default
|
||||||
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
||||||
PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS=8
|
PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS=8
|
||||||
PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
|
|||||||
SearchProviderRegistration,
|
SearchProviderRegistration,
|
||||||
SearchSourceProviderRegistration,
|
SearchSourceProviderRegistration,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.workflows import WorkflowDefinitionContribution
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_MANIFEST_CONTRACT_VERSION = "1"
|
SUPPORTED_MANIFEST_CONTRACT_VERSION = "1"
|
||||||
@@ -440,3 +441,8 @@ class ModuleManifest:
|
|||||||
...,
|
...,
|
||||||
] = ()
|
] = ()
|
||||||
documentation_sources: tuple[DocumentationSourceDefinition, ...] = ()
|
documentation_sources: tuple[DocumentationSourceDefinition, ...] = ()
|
||||||
|
# A renamed or extracted module may continue to own an established
|
||||||
|
# permission namespace. This keeps persisted grants stable while the
|
||||||
|
# runtime module ID changes.
|
||||||
|
permission_namespace: str | None = None
|
||||||
|
workflow_definitions: tuple["WorkflowDefinitionContribution", ...] = ()
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ from govoplan_core.core.views import (
|
|||||||
route_view_surface_id,
|
route_view_surface_id,
|
||||||
validate_view_surface_id,
|
validate_view_surface_id,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.workflows import (
|
||||||
|
workflow_definition_contribution_hash,
|
||||||
|
)
|
||||||
|
|
||||||
_MODULE_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
_MODULE_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||||
_NPM_PACKAGE_RE = re.compile(r"^(?:@[a-z0-9][a-z0-9_.-]*/)?[a-z0-9][a-z0-9_.-]*$")
|
_NPM_PACKAGE_RE = re.compile(r"^(?:@[a-z0-9][a-z0-9_.-]*/)?[a-z0-9][a-z0-9_.-]*$")
|
||||||
@@ -540,8 +543,12 @@ def _validate_manifest_permission(
|
|||||||
permission: PermissionDefinition,
|
permission: PermissionDefinition,
|
||||||
seen_permissions: Mapping[str, PermissionDefinition],
|
seen_permissions: Mapping[str, PermissionDefinition],
|
||||||
) -> None:
|
) -> None:
|
||||||
if permission.module_id != manifest.id:
|
permission_namespace = manifest.permission_namespace or manifest.id
|
||||||
raise RegistryError(f"Permission {permission.scope!r} has mismatched module id {permission.module_id!r}")
|
if permission.module_id != permission_namespace:
|
||||||
|
raise RegistryError(
|
||||||
|
f"Permission {permission.scope!r} has mismatched module id "
|
||||||
|
f"{permission.module_id!r}; expected {permission_namespace!r}"
|
||||||
|
)
|
||||||
if not _SCOPE_RE.match(permission.scope):
|
if not _SCOPE_RE.match(permission.scope):
|
||||||
raise RegistryError(f"Permission scope must be <module>:<resource>:<action>: {permission.scope!r}")
|
raise RegistryError(f"Permission scope must be <module>:<resource>:<action>: {permission.scope!r}")
|
||||||
expected_prefix = f"{permission.module_id}:{permission.resource}:"
|
expected_prefix = f"{permission.module_id}:{permission.resource}:"
|
||||||
@@ -611,6 +618,55 @@ def _validate_manifest_shape(manifest: ModuleManifest) -> None:
|
|||||||
f"Module {manifest.id!r} documentation topic {topic.id!r}: {issue}"
|
f"Module {manifest.id!r} documentation topic {topic.id!r}: {issue}"
|
||||||
)
|
)
|
||||||
_validate_documentation_extensions(manifest)
|
_validate_documentation_extensions(manifest)
|
||||||
|
_validate_workflow_definition_contributions(manifest)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_workflow_definition_contributions(
|
||||||
|
manifest: ModuleManifest,
|
||||||
|
) -> None:
|
||||||
|
seen_keys: set[str] = set()
|
||||||
|
for contribution in manifest.workflow_definitions:
|
||||||
|
if contribution.origin_module_id != manifest.id:
|
||||||
|
raise RegistryError(
|
||||||
|
f"Workflow contribution {contribution.definition_key!r} in "
|
||||||
|
f"module {manifest.id!r} declares origin module "
|
||||||
|
f"{contribution.origin_module_id!r}"
|
||||||
|
)
|
||||||
|
if contribution.origin_module_version != manifest.version:
|
||||||
|
raise RegistryError(
|
||||||
|
f"Workflow contribution {contribution.definition_key!r} in "
|
||||||
|
f"module {manifest.id!r} declares origin version "
|
||||||
|
f"{contribution.origin_module_version!r}, expected "
|
||||||
|
f"{manifest.version!r}"
|
||||||
|
)
|
||||||
|
if not re.match(
|
||||||
|
r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,119}$",
|
||||||
|
contribution.definition_key,
|
||||||
|
):
|
||||||
|
raise RegistryError(
|
||||||
|
f"Workflow contribution key is invalid: "
|
||||||
|
f"{contribution.definition_key!r}"
|
||||||
|
)
|
||||||
|
if contribution.definition_key in seen_keys:
|
||||||
|
raise RegistryError(
|
||||||
|
f"Module {manifest.id!r} declares duplicate Workflow "
|
||||||
|
f"contribution {contribution.definition_key!r}"
|
||||||
|
)
|
||||||
|
seen_keys.add(contribution.definition_key)
|
||||||
|
if contribution.scope_type not in {"system", "tenant"}:
|
||||||
|
raise RegistryError(
|
||||||
|
f"Workflow contribution {contribution.definition_key!r} has "
|
||||||
|
f"unsupported scope {contribution.scope_type!r}"
|
||||||
|
)
|
||||||
|
expected_hash = workflow_definition_contribution_hash(contribution)
|
||||||
|
if (
|
||||||
|
contribution.content_hash is not None
|
||||||
|
and contribution.content_hash != expected_hash
|
||||||
|
):
|
||||||
|
raise RegistryError(
|
||||||
|
f"Workflow contribution {contribution.definition_key!r} has "
|
||||||
|
"a content hash that does not match its canonical content"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_documentation_extensions(manifest: ModuleManifest) -> None:
|
def _validate_documentation_extensions(manifest: ModuleManifest) -> None:
|
||||||
@@ -678,6 +734,14 @@ def _validate_manifest_identity(manifest: ModuleManifest) -> None:
|
|||||||
raise RegistryError(f"Module {manifest.id!r} must declare a non-empty name")
|
raise RegistryError(f"Module {manifest.id!r} must declare a non-empty name")
|
||||||
if not manifest.version.strip():
|
if not manifest.version.strip():
|
||||||
raise RegistryError(f"Module {manifest.id!r} must declare a non-empty version")
|
raise RegistryError(f"Module {manifest.id!r} must declare a non-empty version")
|
||||||
|
if (
|
||||||
|
manifest.permission_namespace is not None
|
||||||
|
and not _MODULE_ID_RE.match(manifest.permission_namespace)
|
||||||
|
):
|
||||||
|
raise RegistryError(
|
||||||
|
f"Module {manifest.id!r} has invalid permission namespace "
|
||||||
|
f"{manifest.permission_namespace!r}"
|
||||||
|
)
|
||||||
if manifest.compatibility.manifest_contract_version != SUPPORTED_MANIFEST_CONTRACT_VERSION:
|
if manifest.compatibility.manifest_contract_version != SUPPORTED_MANIFEST_CONTRACT_VERSION:
|
||||||
raise RegistryError(
|
raise RegistryError(
|
||||||
f"Module {manifest.id!r} uses unsupported manifest contract version "
|
f"Module {manifest.id!r} uses unsupported manifest contract version "
|
||||||
|
|||||||
@@ -1,11 +1,86 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
from typing import Protocol, runtime_checkable
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER = "workflow.runtimeWorker"
|
CAPABILITY_WORKFLOW_RUNTIME_WORKER = "workflow.runtimeWorker"
|
||||||
|
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS = (
|
||||||
|
"workflow.definitionContributions"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class WorkflowDefinitionContribution:
|
||||||
|
"""Versioned, module-owned Workflow Engine baseline definition."""
|
||||||
|
|
||||||
|
origin_module_id: str
|
||||||
|
origin_module_version: str
|
||||||
|
definition_key: str
|
||||||
|
name: str
|
||||||
|
graph: Mapping[str, object]
|
||||||
|
contribution_schema_version: str = "1"
|
||||||
|
description: str | None = None
|
||||||
|
definition_kind: str = "flow"
|
||||||
|
scope_type: str = "system"
|
||||||
|
inherit_to_lower_scopes: bool = True
|
||||||
|
allow_start: bool = True
|
||||||
|
allow_reuse: bool = True
|
||||||
|
allow_automation: bool = False
|
||||||
|
execution_mode: str = "hybrid"
|
||||||
|
view_id: str | None = None
|
||||||
|
view_revision_id: str | None = None
|
||||||
|
bpmn_xml: str | None = None
|
||||||
|
bpmn_adapter_id: str = "govoplan.native.bpmn"
|
||||||
|
bpmn_adapter_version: str | None = None
|
||||||
|
activate_on_install: bool = True
|
||||||
|
required_capabilities: tuple[str, ...] = ()
|
||||||
|
required_interfaces: tuple[str, ...] = ()
|
||||||
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
policy_metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
content_hash: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def workflow_definition_contribution_hash(
|
||||||
|
contribution: WorkflowDefinitionContribution,
|
||||||
|
) -> str:
|
||||||
|
"""Return the stable content hash, excluding package release provenance."""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"schema_version": contribution.contribution_schema_version,
|
||||||
|
"definition_key": contribution.definition_key,
|
||||||
|
"name": contribution.name,
|
||||||
|
"description": contribution.description,
|
||||||
|
"graph": contribution.graph,
|
||||||
|
"definition_kind": contribution.definition_kind,
|
||||||
|
"scope_type": contribution.scope_type,
|
||||||
|
"inherit_to_lower_scopes": contribution.inherit_to_lower_scopes,
|
||||||
|
"allow_start": contribution.allow_start,
|
||||||
|
"allow_reuse": contribution.allow_reuse,
|
||||||
|
"allow_automation": contribution.allow_automation,
|
||||||
|
"execution_mode": contribution.execution_mode,
|
||||||
|
"view_id": contribution.view_id,
|
||||||
|
"view_revision_id": contribution.view_revision_id,
|
||||||
|
"bpmn_xml": contribution.bpmn_xml,
|
||||||
|
"bpmn_adapter_id": contribution.bpmn_adapter_id,
|
||||||
|
"bpmn_adapter_version": contribution.bpmn_adapter_version,
|
||||||
|
"activate_on_install": contribution.activate_on_install,
|
||||||
|
"required_capabilities": contribution.required_capabilities,
|
||||||
|
"required_interfaces": contribution.required_interfaces,
|
||||||
|
"metadata": contribution.metadata,
|
||||||
|
"policy_metadata": contribution.policy_metadata,
|
||||||
|
}
|
||||||
|
encoded = json.dumps(
|
||||||
|
payload,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=True,
|
||||||
|
)
|
||||||
|
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@@ -20,6 +95,17 @@ class WorkflowRuntimeWorker(Protocol):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class WorkflowDefinitionContributionProvider(Protocol):
|
||||||
|
def reconcile(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
tenant_ids: Sequence[str] = (),
|
||||||
|
) -> Mapping[str, object]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
def workflow_runtime_worker(
|
def workflow_runtime_worker(
|
||||||
registry: object | None,
|
registry: object | None,
|
||||||
) -> WorkflowRuntimeWorker | None:
|
) -> WorkflowRuntimeWorker | None:
|
||||||
@@ -37,8 +123,34 @@ def workflow_runtime_worker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def workflow_definition_contribution_provider(
|
||||||
|
registry: object | None,
|
||||||
|
) -> WorkflowDefinitionContributionProvider | None:
|
||||||
|
if (
|
||||||
|
registry is None
|
||||||
|
or not hasattr(registry, "has_capability")
|
||||||
|
or not registry.has_capability(
|
||||||
|
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
capability = registry.capability(
|
||||||
|
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
capability
|
||||||
|
if isinstance(capability, WorkflowDefinitionContributionProvider)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS",
|
||||||
"CAPABILITY_WORKFLOW_RUNTIME_WORKER",
|
"CAPABILITY_WORKFLOW_RUNTIME_WORKER",
|
||||||
|
"WorkflowDefinitionContribution",
|
||||||
|
"WorkflowDefinitionContributionProvider",
|
||||||
"WorkflowRuntimeWorker",
|
"WorkflowRuntimeWorker",
|
||||||
|
"workflow_definition_contribution_hash",
|
||||||
|
"workflow_definition_contribution_provider",
|
||||||
"workflow_runtime_worker",
|
"workflow_runtime_worker",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class Settings(BaseSettings):
|
|||||||
default=(
|
default=(
|
||||||
"tenancy,organizations,identity,idm,access,admin,dashboard,policy,"
|
"tenancy,organizations,identity,idm,access,admin,dashboard,policy,"
|
||||||
"audit,campaigns,files,mail,calendar,poll,scheduling,connectors,"
|
"audit,campaigns,files,mail,calendar,poll,scheduling,connectors,"
|
||||||
"datasources,dataflow,workflow,views,search,risk_compliance,"
|
"datasources,dataflow,workflow_engine,workflow,views,search,risk_compliance,"
|
||||||
"postbox,notifications,docs,ops"
|
"postbox,notifications,docs,ops"
|
||||||
),
|
),
|
||||||
alias="ENABLED_MODULES",
|
alias="ENABLED_MODULES",
|
||||||
@@ -156,7 +156,7 @@ class Settings(BaseSettings):
|
|||||||
celery_queues: str = Field(
|
celery_queues: str = Field(
|
||||||
default=(
|
default=(
|
||||||
"send_email,append_sent,notifications,calendar,"
|
"send_email,append_sent,notifications,calendar,"
|
||||||
"dataflow,events,default"
|
"dataflow,workflow,events,default"
|
||||||
),
|
),
|
||||||
alias="CELERY_QUEUES",
|
alias="CELERY_QUEUES",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ from govoplan_core.core.module_package_catalog import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleMigrationTask, ModuleMigrationTaskContext, ModuleMigrationTaskResult, ModuleUninstallGuardResult, PublicFrontendRoute
|
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleMigrationTask, ModuleMigrationTaskContext, ModuleMigrationTaskResult, ModuleUninstallGuardResult, PublicFrontendRoute
|
||||||
from govoplan_core.core.module_guards import drop_table_retirement_provider
|
from govoplan_core.core.module_guards import drop_table_retirement_provider
|
||||||
from govoplan_core.core.modules import MigrationSpec, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, RoleTemplate
|
from govoplan_core.core.modules import MigrationSpec, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, PermissionDefinition, RoleTemplate
|
||||||
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
||||||
from govoplan_core.admin.models import SystemSettings
|
from govoplan_core.admin.models import SystemSettings
|
||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
@@ -363,6 +363,56 @@ class ModuleSystemTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(RegistryError, "unsupported manifest contract version"):
|
with self.assertRaisesRegex(RegistryError, "unsupported manifest contract version"):
|
||||||
registry.validate()
|
registry.validate()
|
||||||
|
|
||||||
|
def test_manifest_can_preserve_a_legacy_permission_namespace(self) -> None:
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(
|
||||||
|
ModuleManifest(
|
||||||
|
id="workflow_engine",
|
||||||
|
name="Workflow Engine",
|
||||||
|
version="test",
|
||||||
|
permission_namespace="workflow",
|
||||||
|
permissions=(
|
||||||
|
PermissionDefinition(
|
||||||
|
scope="workflow:definition:read",
|
||||||
|
label="Read workflows",
|
||||||
|
description="Read workflow definitions.",
|
||||||
|
category="Workflow",
|
||||||
|
level="tenant",
|
||||||
|
module_id="workflow",
|
||||||
|
resource="definition",
|
||||||
|
action="read",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
registry.validate()
|
||||||
|
|
||||||
|
def test_manifest_rejects_an_undeclared_permission_namespace(self) -> None:
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(
|
||||||
|
ModuleManifest(
|
||||||
|
id="workflow_engine",
|
||||||
|
name="Workflow Engine",
|
||||||
|
version="test",
|
||||||
|
permissions=(
|
||||||
|
PermissionDefinition(
|
||||||
|
scope="workflow:definition:read",
|
||||||
|
label="Read workflows",
|
||||||
|
description="Read workflow definitions.",
|
||||||
|
category="Workflow",
|
||||||
|
level="tenant",
|
||||||
|
module_id="workflow",
|
||||||
|
resource="definition",
|
||||||
|
action="read",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RegistryError, "expected 'workflow_engine'"):
|
||||||
|
registry.validate()
|
||||||
|
|
||||||
def test_default_authenticated_role_templates_are_managed_tenant_roles(self) -> None:
|
def test_default_authenticated_role_templates_are_managed_tenant_roles(self) -> None:
|
||||||
for template, message in (
|
for template, message in (
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ from govoplan_core.celery_app import (
|
|||||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||||
from govoplan_core.core.registry import PlatformRegistry
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
from govoplan_core.core.workflows import (
|
from govoplan_core.core.workflows import (
|
||||||
|
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS,
|
||||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
||||||
|
WorkflowDefinitionContribution,
|
||||||
|
WorkflowDefinitionContributionProvider,
|
||||||
WorkflowRuntimeWorker,
|
WorkflowRuntimeWorker,
|
||||||
|
workflow_definition_contribution_hash,
|
||||||
|
workflow_definition_contribution_provider,
|
||||||
workflow_runtime_worker,
|
workflow_runtime_worker,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +26,47 @@ class _Worker:
|
|||||||
return {"inspected": 0, "advanced": 0}
|
return {"inspected": 0, "advanced": 0}
|
||||||
|
|
||||||
|
|
||||||
|
class _ContributionProvider:
|
||||||
|
def reconcile(self, session, *, tenant_ids=()):
|
||||||
|
return {"created": 0, "tenant_ids": tuple(tenant_ids)}
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
||||||
|
def test_contribution_contract_hash_and_provider_are_stable(self) -> None:
|
||||||
|
contribution = WorkflowDefinitionContribution(
|
||||||
|
origin_module_id="example",
|
||||||
|
origin_module_version="1.0.0",
|
||||||
|
definition_key="approval",
|
||||||
|
name="Approval",
|
||||||
|
graph={"schema_version": 1, "nodes": [], "edges": []},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_definition_contribution_hash(contribution),
|
||||||
|
workflow_definition_contribution_hash(contribution),
|
||||||
|
)
|
||||||
|
provider = _ContributionProvider()
|
||||||
|
self.assertIsInstance(provider, WorkflowDefinitionContributionProvider)
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(
|
||||||
|
ModuleManifest(
|
||||||
|
id="workflow_engine_test",
|
||||||
|
name="Workflow Engine test",
|
||||||
|
version="test",
|
||||||
|
capability_factories={
|
||||||
|
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS: (
|
||||||
|
lambda _context: provider
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
registry.configure_capability_context(
|
||||||
|
ModuleContext(registry=registry, settings=object())
|
||||||
|
)
|
||||||
|
self.assertIs(
|
||||||
|
provider,
|
||||||
|
workflow_definition_contribution_provider(registry),
|
||||||
|
)
|
||||||
|
|
||||||
def test_contract_is_runtime_checkable_and_resolved(self) -> None:
|
def test_contract_is_runtime_checkable_and_resolved(self) -> None:
|
||||||
worker = _Worker()
|
worker = _Worker()
|
||||||
self.assertIsInstance(worker, WorkflowRuntimeWorker)
|
self.assertIsInstance(worker, WorkflowRuntimeWorker)
|
||||||
|
|||||||
Reference in New Issue
Block a user