Add governed institutional capability contracts
This commit is contained in:
@@ -53,7 +53,9 @@ from govoplan_core.core.postbox import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.workflows import (
|
from govoplan_core.core.workflows import (
|
||||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
||||||
|
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER,
|
||||||
WorkflowRuntimeWorker,
|
WorkflowRuntimeWorker,
|
||||||
|
WorkflowTriggerDispatcher,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.registry import PlatformRegistry
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
from govoplan_core.core.runtime import configure_runtime
|
from govoplan_core.core.runtime import configure_runtime
|
||||||
@@ -385,6 +387,18 @@ def _workflow_runtime_worker(
|
|||||||
return capability
|
return capability
|
||||||
|
|
||||||
|
|
||||||
|
def _workflow_trigger_dispatcher(
|
||||||
|
registry: PlatformRegistry | None = None,
|
||||||
|
) -> WorkflowTriggerDispatcher | None:
|
||||||
|
registry = registry or _platform_registry()
|
||||||
|
if not registry.has_capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER):
|
||||||
|
return None
|
||||||
|
capability = registry.require_capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER)
|
||||||
|
if not isinstance(capability, WorkflowTriggerDispatcher):
|
||||||
|
raise RuntimeError("Workflow trigger dispatcher capability is invalid")
|
||||||
|
return capability
|
||||||
|
|
||||||
|
|
||||||
def _postbox_routing_provider(
|
def _postbox_routing_provider(
|
||||||
registry: PlatformRegistry | None = None,
|
registry: PlatformRegistry | None = None,
|
||||||
) -> PostboxRoutingProvider | None:
|
) -> PostboxRoutingProvider | None:
|
||||||
@@ -764,7 +778,8 @@ def dispatch_platform_events(self, limit: int = 100):
|
|||||||
"observer_failed": 0,
|
"observer_failed": 0,
|
||||||
}
|
}
|
||||||
dataflow_dispatcher = _dataflow_trigger_dispatcher(registry)
|
dataflow_dispatcher = _dataflow_trigger_dispatcher(registry)
|
||||||
consumers = ()
|
workflow_dispatcher = _workflow_trigger_dispatcher(registry)
|
||||||
|
consumers: list[DurableEventConsumer] = []
|
||||||
if dataflow_dispatcher is not None:
|
if dataflow_dispatcher is not None:
|
||||||
|
|
||||||
def deliver_to_dataflow(
|
def deliver_to_dataflow(
|
||||||
@@ -776,19 +791,38 @@ def dispatch_platform_events(self, limit: int = 100):
|
|||||||
event=event,
|
event=event,
|
||||||
)
|
)
|
||||||
|
|
||||||
consumers = (
|
consumers.append(
|
||||||
DurableEventConsumer(
|
DurableEventConsumer(
|
||||||
consumer_id="dataflow.event-triggers.v1",
|
consumer_id="dataflow.event-triggers.v1",
|
||||||
event_types=frozenset({"*"}),
|
event_types=frozenset({"*"}),
|
||||||
classifications=frozenset({"public", "internal"}),
|
classifications=frozenset({"public", "internal"}),
|
||||||
handler=deliver_to_dataflow,
|
handler=deliver_to_dataflow,
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
if workflow_dispatcher is not None:
|
||||||
|
|
||||||
|
def deliver_to_workflow(
|
||||||
|
event: PlatformEvent,
|
||||||
|
_delivery_key: str,
|
||||||
|
) -> None:
|
||||||
|
workflow_dispatcher.ingest_event(
|
||||||
|
session,
|
||||||
|
event=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
consumers.append(
|
||||||
|
DurableEventConsumer(
|
||||||
|
consumer_id="workflow.event-triggers.v1",
|
||||||
|
event_types=frozenset({"*"}),
|
||||||
|
classifications=frozenset({"public", "internal"}),
|
||||||
|
handler=deliver_to_workflow,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = dict(
|
result = dict(
|
||||||
outbox.dispatch_pending(
|
outbox.dispatch_pending(
|
||||||
session,
|
session,
|
||||||
consumers=consumers,
|
consumers=tuple(consumers),
|
||||||
observer=publish_platform_event,
|
observer=publish_platform_event,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
|
CAPABILITY_APPROVAL_REQUESTS = "approvals.requests"
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalCapabilityError(ValueError):
|
||||||
|
"""Stable error raised by Approval capability implementations."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalActorSelector:
|
||||||
|
kind: str
|
||||||
|
value: str
|
||||||
|
label: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalStepDefinition:
|
||||||
|
key: str
|
||||||
|
label: str
|
||||||
|
selectors: tuple[ApprovalActorSelector, ...]
|
||||||
|
required_approvals: int = 1
|
||||||
|
rejection_policy: str = "fail_fast"
|
||||||
|
due_at: datetime | None = None
|
||||||
|
signature_required: bool = False
|
||||||
|
forbidden_evidence_roles: tuple[str, ...] = ()
|
||||||
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalRequestCreateCommand:
|
||||||
|
title: str
|
||||||
|
subject_module: str
|
||||||
|
subject_type: str
|
||||||
|
subject_id: str
|
||||||
|
subject_version: str | None
|
||||||
|
subject_digest: str
|
||||||
|
steps: tuple[ApprovalStepDefinition, ...]
|
||||||
|
description: str | None = None
|
||||||
|
separation_of_duties: bool = True
|
||||||
|
unique_actors_across_steps: bool = False
|
||||||
|
expires_at: datetime | None = None
|
||||||
|
policy_refs: tuple[str, ...] = ()
|
||||||
|
evidence_actors: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
|
||||||
|
template_id: str | None = None
|
||||||
|
template_revision: int | None = None
|
||||||
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalTemplateCreateCommand:
|
||||||
|
key: str
|
||||||
|
title: str
|
||||||
|
steps: tuple[ApprovalStepDefinition, ...]
|
||||||
|
description: str | None = None
|
||||||
|
separation_of_duties: bool = True
|
||||||
|
unique_actors_across_steps: bool = False
|
||||||
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalTemplateRef:
|
||||||
|
id: str
|
||||||
|
key: str
|
||||||
|
revision: int
|
||||||
|
state: str
|
||||||
|
content_sha256: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalDecisionCommand:
|
||||||
|
outcome: str
|
||||||
|
reason: str
|
||||||
|
expected_revision: int
|
||||||
|
idempotency_key: str
|
||||||
|
delegated_for_account_id: str | None = None
|
||||||
|
signature_ref: Mapping[str, object] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalRequestRef:
|
||||||
|
id: str
|
||||||
|
revision: int
|
||||||
|
state: str
|
||||||
|
current_step_key: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalDecisionReceipt:
|
||||||
|
request_id: str
|
||||||
|
revision: int
|
||||||
|
state: str
|
||||||
|
step_key: str
|
||||||
|
outcome: str
|
||||||
|
actor_id: str
|
||||||
|
recorded_at: datetime
|
||||||
|
receipt_sha256: str
|
||||||
|
authority_provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
replayed: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalCheck:
|
||||||
|
request_id: str
|
||||||
|
revision: int
|
||||||
|
state: str
|
||||||
|
approved: bool
|
||||||
|
subject_module: str
|
||||||
|
subject_type: str
|
||||||
|
subject_id: str
|
||||||
|
subject_version: str | None
|
||||||
|
subject_digest: str
|
||||||
|
completed_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class ApprovalRequestProvider(Protocol):
|
||||||
|
def create_template(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
command: ApprovalTemplateCreateCommand,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> ApprovalTemplateRef: ...
|
||||||
|
|
||||||
|
def revise_template(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
template_id: str,
|
||||||
|
command: ApprovalTemplateCreateCommand,
|
||||||
|
expected_revision: int,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> ApprovalTemplateRef: ...
|
||||||
|
|
||||||
|
def publish_template(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
template_id: str,
|
||||||
|
expected_revision: int,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> ApprovalTemplateRef: ...
|
||||||
|
|
||||||
|
def create_request(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
command: ApprovalRequestCreateCommand,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> ApprovalRequestRef: ...
|
||||||
|
|
||||||
|
def get_request(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request_id: str,
|
||||||
|
) -> Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
def decide(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request_id: str,
|
||||||
|
command: ApprovalDecisionCommand,
|
||||||
|
) -> ApprovalDecisionReceipt: ...
|
||||||
|
|
||||||
|
def check_approved(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request_id: str,
|
||||||
|
subject_module: str,
|
||||||
|
subject_type: str,
|
||||||
|
subject_id: str,
|
||||||
|
subject_version: str | None,
|
||||||
|
subject_digest: str,
|
||||||
|
) -> ApprovalCheck: ...
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ApprovalActorSelector",
|
||||||
|
"ApprovalCapabilityError",
|
||||||
|
"ApprovalCheck",
|
||||||
|
"ApprovalDecisionCommand",
|
||||||
|
"ApprovalDecisionReceipt",
|
||||||
|
"ApprovalRequestCreateCommand",
|
||||||
|
"ApprovalRequestProvider",
|
||||||
|
"ApprovalRequestRef",
|
||||||
|
"ApprovalStepDefinition",
|
||||||
|
"ApprovalTemplateCreateCommand",
|
||||||
|
"ApprovalTemplateRef",
|
||||||
|
"CAPABILITY_APPROVAL_REQUESTS",
|
||||||
|
]
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
|
CAPABILITY_ENCRYPTION_KEY_VAULT = "encryption.keyVault"
|
||||||
|
CAPABILITY_ENCRYPTION_CONTENT_PROTECTION = "encryption.contentProtection"
|
||||||
|
CAPABILITY_ENCRYPTION_RECOVERY = "encryption.recoveryCeremony"
|
||||||
|
CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT = "encryption.disablePreflight"
|
||||||
|
CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX = "encryption.keyMaterialProvider."
|
||||||
|
ENCRYPTION_CONTRACT_VERSION = "1"
|
||||||
|
|
||||||
|
ProtectionProfileKind = Literal[
|
||||||
|
"server_envelope",
|
||||||
|
"tenant_held",
|
||||||
|
"end_to_end",
|
||||||
|
]
|
||||||
|
KeyLifecycleState = Literal[
|
||||||
|
"active",
|
||||||
|
"rotation_pending",
|
||||||
|
"revoked",
|
||||||
|
"destruction_scheduled",
|
||||||
|
"destroyed",
|
||||||
|
"unavailable",
|
||||||
|
]
|
||||||
|
ProtectionState = Literal[
|
||||||
|
"active",
|
||||||
|
"migration_pending",
|
||||||
|
"migrating",
|
||||||
|
"migrated",
|
||||||
|
"decrypted",
|
||||||
|
"exported",
|
||||||
|
"destroyed",
|
||||||
|
"unavailable",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyMaterialProvisionRequest:
|
||||||
|
tenant_id: str
|
||||||
|
vault_id: str
|
||||||
|
key_version: int
|
||||||
|
algorithm_suite: str
|
||||||
|
purpose: str
|
||||||
|
idempotency_key: str
|
||||||
|
provider_policy: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
import_reference: str | None = None
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.key_version < 1:
|
||||||
|
raise ValueError("Key version must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("vault id", self.vault_id),
|
||||||
|
("algorithm suite", self.algorithm_suite),
|
||||||
|
("purpose", self.purpose),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyMaterialDescriptor:
|
||||||
|
provider_id: str
|
||||||
|
provider_key_ref: str
|
||||||
|
algorithm_suite: str
|
||||||
|
state: KeyLifecycleState
|
||||||
|
created_at: datetime
|
||||||
|
public_key_ref: str | None = None
|
||||||
|
imported: bool = False
|
||||||
|
exportable: bool = False
|
||||||
|
provider_version: str | None = None
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
for label, value in (
|
||||||
|
("provider id", self.provider_id),
|
||||||
|
("provider key reference", self.provider_key_ref),
|
||||||
|
("algorithm suite", self.algorithm_suite),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
_reject_secret_mapping(self.provenance, "Provider provenance")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyVaultCreateRequest:
|
||||||
|
tenant_id: str
|
||||||
|
vault_id: str
|
||||||
|
name: str
|
||||||
|
provider_id: str
|
||||||
|
purpose: str
|
||||||
|
algorithm_suite: str
|
||||||
|
scope_type: str
|
||||||
|
scope_id: str | None
|
||||||
|
policy_ref: str
|
||||||
|
idempotency_key: str
|
||||||
|
recovery_quorum: int = 2
|
||||||
|
profile_kind: ProtectionProfileKind = "server_envelope"
|
||||||
|
import_reference: str | None = None
|
||||||
|
provider_policy: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.recovery_quorum < 1:
|
||||||
|
raise ValueError("Recovery quorum must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("vault id", self.vault_id),
|
||||||
|
("name", self.name),
|
||||||
|
("provider id", self.provider_id),
|
||||||
|
("purpose", self.purpose),
|
||||||
|
("algorithm suite", self.algorithm_suite),
|
||||||
|
("scope type", self.scope_type),
|
||||||
|
("policy reference", self.policy_ref),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
_reject_secret_mapping(self.provider_policy, "Provider policy")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyVersionRef:
|
||||||
|
tenant_id: str
|
||||||
|
vault_id: str
|
||||||
|
version: int
|
||||||
|
provider_id: str
|
||||||
|
provider_key_ref: str
|
||||||
|
algorithm_suite: str
|
||||||
|
state: KeyLifecycleState
|
||||||
|
created_at: datetime
|
||||||
|
activated_at: datetime | None = None
|
||||||
|
revoked_at: datetime | None = None
|
||||||
|
destruction_scheduled_at: datetime | None = None
|
||||||
|
destroyed_at: datetime | None = None
|
||||||
|
public_key_ref: str | None = None
|
||||||
|
imported: bool = False
|
||||||
|
exportable: bool = False
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyVaultRef:
|
||||||
|
tenant_id: str
|
||||||
|
vault_id: str
|
||||||
|
name: str
|
||||||
|
provider_id: str
|
||||||
|
purpose: str
|
||||||
|
profile_kind: ProtectionProfileKind
|
||||||
|
scope_type: str
|
||||||
|
scope_id: str | None
|
||||||
|
policy_ref: str
|
||||||
|
recovery_quorum: int
|
||||||
|
state: str
|
||||||
|
revision: int
|
||||||
|
current_key: KeyVersionRef | None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyRotationRequest:
|
||||||
|
tenant_id: str
|
||||||
|
vault_id: str
|
||||||
|
expected_revision: int
|
||||||
|
reason: str
|
||||||
|
policy_decision_ref: str
|
||||||
|
assurance_evidence_ref: str
|
||||||
|
idempotency_key: str
|
||||||
|
algorithm_suite: str | None = None
|
||||||
|
provider_policy: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.expected_revision < 1:
|
||||||
|
raise ValueError("Expected revision must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("vault id", self.vault_id),
|
||||||
|
("reason", self.reason),
|
||||||
|
("policy decision reference", self.policy_decision_ref),
|
||||||
|
("assurance evidence reference", self.assurance_evidence_ref),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
_reject_secret_mapping(self.provider_policy, "Provider policy")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyLifecycleRequest:
|
||||||
|
tenant_id: str
|
||||||
|
vault_id: str
|
||||||
|
key_version: int
|
||||||
|
expected_revision: int
|
||||||
|
reason: str
|
||||||
|
policy_decision_ref: str
|
||||||
|
assurance_evidence_ref: str
|
||||||
|
idempotency_key: str
|
||||||
|
effective_at: datetime | None = None
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.key_version < 1 or self.expected_revision < 1:
|
||||||
|
raise ValueError("Key version and expected revision must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("vault id", self.vault_id),
|
||||||
|
("reason", self.reason),
|
||||||
|
("policy decision reference", self.policy_decision_ref),
|
||||||
|
("assurance evidence reference", self.assurance_evidence_ref),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContentProtectionEnvelope:
|
||||||
|
envelope_id: str
|
||||||
|
tenant_id: str
|
||||||
|
owner_module: str
|
||||||
|
resource_type: str
|
||||||
|
resource_id: str
|
||||||
|
profile_kind: ProtectionProfileKind
|
||||||
|
profile_id: str
|
||||||
|
provider_id: str
|
||||||
|
vault_id: str
|
||||||
|
key_version: int
|
||||||
|
algorithm_suite: str
|
||||||
|
ciphertext_ref: str
|
||||||
|
ciphertext_digest: str
|
||||||
|
authenticated_context_digest: str
|
||||||
|
state: ProtectionState
|
||||||
|
created_at: datetime
|
||||||
|
wrapped_key_refs: tuple[str, ...] = ()
|
||||||
|
source_envelope_id: str | None = None
|
||||||
|
migration_id: str | None = None
|
||||||
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.key_version < 1:
|
||||||
|
raise ValueError("Envelope key version must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("envelope id", self.envelope_id),
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("owner module", self.owner_module),
|
||||||
|
("resource type", self.resource_type),
|
||||||
|
("resource id", self.resource_id),
|
||||||
|
("profile id", self.profile_id),
|
||||||
|
("provider id", self.provider_id),
|
||||||
|
("vault id", self.vault_id),
|
||||||
|
("algorithm suite", self.algorithm_suite),
|
||||||
|
("ciphertext reference", self.ciphertext_ref),
|
||||||
|
("ciphertext digest", self.ciphertext_digest),
|
||||||
|
("authenticated context digest", self.authenticated_context_digest),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
_reject_secret_mapping(self.metadata, "Protection metadata")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ProtectionRegistrationRequest:
|
||||||
|
envelope: ContentProtectionEnvelope
|
||||||
|
idempotency_key: str
|
||||||
|
policy_decision_ref: str
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
_require_text(self.idempotency_key, "idempotency key")
|
||||||
|
_require_text(self.policy_decision_ref, "policy decision reference")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ProtectionMigrationRequest:
|
||||||
|
tenant_id: str
|
||||||
|
envelope_id: str
|
||||||
|
target_provider_id: str
|
||||||
|
target_vault_id: str
|
||||||
|
target_key_version: int
|
||||||
|
target_algorithm_suite: str
|
||||||
|
mode: Literal["rewrap", "reencrypt", "decrypt", "export", "destroy"]
|
||||||
|
policy_decision_ref: str
|
||||||
|
assurance_evidence_ref: str
|
||||||
|
idempotency_key: str
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.target_key_version < 1:
|
||||||
|
raise ValueError("Target key version must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("envelope id", self.envelope_id),
|
||||||
|
("target provider id", self.target_provider_id),
|
||||||
|
("target vault id", self.target_vault_id),
|
||||||
|
("target algorithm suite", self.target_algorithm_suite),
|
||||||
|
("policy decision reference", self.policy_decision_ref),
|
||||||
|
("assurance evidence reference", self.assurance_evidence_ref),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ProtectionMigrationResult:
|
||||||
|
migration_id: str
|
||||||
|
state: Literal[
|
||||||
|
"requested",
|
||||||
|
"running",
|
||||||
|
"succeeded",
|
||||||
|
"rejected",
|
||||||
|
"outcome_unknown",
|
||||||
|
"reconciled",
|
||||||
|
]
|
||||||
|
source_envelope_id: str
|
||||||
|
target_envelope: ContentProtectionEnvelope | None = None
|
||||||
|
error: str | None = None
|
||||||
|
evidence_refs: tuple[str, ...] = ()
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RecoveryRequest:
|
||||||
|
tenant_id: str
|
||||||
|
vault_id: str
|
||||||
|
reason: str
|
||||||
|
requested_scope: str
|
||||||
|
policy_decision_ref: str
|
||||||
|
assurance_evidence_ref: str
|
||||||
|
idempotency_key: str
|
||||||
|
expires_at: datetime
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("vault id", self.vault_id),
|
||||||
|
("reason", self.reason),
|
||||||
|
("requested scope", self.requested_scope),
|
||||||
|
("policy decision reference", self.policy_decision_ref),
|
||||||
|
("assurance evidence reference", self.assurance_evidence_ref),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RecoveryApprovalRequest:
|
||||||
|
tenant_id: str
|
||||||
|
recovery_id: str
|
||||||
|
decision: Literal["approve", "reject"]
|
||||||
|
reason: str
|
||||||
|
assurance_evidence_ref: str
|
||||||
|
expected_revision: int
|
||||||
|
idempotency_key: str
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.expected_revision < 1:
|
||||||
|
raise ValueError("Expected revision must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("recovery id", self.recovery_id),
|
||||||
|
("reason", self.reason),
|
||||||
|
("assurance evidence reference", self.assurance_evidence_ref),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RecoveryRef:
|
||||||
|
tenant_id: str
|
||||||
|
recovery_id: str
|
||||||
|
vault_id: str
|
||||||
|
state: Literal["pending", "approved", "rejected", "expired", "executed"]
|
||||||
|
requested_scope: str
|
||||||
|
quorum: int
|
||||||
|
approvals: int
|
||||||
|
rejections: int
|
||||||
|
revision: int
|
||||||
|
expires_at: datetime
|
||||||
|
policy_decision_ref: str
|
||||||
|
execution_ref: str | None = None
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DisablePreflightReport:
|
||||||
|
allowed: bool
|
||||||
|
protected_count: int
|
||||||
|
unresolved_count: int
|
||||||
|
state_counts: Mapping[str, int]
|
||||||
|
blocking_envelope_refs: tuple[str, ...]
|
||||||
|
required_actions: tuple[str, ...]
|
||||||
|
generated_at: datetime
|
||||||
|
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class EncryptionKeyMaterialProvider(Protocol):
|
||||||
|
def provision_key(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: KeyMaterialProvisionRequest,
|
||||||
|
) -> KeyMaterialDescriptor: ...
|
||||||
|
|
||||||
|
def schedule_key_destruction(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
provider_key_ref: str,
|
||||||
|
effective_at: datetime,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> KeyMaterialDescriptor: ...
|
||||||
|
|
||||||
|
def revoke_key(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
provider_key_ref: str,
|
||||||
|
reason: str,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> KeyMaterialDescriptor: ...
|
||||||
|
|
||||||
|
def key_status(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
provider_key_ref: str,
|
||||||
|
) -> KeyMaterialDescriptor | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class EncryptionKeyVault(Protocol):
|
||||||
|
def create_vault(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: KeyVaultCreateRequest,
|
||||||
|
) -> KeyVaultRef: ...
|
||||||
|
|
||||||
|
def rotate_key(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: KeyRotationRequest,
|
||||||
|
) -> KeyVaultRef: ...
|
||||||
|
|
||||||
|
def revoke_key(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: KeyLifecycleRequest,
|
||||||
|
) -> KeyVaultRef: ...
|
||||||
|
|
||||||
|
def schedule_destruction(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: KeyLifecycleRequest,
|
||||||
|
) -> KeyVaultRef: ...
|
||||||
|
|
||||||
|
def get_vault(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
vault_id: str,
|
||||||
|
) -> KeyVaultRef | None: ...
|
||||||
|
|
||||||
|
def reconcile_vault(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
vault_id: str,
|
||||||
|
) -> KeyVaultRef: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class ContentProtectionRegistry(Protocol):
|
||||||
|
def register_envelope(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: ProtectionRegistrationRequest,
|
||||||
|
) -> ContentProtectionEnvelope: ...
|
||||||
|
|
||||||
|
def request_migration(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: ProtectionMigrationRequest,
|
||||||
|
) -> ProtectionMigrationResult: ...
|
||||||
|
|
||||||
|
def reconcile_migration(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
migration_id: str,
|
||||||
|
) -> ProtectionMigrationResult: ...
|
||||||
|
|
||||||
|
def get_envelope(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
envelope_id: str,
|
||||||
|
) -> ContentProtectionEnvelope | None: ...
|
||||||
|
|
||||||
|
def record_migration_outcome(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
migration_id: str,
|
||||||
|
state: Literal["succeeded", "rejected", "outcome_unknown"],
|
||||||
|
evidence_refs: tuple[str, ...],
|
||||||
|
target_envelope: ContentProtectionEnvelope | None = None,
|
||||||
|
error_code: str | None = None,
|
||||||
|
) -> ProtectionMigrationResult: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class EncryptionRecoveryCeremony(Protocol):
|
||||||
|
def request_recovery(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: RecoveryRequest,
|
||||||
|
) -> RecoveryRef: ...
|
||||||
|
|
||||||
|
def decide_recovery(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: RecoveryApprovalRequest,
|
||||||
|
) -> RecoveryRef: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class EncryptionDisablePreflight(Protocol):
|
||||||
|
def assess_disable(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str | None = None,
|
||||||
|
limit: int = 100,
|
||||||
|
) -> DisablePreflightReport: ...
|
||||||
|
|
||||||
|
|
||||||
|
def key_material_provider(
|
||||||
|
registry: object | None,
|
||||||
|
provider_id: str,
|
||||||
|
) -> EncryptionKeyMaterialProvider | None:
|
||||||
|
capability = _capability(
|
||||||
|
registry,
|
||||||
|
f"{CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX}{provider_id}",
|
||||||
|
)
|
||||||
|
return capability if isinstance(capability, EncryptionKeyMaterialProvider) else None
|
||||||
|
|
||||||
|
|
||||||
|
def encryption_key_vault(registry: object | None) -> EncryptionKeyVault | None:
|
||||||
|
value = _capability(registry, CAPABILITY_ENCRYPTION_KEY_VAULT)
|
||||||
|
return value if isinstance(value, EncryptionKeyVault) else None
|
||||||
|
|
||||||
|
|
||||||
|
def content_protection_registry(
|
||||||
|
registry: object | None,
|
||||||
|
) -> ContentProtectionRegistry | None:
|
||||||
|
value = _capability(registry, CAPABILITY_ENCRYPTION_CONTENT_PROTECTION)
|
||||||
|
return value if isinstance(value, ContentProtectionRegistry) else None
|
||||||
|
|
||||||
|
|
||||||
|
def encryption_recovery_ceremony(
|
||||||
|
registry: object | None,
|
||||||
|
) -> EncryptionRecoveryCeremony | None:
|
||||||
|
value = _capability(registry, CAPABILITY_ENCRYPTION_RECOVERY)
|
||||||
|
return value if isinstance(value, EncryptionRecoveryCeremony) else None
|
||||||
|
|
||||||
|
|
||||||
|
def encryption_disable_preflight(
|
||||||
|
registry: object | None,
|
||||||
|
) -> EncryptionDisablePreflight | None:
|
||||||
|
value = _capability(registry, CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT)
|
||||||
|
return value if isinstance(value, EncryptionDisablePreflight) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _capability(registry: object | None, name: str) -> object | None:
|
||||||
|
if (
|
||||||
|
registry is None
|
||||||
|
or not hasattr(registry, "has_capability")
|
||||||
|
or not hasattr(registry, "capability")
|
||||||
|
or not registry.has_capability(name)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
return registry.capability(name)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_contract(value: str) -> None:
|
||||||
|
if value != ENCRYPTION_CONTRACT_VERSION:
|
||||||
|
raise ValueError("Unsupported encryption contract version")
|
||||||
|
|
||||||
|
|
||||||
|
def _require_text(value: str, label: str) -> None:
|
||||||
|
if not value.strip():
|
||||||
|
raise ValueError(f"{label.capitalize()} is required")
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_secret_mapping(value: Mapping[str, object], label: str) -> None:
|
||||||
|
forbidden = {
|
||||||
|
"ciphertext",
|
||||||
|
"credential",
|
||||||
|
"key",
|
||||||
|
"key_material",
|
||||||
|
"password",
|
||||||
|
"plaintext",
|
||||||
|
"private_key",
|
||||||
|
"secret",
|
||||||
|
"token",
|
||||||
|
}
|
||||||
|
keys = {str(key).strip().casefold() for key in value}
|
||||||
|
if forbidden & keys:
|
||||||
|
raise ValueError(f"{label} must not contain secret material")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CAPABILITY_ENCRYPTION_CONTENT_PROTECTION",
|
||||||
|
"CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT",
|
||||||
|
"CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX",
|
||||||
|
"CAPABILITY_ENCRYPTION_KEY_VAULT",
|
||||||
|
"CAPABILITY_ENCRYPTION_RECOVERY",
|
||||||
|
"ContentProtectionEnvelope",
|
||||||
|
"ContentProtectionRegistry",
|
||||||
|
"DisablePreflightReport",
|
||||||
|
"EncryptionDisablePreflight",
|
||||||
|
"EncryptionKeyMaterialProvider",
|
||||||
|
"EncryptionKeyVault",
|
||||||
|
"EncryptionRecoveryCeremony",
|
||||||
|
"KeyLifecycleRequest",
|
||||||
|
"KeyMaterialDescriptor",
|
||||||
|
"KeyMaterialProvisionRequest",
|
||||||
|
"KeyRotationRequest",
|
||||||
|
"KeyVaultCreateRequest",
|
||||||
|
"KeyVaultRef",
|
||||||
|
"KeyVersionRef",
|
||||||
|
"ProtectionMigrationRequest",
|
||||||
|
"ProtectionMigrationResult",
|
||||||
|
"ProtectionRegistrationRequest",
|
||||||
|
"RecoveryApprovalRequest",
|
||||||
|
"RecoveryRef",
|
||||||
|
"RecoveryRequest",
|
||||||
|
"content_protection_registry",
|
||||||
|
"encryption_disable_preflight",
|
||||||
|
"encryption_key_vault",
|
||||||
|
"encryption_recovery_ceremony",
|
||||||
|
"key_material_provider",
|
||||||
|
]
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
|
CAPABILITY_IDENTITY_TRUST_DIRECTORY = "identity_trust.directory"
|
||||||
|
CAPABILITY_IDENTITY_TRUST_ASSURANCE = "identity_trust.assurance"
|
||||||
|
IDENTITY_TRUST_CONTRACT_VERSION = "1"
|
||||||
|
|
||||||
|
DeviceKeyPurpose = Literal["encryption", "signing", "encryption_and_signing"]
|
||||||
|
DeviceKeyStatus = Literal["active", "revoked", "expired"]
|
||||||
|
TrustSubjectKind = Literal[
|
||||||
|
"identity",
|
||||||
|
"account",
|
||||||
|
"function",
|
||||||
|
"postbox",
|
||||||
|
"external_recipient",
|
||||||
|
]
|
||||||
|
|
||||||
|
_PRIVATE_JWK_FIELDS = frozenset({"d", "p", "q", "dp", "dq", "qi", "oth", "k"})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DeviceKeyRegistration:
|
||||||
|
tenant_id: str
|
||||||
|
identity_id: str
|
||||||
|
account_id: str
|
||||||
|
device_id: str
|
||||||
|
key_id: str
|
||||||
|
algorithm: str
|
||||||
|
public_jwk: Mapping[str, object]
|
||||||
|
purpose: DeviceKeyPurpose = "encryption"
|
||||||
|
assurance_level: str = "software"
|
||||||
|
attestation_ref: str | None = None
|
||||||
|
expires_at: datetime | None = None
|
||||||
|
idempotency_key: str = ""
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("identity id", self.identity_id),
|
||||||
|
("account id", self.account_id),
|
||||||
|
("device id", self.device_id),
|
||||||
|
("key id", self.key_id),
|
||||||
|
("algorithm", self.algorithm),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
if not self.public_jwk or _PRIVATE_JWK_FIELDS & set(self.public_jwk):
|
||||||
|
raise ValueError("Only a bounded public JWK may be registered")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DeviceKeyRef:
|
||||||
|
tenant_id: str
|
||||||
|
identity_id: str
|
||||||
|
account_id: str
|
||||||
|
device_id: str
|
||||||
|
key_id: str
|
||||||
|
algorithm: str
|
||||||
|
public_jwk: Mapping[str, object]
|
||||||
|
purpose: DeviceKeyPurpose
|
||||||
|
assurance_level: str
|
||||||
|
status: DeviceKeyStatus
|
||||||
|
epoch: int
|
||||||
|
registered_at: datetime
|
||||||
|
attestation_ref: str | None = None
|
||||||
|
expires_at: datetime | None = None
|
||||||
|
revoked_at: datetime | None = None
|
||||||
|
revocation_reason: str | None = None
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyEpochRotationRequest:
|
||||||
|
tenant_id: str
|
||||||
|
subject_kind: TrustSubjectKind
|
||||||
|
subject_id: str
|
||||||
|
reason: str
|
||||||
|
access_decision_ref: str
|
||||||
|
idempotency_key: str
|
||||||
|
history_policy: str = "all_retained"
|
||||||
|
previous_epoch: int | None = None
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("subject id", self.subject_id),
|
||||||
|
("reason", self.reason),
|
||||||
|
("access decision reference", self.access_decision_ref),
|
||||||
|
("idempotency key", self.idempotency_key),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyEpochRef:
|
||||||
|
tenant_id: str
|
||||||
|
subject_kind: TrustSubjectKind
|
||||||
|
subject_id: str
|
||||||
|
epoch: int
|
||||||
|
state: Literal["active", "superseded", "revoked"]
|
||||||
|
history_policy: str
|
||||||
|
effective_at: datetime
|
||||||
|
previous_epoch: int | None = None
|
||||||
|
reason: str | None = None
|
||||||
|
access_decision_ref: str | None = None
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyAccessRequest:
|
||||||
|
tenant_id: str
|
||||||
|
account_id: str
|
||||||
|
device_key_id: str
|
||||||
|
subject_kind: TrustSubjectKind
|
||||||
|
subject_id: str
|
||||||
|
key_epoch: int
|
||||||
|
access_decision_ref: str
|
||||||
|
purpose: str
|
||||||
|
requested_at: datetime
|
||||||
|
function_assignment_id: str | None = None
|
||||||
|
delegation_id: str | None = None
|
||||||
|
resource_ref: str | None = None
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.key_epoch < 1:
|
||||||
|
raise ValueError("Key epoch must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("account id", self.account_id),
|
||||||
|
("device key id", self.device_key_id),
|
||||||
|
("subject id", self.subject_id),
|
||||||
|
("access decision reference", self.access_decision_ref),
|
||||||
|
("purpose", self.purpose),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class KeyAccessDecision:
|
||||||
|
allowed: bool
|
||||||
|
decision_ref: str
|
||||||
|
reason: str
|
||||||
|
device_key: DeviceKeyRef | None = None
|
||||||
|
epoch: KeyEpochRef | None = None
|
||||||
|
audit_event_ref: str | None = None
|
||||||
|
requirements: tuple[str, ...] = ()
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AssuranceCheckRequest:
|
||||||
|
tenant_id: str
|
||||||
|
account_id: str
|
||||||
|
purpose: str
|
||||||
|
minimum_level: str
|
||||||
|
evidence_ref: str
|
||||||
|
evaluated_at: datetime
|
||||||
|
maximum_age_seconds: int = 300
|
||||||
|
device_key_id: str | None = None
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_validate_contract(self.contract_version)
|
||||||
|
if self.maximum_age_seconds < 1:
|
||||||
|
raise ValueError("Assurance maximum age must be positive")
|
||||||
|
for label, value in (
|
||||||
|
("tenant id", self.tenant_id),
|
||||||
|
("account id", self.account_id),
|
||||||
|
("purpose", self.purpose),
|
||||||
|
("minimum level", self.minimum_level),
|
||||||
|
("evidence reference", self.evidence_ref),
|
||||||
|
):
|
||||||
|
_require_text(value, label)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AssuranceDecision:
|
||||||
|
allowed: bool
|
||||||
|
reason: str
|
||||||
|
assurance_level: str | None = None
|
||||||
|
evidence_ref: str | None = None
|
||||||
|
verified_at: datetime | None = None
|
||||||
|
expires_at: datetime | None = None
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class IdentityTrustDirectory(Protocol):
|
||||||
|
def register_device_key(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: DeviceKeyRegistration,
|
||||||
|
) -> DeviceKeyRef: ...
|
||||||
|
|
||||||
|
def revoke_device_key(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
key_id: str,
|
||||||
|
expected_epoch: int,
|
||||||
|
reason: str,
|
||||||
|
) -> DeviceKeyRef: ...
|
||||||
|
|
||||||
|
def list_device_keys(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
account_id: str,
|
||||||
|
active_only: bool = True,
|
||||||
|
) -> tuple[DeviceKeyRef, ...]: ...
|
||||||
|
|
||||||
|
def rotate_epoch(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: KeyEpochRotationRequest,
|
||||||
|
) -> KeyEpochRef: ...
|
||||||
|
|
||||||
|
def resolve_epoch(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
subject_kind: TrustSubjectKind,
|
||||||
|
subject_id: str,
|
||||||
|
epoch: int | None = None,
|
||||||
|
) -> KeyEpochRef | None: ...
|
||||||
|
|
||||||
|
def decide_key_access(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: KeyAccessRequest,
|
||||||
|
) -> KeyAccessDecision: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class IdentityTrustAssurance(Protocol):
|
||||||
|
def verify_assurance(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: AssuranceCheckRequest,
|
||||||
|
) -> AssuranceDecision: ...
|
||||||
|
|
||||||
|
|
||||||
|
def identity_trust_directory(
|
||||||
|
registry: object | None,
|
||||||
|
) -> IdentityTrustDirectory | None:
|
||||||
|
capability = _capability(registry, CAPABILITY_IDENTITY_TRUST_DIRECTORY)
|
||||||
|
return capability if isinstance(capability, IdentityTrustDirectory) else None
|
||||||
|
|
||||||
|
|
||||||
|
def identity_trust_assurance(
|
||||||
|
registry: object | None,
|
||||||
|
) -> IdentityTrustAssurance | None:
|
||||||
|
capability = _capability(registry, CAPABILITY_IDENTITY_TRUST_ASSURANCE)
|
||||||
|
return capability if isinstance(capability, IdentityTrustAssurance) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _capability(registry: object | None, name: str) -> object | None:
|
||||||
|
if (
|
||||||
|
registry is None
|
||||||
|
or not hasattr(registry, "has_capability")
|
||||||
|
or not hasattr(registry, "capability")
|
||||||
|
or not registry.has_capability(name)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
return registry.capability(name)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_contract(value: str) -> None:
|
||||||
|
if value != IDENTITY_TRUST_CONTRACT_VERSION:
|
||||||
|
raise ValueError("Unsupported identity-trust contract version")
|
||||||
|
|
||||||
|
|
||||||
|
def _require_text(value: str, label: str) -> None:
|
||||||
|
if not value.strip():
|
||||||
|
raise ValueError(f"{label.capitalize()} is required")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AssuranceCheckRequest",
|
||||||
|
"AssuranceDecision",
|
||||||
|
"CAPABILITY_IDENTITY_TRUST_ASSURANCE",
|
||||||
|
"CAPABILITY_IDENTITY_TRUST_DIRECTORY",
|
||||||
|
"DeviceKeyRef",
|
||||||
|
"DeviceKeyRegistration",
|
||||||
|
"IdentityTrustAssurance",
|
||||||
|
"IdentityTrustDirectory",
|
||||||
|
"KeyAccessDecision",
|
||||||
|
"KeyAccessRequest",
|
||||||
|
"KeyEpochRef",
|
||||||
|
"KeyEpochRotationRequest",
|
||||||
|
"identity_trust_assurance",
|
||||||
|
"identity_trust_directory",
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
|
CAPABILITY_VOTING_BALLOTS = "voting.ballots"
|
||||||
|
CAPABILITY_VOTING_PROVIDER_PREFIX = "voting.provider."
|
||||||
|
|
||||||
|
VOTING_ASSURANCE_RECORDED = "recorded"
|
||||||
|
VOTING_ASSURANCE_CONFIDENTIAL = "confidential"
|
||||||
|
VOTING_ASSURANCE_SECRET = "secret"
|
||||||
|
VOTING_ASSURANCE_EXTERNAL_CERTIFIED = "external_certified"
|
||||||
|
|
||||||
|
|
||||||
|
class VotingCapabilityError(ValueError):
|
||||||
|
"""Stable error raised by Voting capability implementations."""
|
||||||
|
|
||||||
|
|
||||||
|
def voting_provider_capability(provider_id: str) -> str:
|
||||||
|
normalized = str(provider_id or "").strip().lower()
|
||||||
|
if not normalized or any(
|
||||||
|
character not in "abcdefghijklmnopqrstuvwxyz0123456789_.-"
|
||||||
|
for character in normalized
|
||||||
|
):
|
||||||
|
raise VotingCapabilityError("Voting provider id is invalid.")
|
||||||
|
return f"{CAPABILITY_VOTING_PROVIDER_PREFIX}{normalized}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class VotingOption:
|
||||||
|
key: str
|
||||||
|
label: str
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class VotingElector:
|
||||||
|
subject_id: str
|
||||||
|
label: str | None = None
|
||||||
|
weight: int = 1
|
||||||
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class VotingBallotCreateCommand:
|
||||||
|
title: str
|
||||||
|
method: str
|
||||||
|
assurance_profile: str
|
||||||
|
options: tuple[VotingOption, ...]
|
||||||
|
electorate: tuple[VotingElector, ...]
|
||||||
|
description: str | None = None
|
||||||
|
context_module: str | None = None
|
||||||
|
context_resource_type: str | None = None
|
||||||
|
context_resource_id: str | None = None
|
||||||
|
quorum_weight: int = 0
|
||||||
|
threshold_numerator: int = 1
|
||||||
|
threshold_denominator: int = 2
|
||||||
|
allow_replacement: bool = True
|
||||||
|
opens_at: datetime | None = None
|
||||||
|
closes_at: datetime | None = None
|
||||||
|
provider_id: str | None = None
|
||||||
|
provider_ballot_ref: str | None = None
|
||||||
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class VotingCastCommand:
|
||||||
|
selections: tuple[str, ...]
|
||||||
|
idempotency_key: str
|
||||||
|
elector_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class VotingBallotRef:
|
||||||
|
id: str
|
||||||
|
revision: int
|
||||||
|
state: str
|
||||||
|
assurance_profile: str
|
||||||
|
definition_sha256: str | None = None
|
||||||
|
electorate_sha256: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class VotingReceipt:
|
||||||
|
ballot_id: str
|
||||||
|
revision: int
|
||||||
|
receipt_sha256: str
|
||||||
|
cast_at: datetime
|
||||||
|
replaced_previous: bool = False
|
||||||
|
replayed: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class VotingResult:
|
||||||
|
ballot_id: str
|
||||||
|
revision: int
|
||||||
|
counts: Mapping[str, int]
|
||||||
|
weighted_counts: Mapping[str, int]
|
||||||
|
cast_count: int
|
||||||
|
cast_weight: int
|
||||||
|
eligible_count: int
|
||||||
|
eligible_weight: int
|
||||||
|
quorum_met: bool
|
||||||
|
threshold_met: bool
|
||||||
|
winning_options: tuple[str, ...]
|
||||||
|
result_sha256: str
|
||||||
|
evidence: tuple[Mapping[str, object], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ExternalVotingFinalizationRequest:
|
||||||
|
tenant_id: str
|
||||||
|
ballot_id: str
|
||||||
|
provider_ballot_ref: str
|
||||||
|
definition_sha256: str
|
||||||
|
electorate_sha256: str
|
||||||
|
options: tuple[VotingOption, ...]
|
||||||
|
eligible_count: int
|
||||||
|
eligible_weight: int
|
||||||
|
requested_at: datetime
|
||||||
|
idempotency_key: str
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class ExternalVotingProvider(Protocol):
|
||||||
|
"""Provider boundary for confidential, secret, or certified voting.
|
||||||
|
|
||||||
|
Providers return aggregate results and evidence only. Raw ballots and
|
||||||
|
provider credentials must not cross this boundary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def finalize_ballot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
request: ExternalVotingFinalizationRequest,
|
||||||
|
) -> VotingResult: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class VotingBallotProvider(Protocol):
|
||||||
|
def create_ballot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
command: VotingBallotCreateCommand,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> VotingBallotRef: ...
|
||||||
|
|
||||||
|
def get_ballot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
ballot_id: str,
|
||||||
|
) -> Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
def open_ballot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
ballot_id: str,
|
||||||
|
expected_revision: int,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> VotingBallotRef: ...
|
||||||
|
|
||||||
|
def cast_ballot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
ballot_id: str,
|
||||||
|
command: VotingCastCommand,
|
||||||
|
) -> VotingReceipt: ...
|
||||||
|
|
||||||
|
def close_ballot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
ballot_id: str,
|
||||||
|
expected_revision: int,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> VotingResult: ...
|
||||||
|
|
||||||
|
def certify_ballot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
ballot_id: str,
|
||||||
|
expected_revision: int,
|
||||||
|
evidence: Sequence[Mapping[str, object]],
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> VotingBallotRef: ...
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CAPABILITY_VOTING_BALLOTS",
|
||||||
|
"CAPABILITY_VOTING_PROVIDER_PREFIX",
|
||||||
|
"ExternalVotingFinalizationRequest",
|
||||||
|
"ExternalVotingProvider",
|
||||||
|
"VOTING_ASSURANCE_CONFIDENTIAL",
|
||||||
|
"VOTING_ASSURANCE_EXTERNAL_CERTIFIED",
|
||||||
|
"VOTING_ASSURANCE_RECORDED",
|
||||||
|
"VOTING_ASSURANCE_SECRET",
|
||||||
|
"VotingBallotCreateCommand",
|
||||||
|
"VotingBallotProvider",
|
||||||
|
"VotingBallotRef",
|
||||||
|
"VotingCapabilityError",
|
||||||
|
"VotingCastCommand",
|
||||||
|
"VotingElector",
|
||||||
|
"VotingOption",
|
||||||
|
"VotingReceipt",
|
||||||
|
"VotingResult",
|
||||||
|
"voting_provider_capability",
|
||||||
|
]
|
||||||
@@ -7,10 +7,13 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
from typing import Protocol, runtime_checkable
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from govoplan_core.core.events import PlatformEvent
|
||||||
|
|
||||||
|
|
||||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER = "workflow.runtimeWorker"
|
CAPABILITY_WORKFLOW_RUNTIME_WORKER = "workflow.runtimeWorker"
|
||||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS = "workflow.definitionContributions"
|
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS = "workflow.definitionContributions"
|
||||||
CAPABILITY_WORKFLOW_ORCHESTRATION = "workflow.orchestration"
|
CAPABILITY_WORKFLOW_ORCHESTRATION = "workflow.orchestration"
|
||||||
|
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER = "workflow.triggerDispatcher"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
@@ -126,8 +129,27 @@ class WorkflowRuntimeWorker(Protocol):
|
|||||||
*,
|
*,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> Mapping[str, object]:
|
) -> Mapping[str, object]: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class WorkflowTriggerDispatcher(Protocol):
|
||||||
|
"""Durable schedule/event start and wait-resumption boundary."""
|
||||||
|
|
||||||
|
def dispatch_due(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
now: datetime | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
def ingest_event(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
*,
|
||||||
|
event: PlatformEvent,
|
||||||
|
) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@@ -137,8 +159,7 @@ class WorkflowDefinitionContributionProvider(Protocol):
|
|||||||
session: object,
|
session: object,
|
||||||
*,
|
*,
|
||||||
tenant_ids: Sequence[str] = (),
|
tenant_ids: Sequence[str] = (),
|
||||||
) -> Mapping[str, object]:
|
) -> Mapping[str, object]: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@@ -180,11 +201,20 @@ def workflow_runtime_worker(
|
|||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
capability = registry.capability(CAPABILITY_WORKFLOW_RUNTIME_WORKER)
|
capability = registry.capability(CAPABILITY_WORKFLOW_RUNTIME_WORKER)
|
||||||
return (
|
return capability if isinstance(capability, WorkflowRuntimeWorker) else None
|
||||||
capability
|
|
||||||
if isinstance(capability, WorkflowRuntimeWorker)
|
|
||||||
else None
|
def workflow_trigger_dispatcher(
|
||||||
)
|
registry: object | None,
|
||||||
|
) -> WorkflowTriggerDispatcher | None:
|
||||||
|
if (
|
||||||
|
registry is None
|
||||||
|
or not hasattr(registry, "has_capability")
|
||||||
|
or not registry.has_capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
capability = registry.capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER)
|
||||||
|
return capability if isinstance(capability, WorkflowTriggerDispatcher) else None
|
||||||
|
|
||||||
|
|
||||||
def workflow_definition_contribution_provider(
|
def workflow_definition_contribution_provider(
|
||||||
@@ -193,14 +223,10 @@ def workflow_definition_contribution_provider(
|
|||||||
if (
|
if (
|
||||||
registry is None
|
registry is None
|
||||||
or not hasattr(registry, "has_capability")
|
or not hasattr(registry, "has_capability")
|
||||||
or not registry.has_capability(
|
or not registry.has_capability(CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS)
|
||||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
capability = registry.capability(
|
capability = registry.capability(CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS)
|
||||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
capability
|
capability
|
||||||
if isinstance(capability, WorkflowDefinitionContributionProvider)
|
if isinstance(capability, WorkflowDefinitionContributionProvider)
|
||||||
@@ -225,15 +251,18 @@ __all__ = [
|
|||||||
"CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS",
|
"CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS",
|
||||||
"CAPABILITY_WORKFLOW_ORCHESTRATION",
|
"CAPABILITY_WORKFLOW_ORCHESTRATION",
|
||||||
"CAPABILITY_WORKFLOW_RUNTIME_WORKER",
|
"CAPABILITY_WORKFLOW_RUNTIME_WORKER",
|
||||||
|
"CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER",
|
||||||
"WorkflowCurrentStepResolution",
|
"WorkflowCurrentStepResolution",
|
||||||
"WorkflowDefinitionContribution",
|
"WorkflowDefinitionContribution",
|
||||||
"WorkflowDefinitionContributionProvider",
|
"WorkflowDefinitionContributionProvider",
|
||||||
"WorkflowInstanceRef",
|
"WorkflowInstanceRef",
|
||||||
"WorkflowOrchestrationProvider",
|
"WorkflowOrchestrationProvider",
|
||||||
"WorkflowRuntimeWorker",
|
"WorkflowRuntimeWorker",
|
||||||
|
"WorkflowTriggerDispatcher",
|
||||||
"WorkflowStandardStartRequest",
|
"WorkflowStandardStartRequest",
|
||||||
"workflow_definition_contribution_hash",
|
"workflow_definition_contribution_hash",
|
||||||
"workflow_definition_contribution_provider",
|
"workflow_definition_contribution_provider",
|
||||||
"workflow_orchestration_provider",
|
"workflow_orchestration_provider",
|
||||||
"workflow_runtime_worker",
|
"workflow_runtime_worker",
|
||||||
|
"workflow_trigger_dispatcher",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_core.core.encryption import (
|
||||||
|
CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX,
|
||||||
|
ContentProtectionEnvelope,
|
||||||
|
KeyMaterialDescriptor,
|
||||||
|
KeyVaultCreateRequest,
|
||||||
|
key_material_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 8, 1, 12, 0, tzinfo=UTC)
|
||||||
|
|
||||||
|
|
||||||
|
class Provider:
|
||||||
|
def provision_key(self, _session, _principal, *, request):
|
||||||
|
return KeyMaterialDescriptor(
|
||||||
|
provider_id="test",
|
||||||
|
provider_key_ref=f"test:{request.vault_id}:{request.key_version}",
|
||||||
|
algorithm_suite=request.algorithm_suite,
|
||||||
|
state="active",
|
||||||
|
created_at=NOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
def revoke_key(self, _session, _principal, **_kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def schedule_key_destruction(self, _session, _principal, **_kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def key_status(self, _session, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Registry:
|
||||||
|
def __init__(self, values):
|
||||||
|
self.values = values
|
||||||
|
|
||||||
|
def has_capability(self, name: str) -> bool:
|
||||||
|
return name in self.values
|
||||||
|
|
||||||
|
def capability(self, name: str):
|
||||||
|
return self.values[name]
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptionContractTests(unittest.TestCase):
|
||||||
|
def test_provider_resolution_is_optional_and_structural(self) -> None:
|
||||||
|
name = f"{CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX}test"
|
||||||
|
provider = Provider()
|
||||||
|
self.assertIs(
|
||||||
|
provider, key_material_provider(Registry({name: provider}), "test")
|
||||||
|
)
|
||||||
|
self.assertIsNone(key_material_provider(Registry({}), "test"))
|
||||||
|
|
||||||
|
def test_secret_bearing_policy_and_envelope_metadata_are_rejected(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "secret material"):
|
||||||
|
KeyVaultCreateRequest(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
vault_id="vault-1",
|
||||||
|
name="Vault",
|
||||||
|
provider_id="test",
|
||||||
|
purpose="content",
|
||||||
|
algorithm_suite="AES-256-GCM",
|
||||||
|
scope_type="tenant",
|
||||||
|
scope_id="tenant-1",
|
||||||
|
policy_ref="policy:1",
|
||||||
|
idempotency_key="create-1",
|
||||||
|
provider_policy={"password": "forbidden"},
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "secret material"):
|
||||||
|
ContentProtectionEnvelope(
|
||||||
|
envelope_id="envelope-1",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
owner_module="files",
|
||||||
|
resource_type="file",
|
||||||
|
resource_id="file-1",
|
||||||
|
profile_kind="server_envelope",
|
||||||
|
profile_id="profile-1",
|
||||||
|
provider_id="test",
|
||||||
|
vault_id="vault-1",
|
||||||
|
key_version=1,
|
||||||
|
algorithm_suite="AES-256-GCM",
|
||||||
|
ciphertext_ref="object://ciphertext/1",
|
||||||
|
ciphertext_digest="sha256:ciphertext",
|
||||||
|
authenticated_context_digest="sha256:context",
|
||||||
|
state="active",
|
||||||
|
created_at=NOW,
|
||||||
|
metadata={"private_key": "forbidden"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -232,7 +232,7 @@ class ModuleSystemTests(unittest.TestCase):
|
|||||||
self.assertTrue(manifests["campaigns"].required_capabilities)
|
self.assertTrue(manifests["campaigns"].required_capabilities)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
manifests["campaigns"].optional_dependencies,
|
manifests["campaigns"].optional_dependencies,
|
||||||
("files", "mail", "notifications", "addresses", "postbox"),
|
("files", "mail", "notifications", "addresses", "postbox", "approvals"),
|
||||||
)
|
)
|
||||||
self.assertEqual(manifests["dashboard"].dependencies, ())
|
self.assertEqual(manifests["dashboard"].dependencies, ())
|
||||||
self.assertTrue(manifests["dashboard"].required_capabilities)
|
self.assertTrue(manifests["dashboard"].required_capabilities)
|
||||||
@@ -3433,7 +3433,7 @@ finally:
|
|||||||
"version_max_exclusive": "0.3.0",
|
"version_max_exclusive": "0.3.0",
|
||||||
}, modules["campaigns"]["requires_interfaces"])
|
}, modules["campaigns"]["requires_interfaces"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
["files", "mail", "notifications", "addresses", "postbox"],
|
["files", "mail", "notifications", "addresses", "postbox", "approvals"],
|
||||||
modules["campaigns"]["optional_dependencies"],
|
modules["campaigns"]["optional_dependencies"],
|
||||||
)
|
)
|
||||||
self.assertEqual("requires_review", modules["files"]["migration_safety"])
|
self.assertEqual("requires_review", modules["files"]["migration_safety"])
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
|||||||
"govoplan_core.celery_app._dataflow_trigger_dispatcher",
|
"govoplan_core.celery_app._dataflow_trigger_dispatcher",
|
||||||
return_value=dataflow,
|
return_value=dataflow,
|
||||||
),
|
),
|
||||||
|
patch(
|
||||||
|
"govoplan_core.celery_app._workflow_trigger_dispatcher",
|
||||||
|
return_value=None,
|
||||||
|
),
|
||||||
patch(
|
patch(
|
||||||
"govoplan_core.db.session.get_database",
|
"govoplan_core.db.session.get_database",
|
||||||
return_value=database,
|
return_value=database,
|
||||||
@@ -109,21 +113,15 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
|||||||
def test_worker_routes_and_periodic_tasks_are_registered(self) -> None:
|
def test_worker_routes_and_periodic_tasks_are_registered(self) -> None:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{"queue": "events"},
|
{"queue": "events"},
|
||||||
celery.conf.task_routes[
|
celery.conf.task_routes["govoplan.events.dispatch_outbox"],
|
||||||
"govoplan.events.dispatch_outbox"
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{"queue": "events"},
|
{"queue": "events"},
|
||||||
celery.conf.task_routes[
|
celery.conf.task_routes["govoplan.events.purge_outbox"],
|
||||||
"govoplan.events.purge_outbox"
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"govoplan.events.purge_outbox",
|
"govoplan.events.purge_outbox",
|
||||||
celery.conf.beat_schedule[
|
celery.conf.beat_schedule["platform-event-retention-daily"]["task"],
|
||||||
"platform-event-retention-daily"
|
|
||||||
]["task"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ 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_DEFINITION_CONTRIBUTIONS,
|
||||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
||||||
|
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER,
|
||||||
WorkflowDefinitionContribution,
|
WorkflowDefinitionContribution,
|
||||||
WorkflowDefinitionContributionProvider,
|
WorkflowDefinitionContributionProvider,
|
||||||
WorkflowRuntimeWorker,
|
WorkflowRuntimeWorker,
|
||||||
|
WorkflowTriggerDispatcher,
|
||||||
workflow_definition_contribution_hash,
|
workflow_definition_contribution_hash,
|
||||||
workflow_definition_contribution_provider,
|
workflow_definition_contribution_provider,
|
||||||
workflow_runtime_worker,
|
workflow_runtime_worker,
|
||||||
|
workflow_trigger_dispatcher,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +34,14 @@ class _ContributionProvider:
|
|||||||
return {"created": 0, "tenant_ids": tuple(tenant_ids)}
|
return {"created": 0, "tenant_ids": tuple(tenant_ids)}
|
||||||
|
|
||||||
|
|
||||||
|
class _TriggerDispatcher:
|
||||||
|
def dispatch_due(self, session, *, now=None, limit=50):
|
||||||
|
return {"selected": 0}
|
||||||
|
|
||||||
|
def ingest_event(self, session, *, event):
|
||||||
|
return {"queued": 0}
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
||||||
def test_contribution_contract_hash_and_provider_are_stable(self) -> None:
|
def test_contribution_contract_hash_and_provider_are_stable(self) -> None:
|
||||||
contribution = WorkflowDefinitionContribution(
|
contribution = WorkflowDefinitionContribution(
|
||||||
@@ -77,9 +88,7 @@ class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
|||||||
name="Workflow runtime test",
|
name="Workflow runtime test",
|
||||||
version="test",
|
version="test",
|
||||||
capability_factories={
|
capability_factories={
|
||||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER: (
|
CAPABILITY_WORKFLOW_RUNTIME_WORKER: (lambda _context: worker),
|
||||||
lambda _context: worker
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -90,6 +99,29 @@ class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
|||||||
self.assertIs(worker, workflow_runtime_worker(registry))
|
self.assertIs(worker, workflow_runtime_worker(registry))
|
||||||
self.assertIsNone(workflow_runtime_worker(PlatformRegistry()))
|
self.assertIsNone(workflow_runtime_worker(PlatformRegistry()))
|
||||||
|
|
||||||
|
def test_trigger_dispatch_contract_is_runtime_checkable_and_resolved(self) -> None:
|
||||||
|
dispatcher = _TriggerDispatcher()
|
||||||
|
self.assertIsInstance(dispatcher, WorkflowTriggerDispatcher)
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(
|
||||||
|
ModuleManifest(
|
||||||
|
id="workflow_trigger_test",
|
||||||
|
name="Workflow trigger test",
|
||||||
|
version="test",
|
||||||
|
capability_factories={
|
||||||
|
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER: (
|
||||||
|
lambda _context: dispatcher
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
registry.configure_capability_context(
|
||||||
|
ModuleContext(registry=registry, settings=object())
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(dispatcher, workflow_trigger_dispatcher(registry))
|
||||||
|
self.assertIsNone(workflow_trigger_dispatcher(PlatformRegistry()))
|
||||||
|
|
||||||
def test_celery_task_commits_reconciliation(self) -> None:
|
def test_celery_task_commits_reconciliation(self) -> None:
|
||||||
session = MagicMock()
|
session = MagicMock()
|
||||||
database = MagicMock()
|
database = MagicMock()
|
||||||
@@ -121,9 +153,7 @@ class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
|||||||
{"queue": "workflow"},
|
{"queue": "workflow"},
|
||||||
celery.conf.task_routes["govoplan.workflow.reconcile"],
|
celery.conf.task_routes["govoplan.workflow.reconcile"],
|
||||||
)
|
)
|
||||||
schedule = celery.conf.beat_schedule[
|
schedule = celery.conf.beat_schedule["workflow-reconcile-every-five-seconds"]
|
||||||
"workflow-reconcile-every-five-seconds"
|
|
||||||
]
|
|
||||||
self.assertEqual("govoplan.workflow.reconcile", schedule["task"])
|
self.assertEqual("govoplan.workflow.reconcile", schedule["task"])
|
||||||
self.assertEqual(5.0, schedule["schedule"])
|
self.assertEqual(5.0, schedule["schedule"])
|
||||||
|
|
||||||
|
|||||||
Generated
+40
@@ -11,6 +11,7 @@
|
|||||||
"@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",
|
||||||
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
||||||
|
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
|
||||||
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
||||||
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
||||||
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
||||||
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
||||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||||
|
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||||
"@tiptap/core": "^3.29.2",
|
"@tiptap/core": "^3.29.2",
|
||||||
"@tiptap/extension-image": "^3.29.2",
|
"@tiptap/extension-image": "^3.29.2",
|
||||||
@@ -120,6 +122,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../govoplan-approvals/webui": {
|
||||||
|
"name": "@govoplan/approvals-webui",
|
||||||
|
"version": "0.1.14",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.14",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": ">=19.2.7 <20",
|
||||||
|
"react-dom": ">=19.2.7 <20"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"../../govoplan-audit/webui": {
|
"../../govoplan-audit/webui": {
|
||||||
"name": "@govoplan/audit-webui",
|
"name": "@govoplan/audit-webui",
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
@@ -603,6 +620,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../govoplan-voting/webui": {
|
||||||
|
"name": "@govoplan/voting-webui",
|
||||||
|
"version": "0.1.14",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.14",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": ">=19.2.7 <20",
|
||||||
|
"react-dom": ">=19.2.7 <20"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"../../govoplan-workflow/webui": {
|
"../../govoplan-workflow/webui": {
|
||||||
"name": "@govoplan/workflow-webui",
|
"name": "@govoplan/workflow-webui",
|
||||||
"version": "0.1.14",
|
"version": "0.1.14",
|
||||||
@@ -1385,6 +1417,10 @@
|
|||||||
"resolved": "../../govoplan-admin/webui",
|
"resolved": "../../govoplan-admin/webui",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@govoplan/approvals-webui": {
|
||||||
|
"resolved": "../../govoplan-approvals/webui",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@govoplan/audit-webui": {
|
"node_modules/@govoplan/audit-webui": {
|
||||||
"resolved": "../../govoplan-audit/webui",
|
"resolved": "../../govoplan-audit/webui",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -1497,6 +1533,10 @@
|
|||||||
"resolved": "../../govoplan-views/webui",
|
"resolved": "../../govoplan-views/webui",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@govoplan/voting-webui": {
|
||||||
|
"resolved": "../../govoplan-voting/webui",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@govoplan/workflow-webui": {
|
"node_modules/@govoplan/workflow-webui": {
|
||||||
"resolved": "../../govoplan-workflow/webui",
|
"resolved": "../../govoplan-workflow/webui",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||||
|
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
|
||||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||||
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
||||||
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
||||||
@@ -79,6 +80,7 @@
|
|||||||
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
||||||
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
||||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||||
|
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||||
"@tiptap/core": "^3.29.2",
|
"@tiptap/core": "^3.29.2",
|
||||||
"@tiptap/extension-image": "^3.29.2",
|
"@tiptap/extension-image": "^3.29.2",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|||||||
|
|
||||||
const packageByModule = {
|
const packageByModule = {
|
||||||
access: "@govoplan/access-webui",
|
access: "@govoplan/access-webui",
|
||||||
|
approvals: "@govoplan/approvals-webui",
|
||||||
admin: "@govoplan/admin-webui",
|
admin: "@govoplan/admin-webui",
|
||||||
addresses: "@govoplan/addresses-webui",
|
addresses: "@govoplan/addresses-webui",
|
||||||
audit: "@govoplan/audit-webui",
|
audit: "@govoplan/audit-webui",
|
||||||
@@ -33,6 +34,7 @@ const packageByModule = {
|
|||||||
search: "@govoplan/search-webui",
|
search: "@govoplan/search-webui",
|
||||||
tenancy: "@govoplan/tenancy-webui",
|
tenancy: "@govoplan/tenancy-webui",
|
||||||
views: "@govoplan/views-webui",
|
views: "@govoplan/views-webui",
|
||||||
|
voting: "@govoplan/voting-webui",
|
||||||
workflow: "@govoplan/workflow-webui"
|
workflow: "@govoplan/workflow-webui"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,7 +80,9 @@ const cases = [
|
|||||||
{ name: "search-only", modules: ["search"] },
|
{ name: "search-only", modules: ["search"] },
|
||||||
{ name: "risk-compliance-only", modules: ["risk_compliance"] },
|
{ name: "risk-compliance-only", modules: ["risk_compliance"] },
|
||||||
{ name: "docs-and-ops", modules: ["access", "docs", "ops"] },
|
{ name: "docs-and-ops", modules: ["access", "docs", "ops"] },
|
||||||
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "workflow", "views", "organizations", "idm", "cases", "committee", "campaigns", "files", "forms", "forms_runtime", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "portal", "postbox", "projects", "reporting", "risk_compliance", "search"] }
|
{ name: "approvals-only", modules: ["access", "approvals"] },
|
||||||
|
{ name: "voting-only", modules: ["access", "voting"] },
|
||||||
|
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "workflow", "views", "organizations", "idm", "cases", "committee", "campaigns", "files", "forms", "forms_runtime", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "portal", "postbox", "projects", "reporting", "risk_compliance", "search", "voting"] }
|
||||||
];
|
];
|
||||||
|
|
||||||
const npmExec = process.env.npm_execpath;
|
const npmExec = process.env.npm_execpath;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Activity, Bell, BookUser, BriefcaseBusiness, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, FolderKanban, Form, Gavel, Inbox, Landmark, LayoutDashboard, LayoutTemplate, ListTree, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
|
import { Activity, Bell, BookUser, BriefcaseBusiness, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, FolderKanban, Form, Gavel, Inbox, Landmark, LayoutDashboard, LayoutTemplate, ListChecks, ListTree, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Vote, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
|
||||||
import installedWebModuleLoaderSource from "virtual:govoplan-installed-modules";
|
import installedWebModuleLoaderSource from "virtual:govoplan-installed-modules";
|
||||||
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
|
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
|
||||||
import {
|
import {
|
||||||
@@ -75,6 +75,7 @@ const iconByName: Record<string, LucideIcon> = {
|
|||||||
landmark: Landmark,
|
landmark: Landmark,
|
||||||
"layout-template": LayoutTemplate,
|
"layout-template": LayoutTemplate,
|
||||||
"list-tree": ListTree,
|
"list-tree": ListTree,
|
||||||
|
"list-checks": ListChecks,
|
||||||
mail: Mail,
|
mail: Mail,
|
||||||
notifications: Bell,
|
notifications: Bell,
|
||||||
organizations: Building2,
|
organizations: Building2,
|
||||||
@@ -84,6 +85,7 @@ const iconByName: Record<string, LucideIcon> = {
|
|||||||
"shield-check": ShieldCheck,
|
"shield-check": ShieldCheck,
|
||||||
templates: LayoutTemplate,
|
templates: LayoutTemplate,
|
||||||
users: Users,
|
users: Users,
|
||||||
|
vote: Vote,
|
||||||
waypoints: Waypoints,
|
waypoints: Waypoints,
|
||||||
workflow: WorkflowIcon
|
workflow: WorkflowIcon
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const resolvedInstalledModuleVirtualPrefix = `\0${installedModuleVirtualPrefix}`
|
|||||||
|
|
||||||
const defaultWebModulePackages = [
|
const defaultWebModulePackages = [
|
||||||
"@govoplan/access-webui",
|
"@govoplan/access-webui",
|
||||||
|
"@govoplan/approvals-webui",
|
||||||
"@govoplan/admin-webui",
|
"@govoplan/admin-webui",
|
||||||
"@govoplan/addresses-webui",
|
"@govoplan/addresses-webui",
|
||||||
"@govoplan/audit-webui",
|
"@govoplan/audit-webui",
|
||||||
@@ -43,6 +44,7 @@ const defaultWebModulePackages = [
|
|||||||
"@govoplan/search-webui",
|
"@govoplan/search-webui",
|
||||||
"@govoplan/tenancy-webui",
|
"@govoplan/tenancy-webui",
|
||||||
"@govoplan/views-webui",
|
"@govoplan/views-webui",
|
||||||
|
"@govoplan/voting-webui",
|
||||||
"@govoplan/workflow-webui"
|
"@govoplan/workflow-webui"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -224,6 +226,7 @@ export default defineConfig({
|
|||||||
allow: [
|
allow: [
|
||||||
fileURLToPath(new URL('.', import.meta.url)),
|
fileURLToPath(new URL('.', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-access/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-access/webui', import.meta.url)),
|
||||||
|
fileURLToPath(new URL('../../govoplan-approvals/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-admin/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-admin/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-addresses/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-addresses/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-audit/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-audit/webui', import.meta.url)),
|
||||||
@@ -250,6 +253,7 @@ export default defineConfig({
|
|||||||
fileURLToPath(new URL('../../govoplan-search/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-search/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-tenancy/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-tenancy/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
|
||||||
|
fileURLToPath(new URL('../../govoplan-voting/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
|
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user