feat: add governed service workflow launcher
This commit is contained in:
@@ -14,6 +14,7 @@ from govoplan_core.core.module_guards import (
|
||||
persistent_table_uninstall_guard,
|
||||
)
|
||||
from govoplan_core.core.modules import (
|
||||
CapabilityDocumentation,
|
||||
DocumentationTopic,
|
||||
MigrationSpec,
|
||||
ModuleContext,
|
||||
@@ -23,6 +24,7 @@ from govoplan_core.core.modules import (
|
||||
PermissionDefinition,
|
||||
RoleTemplate,
|
||||
)
|
||||
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||
from govoplan_core.core.policy import (
|
||||
CAPABILITY_POLICY_DEFINITION_GOVERNANCE,
|
||||
)
|
||||
@@ -41,6 +43,10 @@ from govoplan_workflow_engine.backend.db import models as workflow_models
|
||||
from govoplan_workflow_engine.backend.configuration_provider import (
|
||||
WORKFLOW_CONFIGURATION_CAPABILITY,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.service_launcher import (
|
||||
CAPABILITY_WORKFLOW_SERVICE_LAUNCHER,
|
||||
WorkflowServiceLauncher,
|
||||
)
|
||||
|
||||
|
||||
MODULE_ID = "workflow_engine"
|
||||
@@ -164,6 +170,10 @@ def _orchestration_provider(context: ModuleContext):
|
||||
return SqlWorkflowOrchestrationProvider(registry=context.registry)
|
||||
|
||||
|
||||
def _service_launcher(context: ModuleContext) -> WorkflowServiceLauncher:
|
||||
return WorkflowServiceLauncher(registry=context.registry)
|
||||
|
||||
|
||||
manifest = ModuleManifest(
|
||||
id=MODULE_ID,
|
||||
name=MODULE_NAME,
|
||||
@@ -209,6 +219,10 @@ manifest = ModuleManifest(
|
||||
),
|
||||
ModuleInterfaceProvider(name="workflow.runtime_worker", version=MODULE_VERSION),
|
||||
ModuleInterfaceProvider(name="workflow.bpmn_interchange", version="1.0.0"),
|
||||
ModuleInterfaceProvider(
|
||||
name=CAPABILITY_WORKFLOW_SERVICE_LAUNCHER,
|
||||
version="0.1.0",
|
||||
),
|
||||
ModuleInterfaceProvider(
|
||||
name="workflow.bpmn_execution_adapters",
|
||||
version="1.0.0",
|
||||
@@ -262,6 +276,14 @@ manifest = ModuleManifest(
|
||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER: _runtime_worker,
|
||||
CAPABILITY_WORKFLOW_ORCHESTRATION: _orchestration_provider,
|
||||
WORKFLOW_CONFIGURATION_CAPABILITY: _configuration_provider,
|
||||
CAPABILITY_WORKFLOW_SERVICE_LAUNCHER: _service_launcher,
|
||||
},
|
||||
capability_documentation={
|
||||
CAPABILITY_WORKFLOW_SERVICE_LAUNCHER: CapabilityDocumentation(
|
||||
label="Workflow service launcher",
|
||||
summary="Starts an authorized active Workflow from an exact available Service revision.",
|
||||
contract_version="0.1.0",
|
||||
),
|
||||
},
|
||||
migration_spec=MigrationSpec(
|
||||
module_id=MODULE_ID,
|
||||
@@ -302,7 +324,9 @@ manifest = ModuleManifest(
|
||||
"Unlike Dataflow, Workflow permits cycles for correction and retry paths. "
|
||||
"Module actions are addressed through versioned capabilities rather than "
|
||||
"implementation imports. Definitions are persisted as immutable graph "
|
||||
"revisions; activation pins the exact revision used by future instances."
|
||||
"revisions; activation pins the exact revision used by future instances. "
|
||||
"The optional service-launch capability starts an authorized active "
|
||||
"revision from an exact Portal Service binding and records that provenance."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("admin", "user"),
|
||||
@@ -338,6 +362,19 @@ manifest = ModuleManifest(
|
||||
order=77,
|
||||
),
|
||||
),
|
||||
architecture=declared_module_architecture(
|
||||
layer="human_work_procedure",
|
||||
kind="runtime",
|
||||
maturity="vertical_slice",
|
||||
documentation_ref="docs/ENGINE_EDITOR_SPLIT.md",
|
||||
test_ref="tests/test_instance_service.py",
|
||||
known_limits=("Execution adapters support declared conformance profiles but do not cover every editable BPMN semantic.",),
|
||||
owned_concepts=("workflow definition", "workflow revision", "workflow instance", "work transition", "execution adapter binding"),
|
||||
non_owned_concepts=("visual editor", "domain action", "notification", "dataflow run"),
|
||||
recovery_docs=("docs/CONCEPT.md",),
|
||||
security_docs=("docs/CONCEPT.md",),
|
||||
operations_docs=("README.md",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from urllib.parse import quote
|
||||
|
||||
from sqlalchemy import case, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.core.institutional import (
|
||||
InstitutionalContextError,
|
||||
InstitutionalReference,
|
||||
ServiceDefinition,
|
||||
ServiceLaunchRequest,
|
||||
ServiceLaunchResult,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.db.models import WorkflowDefinition
|
||||
from govoplan_workflow_engine.backend.instance_service import start_instance
|
||||
from govoplan_workflow_engine.backend.schemas import WorkflowInstanceStartRequest
|
||||
|
||||
|
||||
CAPABILITY_WORKFLOW_SERVICE_LAUNCHER = "workflow_engine.service_launcher"
|
||||
|
||||
|
||||
class WorkflowServiceLauncher:
|
||||
def __init__(self, registry: object | None = None) -> None:
|
||||
self._registry = registry
|
||||
|
||||
def launch_service(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
definition: ServiceDefinition,
|
||||
request: ServiceLaunchRequest,
|
||||
) -> ServiceLaunchResult:
|
||||
if not isinstance(session, Session) or not isinstance(principal, ApiPrincipal):
|
||||
raise InstitutionalContextError(
|
||||
"Workflow service launch requires a database session and API principal."
|
||||
)
|
||||
if request.service_ref != definition.reference:
|
||||
raise InstitutionalContextError(
|
||||
"Workflow service launch must use the requested exact Service revision."
|
||||
)
|
||||
if (
|
||||
request.binding.kind != "workflow"
|
||||
or request.binding not in definition.bindings
|
||||
):
|
||||
raise InstitutionalContextError(
|
||||
"Workflow service launch requires a workflow binding from the Service definition."
|
||||
)
|
||||
workflow = _workflow_definition(
|
||||
session,
|
||||
tenant_id=definition.reference.tenant_id,
|
||||
reference=request.binding.reference,
|
||||
)
|
||||
correlation_hash = hashlib.sha256(
|
||||
(
|
||||
f"{definition.reference.tenant_id}:"
|
||||
f"{definition.reference.object_id}:"
|
||||
f"{definition.reference.version}:"
|
||||
f"{request.idempotency_key}"
|
||||
).encode("utf-8")
|
||||
).hexdigest()[:32]
|
||||
instance, replayed = start_instance(
|
||||
session,
|
||||
tenant_id=definition.reference.tenant_id,
|
||||
definition_id=workflow.id,
|
||||
actor_id=principal.account_id,
|
||||
principal=principal,
|
||||
registry=self._registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key=request.idempotency_key,
|
||||
input={
|
||||
**dict(request.parameters),
|
||||
"_service": definition.reference.to_dict(),
|
||||
},
|
||||
correlation_id=f"service:{correlation_hash}",
|
||||
),
|
||||
start_origin="user",
|
||||
)
|
||||
target_ref = InstitutionalReference(
|
||||
kind="workflow",
|
||||
owner_module="workflow_engine",
|
||||
object_id=instance.id,
|
||||
tenant_id=definition.reference.tenant_id,
|
||||
version=str(workflow.active_revision or workflow.current_revision),
|
||||
valid_at=request.requested_at,
|
||||
)
|
||||
return ServiceLaunchResult(
|
||||
service_ref=definition.reference,
|
||||
binding=request.binding,
|
||||
state="started",
|
||||
target_ref=target_ref,
|
||||
href=f"/workflow?instance={quote(instance.id, safe='')}",
|
||||
replayed=replayed,
|
||||
metadata={
|
||||
"workflow_definition_id": workflow.id,
|
||||
"workflow_instance_id": instance.id,
|
||||
"workflow_status": instance.status,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _workflow_definition(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
reference: str,
|
||||
) -> WorkflowDefinition:
|
||||
value = reference.removeprefix("workflow:").strip()
|
||||
if not value:
|
||||
raise InstitutionalContextError("Workflow Service binding is invalid.")
|
||||
item = session.scalar(
|
||||
select(WorkflowDefinition)
|
||||
.where(
|
||||
or_(
|
||||
WorkflowDefinition.tenant_id == tenant_id,
|
||||
WorkflowDefinition.tenant_id.is_(None),
|
||||
),
|
||||
WorkflowDefinition.deleted_at.is_(None),
|
||||
or_(
|
||||
WorkflowDefinition.id == value,
|
||||
WorkflowDefinition.definition_key == value,
|
||||
),
|
||||
)
|
||||
.order_by(
|
||||
case((WorkflowDefinition.tenant_id == tenant_id, 0), else_=1),
|
||||
WorkflowDefinition.updated_at.desc(),
|
||||
)
|
||||
)
|
||||
if item is None:
|
||||
raise InstitutionalContextError(
|
||||
"Workflow Service binding does not resolve to an available definition."
|
||||
)
|
||||
return item
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_WORKFLOW_SERVICE_LAUNCHER",
|
||||
"WorkflowServiceLauncher",
|
||||
]
|
||||
Reference in New Issue
Block a user