Files
govoplan-forms-runtime/src/govoplan_forms_runtime/backend/manifest.py
T

541 lines
22 KiB
Python

from __future__ import annotations
import hashlib
from pathlib import Path
from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
)
from govoplan_core.core.institutional import (
CAPABILITY_FORM_DEFINITIONS,
CAPABILITY_SERVICE_DEFINITIONS,
)
from govoplan_core.core.module_guards import (
drop_table_retirement_provider,
persistent_table_uninstall_guard,
)
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationLink,
DocumentationTopic,
FrontendModule,
FrontendRoute,
MigrationSpec,
ModuleContext,
ModuleInterfaceProvider,
ModuleInterfaceRequirement,
ModuleManifest,
NavItem,
PermissionDefinition,
ProductAreaContribution,
PublicFrontendRoute,
RoleTemplate,
)
from govoplan_core.core.provider_governance import declared_module_architecture
from govoplan_core.core.search import SearchSourceProviderRegistration
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_forms_runtime.backend.db import models as runtime_models
from govoplan_forms_runtime.backend.record_source import (
CAPABILITY_RECORD_SOURCE_FORMS_RUNTIME,
create_forms_runtime_record_source,
)
from govoplan_forms_runtime.backend.search_source import (
create_forms_runtime_search_source,
)
from govoplan_forms_runtime.backend.service import (
CAPABILITY_FORMS_RUNTIME_POLICY_EVALUATOR,
CAPABILITY_FORMS_RUNTIME_REGISTRY,
CAPABILITY_FORMS_RUNTIME_SERVICE_LAUNCHER,
FormRuntimeService,
FormsServiceLauncher,
)
MODULE_ID = "forms_runtime"
MODULE_NAME = "Forms Runtime"
MODULE_VERSION = "0.1.18"
PARTICIPATE_SCOPE = "forms_runtime:submission:participate"
READ_SCOPE = "forms_runtime:workspace:read"
WRITE_SCOPE = "forms_runtime:workspace:write"
ADMIN_SCOPE = "forms_runtime:workspace:admin"
OPTIONAL_DEPENDENCIES = (
"files",
"approvals",
"workflow_engine",
"portal",
"cases",
"policy",
"audit",
"records",
)
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
module_id, resource, action = scope.split(":", 2)
return PermissionDefinition(
scope=scope,
label=label,
description=description,
category=MODULE_NAME,
level="tenant",
module_id=module_id,
resource=resource,
action=action,
)
PERMISSIONS = (
_permission(
PARTICIPATE_SCOPE,
"Complete assigned forms",
"Start, read, save, and submit the acting account's own Form instances.",
),
_permission(
READ_SCOPE,
"View form submissions",
"Read tenant Form instances, immutable history, and status evidence.",
),
_permission(
WRITE_SCOPE,
"Manage form submissions",
"Review, transition, and hand off tenant Form submissions.",
),
_permission(
ADMIN_SCOPE,
"Administer Forms Runtime",
"Administer public intake profiles, Forms Runtime policy, recovery, and retirement.",
),
)
ROLE_TEMPLATES = (
RoleTemplate(
slug="forms_runtime_participant",
name="Forms participant",
description="Complete the acting account's own Forms.",
permissions=(PARTICIPATE_SCOPE,),
default_authenticated=True,
),
RoleTemplate(
slug="forms_runtime_manager",
name="Forms Runtime manager",
description="Review, transition, and hand off Form submissions.",
permissions=(PARTICIPATE_SCOPE, READ_SCOPE, WRITE_SCOPE),
),
RoleTemplate(
slug="forms_runtime_viewer",
name="Forms Runtime viewer",
description="Read Form submissions and their immutable history.",
permissions=(READ_SCOPE,),
),
)
def _router(context: ModuleContext):
from govoplan_forms_runtime.backend.router import create_router
return create_router(context.registry)
def _public_tenant_resolver(request: object, session: object) -> str | None:
if not hasattr(session, "query"):
return None
path = str(getattr(getattr(request, "url", None), "path", ""))
path_params = getattr(request, "path_params", {})
if "/forms-runtime/public/profiles/" in path:
public_id = str(path_params.get("public_id") or "").strip()
if not public_id:
return None
profile = (
session.query(runtime_models.FormIntakeProfile)
.filter(runtime_models.FormIntakeProfile.public_id == public_id)
.one_or_none()
)
return profile.tenant_id if profile is not None else None
if "/forms-runtime/public/intake" not in path:
return None
headers = getattr(request, "headers", {})
token = str(headers.get("X-Form-Intake-Token") or "").strip()
if len(token) < 32:
return None
intake_session = (
session.query(runtime_models.FormIntakeSession)
.filter(
runtime_models.FormIntakeSession.token_sha256
== hashlib.sha256(token.encode("utf-8")).hexdigest()
)
.one_or_none()
)
return intake_session.tenant_id if intake_session is not None else None
def _registry(context: ModuleContext) -> FormRuntimeService:
return FormRuntimeService(context.registry)
def _service_launcher(context: ModuleContext) -> FormsServiceLauncher:
return FormsServiceLauncher(context.registry)
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
current = session.query(runtime_models.FormInstanceRevision).filter(
runtime_models.FormInstanceRevision.tenant_id == tenant_id,
runtime_models.FormInstanceRevision.superseded_at.is_(None),
)
return {
"form_instances": current.count(),
"open_form_instances": current.filter(
runtime_models.FormInstanceRevision.status.in_(
("started", "draft", "submitted", "validated", "needs_review")
)
).count(),
"public_intake_profiles": session.query(runtime_models.FormIntakeProfile)
.filter(runtime_models.FormIntakeProfile.tenant_id == tenant_id)
.count(),
"public_intake_sessions": session.query(runtime_models.FormIntakeSession)
.filter(runtime_models.FormIntakeSession.tenant_id == tenant_id)
.count(),
"authenticated_acknowledgements": session.query(
runtime_models.FormAcknowledgement
)
.filter(runtime_models.FormAcknowledgement.tenant_id == tenant_id)
.count(),
}
manifest = ModuleManifest(
id=MODULE_ID,
name=MODULE_NAME,
version=MODULE_VERSION,
dependencies=("access", "forms"),
optional_dependencies=OPTIONAL_DEPENDENCIES,
required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_FORM_DEFINITIONS,
),
optional_capabilities=(
CAPABILITY_FORMS_RUNTIME_POLICY_EVALUATOR,
CAPABILITY_SERVICE_DEFINITIONS,
"cases.service_launcher",
"workflow_engine.service_launcher",
),
permissions=PERMISSIONS,
role_templates=ROLE_TEMPLATES,
route_factory=_router,
public_tenant_resolver=_public_tenant_resolver,
nav_items=(
NavItem(
path="/forms-runtime",
label="Forms",
icon="form",
required_any=(PARTICIPATE_SCOPE, READ_SCOPE),
order=37,
),
),
frontend=FrontendModule(
module_id=MODULE_ID,
package_name="@govoplan/forms-runtime-webui",
routes=(
FrontendRoute(
path="/forms-runtime",
component="FormsRuntimePage",
required_any=(PARTICIPATE_SCOPE, READ_SCOPE),
order=37,
),
FrontendRoute(
path="/forms-runtime/:instanceId",
component="FormInstancePage",
required_any=(PARTICIPATE_SCOPE, READ_SCOPE),
order=38,
),
),
public_routes=(
PublicFrontendRoute(
path="/forms/public/:publicId",
component="PublicFormPage",
order=10,
),
PublicFrontendRoute(
path="/forms/intake/:token",
component="PublicFormPage",
order=11,
),
),
nav_items=(
NavItem(
path="/forms-runtime",
label="Forms",
icon="form",
required_any=(PARTICIPATE_SCOPE, READ_SCOPE),
order=37,
),
),
product_areas=(
ProductAreaContribution(
id="services-cases",
module_id=MODULE_ID,
label="i18n:govoplan-core.product_area.services_cases",
icon="landmark",
description="i18n:govoplan-core.product_area.services_cases_description",
surface_ids=(
"forms_runtime.nav.forms.runtime",
"forms_runtime.route.forms.runtime",
"forms_runtime.route.forms.runtime.instanceid",
),
order=20,
),
),
view_surfaces=(
ViewSurface(
id="forms_runtime.navigation",
module_id=MODULE_ID,
kind="navigation",
label="Forms navigation",
order=10,
),
ViewSurface(
id="forms_runtime.workspace",
module_id=MODULE_ID,
kind="route",
label="Forms workspace",
order=20,
),
ViewSurface(
id="forms_runtime.instance",
module_id=MODULE_ID,
kind="route",
label="Form instance",
order=30,
),
),
),
provides_interfaces=(
ModuleInterfaceProvider(name="forms_runtime.registry", version="0.1.0"),
ModuleInterfaceProvider(name="forms_runtime.service_launcher", version="0.1.0"),
ModuleInterfaceProvider(name="forms_runtime.public_intake", version="1.0.0"),
ModuleInterfaceProvider(
name="forms_runtime.authenticated_acknowledgement",
version="1.0.0",
),
ModuleInterfaceProvider(
name=CAPABILITY_RECORD_SOURCE_FORMS_RUNTIME,
version="1.0.0",
),
),
requires_interfaces=(
ModuleInterfaceRequirement(
name="forms.definitions",
version_min="0.1.0",
version_max_exclusive="0.2.0",
),
ModuleInterfaceRequirement(
name="services.definitions",
version_min="0.1.0",
version_max_exclusive="0.2.0",
optional=True,
),
ModuleInterfaceRequirement(
name="cases.service_launcher",
version_min="0.1.0",
version_max_exclusive="0.2.0",
optional=True,
),
ModuleInterfaceRequirement(
name="workflow_engine.service_launcher",
version_min="0.1.0",
version_max_exclusive="0.2.0",
optional=True,
),
),
capability_factories={
CAPABILITY_FORMS_RUNTIME_REGISTRY: _registry,
CAPABILITY_FORMS_RUNTIME_SERVICE_LAUNCHER: _service_launcher,
CAPABILITY_RECORD_SOURCE_FORMS_RUNTIME: create_forms_runtime_record_source,
},
capability_documentation={
CAPABILITY_FORMS_RUNTIME_REGISTRY: CapabilityDocumentation(
label="Forms Runtime registry",
summary="Manages tenant-bound, revisioned Form instances and handoff evidence.",
contract_version="0.1.0",
),
CAPABILITY_FORMS_RUNTIME_SERVICE_LAUNCHER: CapabilityDocumentation(
label="Form-bound Service launcher",
summary="Starts a replay-safe Form instance from an exact published Service and Form revision.",
contract_version="0.1.0",
),
CAPABILITY_RECORD_SOURCE_FORMS_RUNTIME: CapabilityDocumentation(
label="Form submission record source",
summary="Resolves currently authorized immutable submission revisions for Records filing.",
contract_version="1.0.0",
),
},
migration_spec=MigrationSpec(
module_id=MODULE_ID,
metadata=Base.metadata,
script_location=str(Path(__file__).with_name("migrations") / "versions"),
migration_after=("forms",),
retirement_supported=True,
retirement_provider=drop_table_retirement_provider(
runtime_models.FormAcknowledgement,
runtime_models.FormIntakeSession,
runtime_models.FormIntakeProfile,
runtime_models.FormHandoffEffect,
runtime_models.FormInstanceEvent,
runtime_models.FormInstanceRevision,
runtime_models.FormInstanceIdentity,
label=MODULE_NAME,
),
retirement_notes="Destructive retirement removes submissions and immutable status evidence and requires a verified database and referenced-evidence recovery plan.",
),
uninstall_guard_providers=(
persistent_table_uninstall_guard(
runtime_models.FormAcknowledgement,
runtime_models.FormIntakeSession,
runtime_models.FormIntakeProfile,
runtime_models.FormHandoffEffect,
runtime_models.FormInstanceIdentity,
runtime_models.FormInstanceRevision,
runtime_models.FormInstanceEvent,
label=MODULE_NAME,
),
),
search_sources=(
SearchSourceProviderRegistration(
id="forms_runtime.submissions",
factory=create_forms_runtime_search_source,
),
),
tenant_summary_providers=(_tenant_summary,),
documentation=(
DocumentationTopic(
id="forms_runtime.submissions",
title="Complete and manage Forms",
summary="Save permitted drafts, submit validated values, and retain exact definition and handoff evidence.",
body=(
"Every instance resolves one immutable published Form revision. Draft and final values are validated on the server; final submission also enforces attachment, signature, and policy requirements. "
"Service launches retain the exact Service and binding. Native Case and Workflow handoffs persist intent before execution, use owner capabilities with stable provider keys, and require reconciliation after unknown outcomes. History, receipts, and handoffs are replay-safe and optimistic-concurrency guarded."
" Invitation and explicitly enabled anonymous intake use hash-only expiring tokens, bounded rate limits, and isolated synthetic actors. Files-backed attachments use one-time purpose-bound grants, while authenticated acknowledgements bind an exact actor and payload digest without claiming advanced or qualified signature assurance. When Search is enabled, Forms Runtime contributes a rebuildable metadata-only projection; submitted values and evidence content are excluded, and every candidate receives a current workspace or participant access check."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "product_owner"),
links=(
DocumentationLink(
label="Forms Runtime security and recovery",
href="govoplan-forms-runtime/docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md",
kind="repository",
),
),
metadata={
"seed": True,
"help_contexts": [
"forms_runtime.navigation",
"forms_runtime.workspace",
"forms_runtime.instance",
"forms_runtime.public-intake",
"forms_runtime.search.result",
"forms_runtime.state.read-only",
"forms_runtime.state.permission-blocked",
],
"privacy_notes": [
"Form values are returned only through tenant-bound instance permissions and ownership rules.",
"Validation messages expose field-level diagnostics without disclosing unrelated submissions.",
"Handoff rows retain provider references and outcomes but do not bypass target-module authorization.",
"Public intake tokens and Files upload tokens are retained only as cryptographic digests; anonymous submissions cannot later be claimed by an identity.",
],
},
),
DocumentationTopic(
id="forms_runtime.reference.fields-and-consequences",
title="Form values, submission, and handoff consequences",
summary="Runtime field behavior, immutable receipts, draft revisions, optional evidence, and recoverable external effects.",
body=(
"The active instance resolves one exact published Form definition revision. Visibility conditions alter presentation, "
"not server validation or authorization. Saving a permitted draft creates a new revision with its change reason. "
"Submitting validates values, attachments, signatures, and policy requirements and records an immutable receipt; it is "
"not an editable draft save. A Case or Workflow handoff records intent before calling its optional provider and uses a "
"stable idempotency key. Rejected effects may be retried. Unknown outcomes must be reconciled before retry to avoid a "
"duplicate target. Administrative compensation records verified absence and never deletes a remote target. "
"When Records is enabled, only immutable submitted revisions can be resolved for filing; current Forms Runtime access is rechecked and editable drafts fail closed."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "auditor"),
related_modules=OPTIONAL_DEPENDENCIES,
links=(
DocumentationLink(
label="Forms Runtime security and recovery",
href="govoplan-forms-runtime/docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md",
kind="repository",
),
),
metadata={
"seed": True,
"help_contexts": [
"forms_runtime.field.dynamic-value",
"forms_runtime.field.change-reason",
"forms_runtime.field.handoff-kind",
"forms_runtime.field.target-binding",
"forms_runtime.action.save-draft",
"forms_runtime.action.submit",
"forms_runtime.action.start-handoff",
"forms_runtime.action.reconcile-handoff",
"forms_runtime.action.compensate-handoff",
"forms_runtime.action.create-intake-profile",
"forms_runtime.action.issue-invitation",
"forms_runtime.action.upload-evidence",
"forms_runtime.action.acknowledge",
"records.action.file",
],
"consequence_classes": {
"save_draft": "Creates an immutable draft revision with a change reason.",
"submit": "Validates the exact definition and creates an immutable submission receipt.",
"start_handoff": "Persists intent before invoking an optional Case or Workflow provider.",
"reconcile": "Resolves an outcome-unknown effect without unsafe duplicate execution.",
"compensate": "Records an administrative proof that no target effect exists.",
"public_intake": "Starts an isolated, expiring invitation or explicitly enabled anonymous submission without granting general platform access.",
"upload_evidence": "Issues a short-lived provider grant; the returned immutable evidence reference must pass owner verification again at submission.",
"acknowledge": "Binds the acting account, statement version, exact Form revision, values, and attachments in an authenticated acknowledgement digest.",
"file_submission": "Resolves the exact immutable submission revision under current access and preserves only a digest-bound reference in Records.",
},
},
),
),
architecture=declared_module_architecture(
layer="human_work_procedure",
kind="runtime",
maturity="vertical_slice",
documentation_ref="docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md",
test_ref="tests/test_forms_runtime.py",
known_limits=(
"External advanced or qualified signature providers, scheduled expiry cleanup, and target kinds beyond native Case/Workflow handoffs remain adapter depth. Public links intentionally cannot substitute the native authenticated acknowledgement profile.",
),
supported_authority_modes=("native_authoritative",),
owned_concepts=(
"form instance",
"form submission",
"runtime validation",
"submission receipt",
"form handoff evidence",
),
non_owned_concepts=(
"form definition",
"file content",
"case",
"workflow definition",
"signature key custody",
),
reference_packages=("product.service-to-decision",),
migration_docs=("docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md",),
recovery_docs=("docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md",),
security_docs=("docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md",),
operations_docs=("docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md",),
),
)
def get_manifest() -> ModuleManifest:
return manifest