From b8b6fc70cc4b72316475c167743d25af63276d88 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Sat, 13 Jun 2026 00:44:14 -0600 Subject: [PATCH 1/2] Add Windows local platform contracts --- README.md | 45 ++- civiccore/__init__.py | 76 ++++ civiccore/platform/__init__.py | 88 +++++ civiccore/platform/backup.py | 209 ++++++++++ civiccore/platform/health.py | 104 +++++ civiccore/platform/modules.py | 368 ++++++++++++++++++ civiccore/platform/runtime.py | 63 +++ civiccore/platform/tasks.py | 166 ++++++++ ...ows-local-platform-contracts-2026-06-13.md | 33 ++ pyproject.toml | 2 +- tests/test_platform_contracts.py | 304 +++++++++++++++ 11 files changed, 1452 insertions(+), 6 deletions(-) create mode 100644 civiccore/platform/__init__.py create mode 100644 civiccore/platform/backup.py create mode 100644 civiccore/platform/health.py create mode 100644 civiccore/platform/modules.py create mode 100644 civiccore/platform/runtime.py create mode 100644 civiccore/platform/tasks.py create mode 100644 docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md create mode 100644 tests/test_platform_contracts.py diff --git a/README.md b/README.md index bbbaa22..1c0ef42 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,12 @@ router. `civiccore.search` now ships normalization and fusion helpers, but not a full search engine or indexer. `civiccore.notifications` now ships notice deadline and compliance helpers, but not delivery queues or outbound notification orchestration. +`civiccore.platform` now ships the Windows-local desktop contracts for module +manifests, install-profile validation, operator health summaries, durable local +task envelopes, backup/restore manifests, and installer/runtime action results. +Those contracts are deliberately storage-neutral: the desktop shell and product +modules own process supervision, persistence tables, workers, and UI, while +CivicCore owns the shared validation and plain-English operator state. `civiccore.verification` now ships the first release-evidence helper surface, while sovereignty verification remains future work. `civiccore.connectors` now also ships shared local-payload import @@ -83,7 +89,10 @@ on top of shared vendor delta request planning plus reusable no-network mock-city proof contracts for agenda vendors, municipal OIDC, and backup retention/off-host storage, on top of shared live connector sync retry/circuit-breaker primitives, including run-result normalization, operator health copy, retry delay policy, and async HTTP retry, -on top of shared persisted audit-log hash and verification helpers for +on top of shared Windows-local module/runtime contracts for the desktop shell, +including no-Docker/no-WSL manifest validation, plain-English health summaries, +local task retry envelopes, and backup/restore checksum manifests, on top of +shared persisted audit-log hash and verification helpers for database-backed module audit rows on top of shared trusted-header auth config loading and proxy-source enforcement helpers on top of shipped trusted-header auth helpers on top of shipped @@ -345,7 +354,32 @@ assert verify_persisted_audit_chain([ These APIs are deliberately offline-first. They do not provide JWT issuance, SSO, user directories, credential storage, vendor-specific network -adapters, worker scheduling, legal determinations, or vendor write-back. +adapters, worker execution, legal determinations, or vendor write-back. + +## Windows-local platform contracts + +`civiccore.platform` exposes the shared contracts the CivicSuite Windows +desktop shell uses to keep future modules pluggable without making clerks learn +infrastructure: + +```python +from civiccore.platform import ( + ModuleManifest, + build_module_registry, + PlatformHealthCheck, + summarize_platform_health, + LocalTaskEnvelope, + record_task_attempt, + build_backup_manifest, + plan_restore, +) +``` + +For the `windows_local` install profile, module manifests cannot require +Docker, WSL, Linux shell setup, or a terminal-only operator path. CivicCore also +ships storage-neutral task envelopes and backup manifests so downstream modules +share retry, health, checksum, and restore-safety semantics while keeping their +own database tables and worker implementations. ## Document ingestion @@ -534,9 +568,10 @@ router integration, and persistence orchestration remain future work. ## Scheduling helper `civiccore.scheduling` exposes the shared cron expression contract used by -module background jobs. Modules keep their own Celery/worker/runtime wiring, -but should reuse this validation so one-minute accidental or adversarial -schedules are blocked consistently across CivicSuite. +module background jobs. `civiccore.platform` exposes the storage-neutral local +task envelope and retry contract. Modules keep their own worker/runtime wiring, +but should reuse these helpers so one-minute accidental or adversarial schedules +and task retry behavior are handled consistently across CivicSuite. ```python from civiccore.scheduling import compute_next_sync_at, validate_cron_expression diff --git a/civiccore/__init__.py b/civiccore/__init__.py index c5e1583..d6a9d52 100644 --- a/civiccore/__init__.py +++ b/civiccore/__init__.py @@ -112,6 +112,45 @@ next_profile_prompt, parse_profile_answer, ) +from civiccore.platform import ( + WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS, + BackupItem, + BackupManifest, + BackupRestoreAction, + BackupRestorePlan, + BackupValidationResult, + LocalRuntimeProfile, + LocalTaskEnvelope, + LocalTaskResult, + ModuleBackupHook, + ModuleDependency, + ModuleHealthCheck, + ModuleManifest, + ModuleMigration, + ModuleModelRequirement, + ModulePermission, + ModuleRegistryEntry, + ModuleRegistryState, + ModuleRoute, + ModuleRuntimeRequirement, + ModuleService, + PlatformHealthCheck, + PlatformHealthStatus, + PlatformHealthSummary, + RuntimeActionResult, + TaskQueueSummary, + TaskRetryPolicy, + build_backup_manifest, + build_module_registry, + can_run_task, + next_retry_at, + plan_restore, + record_task_attempt, + summarize_platform_health, + summarize_task_queue, + validate_backup_manifest, + validate_windows_local_manifest, +) from civiccore.provenance import ( CitationTarget, DocumentMetadata, @@ -245,6 +284,36 @@ "ModuleEnablement", "OnboardingField", "OnboardingProgress", + "BackupItem", + "BackupManifest", + "BackupRestoreAction", + "BackupRestorePlan", + "BackupValidationResult", + "LocalRuntimeProfile", + "LocalTaskEnvelope", + "LocalTaskResult", + "ModuleBackupHook", + "ModuleDependency", + "ModuleHealthCheck", + "ModuleManifest", + "ModuleMigration", + "ModuleModelRequirement", + "ModulePermission", + "ModuleRegistryEntry", + "ModuleRegistryState", + "ModuleRoute", + "ModuleRuntimeRequirement", + "ModuleService", + "PlatformHealthCheck", + "PlatformHealthStatus", + "PlatformHealthSummary", + "RuntimeActionResult", + "TaskQueueSummary", + "TaskRetryPolicy", + "WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS", + "build_backup_manifest", + "build_module_registry", + "can_run_task", "access_level_allows", "completed_profile_fields", "compute_onboarding_status", @@ -269,7 +338,10 @@ "normalize_search_query", "normalize_search_text", "min_interval_minutes", + "next_retry_at", "parse_profile_answer", + "plan_restore", + "record_task_attempt", "roles_grant_access", "search_text_matches_query", "reciprocal_rank_fusion", @@ -282,8 +354,12 @@ "ingest_structured_record", "register_handler", "validate_cited_sentences", + "summarize_platform_health", + "summarize_task_queue", + "validate_backup_manifest", "validate_cron_expression", "validate_fernet_key_setting", + "validate_windows_local_manifest", "validate_odbc_connection_string", "validate_password_setting", "validate_secret_setting", diff --git a/civiccore/platform/__init__.py b/civiccore/platform/__init__.py new file mode 100644 index 0000000..747c97f --- /dev/null +++ b/civiccore/platform/__init__.py @@ -0,0 +1,88 @@ +"""Windows-local platform contracts for CivicSuite desktop deployments.""" + +from __future__ import annotations + +from civiccore.platform.backup import ( + BackupItem, + BackupManifest, + BackupRestoreAction, + BackupRestorePlan, + BackupValidationResult, + build_backup_manifest, + plan_restore, + validate_backup_manifest, +) +from civiccore.platform.health import ( + PlatformHealthCheck, + PlatformHealthStatus, + PlatformHealthSummary, + summarize_platform_health, +) +from civiccore.platform.modules import ( + WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS, + ModuleBackupHook, + ModuleDependency, + ModuleHealthCheck, + ModuleManifest, + ModuleMigration, + ModuleModelRequirement, + ModulePermission, + ModuleRegistryEntry, + ModuleRegistryState, + ModuleRoute, + ModuleRuntimeRequirement, + ModuleService, + build_module_registry, + validate_windows_local_manifest, +) +from civiccore.platform.runtime import LocalRuntimeProfile, RuntimeActionResult +from civiccore.platform.tasks import ( + LocalTaskEnvelope, + LocalTaskResult, + TaskQueueSummary, + TaskRetryPolicy, + can_run_task, + next_retry_at, + record_task_attempt, + summarize_task_queue, +) + +__all__ = [ + "BackupItem", + "BackupManifest", + "BackupRestoreAction", + "BackupRestorePlan", + "BackupValidationResult", + "LocalRuntimeProfile", + "LocalTaskEnvelope", + "LocalTaskResult", + "ModuleBackupHook", + "ModuleDependency", + "ModuleHealthCheck", + "ModuleManifest", + "ModuleMigration", + "ModuleModelRequirement", + "ModulePermission", + "ModuleRegistryEntry", + "ModuleRegistryState", + "ModuleRoute", + "ModuleRuntimeRequirement", + "ModuleService", + "PlatformHealthCheck", + "PlatformHealthStatus", + "PlatformHealthSummary", + "RuntimeActionResult", + "TaskQueueSummary", + "TaskRetryPolicy", + "WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS", + "build_backup_manifest", + "build_module_registry", + "can_run_task", + "next_retry_at", + "plan_restore", + "record_task_attempt", + "summarize_platform_health", + "summarize_task_queue", + "validate_backup_manifest", + "validate_windows_local_manifest", +] diff --git a/civiccore/platform/backup.py b/civiccore/platform/backup.py new file mode 100644 index 0000000..6e27da3 --- /dev/null +++ b/civiccore/platform/backup.py @@ -0,0 +1,209 @@ +"""Backup and restore manifest contracts for local CivicSuite installs.""" + +from __future__ import annotations + +import hashlib +from datetime import UTC, datetime +from pathlib import Path +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + +RestoreActionStatus = Literal["ready", "missing", "hash_mismatch", "would_overwrite"] + + +class BackupItem(BaseModel): + """One file captured in a local backup.""" + + model_config = ConfigDict(extra="forbid") + + module_id: str = Field(pattern=r"^[a-z][a-z0-9-]{1,63}$") + relative_path: str = Field(min_length=1) + size_bytes: int = Field(ge=0) + sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + required: bool = True + + +class BackupManifest(BaseModel): + """Versioned backup manifest for a CivicSuite local profile.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: str = "1.0" + backup_id: str = Field(min_length=1) + city_profile_id: str = Field(min_length=1) + civiccore_version: str = Field(min_length=1) + created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + modules: list[str] = Field(default_factory=list) + items: list[BackupItem] + + +class BackupValidationResult(BaseModel): + """Result of checking a backup manifest against files on disk.""" + + model_config = ConfigDict(extra="forbid") + + valid: bool + missing: list[str] = Field(default_factory=list) + hash_mismatches: list[str] = Field(default_factory=list) + + +class BackupRestoreAction(BaseModel): + """One file restore action and its safety status.""" + + model_config = ConfigDict(extra="forbid") + + source_relative_path: str + target_relative_path: str + status: RestoreActionStatus + message: str + + +class BackupRestorePlan(BaseModel): + """A non-destructive restore plan.""" + + model_config = ConfigDict(extra="forbid") + + ready: bool + actions: list[BackupRestoreAction] + blocked_reason: str | None = None + + +def build_backup_manifest( + *, + root: Path, + files: list[Path], + backup_id: str, + city_profile_id: str, + civiccore_version: str, + module_id: str, +) -> BackupManifest: + """Build a checksum manifest for files under a local data root.""" + + resolved_root = root.resolve() + items: list[BackupItem] = [] + for path in files: + resolved_path = path.resolve() + _assert_inside(resolved_path, resolved_root) + if not resolved_path.is_file(): + raise ValueError(f"backup item is not a file: {resolved_path}") + relative_path = resolved_path.relative_to(resolved_root).as_posix() + items.append( + BackupItem( + module_id=module_id, + relative_path=relative_path, + size_bytes=resolved_path.stat().st_size, + sha256=_sha256_file(resolved_path), + ) + ) + + return BackupManifest( + backup_id=backup_id, + city_profile_id=city_profile_id, + civiccore_version=civiccore_version, + modules=sorted({module_id for _item in items}), + items=sorted(items, key=lambda item: item.relative_path), + ) + + +def validate_backup_manifest(manifest: BackupManifest, *, root: Path) -> BackupValidationResult: + """Check that a manifest still matches a backup directory.""" + + resolved_root = root.resolve() + missing: list[str] = [] + mismatches: list[str] = [] + for item in manifest.items: + path = (resolved_root / item.relative_path).resolve() + _assert_inside(path, resolved_root) + if not path.is_file(): + missing.append(item.relative_path) + continue + if _sha256_file(path) != item.sha256: + mismatches.append(item.relative_path) + + return BackupValidationResult( + valid=not missing and not mismatches, + missing=missing, + hash_mismatches=mismatches, + ) + + +def plan_restore( + manifest: BackupManifest, + *, + backup_root: Path, + restore_root: Path, + overwrite: bool = False, +) -> BackupRestorePlan: + """Return a non-destructive restore plan for a backup manifest.""" + + resolved_backup_root = backup_root.resolve() + resolved_restore_root = restore_root.resolve() + actions: list[BackupRestoreAction] = [] + + for item in manifest.items: + source = (resolved_backup_root / item.relative_path).resolve() + target = (resolved_restore_root / item.relative_path).resolve() + _assert_inside(source, resolved_backup_root) + _assert_inside(target, resolved_restore_root) + + if not source.is_file(): + actions.append( + BackupRestoreAction( + source_relative_path=item.relative_path, + target_relative_path=item.relative_path, + status="missing", + message="Backup file is missing.", + ) + ) + continue + if _sha256_file(source) != item.sha256: + actions.append( + BackupRestoreAction( + source_relative_path=item.relative_path, + target_relative_path=item.relative_path, + status="hash_mismatch", + message="Backup file checksum does not match the manifest.", + ) + ) + continue + if target.exists() and not overwrite: + actions.append( + BackupRestoreAction( + source_relative_path=item.relative_path, + target_relative_path=item.relative_path, + status="would_overwrite", + message="Restore target already exists.", + ) + ) + continue + actions.append( + BackupRestoreAction( + source_relative_path=item.relative_path, + target_relative_path=item.relative_path, + status="ready", + message="Ready to restore.", + ) + ) + + blockers = [action for action in actions if action.status != "ready"] + return BackupRestorePlan( + ready=not blockers, + actions=actions, + blocked_reason=None if not blockers else "Restore plan has file safety blockers.", + ) + + +def _assert_inside(path: Path, root: Path) -> None: + try: + path.relative_to(root) + except ValueError as exc: + raise ValueError(f"path is outside the expected root: {path}") from exc + + +def _sha256_file(path: Path) -> str: + hasher = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + hasher.update(chunk) + return hasher.hexdigest() diff --git a/civiccore/platform/health.py b/civiccore/platform/health.py new file mode 100644 index 0000000..d6fe659 --- /dev/null +++ b/civiccore/platform/health.py @@ -0,0 +1,104 @@ +"""Shared platform health projection for local CivicSuite surfaces.""" + +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + +PlatformHealthStatus = Literal["ok", "needs_setup", "degraded", "blocked"] +_STATUS_RANK: dict[PlatformHealthStatus, int] = { + "ok": 0, + "needs_setup": 1, + "degraded": 2, + "blocked": 3, +} + + +class PlatformHealthCheck(BaseModel): + """One health check result suitable for staff/admin health screens.""" + + model_config = ConfigDict(extra="forbid") + + component: str = Field(min_length=1) + label: str = Field(min_length=1) + status: PlatformHealthStatus + message: str = Field(min_length=1) + next_action: str = Field(min_length=1) + blocking: bool = False + checked_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + admin_detail: str | None = Field(default=None, min_length=1) + + +class PlatformHealthSummary(BaseModel): + """Aggregated health status for the umbrella shell.""" + + model_config = ConfigDict(extra="forbid") + + status: PlatformHealthStatus + checks: list[PlatformHealthCheck] + next_action: str + staff_message: str + admin_message: str + blocked_components: list[str] + degraded_components: list[str] + + @property + def is_release_blocked(self) -> bool: + return self.status == "blocked" or bool(self.blocked_components) + + +def summarize_platform_health(checks: list[PlatformHealthCheck]) -> PlatformHealthSummary: + """Return the worst health status plus plain-English action copy.""" + + if not checks: + return PlatformHealthSummary( + status="needs_setup", + checks=[], + next_action="Run first-run setup so CivicSuite can verify the local services.", + staff_message="CivicSuite has not completed setup yet.", + admin_message="No platform health checks have reported.", + blocked_components=[], + degraded_components=[], + ) + + worst = max((check.status for check in checks), key=lambda status: _STATUS_RANK[status]) + action_source = next( + ( + check + for check in checks + if check.blocking or check.status in {"blocked", "needs_setup", "degraded"} + ), + checks[0], + ) + blocked = [ + check.component for check in checks if check.blocking or check.status == "blocked" + ] + degraded = [check.component for check in checks if check.status == "degraded"] + + if blocked: + staff_message = "CivicSuite needs attention before affected workflows can continue." + elif worst == "degraded": + staff_message = "CivicSuite is running, but one or more background checks need attention." + elif worst == "needs_setup": + staff_message = "CivicSuite needs setup before all workflows are available." + else: + staff_message = "CivicSuite is healthy." + + return PlatformHealthSummary( + status=worst, + checks=checks, + next_action=action_source.next_action, + staff_message=staff_message, + admin_message=_admin_message(checks), + blocked_components=blocked, + degraded_components=degraded, + ) + + +def _admin_message(checks: list[PlatformHealthCheck]) -> str: + details = [check.admin_detail for check in checks if check.admin_detail] + if details: + return " ".join(details) + return "All reported checks are available in the local health center." diff --git a/civiccore/platform/modules.py b/civiccore/platform/modules.py new file mode 100644 index 0000000..9610617 --- /dev/null +++ b/civiccore/platform/modules.py @@ -0,0 +1,368 @@ +"""Module manifest and registry contracts for local CivicSuite deployments.""" + +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +ModuleSurface = Literal["staff", "resident", "admin", "system"] +InstallProfile = Literal["windows_local", "server", "developer"] +ServiceType = Literal[ + "python-service", + "postgres-schema", + "file-store", + "ollama-model", + "tauri-command", + "local-api", + "background-worker", +] +RuntimeRequirementKind = Literal[ + "civiccore", + "postgres", + "pgvector", + "python", + "file_storage", + "ollama", + "llm_model", + "tauri_command", + "local_api", + "windows_service", + "webview2", + "external_connector", + "docker", + "wsl", + "linux_shell", + "terminal", +] +ModuleRegistryStatus = Literal["enabled", "disabled", "blocked"] + +MODULE_ID_PATTERN = r"^[a-z][a-z0-9-]{1,63}$" +SLUG_PATTERN = r"^[a-z][a-z0-9_-]{1,63}$" +WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS = frozenset({"docker", "wsl", "linux_shell", "terminal"}) + + +class ModuleDependency(BaseModel): + """Another CivicSuite module required by a manifest.""" + + model_config = ConfigDict(extra="forbid") + + module_id: str = Field(pattern=MODULE_ID_PATTERN) + min_version: str | None = Field(default=None, min_length=1) + required: bool = True + + +class ModulePermission(BaseModel): + """Permission surfaced to the shell and module manager.""" + + model_config = ConfigDict(extra="forbid") + + key: str = Field(pattern=SLUG_PATTERN) + label: str = Field(min_length=1) + description: str = Field(min_length=1) + surface: ModuleSurface = "staff" + roles: list[str] = Field(default_factory=list) + + @field_validator("roles") + @classmethod + def validate_roles(cls, value: list[str]) -> list[str]: + if not value: + raise ValueError("roles must name at least one role that grants the permission") + return value + + +class ModuleRoute(BaseModel): + """A route the desktop shell may expose for a module.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=SLUG_PATTERN) + path: str = Field(pattern=r"^/[a-z0-9/_:-]*$") + label: str = Field(min_length=1) + surface: ModuleSurface = "staff" + permission: str | None = Field(default=None, pattern=SLUG_PATTERN) + + +class ModuleService(BaseModel): + """A local service, schema, or command needed by a module.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=SLUG_PATTERN) + service_type: ServiceType + description: str = Field(min_length=1) + required: bool = True + start_order: int = Field(default=100, ge=0) + health_check_id: str | None = Field(default=None, pattern=SLUG_PATTERN) + + +class ModuleMigration(BaseModel): + """A migration contract that the installer/runtime must apply or verify.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=SLUG_PATTERN) + description: str = Field(min_length=1) + owner_schema: str | None = Field(default=None, min_length=1) + required: bool = True + + +class ModuleHealthCheck(BaseModel): + """A module health check known to the umbrella health center.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=SLUG_PATTERN) + label: str = Field(min_length=1) + description: str = Field(min_length=1) + blocking: bool = True + surface: Literal["staff", "admin", "system"] = "admin" + repair_action: str | None = Field(default=None, min_length=1) + + +class ModuleBackupHook(BaseModel): + """A backup/restore hook the module contributes to the shared backup plan.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=SLUG_PATTERN) + label: str = Field(min_length=1) + includes: list[str] = Field(min_length=1) + restore_supported: bool = True + + +class ModuleModelRequirement(BaseModel): + """A model requirement declared by a module.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=SLUG_PATTERN) + provider: Literal["ollama", "local", "none", "openai", "anthropic"] = "ollama" + model_name: str = Field(min_length=1) + required: bool = True + local_only: bool = True + minimum_context_tokens: int | None = Field(default=None, ge=1) + checksum_sha256: str | None = Field(default=None, pattern=r"^[a-fA-F0-9]{64}$") + download_url: str | None = Field(default=None, min_length=1) + license_name: str | None = Field(default=None, min_length=1) + + +class ModuleRuntimeRequirement(BaseModel): + """Runtime capability needed by a module.""" + + model_config = ConfigDict(extra="forbid") + + kind: RuntimeRequirementKind + name: str = Field(min_length=1) + description: str = Field(min_length=1) + required: bool = True + operator_visible: bool = False + + +class ModuleManifest(BaseModel): + """Complete manifest for a CivicSuite module package.""" + + model_config = ConfigDict(extra="forbid") + + module_id: str = Field(pattern=MODULE_ID_PATTERN) + name: str = Field(min_length=1) + version: str = Field(min_length=1) + package_name: str = Field(min_length=1) + civiccore_min_version: str = Field(min_length=1) + enabled_by_default: bool = True + surfaces: list[ModuleSurface] = Field(default_factory=lambda: ["staff"]) + install_profiles: list[InstallProfile] = Field(default_factory=lambda: ["windows_local"]) + dependencies: list[ModuleDependency] = Field(default_factory=list) + routes: list[ModuleRoute] = Field(default_factory=list) + permissions: list[ModulePermission] = Field(default_factory=list) + services: list[ModuleService] = Field(default_factory=list) + migrations: list[ModuleMigration] = Field(default_factory=list) + health_checks: list[ModuleHealthCheck] = Field(default_factory=list) + backup_hooks: list[ModuleBackupHook] = Field(default_factory=list) + model_requirements: list[ModuleModelRequirement] = Field(default_factory=list) + runtime_requirements: list[ModuleRuntimeRequirement] = Field(default_factory=list) + settings_schema_version: str | None = Field(default=None, min_length=1) + metadata: dict[str, Any] = Field(default_factory=dict) + + @field_validator("surfaces", "install_profiles") + @classmethod + def validate_non_empty_list(cls, value: list[str]) -> list[str]: + if not value: + raise ValueError("list must contain at least one item") + return value + + @model_validator(mode="after") + def validate_cross_references(self) -> ModuleManifest: + _assert_unique("permission", [item.key for item in self.permissions]) + _assert_unique("route", [item.id for item in self.routes]) + _assert_unique("service", [item.id for item in self.services]) + _assert_unique("migration", [item.id for item in self.migrations]) + _assert_unique("health_check", [item.id for item in self.health_checks]) + _assert_unique("backup_hook", [item.id for item in self.backup_hooks]) + _assert_unique("model_requirement", [item.id for item in self.model_requirements]) + + permission_keys = {item.key for item in self.permissions} + for route in self.routes: + if route.permission and route.permission not in permission_keys: + raise ValueError( + f"route {route.id!r} references unknown permission {route.permission!r}" + ) + + health_check_ids = {item.id for item in self.health_checks} + for service in self.services: + if service.health_check_id and service.health_check_id not in health_check_ids: + raise ValueError( + f"service {service.id!r} references unknown health check " + f"{service.health_check_id!r}" + ) + + for dependency in self.dependencies: + if dependency.module_id == self.module_id: + raise ValueError("module cannot depend on itself") + + if "windows_local" in self.install_profiles: + _validate_windows_runtime_requirements(self) + + return self + + +class ModuleRegistryEntry(BaseModel): + """One module's resolved state in a runtime profile.""" + + model_config = ConfigDict(extra="forbid") + + module_id: str = Field(pattern=MODULE_ID_PATTERN) + name: str = Field(min_length=1) + version: str = Field(min_length=1) + enabled: bool + locked: bool = False + status: ModuleRegistryStatus + reason: str | None = None + manifest: ModuleManifest + + +class ModuleRegistryState(BaseModel): + """Resolved module registry for the desktop shell and installer.""" + + model_config = ConfigDict(extra="forbid") + + profile: InstallProfile = "windows_local" + civiccore_version: str = Field(min_length=1) + civiccore_locked: bool = True + entries: list[ModuleRegistryEntry] + + @property + def enabled_module_ids(self) -> list[str]: + return [entry.module_id for entry in self.entries if entry.enabled] + + @property + def blocked_module_ids(self) -> list[str]: + return [entry.module_id for entry in self.entries if entry.status == "blocked"] + + +def validate_windows_local_manifest(manifest: ModuleManifest | dict[str, Any]) -> ModuleManifest: + """Validate a manifest for the Windows desktop profile.""" + + parsed = manifest if isinstance(manifest, ModuleManifest) else ModuleManifest.model_validate(manifest) + if "windows_local" not in parsed.install_profiles: + raise ValueError(f"{parsed.module_id} does not declare support for windows_local") + _validate_windows_runtime_requirements(parsed) + return parsed + + +def build_module_registry( + manifests: list[ModuleManifest | dict[str, Any]], + *, + civiccore_version: str, + selected_module_ids: set[str] | None = None, + profile: InstallProfile = "windows_local", +) -> ModuleRegistryState: + """Resolve manifests into an installer/runtime registry state.""" + + parsed = [ + item if isinstance(item, ModuleManifest) else ModuleManifest.model_validate(item) + for item in manifests + ] + ids = [item.module_id for item in parsed] + _assert_unique("module", ids) + + by_id = {item.module_id: item for item in parsed} + selected = ( + set(selected_module_ids) + if selected_module_ids is not None + else {item.module_id for item in parsed if item.enabled_by_default} + ) + unknown = selected - set(by_id) + if unknown: + raise ValueError(f"selected modules are not installed: {sorted(unknown)}") + + entries: list[ModuleRegistryEntry] = [] + for manifest in parsed: + enabled = manifest.module_id in selected + status: ModuleRegistryStatus = "enabled" if enabled else "disabled" + reason: str | None = None + + if profile not in manifest.install_profiles: + enabled = False + status = "blocked" + reason = f"{manifest.name} is not available for the {profile} profile." + elif enabled: + missing = [ + dependency.module_id + for dependency in manifest.dependencies + if dependency.required and dependency.module_id not in selected + ] + if missing: + enabled = False + status = "blocked" + reason = f"Missing required module dependencies: {', '.join(sorted(missing))}." + + entries.append( + ModuleRegistryEntry( + module_id=manifest.module_id, + name=manifest.name, + version=manifest.version, + enabled=enabled, + locked=False, + status=status, + reason=reason, + manifest=manifest, + ) + ) + + return ModuleRegistryState( + profile=profile, + civiccore_version=civiccore_version, + civiccore_locked=True, + entries=sorted(entries, key=lambda entry: entry.module_id), + ) + + +def _assert_unique(label: str, values: list[str]) -> None: + seen: set[str] = set() + duplicates = sorted({value for value in values if value in seen or seen.add(value)}) + if duplicates: + raise ValueError(f"duplicate {label} ids: {', '.join(duplicates)}") + + +def _validate_windows_runtime_requirements(manifest: ModuleManifest) -> None: + blocked = [ + requirement.kind + for requirement in manifest.runtime_requirements + if requirement.required and requirement.kind in WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS + ] + if blocked: + blocked_list = ", ".join(sorted(set(blocked))) + raise ValueError( + f"{manifest.module_id} cannot require {blocked_list} for the Windows local profile" + ) + terminal_only = [ + requirement.name + for requirement in manifest.runtime_requirements + if requirement.required and requirement.operator_visible and requirement.kind == "terminal" + ] + if terminal_only: + raise ValueError( + f"{manifest.module_id} exposes terminal-only operator setup: " + f"{', '.join(sorted(terminal_only))}" + ) diff --git a/civiccore/platform/runtime.py b/civiccore/platform/runtime.py new file mode 100644 index 0000000..24d7dfd --- /dev/null +++ b/civiccore/platform/runtime.py @@ -0,0 +1,63 @@ +"""Runtime action contracts for the Windows local desktop shell.""" + +from __future__ import annotations + +from datetime import UTC, datetime +from pathlib import Path +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from civiccore.platform.modules import ModuleRegistryState + +RuntimeAction = Literal[ + "install", + "first_run", + "health_check", + "repair", + "backup", + "restore", + "uninstall", + "module_install", + "module_disable", + "model_download", +] +RuntimeActionStatus = Literal["pending", "running", "succeeded", "needs_action", "failed", "blocked"] + + +class RuntimeActionResult(BaseModel): + """Plain-English result returned by installer/runtime APIs.""" + + model_config = ConfigDict(extra="forbid") + + action: RuntimeAction + status: RuntimeActionStatus + title: str = Field(min_length=1) + message: str = Field(min_length=1) + next_action: str | None = Field(default=None, min_length=1) + evidence: dict[str, str | int | bool | None] = Field(default_factory=dict) + started_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + completed_at: datetime | None = None + + +class LocalRuntimeProfile(BaseModel): + """Resolved local runtime profile for one installed city.""" + + model_config = ConfigDict(extra="forbid") + + profile_id: str = Field(min_length=1) + city_name: str = Field(min_length=1) + data_root: Path + backup_root: Path + module_registry: ModuleRegistryState + model_provider: str = "ollama" + local_only: bool = True + network_allowed_for: list[str] = Field(default_factory=list) + + @field_validator("network_allowed_for") + @classmethod + def validate_network_reasons(cls, value: list[str]) -> list[str]: + for reason in value: + if not reason.strip(): + raise ValueError("network_allowed_for entries cannot be blank") + return value diff --git a/civiccore/platform/tasks.py b/civiccore/platform/tasks.py new file mode 100644 index 0000000..dfa5de7 --- /dev/null +++ b/civiccore/platform/tasks.py @@ -0,0 +1,166 @@ +"""Storage-neutral local task contracts for CivicSuite modules.""" + +from __future__ import annotations + +from collections import Counter +from datetime import UTC, datetime, timedelta +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +TaskStatus = Literal["queued", "running", "succeeded", "retry_wait", "failed", "cancelled"] + + +class TaskRetryPolicy(BaseModel): + """Retry settings for local durable task queues.""" + + model_config = ConfigDict(extra="forbid") + + max_attempts: int = Field(default=3, ge=1) + base_delay_seconds: int = Field(default=30, ge=0) + max_delay_seconds: int = Field(default=3600, ge=0) + + +class LocalTaskEnvelope(BaseModel): + """Storage-neutral task record for module-owned persistence.""" + + model_config = ConfigDict(extra="forbid") + + task_id: str = Field(min_length=1) + module_id: str = Field(pattern=r"^[a-z][a-z0-9-]{1,63}$") + task_type: str = Field(min_length=1) + status: TaskStatus = "queued" + payload: dict[str, Any] = Field(default_factory=dict) + attempt_count: int = Field(default=0, ge=0) + queued_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + available_at: datetime | None = None + last_error: str | None = Field(default=None, min_length=1) + idempotency_key: str | None = Field(default=None, min_length=1) + audit_subject_id: str | None = Field(default=None, min_length=1) + + @model_validator(mode="after") + def validate_retry_state(self) -> LocalTaskEnvelope: + if self.status == "retry_wait" and self.available_at is None: + raise ValueError("retry_wait tasks must include available_at") + if self.status == "failed" and not self.last_error: + raise ValueError("failed tasks must include last_error") + return self + + +class LocalTaskResult(BaseModel): + """Result returned after recording a local task attempt.""" + + model_config = ConfigDict(extra="forbid") + + task: LocalTaskEnvelope + message: str + retry_at: datetime | None = None + + +class TaskQueueSummary(BaseModel): + """Counts and operator copy for a module task queue.""" + + model_config = ConfigDict(extra="forbid") + + total: int + counts: dict[str, int] + blocked: bool + message: str + + +def next_retry_at( + attempt_count: int, + failed_at: datetime, + *, + policy: TaskRetryPolicy | None = None, +) -> datetime | None: + """Return the next retry time after a failed attempt, or None when exhausted.""" + + active_policy = policy or TaskRetryPolicy() + if attempt_count >= active_policy.max_attempts: + return None + delay = active_policy.base_delay_seconds * (2 ** max(0, attempt_count - 1)) + delay = min(delay, active_policy.max_delay_seconds) + return failed_at + timedelta(seconds=delay) + + +def can_run_task(task: LocalTaskEnvelope, *, now: datetime | None = None) -> bool: + """Return True when a queued or retry-wait task can be claimed.""" + + current_time = now or datetime.now(UTC) + if task.status == "queued": + return True + if task.status == "retry_wait" and task.available_at is not None: + return task.available_at <= current_time + return False + + +def record_task_attempt( + task: LocalTaskEnvelope, + *, + success: bool, + now: datetime | None = None, + error: str | None = None, + policy: TaskRetryPolicy | None = None, +) -> LocalTaskResult: + """Return an updated task envelope after one local worker attempt.""" + + current_time = now or datetime.now(UTC) + attempt_count = task.attempt_count + 1 + if success: + updated = task.model_copy( + update={ + "status": "succeeded", + "attempt_count": attempt_count, + "available_at": None, + "last_error": None, + } + ) + return LocalTaskResult(task=updated, message="Task completed successfully.") + + if not error: + raise ValueError("error is required when recording a failed task attempt") + + retry_at = next_retry_at(attempt_count, current_time, policy=policy) + if retry_at is None: + updated = task.model_copy( + update={ + "status": "failed", + "attempt_count": attempt_count, + "available_at": None, + "last_error": error, + } + ) + return LocalTaskResult(task=updated, message="Task failed and retries are exhausted.") + + updated = task.model_copy( + update={ + "status": "retry_wait", + "attempt_count": attempt_count, + "available_at": retry_at, + "last_error": error, + } + ) + return LocalTaskResult(task=updated, message="Task failed and is waiting to retry.", retry_at=retry_at) + + +def summarize_task_queue(tasks: list[LocalTaskEnvelope]) -> TaskQueueSummary: + """Return counts and operator copy for a queue view.""" + + counts = Counter(task.status for task in tasks) + blocked = bool(counts.get("failed")) + if blocked: + message = "One or more local tasks failed and need review." + elif counts.get("retry_wait"): + message = "Some local tasks are waiting to retry." + elif counts.get("running") or counts.get("queued"): + message = "Local tasks are active." + else: + message = "No local task issues are reported." + + return TaskQueueSummary( + total=len(tasks), + counts=dict(sorted(counts.items())), + blocked=blocked, + message=message, + ) diff --git a/docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md b/docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md new file mode 100644 index 0000000..1265773 --- /dev/null +++ b/docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md @@ -0,0 +1,33 @@ +# Audit Lite: Windows-Local Platform Contracts + +Date: 2026-06-13 +Repo: CivicSuite/civiccore +Branch: work/windows-local-platform-contracts +Scope: `civiccore.platform` module manifest, health, task, backup/restore, and runtime contracts. + +## Verdict + +Unresolved findings: 0 Blocker / 0 Critical / 0 Major / 0 Minor / 0 Nit + +This slice is acceptable to push. It adds real importable CivicCore contracts for the Windows-local desktop path without claiming downstream module wiring is complete. + +## Evidence + +- `python -m pip install -e .[dev]` passed after installing the repo-declared dev extras. +- `python -m pytest` passed: 297 passed. +- `python -m ruff check civiccore tests` passed. +- `python -m build` passed and included `civiccore/platform` in the sdist and wheel. +- `git diff --check` passed. +- Focused platform/public API smoke passed: `tests/test_platform_contracts.py`, `tests/test_public_api_v03.py`, and `tests/test_smoke.py`. + +## Five-Lens Review + +- Engineering: Pass. New contracts are storage-neutral and validate the Windows-local profile against required Docker, WSL, Linux shell, and terminal requirements. +- UX: Pass. Health and runtime result contracts require plain-English messages and next actions for clerk/admin surfaces. +- Tests: Pass. Tests cover positive registry resolution, blocked dependency state, forbidden runtime requirements, local task retry/exhaustion, backup checksum validation, restore overwrite blocking, and local-first runtime profile defaults. +- Docs: Pass. README and package metadata now describe the shipped platform contracts and distinguish contracts from worker/runtime execution. +- QA: Pass. Public package root exports were updated and covered by the public API smoke tests; packaging confirmed the new package is included. + +## Notes + +The configured `audit-lite` skill file was not present at `C:\Users\scott\.codex\skills\audit-lite\SKILL.md` in this session, so this report follows the in-repo five-lens self-audit format as the fallback. Downstream CivicRecords, CivicClerk, CivicCode, and desktop-shell adoption of these contracts remains in subsequent slices. diff --git a/pyproject.toml b/pyproject.toml index d670813..744a991 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "civiccore" version = "1.2.0" -description = "CivicSuite shared platform package - migrations, shared SQLAlchemy base, LLM abstraction, audit/provenance, document ingestion, ingest and connector contracts, connector sync primitives, connector delta planning, reusable mock-city proof contracts, connector security helpers, startup config validation helpers, schedule validation helpers, encrypted config primitives, notice helpers, onboarding profile helpers, search access helpers, small auth helpers, manifests, exports, and city profile primitives" +description = "CivicSuite shared platform package - migrations, shared SQLAlchemy base, LLM abstraction, audit/provenance, document ingestion, ingest and connector contracts, connector sync primitives, connector delta planning, reusable mock-city proof contracts, connector security helpers, startup config validation helpers, schedule validation helpers, encrypted config primitives, notice helpers, onboarding profile helpers, search access helpers, small auth helpers, manifests, exports, city profile primitives, and Windows-local module/runtime contracts" readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0" diff --git a/tests/test_platform_contracts.py b/tests/test_platform_contracts.py new file mode 100644 index 0000000..fab10f6 --- /dev/null +++ b/tests/test_platform_contracts.py @@ -0,0 +1,304 @@ +"""Tests for CivicCore Windows-local platform contracts.""" + +from __future__ import annotations + +from datetime import UTC, datetime, timedelta + +import pytest + +from civiccore.platform import ( + BackupManifest, + LocalRuntimeProfile, + LocalTaskEnvelope, + ModuleManifest, + PlatformHealthCheck, + RuntimeActionResult, + TaskRetryPolicy, + build_backup_manifest, + build_module_registry, + can_run_task, + plan_restore, + record_task_attempt, + summarize_platform_health, + summarize_task_queue, + validate_backup_manifest, +) + + +def _module_manifest(module_id: str = "civicclerk", **overrides) -> ModuleManifest: + data = { + "module_id": module_id, + "name": "CivicClerk", + "version": "1.0.0", + "package_name": "civicclerk", + "civiccore_min_version": "1.2.0", + "surfaces": ["staff", "admin"], + "install_profiles": ["windows_local"], + "permissions": [ + { + "key": "meeting-editor", + "label": "Meeting editor", + "description": "Can prepare meeting materials.", + "roles": ["clerk_admin", "meeting_editor"], + } + ], + "routes": [ + { + "id": "meetings", + "path": "/meetings", + "label": "Meetings & Notices", + "permission": "meeting-editor", + } + ], + "health_checks": [ + { + "id": "database-ready", + "label": "Database ready", + "description": "Checks the local database schema.", + "repair_action": "repair-database", + } + ], + "services": [ + { + "id": "api", + "service_type": "local-api", + "description": "Local module API", + "health_check_id": "database-ready", + } + ], + "migrations": [ + { + "id": "initial-schema", + "description": "Create local CivicClerk tables.", + "owner_schema": "civicclerk", + } + ], + "backup_hooks": [ + { + "id": "module-data", + "label": "Module data", + "includes": ["civicclerk/**"], + } + ], + "model_requirements": [ + { + "id": "agenda-drafts", + "provider": "ollama", + "model_name": "gemma-4-12b-it-qat-q4_0", + "minimum_context_tokens": 8192, + } + ], + "runtime_requirements": [ + { + "kind": "postgres", + "name": "Portable PostgreSQL", + "description": "Local PostgreSQL runtime managed by the desktop shell.", + "required": True, + }, + { + "kind": "ollama", + "name": "Local Ollama runtime", + "description": "Local model runner managed by the desktop shell.", + "required": True, + }, + ], + } + data.update(overrides) + return ModuleManifest.model_validate(data) + + +def test_windows_manifest_rejects_docker_wsl_and_terminal_requirements() -> None: + for kind in ("docker", "wsl", "linux_shell", "terminal"): + with pytest.raises(ValueError, match="Windows local profile"): + _module_manifest( + runtime_requirements=[ + { + "kind": kind, + "name": kind, + "description": "Not allowed for clerk installs.", + "required": True, + } + ] + ) + + +def test_module_registry_blocks_missing_required_dependencies() -> None: + manifest = _module_manifest( + dependencies=[{"module_id": "civicrecords-ai", "required": True}] + ) + + registry = build_module_registry( + [manifest], + civiccore_version="1.2.0", + selected_module_ids={"civicclerk"}, + ) + + entry = registry.entries[0] + assert registry.civiccore_locked is True + assert entry.status == "blocked" + assert entry.enabled is False + assert "civicrecords-ai" in (entry.reason or "") + + +def test_module_registry_enables_valid_windows_local_module() -> None: + manifest = _module_manifest() + + registry = build_module_registry([manifest], civiccore_version="1.2.0") + + assert registry.enabled_module_ids == ["civicclerk"] + assert registry.blocked_module_ids == [] + assert registry.entries[0].manifest.routes[0].label == "Meetings & Notices" + + +def test_platform_health_summary_uses_plain_english_blocking_action() -> None: + checks = [ + PlatformHealthCheck( + component="postgres", + label="Local database", + status="ok", + message="Database is reachable.", + next_action="No action needed.", + ), + PlatformHealthCheck( + component="model", + label="Gemma model", + status="blocked", + message="The local model is missing.", + next_action="Open Model Setup and download the approved local model.", + blocking=True, + admin_detail="Expected Gemma 4 12B QAT Q4_0.", + ), + ] + + summary = summarize_platform_health(checks) + + assert summary.status == "blocked" + assert summary.is_release_blocked is True + assert summary.blocked_components == ["model"] + assert summary.next_action == "Open Model Setup and download the approved local model." + + +def test_local_task_retry_contract_has_no_external_runtime_assumption() -> None: + now = datetime(2026, 6, 13, 12, 0, tzinfo=UTC) + task = LocalTaskEnvelope( + task_id="packet-export-1", + module_id="civicclerk", + task_type="packet_export", + ) + + result = record_task_attempt( + task, + success=False, + error="PDF render failed", + now=now, + policy=TaskRetryPolicy(max_attempts=3, base_delay_seconds=60), + ) + + assert result.task.status == "retry_wait" + assert result.retry_at == now + timedelta(seconds=60) + assert can_run_task(result.task, now=now + timedelta(seconds=59)) is False + assert can_run_task(result.task, now=now + timedelta(seconds=60)) is True + + summary = summarize_task_queue([result.task]) + assert summary.counts == {"retry_wait": 1} + assert summary.blocked is False + + +def test_local_task_exhaustion_is_blocking() -> None: + now = datetime(2026, 6, 13, 12, 0, tzinfo=UTC) + task = LocalTaskEnvelope( + task_id="records-export-1", + module_id="civicrecords-ai", + task_type="records_export", + attempt_count=1, + ) + + result = record_task_attempt( + task, + success=False, + error="Export destination unavailable", + now=now, + policy=TaskRetryPolicy(max_attempts=2, base_delay_seconds=1), + ) + + assert result.task.status == "failed" + assert summarize_task_queue([result.task]).blocked is True + + +def test_backup_manifest_validates_hashes_and_restore_safety(tmp_path) -> None: + data_root = tmp_path / "data" + data_root.mkdir() + (data_root / "civicclerk").mkdir() + payload = data_root / "civicclerk" / "meetings.json" + payload.write_text('{"meeting":"Regular Council"}', encoding="utf-8") + + manifest = build_backup_manifest( + root=data_root, + files=[payload], + backup_id="backup-1", + city_profile_id="sampleville", + civiccore_version="1.2.0", + module_id="civicclerk", + ) + + assert isinstance(manifest, BackupManifest) + assert validate_backup_manifest(manifest, root=data_root).valid is True + + restore_root = tmp_path / "restore" + restore_root.mkdir() + plan = plan_restore(manifest, backup_root=data_root, restore_root=restore_root) + + assert plan.ready is True + assert plan.actions[0].status == "ready" + + payload.write_text("tampered", encoding="utf-8") + tampered = validate_backup_manifest(manifest, root=data_root) + assert tampered.valid is False + assert tampered.hash_mismatches == ["civicclerk/meetings.json"] + + +def test_restore_plan_blocks_overwrite_by_default(tmp_path) -> None: + backup_root = tmp_path / "backup" + restore_root = tmp_path / "restore" + (backup_root / "civiccode").mkdir(parents=True) + (restore_root / "civiccode").mkdir(parents=True) + source = backup_root / "civiccode" / "code.json" + target = restore_root / "civiccode" / "code.json" + source.write_text('{"title":"Code"}', encoding="utf-8") + target.write_text("existing", encoding="utf-8") + + manifest = build_backup_manifest( + root=backup_root, + files=[source], + backup_id="backup-2", + city_profile_id="sampleville", + civiccore_version="1.2.0", + module_id="civiccode", + ) + plan = plan_restore(manifest, backup_root=backup_root, restore_root=restore_root) + + assert plan.ready is False + assert plan.actions[0].status == "would_overwrite" + + +def test_runtime_profile_and_action_result_are_local_first(tmp_path) -> None: + registry = build_module_registry([_module_manifest()], civiccore_version="1.2.0") + + profile = LocalRuntimeProfile( + profile_id="sampleville", + city_name="Sampleville", + data_root=tmp_path / "data", + backup_root=tmp_path / "backups", + module_registry=registry, + network_allowed_for=["model_download"], + ) + result = RuntimeActionResult( + action="first_run", + status="succeeded", + title="Setup complete", + message="CivicSuite is ready on this computer.", + ) + + assert profile.local_only is True + assert profile.module_registry.enabled_module_ids == ["civicclerk"] + assert result.status == "succeeded" From e2d035b044437761efbe9c5f963c7a39abd762f6 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Sat, 13 Jun 2026 01:05:13 -0600 Subject: [PATCH 2/2] Add PostgreSQL local task queue worker --- README.md | 35 +-- civiccore/__init__.py | 16 ++ .../civiccore_0003_local_task_queue.py | 44 ++++ civiccore/models/__init__.py | 2 + civiccore/platform/__init__.py | 18 ++ civiccore/platform/task_queue.py | 211 +++++++++++++++++ civiccore/tasks/__init__.py | 7 + civiccore/tasks/registry.py | 28 +++ civiccore/tasks/worker.py | 78 ++++++ ...ows-local-platform-contracts-2026-06-13.md | 12 +- docs/diagrams/civiccore-extraction-map.mmd | 4 +- docs/diagrams/civiccore-extraction-map.png | Bin 62209 -> 48625 bytes docs/diagrams/civiccore-extraction-map.svg | 126 +--------- docs/diagrams/migration-order.mmd | 3 +- docs/diagrams/migration-order.png | Bin 42707 -> 30738 bytes docs/diagrams/migration-order.svg | 2 +- docs/index.html | 2 +- tests/test_baseline_idempotency.py | 2 +- tests/test_llm_migration_0002.py | 16 +- tests/test_platform_task_queue.py | 223 ++++++++++++++++++ tests/test_public_api_v03.py | 45 ++++ tests/test_smoke.py | 4 + 22 files changed, 718 insertions(+), 160 deletions(-) create mode 100644 civiccore/migrations/versions/civiccore_0003_local_task_queue.py create mode 100644 civiccore/platform/task_queue.py create mode 100644 civiccore/tasks/__init__.py create mode 100644 civiccore/tasks/registry.py create mode 100644 civiccore/tasks/worker.py create mode 100644 tests/test_platform_task_queue.py diff --git a/README.md b/README.md index 1c0ef42..556184c 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,11 @@ helpers, but not delivery queues or outbound notification orchestration. `civiccore.platform` now ships the Windows-local desktop contracts for module manifests, install-profile validation, operator health summaries, durable local task envelopes, backup/restore manifests, and installer/runtime action results. -Those contracts are deliberately storage-neutral: the desktop shell and product -modules own process supervision, persistence tables, workers, and UI, while -CivicCore owns the shared validation and plain-English operator state. +It also ships the PostgreSQL-backed `civiccore_local_tasks` migration, async +queue helpers, and `python -m civiccore.tasks.worker` entry point for the local +desktop worker. Product modules still own task handlers and UI, while CivicCore +owns shared validation, queue state, retry semantics, and plain-English +operator state. `civiccore.verification` now ships the first release-evidence helper surface, while sovereignty verification remains future work. `civiccore.connectors` now also ships shared local-payload import @@ -91,8 +93,8 @@ retention/off-host storage, on top of shared live connector sync retry/circuit-b including run-result normalization, operator health copy, retry delay policy, and async HTTP retry, on top of shared Windows-local module/runtime contracts for the desktop shell, including no-Docker/no-WSL manifest validation, plain-English health summaries, -local task retry envelopes, and backup/restore checksum manifests, on top of -shared persisted audit-log hash and verification helpers for +PostgreSQL-backed local task queue helpers, and backup/restore checksum +manifests, on top of shared persisted audit-log hash and verification helpers for database-backed module audit rows on top of shared trusted-header auth config loading and proxy-source enforcement helpers on top of shipped trusted-header auth helpers on top of shipped @@ -358,9 +360,9 @@ adapters, worker execution, legal determinations, or vendor write-back. ## Windows-local platform contracts -`civiccore.platform` exposes the shared contracts the CivicSuite Windows -desktop shell uses to keep future modules pluggable without making clerks learn -infrastructure: +`civiccore.platform` exposes the shared contracts and queue helpers the +CivicSuite Windows desktop shell uses to keep future modules pluggable without +making clerks learn infrastructure: ```python from civiccore.platform import ( @@ -369,7 +371,10 @@ from civiccore.platform import ( PlatformHealthCheck, summarize_platform_health, LocalTaskEnvelope, + enqueue_local_task, + claim_next_local_task, record_task_attempt, + run_one_local_task, build_backup_manifest, plan_restore, ) @@ -377,9 +382,9 @@ from civiccore.platform import ( For the `windows_local` install profile, module manifests cannot require Docker, WSL, Linux shell setup, or a terminal-only operator path. CivicCore also -ships storage-neutral task envelopes and backup manifests so downstream modules -share retry, health, checksum, and restore-safety semantics while keeping their -own database tables and worker implementations. +ships task envelopes, a durable PostgreSQL task table, async queue helpers, and +a worker CLI so downstream modules share retry, health, checksum, and +restore-safety semantics while registering their own task handlers. ## Document ingestion @@ -568,10 +573,10 @@ router integration, and persistence orchestration remain future work. ## Scheduling helper `civiccore.scheduling` exposes the shared cron expression contract used by -module background jobs. `civiccore.platform` exposes the storage-neutral local -task envelope and retry contract. Modules keep their own worker/runtime wiring, -but should reuse these helpers so one-minute accidental or adversarial schedules -and task retry behavior are handled consistently across CivicSuite. +module background jobs. `civiccore.platform` exposes the local task envelope, +PostgreSQL queue helpers, and retry contract. Modules keep their own task +handlers, but should reuse these helpers so one-minute accidental or adversarial +schedules and task retry behavior are handled consistently across CivicSuite. ```python from civiccore.scheduling import compute_next_sync_at, validate_cron_expression diff --git a/civiccore/__init__.py b/civiccore/__init__.py index d6a9d52..1d85980 100644 --- a/civiccore/__init__.py +++ b/civiccore/__init__.py @@ -120,6 +120,7 @@ BackupRestorePlan, BackupValidationResult, LocalRuntimeProfile, + LocalTask, LocalTaskEnvelope, LocalTaskResult, ModuleBackupHook, @@ -139,15 +140,22 @@ PlatformHealthSummary, RuntimeActionResult, TaskQueueSummary, + TaskHandler, TaskRetryPolicy, build_backup_manifest, build_module_registry, can_run_task, + claim_next_local_task, + complete_local_task, + enqueue_local_task, + fail_local_task, next_retry_at, plan_restore, record_task_attempt, + run_one_local_task, summarize_platform_health, summarize_task_queue, + task_row_to_envelope, validate_backup_manifest, validate_windows_local_manifest, ) @@ -290,6 +298,7 @@ "BackupRestorePlan", "BackupValidationResult", "LocalRuntimeProfile", + "LocalTask", "LocalTaskEnvelope", "LocalTaskResult", "ModuleBackupHook", @@ -309,11 +318,14 @@ "PlatformHealthSummary", "RuntimeActionResult", "TaskQueueSummary", + "TaskHandler", "TaskRetryPolicy", "WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS", "build_backup_manifest", "build_module_registry", "can_run_task", + "claim_next_local_task", + "complete_local_task", "access_level_allows", "completed_profile_fields", "compute_onboarding_status", @@ -338,10 +350,13 @@ "normalize_search_query", "normalize_search_text", "min_interval_minutes", + "enqueue_local_task", + "fail_local_task", "next_retry_at", "parse_profile_answer", "plan_restore", "record_task_attempt", + "run_one_local_task", "roles_grant_access", "search_text_matches_query", "reciprocal_rank_fusion", @@ -356,6 +371,7 @@ "validate_cited_sentences", "summarize_platform_health", "summarize_task_queue", + "task_row_to_envelope", "validate_backup_manifest", "validate_cron_expression", "validate_fernet_key_setting", diff --git a/civiccore/migrations/versions/civiccore_0003_local_task_queue.py b/civiccore/migrations/versions/civiccore_0003_local_task_queue.py new file mode 100644 index 0000000..fdec2b6 --- /dev/null +++ b/civiccore/migrations/versions/civiccore_0003_local_task_queue.py @@ -0,0 +1,44 @@ +"""CivicCore migration 0003 - local task queue.""" + +from __future__ import annotations + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from civiccore.migrations.guards import idempotent_create_index, idempotent_create_table + + +revision = "civiccore_0003_local_task_queue" +down_revision = "civiccore_0002_llm" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + idempotent_create_table( + "civiccore_local_tasks", + sa.Column("task_id", sa.String(128), primary_key=True), + sa.Column("module_id", sa.String(64), nullable=False), + sa.Column("task_type", sa.String(100), nullable=False), + sa.Column("status", sa.String(20), nullable=False, server_default="queued"), + sa.Column( + "payload", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + server_default=sa.text("'{}'::jsonb"), + ), + sa.Column("attempt_count", sa.Integer(), nullable=False, server_default="0"), + sa.Column("queued_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), + sa.Column("available_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("last_error", sa.Text(), nullable=True), + sa.Column("idempotency_key", sa.String(255), nullable=True, unique=True), + sa.Column("audit_subject_id", sa.String(255), nullable=True), + ) + idempotent_create_index("ix_civiccore_local_tasks_module", "civiccore_local_tasks", ["module_id"]) + idempotent_create_index("ix_civiccore_local_tasks_status", "civiccore_local_tasks", ["status"]) + idempotent_create_index("ix_civiccore_local_tasks_type", "civiccore_local_tasks", ["task_type"]) + + +def downgrade() -> None: + """No-op; local task queue data is preserved for point-in-time restore.""" + return None diff --git a/civiccore/models/__init__.py b/civiccore/models/__init__.py index 450d112..f960306 100644 --- a/civiccore/models/__init__.py +++ b/civiccore/models/__init__.py @@ -1,11 +1,13 @@ """CivicCore shared SQLAlchemy ORM model exports.""" from civiccore.ingest.models import DataSource, Document, DocumentChunk, IngestionStatus, SourceType +from civiccore.platform.task_queue import LocalTask __all__ = [ "DataSource", "Document", "DocumentChunk", "IngestionStatus", + "LocalTask", "SourceType", ] diff --git a/civiccore/platform/__init__.py b/civiccore/platform/__init__.py index 747c97f..e0db969 100644 --- a/civiccore/platform/__init__.py +++ b/civiccore/platform/__init__.py @@ -36,6 +36,16 @@ validate_windows_local_manifest, ) from civiccore.platform.runtime import LocalRuntimeProfile, RuntimeActionResult +from civiccore.platform.task_queue import ( + LocalTask, + TaskHandler, + claim_next_local_task, + complete_local_task, + enqueue_local_task, + fail_local_task, + run_one_local_task, + task_row_to_envelope, +) from civiccore.platform.tasks import ( LocalTaskEnvelope, LocalTaskResult, @@ -54,6 +64,7 @@ "BackupRestorePlan", "BackupValidationResult", "LocalRuntimeProfile", + "LocalTask", "LocalTaskEnvelope", "LocalTaskResult", "ModuleBackupHook", @@ -73,16 +84,23 @@ "PlatformHealthSummary", "RuntimeActionResult", "TaskQueueSummary", + "TaskHandler", "TaskRetryPolicy", "WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS", "build_backup_manifest", "build_module_registry", "can_run_task", + "claim_next_local_task", + "complete_local_task", + "enqueue_local_task", + "fail_local_task", "next_retry_at", "plan_restore", "record_task_attempt", + "run_one_local_task", "summarize_platform_health", "summarize_task_queue", + "task_row_to_envelope", "validate_backup_manifest", "validate_windows_local_manifest", ] diff --git a/civiccore/platform/task_queue.py b/civiccore/platform/task_queue.py new file mode 100644 index 0000000..24f6fc1 --- /dev/null +++ b/civiccore/platform/task_queue.py @@ -0,0 +1,211 @@ +"""PostgreSQL-backed local task queue for CivicSuite desktop runtimes.""" + +from __future__ import annotations + +import inspect +from collections.abc import Awaitable, Callable +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy import DateTime, Integer, String, Text, select +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, mapped_column + +from civiccore.db import Base +from civiccore.platform.tasks import ( + LocalTaskEnvelope, + TaskRetryPolicy, + record_task_attempt, +) + +TaskHandler = Callable[[LocalTaskEnvelope], object | Awaitable[object]] + + +class LocalTask(Base): + """Durable local task row used by the Windows local runtime.""" + + __tablename__ = "civiccore_local_tasks" + + task_id: Mapped[str] = mapped_column(String(128), primary_key=True) + module_id: Mapped[str] = mapped_column(String(64), index=True) + task_type: Mapped[str] = mapped_column(String(100), index=True) + status: Mapped[str] = mapped_column(String(20), index=True, default="queued") + payload: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict) + attempt_count: Mapped[int] = mapped_column(Integer, default=0) + queued_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + available_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + last_error: Mapped[str | None] = mapped_column(Text, nullable=True) + idempotency_key: Mapped[str | None] = mapped_column(String(255), nullable=True, unique=True) + audit_subject_id: Mapped[str | None] = mapped_column(String(255), nullable=True) + + +def task_row_to_envelope(row: LocalTask) -> LocalTaskEnvelope: + """Convert an ORM row into the public task contract.""" + + return LocalTaskEnvelope( + task_id=row.task_id, + module_id=row.module_id, + task_type=row.task_type, + status=row.status, # type: ignore[arg-type] + payload=row.payload or {}, + attempt_count=row.attempt_count, + queued_at=row.queued_at, + available_at=row.available_at, + last_error=row.last_error, + idempotency_key=row.idempotency_key, + audit_subject_id=row.audit_subject_id, + ) + + +async def enqueue_local_task( + session: AsyncSession, + task: LocalTaskEnvelope, +) -> LocalTaskEnvelope: + """Insert a task unless its idempotency key already exists.""" + + if task.idempotency_key: + existing = await session.scalar( + select(LocalTask).where(LocalTask.idempotency_key == task.idempotency_key) + ) + if existing is not None: + return task_row_to_envelope(existing) + + row = LocalTask( + task_id=task.task_id, + module_id=task.module_id, + task_type=task.task_type, + status=task.status, + payload=task.payload, + attempt_count=task.attempt_count, + queued_at=task.queued_at, + available_at=task.available_at, + last_error=task.last_error, + idempotency_key=task.idempotency_key, + audit_subject_id=task.audit_subject_id, + ) + session.add(row) + await session.flush() + return task_row_to_envelope(row) + + +async def claim_next_local_task( + session: AsyncSession, + *, + module_id: str | None = None, + now: datetime | None = None, +) -> LocalTaskEnvelope | None: + """Claim the next queued or due retry task.""" + + current_time = now or datetime.now(UTC) + query = ( + select(LocalTask) + .where( + (LocalTask.status == "queued") + | ((LocalTask.status == "retry_wait") & (LocalTask.available_at <= current_time)) + ) + .order_by(LocalTask.queued_at.asc()) + .with_for_update(skip_locked=True) + .limit(1) + ) + if module_id is not None: + query = query.where(LocalTask.module_id == module_id) + + row = await session.scalar(query) + if row is None: + return None + + row.status = "running" + row.available_at = None + await session.flush() + return task_row_to_envelope(row) + + +async def complete_local_task(session: AsyncSession, task_id: str) -> LocalTaskEnvelope: + """Mark a claimed task as succeeded.""" + + row = await _get_task_row(session, task_id) + row.status = "succeeded" + row.last_error = None + row.available_at = None + await session.flush() + return task_row_to_envelope(row) + + +async def fail_local_task( + session: AsyncSession, + task_id: str, + *, + error: str, + now: datetime | None = None, + policy: TaskRetryPolicy | None = None, +) -> LocalTaskEnvelope: + """Record a failed attempt and either schedule retry or exhaust the task.""" + + row = await _get_task_row(session, task_id) + result = record_task_attempt( + task_row_to_envelope(row), + success=False, + error=error, + now=now, + policy=policy, + ) + _apply_envelope(row, result.task) + await session.flush() + return task_row_to_envelope(row) + + +async def run_one_local_task( + session: AsyncSession, + *, + handlers: dict[str, TaskHandler], + module_id: str | None = None, + now: datetime | None = None, + retry_policy: TaskRetryPolicy | None = None, +) -> LocalTaskEnvelope | None: + """Claim and run one task with a registered in-process handler.""" + + task = await claim_next_local_task(session, module_id=module_id, now=now) + if task is None: + return None + + handler = handlers.get(task.task_type) + if handler is None: + return await fail_local_task( + session, + task.task_id, + error=f"No handler registered for task type {task.task_type!r}.", + now=now, + policy=retry_policy, + ) + + try: + result = handler(task) + if inspect.isawaitable(result): + await result + except Exception as exc: # pragma: no cover - exact handler failures are module-owned + return await fail_local_task( + session, + task.task_id, + error=str(exc), + now=now, + policy=retry_policy, + ) + return await complete_local_task(session, task.task_id) + + +async def _get_task_row(session: AsyncSession, task_id: str) -> LocalTask: + row = await session.get(LocalTask, task_id) + if row is None: + raise LookupError(f"local task not found: {task_id}") + return row + + +def _apply_envelope(row: LocalTask, envelope: LocalTaskEnvelope) -> None: + row.status = envelope.status + row.payload = envelope.payload + row.attempt_count = envelope.attempt_count + row.available_at = envelope.available_at + row.last_error = envelope.last_error + row.idempotency_key = envelope.idempotency_key + row.audit_subject_id = envelope.audit_subject_id diff --git a/civiccore/tasks/__init__.py b/civiccore/tasks/__init__.py new file mode 100644 index 0000000..547de9a --- /dev/null +++ b/civiccore/tasks/__init__.py @@ -0,0 +1,7 @@ +"""Local task worker entry points for CivicCore.""" + +from __future__ import annotations + +from civiccore.tasks.registry import get_task_handlers, register_task_handler + +__all__ = ["get_task_handlers", "register_task_handler"] diff --git a/civiccore/tasks/registry.py b/civiccore/tasks/registry.py new file mode 100644 index 0000000..09b3a92 --- /dev/null +++ b/civiccore/tasks/registry.py @@ -0,0 +1,28 @@ +"""In-process task handler registry for local CivicSuite workers.""" + +from __future__ import annotations + +from collections.abc import Callable + +from civiccore.platform.task_queue import TaskHandler + +_HANDLERS: dict[str, TaskHandler] = {} + + +def register_task_handler(task_type: str) -> Callable[[TaskHandler], TaskHandler]: + """Register a callable task handler by task type.""" + + if not task_type.strip(): + raise ValueError("task_type cannot be blank") + + def decorator(handler: TaskHandler) -> TaskHandler: + _HANDLERS[task_type] = handler + return handler + + return decorator + + +def get_task_handlers() -> dict[str, TaskHandler]: + """Return a copy of the registered task handlers.""" + + return dict(_HANDLERS) diff --git a/civiccore/tasks/worker.py b/civiccore/tasks/worker.py new file mode 100644 index 0000000..189e2d6 --- /dev/null +++ b/civiccore/tasks/worker.py @@ -0,0 +1,78 @@ +"""CLI worker for the CivicCore PostgreSQL-backed local task queue.""" + +from __future__ import annotations + +import argparse +import asyncio +import importlib +import os +from collections.abc import Sequence + +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +from civiccore.platform.task_queue import run_one_local_task +from civiccore.platform.tasks import TaskRetryPolicy +from civiccore.tasks.registry import get_task_handlers + + +def _async_database_url(url: str) -> str: + if "+psycopg2" in url: + return url.replace("postgresql+psycopg2", "postgresql+asyncpg") + if url.startswith("postgresql://"): + return url.replace("postgresql://", "postgresql+asyncpg://", 1) + return url + + +def _load_handler_modules(raw_modules: str | None) -> None: + if not raw_modules: + return + for module_name in [item.strip() for item in raw_modules.split(",") if item.strip()]: + importlib.import_module(module_name) + + +async def _run_worker(args: argparse.Namespace) -> int: + database_url = os.environ.get("DATABASE_URL") + if not database_url: + raise RuntimeError("DATABASE_URL is required for civiccore.tasks.worker") + _load_handler_modules(os.environ.get("CIVICCORE_TASK_HANDLER_MODULES")) + handlers = get_task_handlers() + + engine = create_async_engine(_async_database_url(database_url)) + session_factory = async_sessionmaker(engine, expire_on_commit=False) + retry_policy = TaskRetryPolicy( + max_attempts=args.max_attempts, + base_delay_seconds=args.base_delay_seconds, + max_delay_seconds=args.max_delay_seconds, + ) + + try: + while True: + async with session_factory() as session: + task = await run_one_local_task( + session, + handlers=handlers, + retry_policy=retry_policy, + ) + await session.commit() + if args.once: + return 0 + if task is None: + await asyncio.sleep(args.poll_seconds) + finally: + await engine.dispose() + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Run the CivicCore local task worker.") + parser.add_argument("--backend", choices=["postgres"], default="postgres") + parser.add_argument("--once", action="store_true") + parser.add_argument("--poll-seconds", type=float, default=2.0) + parser.add_argument("--max-attempts", type=int, default=3) + parser.add_argument("--base-delay-seconds", type=int, default=30) + parser.add_argument("--max-delay-seconds", type=int, default=3600) + args = parser.parse_args(argv) + return asyncio.run(_run_worker(args)) + + +if __name__ == "__main__": # pragma: no cover - exercised through CLI smoke + raise SystemExit(main()) diff --git a/docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md b/docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md index 1265773..1952b79 100644 --- a/docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md +++ b/docs/audits/audit-lite-windows-local-platform-contracts-2026-06-13.md @@ -3,18 +3,18 @@ Date: 2026-06-13 Repo: CivicSuite/civiccore Branch: work/windows-local-platform-contracts -Scope: `civiccore.platform` module manifest, health, task, backup/restore, and runtime contracts. +Scope: `civiccore.platform` module manifest, health, task, backup/restore, runtime contracts, and PostgreSQL-backed local task queue. ## Verdict Unresolved findings: 0 Blocker / 0 Critical / 0 Major / 0 Minor / 0 Nit -This slice is acceptable to push. It adds real importable CivicCore contracts for the Windows-local desktop path without claiming downstream module wiring is complete. +This slice is acceptable to push. It adds real importable CivicCore contracts and a launchable local task worker for the Windows-local desktop path without claiming downstream module wiring is complete. ## Evidence - `python -m pip install -e .[dev]` passed after installing the repo-declared dev extras. -- `python -m pytest` passed: 297 passed. +- `python -m pytest` passed: 302 passed. - `python -m ruff check civiccore tests` passed. - `python -m build` passed and included `civiccore/platform` in the sdist and wheel. - `git diff --check` passed. @@ -22,12 +22,12 @@ This slice is acceptable to push. It adds real importable CivicCore contracts fo ## Five-Lens Review -- Engineering: Pass. New contracts are storage-neutral and validate the Windows-local profile against required Docker, WSL, Linux shell, and terminal requirements. +- Engineering: Pass. New contracts validate the Windows-local profile against required Docker, WSL, Linux shell, and terminal requirements. The task queue now has a real migration, ORM row, async queue helpers, handler runner, and worker CLI. - UX: Pass. Health and runtime result contracts require plain-English messages and next actions for clerk/admin surfaces. -- Tests: Pass. Tests cover positive registry resolution, blocked dependency state, forbidden runtime requirements, local task retry/exhaustion, backup checksum validation, restore overwrite blocking, and local-first runtime profile defaults. +- Tests: Pass. Tests cover positive registry resolution, blocked dependency state, forbidden runtime requirements, local task retry/exhaustion, PostgreSQL-backed enqueue/claim/complete/fail/worker behavior, backup checksum validation, restore overwrite blocking, and local-first runtime profile defaults. - Docs: Pass. README and package metadata now describe the shipped platform contracts and distinguish contracts from worker/runtime execution. - QA: Pass. Public package root exports were updated and covered by the public API smoke tests; packaging confirmed the new package is included. ## Notes -The configured `audit-lite` skill file was not present at `C:\Users\scott\.codex\skills\audit-lite\SKILL.md` in this session, so this report follows the in-repo five-lens self-audit format as the fallback. Downstream CivicRecords, CivicClerk, CivicCode, and desktop-shell adoption of these contracts remains in subsequent slices. +The configured `audit-lite` skill file was not present at `C:\Users\scott\.codex\skills\audit-lite\SKILL.md` in this session, so this report follows the in-repo five-lens self-audit format as the fallback. Downstream CivicRecords, CivicClerk, CivicCode, and desktop-shell adoption of these contracts and task handlers remains in subsequent slices. diff --git a/docs/diagrams/civiccore-extraction-map.mmd b/docs/diagrams/civiccore-extraction-map.mmd index 40ad2a2..608e5f5 100644 --- a/docs/diagrams/civiccore-extraction-map.mmd +++ b/docs/diagrams/civiccore-extraction-map.mmd @@ -1,6 +1,6 @@ graph TB - subgraph SHIPPED["civiccore v0.2.0 — SHIPPING TODAY"] - Mig["civiccore.migrations
runner + idempotent guards
civiccore_0001_baseline_v1
civiccore_0002_llm"] + subgraph SHIPPED["civiccore v1.2.0 - shipped core surface"] + Mig["civiccore.migrations
runner + idempotent guards
civiccore_0001_baseline_v1
civiccore_0002_llm
civiccore_0003_local_task_queue"] DB["civiccore.db.Base
shared SQLAlchemy
declarative base"] LLM["civiccore.llm
providers + templates +
registry + context +
structured output"] end diff --git a/docs/diagrams/civiccore-extraction-map.png b/docs/diagrams/civiccore-extraction-map.png index 51f8cbfeecfd15bf0b6572c71672a785879694a4..55cd6ae517182d664a2b75ee27044c7eff3f23d0 100644 GIT binary patch literal 48625 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�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 Nsj-z2-r!Qye*qy6={NuY diff --git a/docs/diagrams/civiccore-extraction-map.svg b/docs/diagrams/civiccore-extraction-map.svg index d4ba924..a5c10c9 100644 --- a/docs/diagrams/civiccore-extraction-map.svg +++ b/docs/diagrams/civiccore-extraction-map.svg @@ -1,125 +1 @@ - -CivicCore v0.11.0 shipped and partial/future namespaces -Architecture map showing CivicCore v0.11.0 shipped primitives on the left and partial or future extraction targets on the right. - - - - - -CivicCore v0.11.0 - Shared platform primitives for CivicSuite modules. Library only; not an end-user app. - - - Shipped implementation - Safe for downstream modules to import in the current development line. - - - - migrations - runner + shared baseline - - - - db - shared SQLAlchemy Base - - - - llm - providers + templates - - - - audit - hash-chained events - - - - provenance - source + citation metadata - - - - connectors - offline manifests + import helpers - - - - exports - bundles + checksums - - - - city_profile - local deployment config - - - - Partial or future extraction targets - Some namespaces ship helper slices; deeper platform behavior remains future work. - - - - auth - shipped bearer helpers - - - - catalog - catalog primitives - - - - exemptions - legal exemption engine - - - - ingest - document ingestion - - - - notifications - shipped notice helpers - - - - onboarding - web onboarding flows - - - - scaffold - module templates - - - - search - shipped helper surface - - - - verification - shipped QA helpers - - - - live connectors - sync + write-back - - -Boundary: v0.11.0 ships helper surfaces for auth, notice compliance, search access/ranking, verification, onboarding, and local imports. It does not ship live sync, ingestion, search indexing, delivery queues, or legal determinations. - +

Planned extraction targets — placeholder __init__.py only, no implementation

civiccore.auth

civiccore.audit

civiccore.catalog

civiccore.connectors

civiccore.exemptions

civiccore.ingest

civiccore.notifications

civiccore.onboarding

civiccore.scaffold

civiccore.search

civiccore.verification

civiccore v1.2.0 - shipped core surface

civiccore.migrations
runner + idempotent guards
civiccore_0001_baseline_v1
civiccore_0002_llm
civiccore_0003_local_task_queue

civiccore.db.Base
shared SQLAlchemy
declarative base

civiccore.llm
providers + templates +
registry + context +
structured output

\ No newline at end of file diff --git a/docs/diagrams/migration-order.mmd b/docs/diagrams/migration-order.mmd index 1a2fbe3..8f6b951 100644 --- a/docs/diagrams/migration-order.mmd +++ b/docs/diagrams/migration-order.mmd @@ -7,7 +7,8 @@ sequenceDiagram Consumer->>Civic: upgrade_to_head() [subprocess] Civic->>Civic: civiccore_0001_baseline_v1 Civic->>Civic: civiccore_0002_llm (ALTER prompt_templates) - Civic->>CivicVer: stamp 'civiccore_0002_llm' + Civic->>Civic: civiccore_0003_local_task_queue + Civic->>CivicVer: stamp 'civiccore_0003_local_task_queue' Civic-->>Consumer: done Consumer->>Consumer: run consumer-side alembic chain Consumer->>ConsumerVer: stamp '020_phase2_consumer_app_backfill' diff --git a/docs/diagrams/migration-order.png b/docs/diagrams/migration-order.png index 6dfb8ce9de3043233ed67c50733970d3fb74efb2..7b937c7eb9f6e5ec6be98a45fc089b5421f2b3fc 100644 GIT binary patch literal 30738 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%0AqHD3T{gqjFM&r}# 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 b!Ca@@+D6!qFGO+r7vIv;Hqxrn1jYOhi3Aip diff --git a/docs/diagrams/migration-order.svg b/docs/diagrams/migration-order.svg index 2c26aee..b0be701 100644 --- a/docs/diagrams/migration-order.svg +++ b/docs/diagrams/migration-order.svg @@ -1 +1 @@ -alembic_versionalembic_version_civiccoreciviccore.migrations.runner.upgrade_to_headConsumer app(records-ai env.py)alembic_versionalembic_version_civiccoreciviccore.migrations.runner.upgrade_to_headConsumer app(records-ai env.py)Two separate version tablesprevent revision-name collisionsbetween civiccore and consumers.upgrade_to_head() [subprocess]1civiccore_0001_baseline_v12civiccore_0002_llm (ALTER prompt_templates)3stamp 'civiccore_0002_llm'4done5run consumer-side alembic chain6stamp '020_phase2_consumer_app_backfill'7 \ No newline at end of file +alembic_versionalembic_version_civiccoreciviccore.migrations.runner.upgrade_to_headConsumer app(records-ai env.py)alembic_versionalembic_version_civiccoreciviccore.migrations.runner.upgrade_to_headConsumer app(records-ai env.py)Two separate version tablesprevent revision-name collisionsbetween civiccore and consumers.upgrade_to_head() [subprocess]1civiccore_0001_baseline_v12civiccore_0002_llm (ALTER prompt_templates)3civiccore_0003_local_task_queue4stamp 'civiccore_0003_local_task_queue'5done6run consumer-side alembic chain7stamp '020_phase2_consumer_app_backfill'8 \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 539d577..0f5cdd6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -131,7 +131,7 @@

What it is

Shipped core surface

    -
  • civiccore.migrations - migration runner + idempotent guards + civiccore_0001_baseline_v1 + civiccore_0002_llm.
  • +
  • civiccore.migrations - migration runner + idempotent guards + civiccore_0001_baseline_v1 + civiccore_0002_llm + civiccore_0003_local_task_queue.
  • civiccore.db - shared SQLAlchemy declarative Base.
  • civiccore.llm - providers, templates, model registry, context utilities, and structured output.
  • civiccore.audit - hash-chained audit primitives plus legacy-compatible persisted audit-log verification helpers.
  • diff --git a/tests/test_baseline_idempotency.py b/tests/test_baseline_idempotency.py index 69a3ab9..c982b08 100644 --- a/tests/test_baseline_idempotency.py +++ b/tests/test_baseline_idempotency.py @@ -23,7 +23,7 @@ _SHARED_TABLE_ORDER, ) -EXPECTED_HEAD = "civiccore_0002_llm" +EXPECTED_HEAD = "civiccore_0003_local_task_queue" def _docker_available() -> bool: diff --git a/tests/test_llm_migration_0002.py b/tests/test_llm_migration_0002.py index b168092..54d3652 100644 --- a/tests/test_llm_migration_0002.py +++ b/tests/test_llm_migration_0002.py @@ -4,8 +4,8 @@ - New columns (template_name, consumer_app, is_override) exist after upgrade_to_head() - Old column (name) no longer exists after migration - New unique constraint uq_prompt_templates_app_name_version is in place -- Incremental upgrade path from 0001 → 0002 works correctly -- Migration is idempotent (running upgrade_to_head() twice is a no-op) +- Incremental upgrade path from 0001 through 0002 works correctly +- Migration remains idempotent when the full head chain is run twice """ from __future__ import annotations @@ -29,7 +29,7 @@ _SHARED_TABLE_ORDER, ) -EXPECTED_HEAD = "civiccore_0002_llm" +EXPECTED_HEAD = "civiccore_0003_local_task_queue" _ALEMBIC_INI = Path(civiccore.__file__).parent / "migrations" / "alembic.ini" @@ -119,7 +119,7 @@ def test_0002_prompt_templates_has_new_columns(engine, pg_container): """After upgrade_to_head(), prompt_templates has the Phase 2 override columns. Verifies: - - Current revision is civiccore_0002_llm + - Current revision is the latest CivicCore head - template_name column exists (renamed from name) - consumer_app column exists - is_override column exists @@ -158,10 +158,10 @@ def test_0002_prompt_templates_has_new_columns(engine, pg_container): def test_0002_upgrade_from_0001_baseline(pg_container): - """Incremental upgrade from 0001 → 0002 transforms prompt_templates correctly. + """Incremental upgrade from 0001 through 0002 transforms prompt_templates correctly. Step 1: Apply only civiccore_0001_baseline_v1 — verify old schema shape (name column). - Step 2: Apply upgrade_to_head() — verify 0002 shape (template_name, consumer_app, is_override). + Step 2: Apply upgrade_to_head() — verify 0002 shape plus any later head migrations. """ # Step 1: only 0001 applied _upgrade_to_revision(pg_container, "civiccore_0001_baseline_v1") @@ -183,7 +183,7 @@ def test_0002_upgrade_from_0001_baseline(pg_container): f"After 0001 only, 'consumer_app' should not yet exist; found: {sorted(col_names_0001)}" ) - # Step 2: upgrade to head (0002 applied) + # Step 2: upgrade to head (0002 and later migrations applied) with _database_url_env(pg_container): upgrade_to_head() @@ -210,7 +210,7 @@ def test_0002_upgrade_from_0001_baseline(pg_container): def test_0002_is_idempotent(engine, pg_container): - """Re-running upgrade_to_head() after 0002 is already applied is a no-op. + """Re-running upgrade_to_head() after the full head chain is already applied is a no-op. Proves idempotent guards in the 0002 migration skip existing objects rather than raising or mutating schema. diff --git a/tests/test_platform_task_queue.py b/tests/test_platform_task_queue.py new file mode 100644 index 0000000..73072b8 --- /dev/null +++ b/tests/test_platform_task_queue.py @@ -0,0 +1,223 @@ +"""PostgreSQL-backed task queue tests for the Windows local runtime.""" + +from __future__ import annotations + +import asyncio +import contextlib +import os +import sys +from argparse import Namespace + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +testcontainers = pytest.importorskip( + "testcontainers.postgres", + reason="testcontainers[postgres] not installed; install dev extras to run queue tests", +) +PostgresContainer = testcontainers.PostgresContainer + +from civiccore.migrations.runner import upgrade_to_head # noqa: E402 +from civiccore.platform import ( # noqa: E402 + LocalTask, + LocalTaskEnvelope, + TaskRetryPolicy, + claim_next_local_task, + complete_local_task, + enqueue_local_task, + fail_local_task, + run_one_local_task, +) +from civiccore.tasks import register_task_handler # noqa: E402 +from civiccore.tasks.worker import _run_worker # noqa: E402 + + +def _docker_available() -> bool: + try: + import docker # type: ignore[import-untyped] + + docker.from_env().ping() + return True + except Exception: + return False + + +@contextlib.contextmanager +def _database_url_env(pg_container): + old_url = os.environ.get("DATABASE_URL") + os.environ["DATABASE_URL"] = pg_container.get_connection_url() + try: + yield + finally: + if old_url is None: + os.environ.pop("DATABASE_URL", None) + else: + os.environ["DATABASE_URL"] = old_url + + +def _async_url(sync_url: str) -> str: + if "+psycopg2" in sync_url: + return sync_url.replace("postgresql+psycopg2", "postgresql+asyncpg") + if sync_url.startswith("postgresql://"): + return sync_url.replace("postgresql://", "postgresql+asyncpg://", 1) + return sync_url + + +@pytest.fixture(scope="function") +def pg_container(): + if not _docker_available(): + pytest.skip("Docker daemon not reachable; queue tests require a PostgreSQL container.") + with PostgresContainer("pgvector/pgvector:pg17") as pg: + yield pg + + +@pytest_asyncio.fixture(loop_scope="function") +async def session_factory(pg_container): + with _database_url_env(pg_container): + upgrade_to_head() + + engine = create_async_engine(_async_url(pg_container.get_connection_url())) + factory = async_sessionmaker(engine, expire_on_commit=False) + try: + yield factory + finally: + await engine.dispose() + + +@pytest_asyncio.fixture(loop_scope="function") +async def session(session_factory): + async with session_factory() as sess: + yield sess + + +@pytest.mark.asyncio(loop_scope="function") +async def test_local_task_queue_enqueues_claims_and_completes(session) -> None: + task = LocalTaskEnvelope( + task_id="records-response-1", + module_id="civicrecords-ai", + task_type="records_response_export", + payload={"request_id": "RR-1001"}, + idempotency_key="RR-1001:response-export", + ) + + created = await enqueue_local_task(session, task) + duplicate = await enqueue_local_task(session, task.model_copy(update={"task_id": "duplicate"})) + claimed = await claim_next_local_task(session) + completed = await complete_local_task(session, "records-response-1") + await session.commit() + + persisted = await session.get(LocalTask, "records-response-1") + assert created.task_id == "records-response-1" + assert duplicate.task_id == "records-response-1" + assert claimed is not None + assert claimed.status == "running" + assert completed.status == "succeeded" + assert persisted is not None + assert persisted.status == "succeeded" + + +@pytest.mark.asyncio(loop_scope="function") +async def test_local_task_queue_records_retry_state(session) -> None: + task = LocalTaskEnvelope( + task_id="records-search-1", + module_id="civicrecords-ai", + task_type="records_search_index", + ) + await enqueue_local_task(session, task) + await claim_next_local_task(session) + + failed = await fail_local_task( + session, + "records-search-1", + error="Local index unavailable", + policy=TaskRetryPolicy(max_attempts=3, base_delay_seconds=1), + ) + await session.commit() + + assert failed.status == "retry_wait" + assert failed.attempt_count == 1 + assert failed.available_at is not None + assert failed.last_error == "Local index unavailable" + + +@pytest.mark.asyncio(loop_scope="function") +async def test_run_one_local_task_uses_registered_handler(session) -> None: + seen: list[str] = [] + await enqueue_local_task( + session, + LocalTaskEnvelope( + task_id="records-review-1", + module_id="civicrecords-ai", + task_type="records_review_packet", + ), + ) + + async def handler(task: LocalTaskEnvelope) -> None: + seen.append(task.task_id) + + completed = await run_one_local_task(session, handlers={"records_review_packet": handler}) + await session.commit() + + assert completed is not None + assert completed.status == "succeeded" + assert seen == ["records-review-1"] + + +@pytest.mark.asyncio(loop_scope="function") +async def test_worker_once_exits_when_queue_is_empty(pg_container) -> None: + with _database_url_env(pg_container): + upgrade_to_head() + result = await _run_worker( + Namespace( + once=True, + poll_seconds=0.01, + max_attempts=1, + base_delay_seconds=1, + max_delay_seconds=1, + ) + ) + + assert result == 0 + + +@pytest.mark.asyncio(loop_scope="function") +async def test_worker_once_runs_registered_handler(pg_container, session_factory) -> None: + seen: list[str] = [] + + @register_task_handler("records_worker_registry_probe") + async def handler(task: LocalTaskEnvelope) -> None: + seen.append(task.task_id) + + async with session_factory() as sess: + await enqueue_local_task( + sess, + LocalTaskEnvelope( + task_id="records-worker-1", + module_id="civicrecords-ai", + task_type="records_worker_registry_probe", + ), + ) + await sess.commit() + + with _database_url_env(pg_container): + result = await _run_worker( + Namespace( + once=True, + poll_seconds=0.01, + max_attempts=1, + base_delay_seconds=1, + max_delay_seconds=1, + ) + ) + + async with session_factory() as sess: + persisted = await sess.get(LocalTask, "records-worker-1") + + assert result == 0 + assert seen == ["records-worker-1"] + assert persisted is not None + assert persisted.status == "succeeded" diff --git a/tests/test_public_api_v03.py b/tests/test_public_api_v03.py index fa75c90..707303f 100644 --- a/tests/test_public_api_v03.py +++ b/tests/test_public_api_v03.py @@ -94,6 +94,51 @@ def test_v03_public_api_symbols_import_from_package_root() -> None: "validate_cited_sentences", "normalized_text_sha256", "validate_release_browser_evidence", + "BackupItem", + "BackupManifest", + "BackupRestoreAction", + "BackupRestorePlan", + "BackupValidationResult", + "LocalRuntimeProfile", + "LocalTask", + "LocalTaskEnvelope", + "LocalTaskResult", + "ModuleBackupHook", + "ModuleDependency", + "ModuleHealthCheck", + "ModuleManifest", + "ModuleMigration", + "ModuleModelRequirement", + "ModulePermission", + "ModuleRegistryEntry", + "ModuleRegistryState", + "ModuleRoute", + "ModuleRuntimeRequirement", + "ModuleService", + "PlatformHealthCheck", + "PlatformHealthStatus", + "PlatformHealthSummary", + "RuntimeActionResult", + "TaskHandler", + "TaskQueueSummary", + "TaskRetryPolicy", + "WINDOWS_LOCAL_BLOCKED_RUNTIME_KINDS", + "build_backup_manifest", + "build_module_registry", + "can_run_task", + "claim_next_local_task", + "complete_local_task", + "enqueue_local_task", + "fail_local_task", + "next_retry_at", + "plan_restore", + "record_task_attempt", + "run_one_local_task", + "summarize_platform_health", + "summarize_task_queue", + "task_row_to_envelope", + "validate_backup_manifest", + "validate_windows_local_manifest", } missing = expected - set(dir(civiccore)) diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 503a811..ec36e3f 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -28,6 +28,10 @@ def test_import_civiccore() -> None: assert civiccore.build_deadline_plan assert civiccore.evaluate_notice_compliance assert civiccore.encrypt_json + assert civiccore.LocalTask + assert civiccore.enqueue_local_task + assert civiccore.claim_next_local_task + assert civiccore.run_one_local_task assert civiccore.validate_url_host assert civiccore.normalize_trusted_proxy_cidrs assert civiccore.is_trusted_proxy_ip