213 Commits
Author SHA1 Message Date
zemion a24c94435e Release v0.1.15
Dependency Audit / dependency-audit (push) Failing after 1m49s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m50s
Developer Meta-package Release / publish-package (push) Failing after 4s
2026-08-04 15:20:50 +02:00
zemion 774793976c Support legacy module version declarations
Deployment Installer / deployment-installer (push) Successful in 6s
Dependency Audit / dependency-audit (push) Failing after 1m42s
Security Audit / security-audit (push) Successful in 10m43s
2026-08-04 15:08:46 +02:00
zemion 492449a4e2 Align all release version declarations
Dependency Audit / dependency-audit (push) Failing after 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 10m42s
2026-08-04 15:06:36 +02:00
zemion 8e890b37ed Validate candidate migration baseline during release
Dependency Audit / dependency-audit (push) Failing after 1m42s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m38s
2026-08-04 15:01:50 +02:00
zemion 077735bc24 Bind migration heads to coordinated releases
Deployment Installer / deployment-installer (push) Successful in 6s
Dependency Audit / dependency-audit (push) Failing after 1m44s
Security Audit / security-audit (push) Successful in 10m36s
2026-08-04 14:55:58 +02:00
zemion d9522d3cc4 Publish release tags in dependency order
Dependency Audit / dependency-audit (push) Failing after 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m53s
2026-08-04 14:54:54 +02:00
zemion bad0ea37a7 Automate exact package-set publication
Dependency Audit / dependency-audit (push) Failing after 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m46s
2026-08-04 14:32:06 +02:00
zemion 9ffd46fe22 Make package publication retries hash-safe
Dependency Audit / dependency-audit (push) Failing after 1m48s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m35s
2026-08-04 14:18:49 +02:00
zemion be51a9c347 Document production target evidence handoff
Dependency Audit / dependency-audit (push) Failing after 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m40s
2026-08-04 14:00:45 +02:00
zemion e36a6573bf Harden package release workflows for Gitea 2026-08-04 14:00:32 +02:00
zemion 629bfec1f1 Cover datasource publication change events
Dependency Audit / dependency-audit (push) Failing after 1m40s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m48s
2026-08-04 12:05:48 +02:00
zemion 9e956eec6f Classify first-run bootstrap endpoints
Dependency Audit / dependency-audit (push) Failing after 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m52s
2026-08-04 10:07:24 +02:00
zemion 9bb2c808a6 Enforce declared platform interface inventory
Dependency Audit / dependency-audit (push) Failing after 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m2s
2026-08-04 05:20:47 +02:00
zemion f7590a7b8b Add registry-backed module package releases
Dependency Audit / dependency-audit (push) Failing after 1m37s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m21s
2026-08-04 04:14:28 +02:00
zemion 1a68565ba0 Reconcile encryption interface surfaces
Dependency Audit / dependency-audit (push) Failing after 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m33s
2026-08-04 01:28:03 +02:00
zemion d9f67b5c26 Reconcile implemented platform interface surfaces
Dependency Audit / dependency-audit (push) Failing after 1m39s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m32s
2026-08-04 01:04:40 +02:00
zemion 1cfdaec250 inventory: mark campaign archive UI reachable
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m54s
2026-08-03 20:39:16 +02:00
zemion 62501d399a ci: enforce endpoint inventory independently
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m43s
2026-08-03 20:29:14 +02:00
zemion ce5528e3b8 Record first verified runtime distribution 2026-08-03 20:17:29 +02:00
zemion 1f039dd39c Retain Caddy file capability at ingress boundary 2026-08-03 20:02:29 +02:00
zemion d107d94fec Add standalone managed ingress diagnostics 2026-08-03 19:59:12 +02:00
zemion 6163c5992f Fix ingress probe image selection 2026-08-03 19:49:35 +02:00
zemion 2f28f22fd1 Probe managed ingress across Docker namespaces 2026-08-03 19:41:04 +02:00
zemion 909862afdb Fix managed ingress loopback publication 2026-08-03 19:28:10 +02:00
zemion eb04804d36 Handle Redis under arm64 CI emulation 2026-08-03 19:18:01 +02:00
zemion 017aa7a702 Enable arm64 runtime smoke execution 2026-08-03 19:11:45 +02:00
zemion af27b9fbdf Resolve runtime dependency platform digests 2026-08-03 19:04:06 +02:00
zemion cb45251c59 Fix read-only Web runtime publication 2026-08-03 18:56:23 +02:00
zemion 3b3d5b3386 Redact runtime smoke diagnostics
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 7s
2026-08-03 18:36:00 +02:00
zemion 313249b8fc Exercise packaged runtime topology
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
2026-08-03 18:26:04 +02:00
zemion 282c90c54b Make ingress drill Docker socket portable
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m56s
2026-08-03 17:55:48 +02:00
zemion 25424187a8 Package runtime migration scripts correctly
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m1s
2026-08-03 17:54:09 +02:00
zemion ff8ee991c3 Harden runtime distribution acceptance
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m49s
2026-08-03 17:32:52 +02:00
zemion a5a0731d20 Fix runtime distribution signing environment
Dependency Audit / dependency-audit (push) Successful in 1m48s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m50s
2026-08-03 17:09:50 +02:00
zemion a0f161041d Record Risk Compliance interface migration
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m42s
2026-08-03 15:47:41 +02:00
zemion cb85999a14 Record Notifications interface migration
Dependency Audit / dependency-audit (push) Successful in 1m52s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m42s
2026-08-03 15:34:18 +02:00
zemion cbfe8b03a7 Record Ops interface migration
Dependency Audit / dependency-audit (push) Successful in 1m57s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m5s
2026-08-03 15:19:07 +02:00
zemion 768e9a51c9 Record Calendar interface migration
Dependency Audit / dependency-audit (push) Successful in 1m59s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m5s
2026-08-03 15:09:01 +02:00
zemion 087561ee12 Record Cases interface migration
Dependency Audit / dependency-audit (push) Successful in 1m52s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 11m2s
2026-08-03 14:44:14 +02:00
zemion 11c1aa1815 Record Dashboard interface migration
Dependency Audit / dependency-audit (push) Successful in 1m47s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m49s
2026-08-03 14:25:45 +02:00
zemion 478ecb5d0e Record Dataflow interface migration
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m40s
2026-08-03 14:20:21 +02:00
zemion 32689d027a Repair missing packages during environment sync
Dependency Audit / dependency-audit (push) Successful in 1m50s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m50s
2026-08-03 14:06:41 +02:00
zemion b7cc2d2df4 Record Datasources interface migration
Dependency Audit / dependency-audit (push) Successful in 1m47s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m43s
2026-08-03 13:47:26 +02:00
zemion b70869e747 Record Addresses interface migration
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m43s
2026-08-03 13:41:04 +02:00
zemion 0c84afb158 Record Templates interface migration
Dependency Audit / dependency-audit (push) Successful in 1m47s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m56s
2026-08-03 13:35:11 +02:00
zemion 145aa58c11 Record Distribution Lists interface migration
Dependency Audit / dependency-audit (push) Successful in 1m58s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m58s
2026-08-03 13:28:33 +02:00
zemion a3566c9311 Record Voting interface migration
Dependency Audit / dependency-audit (push) Successful in 2m4s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m49s
2026-08-03 13:17:08 +02:00
zemion 3219460064 Record Forms interface migration
Dependency Audit / dependency-audit (push) Successful in 2m3s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m0s
2026-08-03 13:09:24 +02:00
zemion 4b2a15adb5 Record Forms Runtime interface migration
Dependency Audit / dependency-audit (push) Successful in 2m1s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Successful in 10m49s
2026-08-03 13:02:51 +02:00
zemion acc5ffc247 Record Approvals interface migration
Dependency Audit / dependency-audit (push) Successful in 2m0s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m0s
2026-08-03 12:56:07 +02:00
zemion f4f9836a09 Record Committee interface migration
Dependency Audit / dependency-audit (push) Successful in 1m57s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m53s
2026-08-03 12:49:58 +02:00
zemion 758fa1bba7 Record IDM interface migration
Dependency Audit / dependency-audit (push) Successful in 1m55s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m54s
2026-08-03 12:37:12 +02:00
zemion 0acc8cfc31 Record Postbox interface migration
Dependency Audit / dependency-audit (push) Successful in 1m51s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m52s
2026-08-03 12:26:52 +02:00
zemion 344bcaf1bc Record Organizations interface migration
Dependency Audit / dependency-audit (push) Successful in 1m47s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m59s
2026-08-03 12:10:13 +02:00
zemion 794622e4ed Record Views interface migration
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Successful in 10m42s
2026-08-03 11:58:24 +02:00
zemion 7653e9851f Record Tenancy interface migration
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m56s
2026-08-03 11:43:28 +02:00
zemion 6f896d9c04 Record Admin interface migration
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m48s
2026-08-03 11:30:45 +02:00
zemion 8f5ac52b58 Record Access interface migration
Dependency Audit / dependency-audit (push) Successful in 1m42s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m47s
2026-08-03 11:01:57 +02:00
zemion 2bc9ad7f00 Record completed Audit interface migration
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m51s
2026-08-03 10:43:57 +02:00
zemion fa1a4bacfb Record completed Scheduling interface migration
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m43s
2026-08-03 10:36:39 +02:00
zemion 0c0669768d Record Policy interface migration
Dependency Audit / dependency-audit (push) Successful in 1m42s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m45s
2026-08-03 10:23:22 +02:00
zemion 7b6ceeb185 Record completed Core configuration patterns
Dependency Audit / dependency-audit (push) Successful in 1m47s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m37s
2026-08-03 10:17:18 +02:00
zemion ff12f676a1 Record Mail interface pattern migration
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m8s
2026-08-03 10:07:19 +02:00
zemion eb9ab9ef1c Record Files interface pattern migration
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m49s
2026-08-03 09:51:01 +02:00
zemion 935c1fe162 Track module interface migration work
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m19s
2026-08-03 08:31:34 +02:00
zemion c7d1cd0e8f Exercise recovery in datasource composition check
Dependency Audit / dependency-audit (push) Successful in 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m14s
2026-08-03 07:46:57 +02:00
zemion ac80d7e4e3 Record Campaign review pattern evidence
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m18s
2026-08-03 07:24:23 +02:00
zemion 5e449b0983 Record Core lifecycle recovery adoption
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m41s
2026-08-03 07:02:34 +02:00
zemion d4bf07b446 Record workflow recovery ledger adoption
Dependency Audit / dependency-audit (push) Successful in 1m47s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m35s
2026-08-03 06:37:45 +02:00
zemion adc4db9fdf Record Dataflow recovery adoption
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m16s
2026-08-03 06:09:53 +02:00
zemion 484f2af3ac Record Connectors recovery adoption
Dependency Audit / dependency-audit (push) Successful in 1m40s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m11s
2026-08-03 05:43:58 +02:00
zemion abf9564cee Record Mail recovery adoption
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m13s
2026-08-03 05:00:46 +02:00
zemion 5bef966119 Record Files recovery ledger adoption
Dependency Audit / dependency-audit (push) Successful in 2m0s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m13s
2026-08-03 04:22:28 +02:00
zemion 5e80b39bbd Record Campaign recovery ledger adoption
Dependency Audit / dependency-audit (push) Successful in 1m47s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m27s
2026-08-03 03:55:16 +02:00
zemion 9370f501a0 Inventory module recovery ledger adoption
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 9m54s
2026-08-03 03:02:53 +02:00
zemion e8f7e2c194 Repair release and interface CI gates
Dependency Audit / dependency-audit (push) Successful in 1m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m5s
2026-08-03 02:02:48 +02:00
zemion cbbe08d912 Enforce signed backup evidence before migrations 2026-08-03 02:01:13 +02:00
zemion 2c515f73c2 Implement supported ingress and TLS profiles
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m2s
2026-08-03 01:15:06 +02:00
zemion b40f1428fd Resolve backend inventory from checkout
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m2s
2026-08-03 01:12:41 +02:00
zemion 43380eb068 Add signed runtime distribution pipeline
Dependency Audit / dependency-audit (push) Successful in 1m39s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m3s
2026-08-03 00:54:06 +02:00
zemion 29acb55b7c Resolve inventory workspace from checkout
Dependency Audit / dependency-audit (push) Successful in 1m37s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m1s
2026-08-03 00:51:20 +02:00
zemion 4f08b52333 Allow tagged module build-tool peer drift
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m23s
2026-08-03 00:26:55 +02:00
zemion d3713bf2ee Automate worker runtime delivery drill
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Failing after 8s
2026-08-03 00:20:57 +02:00
zemion be4410ef1a Record Calendar outbox recovery surface
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 8s
2026-08-02 15:57:12 +02:00
zemion 29d07fe375 Integrate Templates into development profiles
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 8s
2026-08-02 12:38:55 +02:00
zemion d9003bf63a fix: authenticate private module bootstrap in CI
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 8s
2026-08-02 05:40:22 +02:00
zemion ed31409034 feat: add bounded Kubernetes runtime verification
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 8s
2026-08-02 05:30:26 +02:00
zemion 50e9607e72 feat: enforce backend endpoint classification 2026-08-02 05:30:20 +02:00
zemion f7a30682b3 refactor: reduce audited source duplication 2026-08-02 05:30:15 +02:00
zemion 2c56a0fc11 feat: enforce signed target maturity evidence
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 8s
2026-08-02 03:41:09 +02:00
zemion be8ba10ae3 Wire institutional governance modules and checks
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 7s
2026-08-01 20:57:27 +02:00
zemion d78b13f9d3 feat: implement institutional governance and recovery architecture
Dependency Audit / dependency-audit (push) Failing after 10s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 9s
2026-08-01 17:46:53 +02:00
zemion 3c658fa32d Harden worker queue and restore drill defaults
Dependency Audit / dependency-audit (push) Successful in 2m23s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m22s
2026-07-31 22:48:06 +02:00
zemion efd734fad2 build: integrate workflow engine repository
Dependency Audit / dependency-audit (push) Successful in 2m26s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m0s
2026-07-31 16:59:08 +02:00
zemion 186aa104ce Align release harness with optional modules [skip ci] 2026-07-31 06:37:56 +02:00
zemion ff9d2404ab Propagate CI safety settings into nested checks
Deployment Installer / deployment-installer (push) Successful in 7s
Dependency Audit / dependency-audit (push) Successful in 2m37s
Security Audit / security-audit (push) Successful in 10m59s
2026-07-31 06:15:56 +02:00
zemion ba82a85547 Repair Gitea Actions bootstrap and module matrix
Dependency Audit / dependency-audit (push) Successful in 2m28s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 10m36s
2026-07-31 05:57:33 +02:00
zemion f1fd143ef5 Complete guided release console workflow
Dependency Audit / dependency-audit (push) Successful in 2m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 15s
2026-07-31 05:46:50 +02:00
zemion b4248a849e chore: register tenancy WebUI in release catalog
Dependency Audit / dependency-audit (push) Successful in 2m27s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 14s
2026-07-31 04:21:34 +02:00
zemion ff47659899 Provision Python for security audit workflow
Dependency Audit / dependency-audit (push) Failing after 7s
Security Audit / security-audit (push) Failing after 6s
Deployment Installer / deployment-installer (push) Failing after 4s
2026-07-31 02:53:37 +02:00
zemion 908090dd0f Add scalable deployment planning and guided releases
Security Audit / security-audit (push) Failing after 4s
Dependency Audit / dependency-audit (push) Failing after 7s
Deployment Installer / deployment-installer (push) Failing after 4s
2026-07-31 02:49:03 +02:00
zemion 3864ce28b1 Delete .gitea/workflows/runner_test.yml
Dependency Audit / dependency-audit (push) Failing after 6s
Deployment Installer / deployment-installer (push) Failing after 4s
Security Audit / security-audit (push) Failing after 4s
2026-07-31 02:24:30 +02:00
zemion 857dbe55f7 Add .gitea/workflows/runner_test.yml
Dependency Audit / dependency-audit (push) Failing after 6s
Deployment Installer / deployment-installer (push) Failing after 4s
Security Audit / security-audit (push) Failing after 5s
2026-07-31 02:15:18 +02:00
zemion c9fcdc90c1 feat: add platform interface inventory and encryption registry
Dependency Audit / dependency-audit (push) Failing after 24s
Deployment Installer / deployment-installer (push) Failing after 4s
Security Audit / security-audit (push) Failing after 5s
2026-07-30 17:42:11 +02:00
zemion 82e836b720 feat(deploy): add declarative installation workflow
Deployment Installer / deployment-installer (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
Dependency Audit / dependency-audit (push) Has been cancelled
2026-07-30 15:36:37 +02:00
zemion fcb8296812 test: dispatch queued dataflow composition runs
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-30 02:33:16 +02:00
zemion f2e2eb5517 ci: enforce complete security audit coverage
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-30 01:40:36 +02:00
zemion 1aea3e7c4f fix: repair dev modules and scoped release git trust
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 20:54:58 +02:00
zemion 3f9567af18 Gate Search on PostgreSQL in module CI
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 20:37:08 +02:00
zemion de16f11ce8 test(datasources): include immutable payload schema
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 20:13:49 +02:00
zemion 9d6cdff4b8 CSS changes for release console
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 19:35:49 +02:00
zemion aa4050c0ca feat: expose focused backend reload mode
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 18:52:55 +02:00
zemion d3cdbd8c7a feat: include risk compliance in dev composition
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 18:46:53 +02:00
zemion 7115c4711d chore: register search wiki projects and tickets
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 15:50:35 +02:00
zemion a3beca6fc5 chore: harden audit checks and issue taxonomy
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-29 14:16:27 +02:00
zemion 11d45bce25 Register governed Views module
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-28 21:04:55 +02:00
zemion 7b6135b89b Document governed Views architecture
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-28 19:05:37 +02:00
zemion 5b79e7d377 test: verify pinned dataflow publication composition
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-28 13:48:11 +02:00
zemion 6afb8fea76 fix: exclude generated artifacts from wiki sync
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-28 12:52:46 +02:00
zemion 163b35c0af feat: integrate datasource and workflow modules
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-28 12:45:05 +02:00
zemion 603e07cec5 Integrate Connectors and Dataflow validation
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-28 11:14:38 +02:00
zemion 97dfd333c6 Register Dataflow module and work taxonomy
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-28 01:34:06 +02:00
zemion ba88c574b9 chore: move repositories to GovOPlaN organization
Dependency Audit / dependency-audit (push) Has been cancelled
Security Audit / security-audit (push) Has been cancelled
2026-07-27 15:46:51 +02:00
zemion 8d292184d4 ci: execute tagged module pytest suites
Security Audit / security-audit (push) Successful in 4m3s
Security Audit Toolbox Update / toolbox-update (push) Successful in 4s
Dependency Audit / dependency-audit (push) Successful in 1m51s
2026-07-23 01:18:47 +02:00
zemion 52bd3527cd ci: install release test harness dependencies
Dependency Audit / dependency-audit (push) Successful in 1m37s
Security Audit / security-audit (push) Has been cancelled
2026-07-23 01:15:07 +02:00
zemion ff49dabf8f ci: retain installed artifact checks in matrix
Dependency Audit / dependency-audit (push) Successful in 1m42s
Security Audit / security-audit (push) Successful in 4m7s
2026-07-23 00:57:24 +02:00
zemion cd15aa514e ci: run artifact-aware module matrix tests
Dependency Audit / dependency-audit (push) Successful in 1m36s
Security Audit / security-audit (push) Has been cancelled
2026-07-23 00:53:16 +02:00
zemion a042baa1d3 fix(release): verify tags in mounted workspaces 2026-07-23 00:53:16 +02:00
zemion e80de00f29 chore(release): align Mail with Campaign contract
Dependency Audit / dependency-audit (push) Successful in 1m37s
Security Audit / security-audit (push) Has been cancelled
2026-07-23 00:47:46 +02:00
zemion c69acf0dee ci: use Gitea-compatible artifact upload
Dependency Audit / dependency-audit (push) Successful in 1m39s
Security Audit / security-audit (push) Successful in 4m11s
2026-07-23 00:36:59 +02:00
zemion 8b93bbc6b6 ci: mount audit workspace safely
Dependency Audit / dependency-audit (push) Successful in 1m43s
Security Audit / security-audit (push) Failing after 4m10s
2026-07-23 00:30:28 +02:00
zemion 3fc17701df ci: fix action repository access
Dependency Audit / dependency-audit (push) Successful in 1m45s
Security Audit / security-audit (push) Failing after 3m33s
2026-07-23 00:10:29 +02:00
zemion 9788bdde0c ci: harden repository bootstrap transport 2026-07-23 00:09:43 +02:00
zemion a10a01c903 docs: surface system action status
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 23:45:42 +02:00
zemion e005e54333 Seal catalog publication Git metadata
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 22:31:58 +02:00
zemion 76e4baa76e Define the catalog publication identity
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 22:23:01 +02:00
zemion b6e9a9bd81 Pin the website publication toolchain
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 22:20:09 +02:00
zemion dc46bda224 Validate complete Core release bundle sources
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 22:10:35 +02:00
zemion b00d4e55ee Harden isolated release builds in Flatpak
Dependency Audit / dependency-audit (push) Failing after 16s
Security Audit / security-audit (push) Failing after 15s
2026-07-22 21:48:48 +02:00
zemion 76f19dc602 Lock down the local release console 2026-07-22 21:37:33 +02:00
zemion 5381e37a9e Execute receipt-bound durable release runs 2026-07-22 21:37:18 +02:00
zemion 7ecf1f17b0 Harden release source execution 2026-07-22 21:37:14 +02:00
zemion 349a099e5a Harden catalog publication transaction 2026-07-22 21:20:08 +02:00
zemion fdee766993 Authenticate selective catalog base pairs 2026-07-22 20:59:00 +02:00
zemion 47b9c05bde chore(release): align Campaign release dependency 2026-07-22 20:44:43 +02:00
zemion 4e42911477 Document artifact origin and installer receipts 2026-07-22 20:42:35 +02:00
zemion 9c0650b2ed Bind installed releases to signed artifact receipts 2026-07-22 20:42:05 +02:00
zemion 4dc0c8d013 Harden release artifact publication 2026-07-22 20:41:42 +02:00
zemion 35c346a1fa Add private release candidate handles 2026-07-22 20:41:36 +02:00
zemion 4edbc11fe5 feat(release): bound durable run retention 2026-07-22 20:16:19 +02:00
zemion ddee5c00dc fix(assessment): keep release origin unproven
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 14s
2026-07-22 19:55:35 +02:00
zemion 4c16069f88 fix(release): preserve durable recovery capacity 2026-07-22 19:48:55 +02:00
zemion 22f5c2ff4e fix(release): make durable runs recovery safe 2026-07-22 19:30:12 +02:00
zemion 19c4b63ade fix(assessment): harden capability evidence trust 2026-07-22 19:21:06 +02:00
zemion 1dc9148ec3 fix(release): harden durable run recovery 2026-07-22 18:58:54 +02:00
zemion ead697d049 fix(assessment): constrain installed record reads 2026-07-22 18:36:12 +02:00
zemion 65aa8e0b7c docs(assessment): define external proof boundaries 2026-07-22 18:34:06 +02:00
zemion 6c0b003dee feat(assessment): verify installed composition evidence 2026-07-22 18:34:02 +02:00
zemion d0dc916837 feat(release): persist resumable run state 2026-07-22 18:32:19 +02:00
zemion bd715a8473 fix(assessment): reject ambiguous trust keyrings
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 17:17:34 +02:00
zemion ad8dd4f319 fix(assessment): establish independent release trust
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 17:09:35 +02:00
zemion 753dd2a3ee fix(release): fail closed on dashboard parse errors 2026-07-22 17:07:30 +02:00
zemion b6bef410d6 fix(release): fail closed on malformed WebUI metadata 2026-07-22 16:55:06 +02:00
zemion 72f697c341 fix(release): harden review gate diagnostics 2026-07-22 16:52:27 +02:00
zemion 36f662c56f test(release): isolate manifest gate remote 2026-07-22 16:34:39 +02:00
zemion 25fbbaf5ad feat(assessment): gate signed release drift 2026-07-22 16:31:28 +02:00
zemion bcab7095c3 feat(release): guide selective gate remediation 2026-07-22 16:28:13 +02:00
zemion 99534c0251 fix(release): align Campaign package refs
Dependency Audit / dependency-audit (push) Failing after 18s
Security Audit / security-audit (push) Failing after 15s
2026-07-22 15:30:43 +02:00
zemion 71e74601cf test(release): require retirement atomicity proof 2026-07-22 15:30:16 +02:00
zemion c29f1e9977 docs: refresh capability release baseline 2026-07-22 15:21:27 +02:00
zemion 5447299289 Block mismatched selective WebUI releases
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-22 10:37:19 +02:00
zemion 568ea6059a docs(ui): record completed Campaign pilot evidence
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 14s
2026-07-22 09:40:32 +02:00
zemion 8b6bcb7a4d docs(ui): record delivered Campaign pilot slices 2026-07-22 08:49:57 +02:00
zemion 2fb0a71bfb docs(ui): reconcile Campaign pilot rollout 2026-07-22 08:09:06 +02:00
zemion 8ecf8770ab feat(release): synthesize initial catalog entries
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 12s
2026-07-22 04:56:36 +02:00
zemion d53db2da06 fix(audit): bridge Docker from Flatpak sandboxes 2026-07-22 04:48:10 +02:00
zemion 8906704dc0 feat(release): gate tags on scoped workflows
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 12s
2026-07-22 01:56:18 +02:00
zemion 30f9ed152a feat(release): publish verified source tags
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 12s
2026-07-22 01:17:07 +02:00
zemion eca75cbc93 fix(dev): resolve stale editable packages together 2026-07-22 00:58:16 +02:00
zemion a8abb85676 docs(roadmap): refine postbox and university BI paths
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 12s
2026-07-21 20:48:55 +02:00
zemion 097f9ca2f4 docs(ui): codify universal interaction patterns 2026-07-21 20:48:55 +02:00
zemion 89771d96fb docs: define connector secret retirement
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-21 16:00:29 +02:00
zemion 492fec46b6 docs: separate installer artifact trust boundary 2026-07-21 15:55:30 +02:00
zemion a863767233 docs: select connected reference journey program
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-21 15:41:54 +02:00
zemion 99e255cf81 fix(release): separate artifact and source contracts
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s
2026-07-21 13:24:37 +02:00
zemion d9819c4aad test(release): validate installed module artifacts 2026-07-21 13:24:02 +02:00
zemion 78b601a2e5 fix(release): reject unpackageable tagged artifacts 2026-07-21 12:58:15 +02:00
zemion a18c7ea040 fix(release): validate independent module versions 2026-07-21 12:51:25 +02:00
zemion b193d4555f fix(audit): accept empty OSV source reports 2026-07-21 12:51:19 +02:00
zemion a81dc21e90 chore: clean release tooling imports 2026-07-21 12:39:36 +02:00
zemion 676030b993 Harden security audit result contracts 2026-07-21 12:29:53 +02:00
zemion a15a74c54c feat(release): enforce aligned trusted publications 2026-07-21 12:25:06 +02:00
zemion c34892f6ea feat(release): generate reproducible CycloneDX SBOM 2026-07-21 12:24:49 +02:00
zemion 397c87a012 docs: define system administrator lifecycle story 2026-07-21 12:24:32 +02:00
zemion f1b07354a0 Surface connector deployment allowlists 2026-07-21 12:10:44 +02:00
zemion 17f8036bdc Harden release console token bootstrap
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 13s
2026-07-21 03:16:57 +02:00
zemion fcc5885dcf Validate manifest module imports 2026-07-21 03:16:57 +02:00
zemion 0b1506f860 Cover live worktrees in security audits 2026-07-21 03:16:57 +02:00
zemion 295b49bb04 docs: add connected governance platform roadmap
Dependency Audit / dependency-audit (push) Failing after 13s
Security Audit / security-audit (push) Failing after 13s
2026-07-20 20:48:41 +02:00
zemion 65cc099839 chore(meta): register current workspace modules
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 12s
2026-07-20 20:01:35 +02:00
zemion 1ce3d02239 fix(dev): make browser launch opt in 2026-07-20 20:01:27 +02:00
zemion 9805d085fa test(release): keep release tooling coverage with its owner 2026-07-20 20:01:19 +02:00
zemion 2e21397868 chore(checks): discover workspace module boundaries 2026-07-20 20:01:14 +02:00
zemion 4b217c7aba docs(ui): require central component reuse 2026-07-20 17:56:14 +02:00
zemion 8b80afbd60 feat(calendar): run scheduled outbox recovery
Dependency Audit / dependency-audit (push) Failing after 18s
Security Audit / security-audit (push) Failing after 13s
2026-07-20 17:05:17 +02:00
zemion 8255665349 chore(gitea): add the user-story issue type 2026-07-20 17:03:23 +02:00
zemion 2fc3b66c07 docs: add capability and infrastructure fit assessment 2026-07-20 16:50:06 +02:00
zemion 9e7103f613 docs: define the interface pattern language 2026-07-20 16:49:30 +02:00
zemion 05ae81d641 intermediate commit
Security Audit / security-audit (push) Failing after 11s
Security Audit Toolbox Update / toolbox-update (push) Failing after 4s
Dependency Audit / dependency-audit (push) Failing after 17s
2026-07-14 13:32:09 +02:00
zemion dd796b4d3c Register poll and evaluation repositories
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 12s
2026-07-12 09:23:18 +02:00
zemion 27ea3c82d4 Reuse Gitea API connections in maintenance scripts
Dependency Audit / dependency-audit (push) Failing after 25s
Security Audit / security-audit (push) Failing after 22s
2026-07-12 00:18:40 +02:00
zemion 342582645b Harden audit tooling and release helpers
Dependency Audit / dependency-audit (push) Failing after 1m14s
Security Audit / security-audit (push) Failing after 31s
2026-07-11 18:49:36 +02:00
zemion 54ab1945ff Update release refs for v0.1.8 2026-07-11 17:22:53 +02:00
238 changed files with 76986 additions and 556 deletions
+30 -2
View File
@@ -2,19 +2,47 @@
# Copy to a deployment-local .env or secret store. Do not commit populated secrets.
APP_ENV=production
# Live graph changes are useful in development. Production should apply saved
# module state through a coordinated restart of all API and worker processes.
GOVOPLAN_MODULE_LIVE_APPLY_ENABLED=
GOVOPLAN_INSTALL_PROFILE=self-hosted
MASTER_KEY_B64=<generate-with-govoplan-config-env-template-generate-secrets>
DATABASE_URL=postgresql+psycopg://govoplan:change-me@127.0.0.1:5432/govoplan
GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:change-me@127.0.0.1:5432/govoplan
GOVOPLAN_DB_POOL_SIZE=5
GOVOPLAN_DB_MAX_OVERFLOW=10
GOVOPLAN_DB_POOL_TIMEOUT_SECONDS=30
GOVOPLAN_DB_POOL_RECYCLE_SECONDS=1800
ENABLED_MODULES=tenancy,organizations,identity,access,admin,dashboard,policy,audit,files,mail,campaigns,calendar,docs,ops
ENABLED_MODULES=tenancy,organizations,identity,idm,access,admin,dashboard,policy,audit,files,templates,mail,campaigns,calendar,poll,scheduling,connectors,datasources,dataflow,dist_lists,workflow_engine,workflow,views,search,risk_compliance,postbox,notifications,services,parties,mandates,decisions,portal,cases,committee,docs,ops
CELERY_ENABLED=true
REDIS_URL=redis://127.0.0.1:6379/0
CELERY_QUEUES=send_email,append_sent,default
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false
GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES=16777216
GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES=536870912
GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST=
GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST=
GOVOPLAN_HTTP_MAX_REQUEST_BODY_BYTES=536870912
GOVOPLAN_HTTP_HSTS_SECONDS=31536000
AUTH_LOGIN_THROTTLE_ENABLED=true
AUTH_ACTIVITY_TOUCH_INTERVAL_SECONDS=300
AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10
AUTH_LOGIN_THROTTLE_CLIENT_LIMIT=100
AUTH_LOGIN_THROTTLE_WINDOW_SECONDS=900
AUTH_LOGIN_THROTTLE_REDIS_RETRY_SECONDS=30
# Production startup fails without Redis unless this explicit single-process
# risk acknowledgement is enabled.
GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE=false
CORS_ORIGINS=https://govoplan.example.org
GOVOPLAN_TRUSTED_HOSTS=govoplan.example.org
FORWARDED_ALLOW_IPS=127.0.0.1
AUTH_COOKIE_SECURE=true
AUTH_COOKIE_SAMESITE=lax
AUTH_COOKIE_DOMAIN=
+7 -16
View File
@@ -1,5 +1,7 @@
name: Dependency Audit
permissions: read-all
on:
pull_request:
push:
@@ -21,29 +23,18 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Configure SSH for release dependencies
env:
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
- name: Use HTTPS for GovOPlaN repositories
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh release dependencies."
exit 1
fi
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
- name: Bootstrap GovOPlaN repositories
working-directory: govoplan
run: python tools/repo/bootstrap-repositories.py --parent ..
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Install backend dev audit dependencies
working-directory: govoplan
run: |
python -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -r requirements-release.txt
.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip
.venv/bin/python -m pip install 'pip-audit>=2.9,<3'
- name: Install WebUI release dependencies
working-directory: govoplan
+30
View File
@@ -0,0 +1,30 @@
name: Deployment Installer
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
deployment-installer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
path: govoplan
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Compile deployment tooling
working-directory: govoplan
run: python -m py_compile tools/deployment/govoplan-deploy.py tools/deployment/govoplan_deploy/*.py
- name: Test declarative deployment bundle
working-directory: govoplan
run: python -m unittest -v tests.test_deployment_installer
- name: Build single-file deployer artifact
working-directory: govoplan
run: |
python tools/deployment/build-deployer-zipapp.py --output /tmp/govoplan-deploy.pyz
python /tmp/govoplan-deploy.pyz --help
+52 -16
View File
@@ -1,5 +1,7 @@
name: Module Matrix
permissions: read-all
on:
workflow_dispatch:
pull_request:
@@ -7,6 +9,25 @@ on:
jobs:
module-matrix:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: govoplan_test
POSTGRES_USER: govoplan
POSTGRES_PASSWORD: govoplan_test
options: >-
--health-cmd "pg_isready -U govoplan -d govoplan_test"
--health-interval 5s
--health-timeout 5s
--health-retries 20
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
@@ -17,33 +38,48 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Configure SSH for release dependencies
env:
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
- name: Use HTTPS for GovOPlaN repositories
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh release dependencies."
exit 1
fi
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
- name: Bootstrap GovOPlaN repositories
working-directory: govoplan
run: python tools/repo/bootstrap-repositories.py --parent ..
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Install backend release dependencies
working-directory: govoplan
run: |
python -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -r requirements-release.txt
.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip
.venv/bin/python -m pip install '../govoplan-core[dev]'
.venv/bin/python -m pip install --no-deps ../govoplan-search
- name: Install WebUI release dependencies with test scripts
working-directory: govoplan
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
- name: Validate platform interface and endpoint declarations
working-directory: govoplan
run: .venv/bin/python tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
- name: Validate Search against PostgreSQL
working-directory: govoplan
env:
GOVOPLAN_SEARCH_POSTGRES_URL: postgresql+psycopg://govoplan:govoplan_test@postgres:5432/govoplan_test
run: |
GOVOPLAN_CORE_ROOT="$PWD/../govoplan-core" .venv/bin/python tools/checks/postgres-integration-check.py \
--database-url "$GOVOPLAN_SEARCH_POSTGRES_URL" \
--module-set search=tenancy,access,search \
--reset-schema \
--skip-retirement-atomicity
PYTHONPATH="$PWD/../govoplan-search/src:$PWD/../govoplan-core/src" \
.venv/bin/python -m unittest discover \
-s ../govoplan-search/tests \
-p test_postgres_search.py \
-v
- name: Prove worker delivery and shutdown guarantees
working-directory: govoplan
env:
GOVOPLAN_WORKER_DRILL_REDIS_URL: redis://redis:6379/15
run: |
.venv/bin/python tools/checks/worker-runtime-drill.py \
--output audit-reports/worker-runtime.json
- name: Run module matrix and contract tests
working-directory: govoplan
run: GOVOPLAN_CORE_ROOT="$PWD/../govoplan-core" PYTHON="$PWD/.venv/bin/python" bash tools/checks/check-module-matrix.sh
@@ -0,0 +1,49 @@
name: Developer Meta-package Release
on:
push:
tags:
- "v*"
jobs:
publish-package:
runs-on: ubuntu-latest
env:
GITEA_REPOSITORY: ${{ gitea.repository }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Validate protected release tag and package version
run: |
python - <<'PY'
import os
from pathlib import Path
import subprocess
import tomllib
tag = os.environ["GITEA_REF_NAME"]
project = tomllib.loads(Path("packages/govoplan-meta/pyproject.toml").read_text(encoding="utf-8"))["project"]
if tag != f"v{project['version']}":
raise SystemExit("meta-package version does not match the release tag")
if subprocess.run(["git", "merge-base", "--is-ancestor", "HEAD", "origin/main"]).returncode:
raise SystemExit("release tag is not contained in main")
PY
- name: Build and publish developer package
env:
PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_USERNAME"
test -n "$PACKAGE_TOKEN"
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
python -m build --wheel --outdir dist packages/govoplan-meta
python -m twine check dist/*.whl
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
python -m twine upload --non-interactive \
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
dist/*.whl
+14 -17
View File
@@ -1,5 +1,7 @@
name: Release Integration
permissions: read-all
on:
workflow_dispatch:
@@ -16,30 +18,25 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Configure SSH for release dependencies
env:
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
- name: Use HTTPS for GovOPlaN repositories
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh release dependencies."
exit 1
fi
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
- name: Bootstrap GovOPlaN repositories
working-directory: govoplan
run: python tools/repo/bootstrap-repositories.py --parent ..
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Validate package publication contracts
working-directory: govoplan
run: |
python tools/repo/sync-module-package-workflows.py --check
python tools/release/generate-developer-meta-package.py --check
python -m unittest tests.test_module_package_workflows tests.test_package_registry_release
- name: Install backend release integration dependencies
working-directory: govoplan
run: |
python -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -r requirements-release.txt
.venv/bin/python -m pip install '../govoplan-core[dev]'
.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip
.venv/bin/python -m pip install -r requirements-release-tests.txt '../govoplan-core[dev]'
- name: Install WebUI release dependencies
working-directory: govoplan
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
+388
View File
@@ -0,0 +1,388 @@
name: Runtime Distribution
on:
workflow_dispatch:
inputs:
version:
description: Release version without leading v
required: true
type: string
python_image:
description: Digest-pinned multi-architecture Python 3.12 slim image
required: true
type: string
nginx_image:
description: Digest-pinned multi-architecture nginx-unprivileged image
required: true
type: string
postgres_image:
description: Digest-pinned PostgreSQL image
required: true
type: string
redis_image:
description: Digest-pinned Redis image
required: true
type: string
load_balancer_image:
description: Digest-pinned HAProxy image
required: true
type: string
managed_ingress_image:
description: Digest-pinned Caddy image
required: true
type: string
garage_image:
description: Digest-pinned Garage image
required: true
type: string
test_mail_image:
description: Digest-pinned GreenMail image
required: true
type: string
binfmt_image:
description: Digest-pinned tonistiigi/binfmt image for arm64 CI execution
required: true
type: string
jobs:
publish-runtime:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
path: govoplan
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Validate immutable release inputs
env:
VERSION: ${{ inputs.version }}
PYTHON_IMAGE: ${{ inputs.python_image }}
NGINX_IMAGE: ${{ inputs.nginx_image }}
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
REDIS_IMAGE: ${{ inputs.redis_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
GARAGE_IMAGE: ${{ inputs.garage_image }}
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
BINFMT_IMAGE: ${{ inputs.binfmt_image }}
run: |
python - <<'PY'
import os
import re
version = os.environ["VERSION"]
if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?", version) is None:
raise SystemExit("version must be a SemVer value without a leading v")
image_pattern = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$")
for name in (
"PYTHON_IMAGE",
"NGINX_IMAGE",
"POSTGRES_IMAGE",
"REDIS_IMAGE",
"LOAD_BALANCER_IMAGE",
"MANAGED_INGRESS_IMAGE",
"GARAGE_IMAGE",
"TEST_MAIL_IMAGE",
"BINFMT_IMAGE",
):
if image_pattern.fullmatch(os.environ[name]) is None:
raise SystemExit(f"{name} must be an exact sha256 image reference")
PY
- name: Use HTTPS for GovOPlaN repositories
run: |
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
- name: Bootstrap release sources
working-directory: govoplan
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Build release wheel roots and WebUI
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
GOVOPLAN_PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
GOVOPLAN_PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
python -m venv .runtime-build
.runtime-build/bin/python -m pip install --upgrade pip cryptography
.runtime-build/bin/python tools/release/generate-release-package-set.py \
--version "$VERSION" \
--output runtime-output/release-packages.json
.runtime-build/bin/python tools/release/resolve-package-artifacts.py \
--package-set runtime-output/release-packages.json \
--wheelhouse runtime-output/local-wheels \
--webui-packages runtime-output/webui-packages \
--lock-output runtime-output/package-artifacts.lock.json \
--requirements-output runtime-output/requirements-release.packages.txt \
--python .runtime-build/bin/python
PYTHON=.runtime-build/bin/python \
GOVOPLAN_WEBUI_PACKAGE_LOCK="$PWD/runtime-output/package-artifacts.lock.json" \
GOVOPLAN_WEBUI_PACKAGE_DIR="$PWD/runtime-output/webui-packages" \
bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
npm --prefix ../govoplan-core/webui run build
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
--wheelhouse runtime-output/local-wheels \
--web-dist ../govoplan-core/webui/dist \
--output runtime-output/common \
--required-module tenancy \
--required-module organizations \
--required-module identity \
--required-module idm \
--required-module access \
--required-module admin \
--required-module dashboard \
--required-module policy \
--required-module audit \
--required-module docs \
--required-module ops
- name: Resolve architecture-specific offline wheelhouses
working-directory: govoplan
run: |
mkdir -p runtime-output/wheels-amd64 runtime-output/wheels-arm64
cp runtime-output/local-wheels/*.whl runtime-output/wheels-amd64/
cp runtime-output/local-wheels/*.whl runtime-output/wheels-arm64/
.runtime-build/bin/python -m pip download --only-binary=:all: \
--platform manylinux_2_17_x86_64 --platform manylinux2014_x86_64 \
--implementation cp --python-version 3.12 --abi cp312 \
--find-links runtime-output/local-wheels \
--dest runtime-output/wheels-amd64 \
--requirement runtime-output/common/requirements-runtime.txt
.runtime-build/bin/python -m pip download --only-binary=:all: \
--platform manylinux_2_17_aarch64 --platform manylinux2014_aarch64 \
--implementation cp --python-version 3.12 --abi cp312 \
--find-links runtime-output/local-wheels \
--dest runtime-output/wheels-arm64 \
--requirement runtime-output/common/requirements-runtime.txt
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
--wheelhouse runtime-output/wheels-amd64 \
--web-dist ../govoplan-core/webui/dist \
--output runtime-output/context-amd64
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
--wheelhouse runtime-output/wheels-arm64 \
--web-dist ../govoplan-core/webui/dist \
--output runtime-output/context-arm64
cmp runtime-output/context-amd64/composition.json runtime-output/context-arm64/composition.json
- name: Build one-file deployer
working-directory: govoplan
run: python tools/deployment/build-deployer-zipapp.py --output runtime-output/govoplan-deploy.pyz
- name: Authenticate OCI publication
working-directory: govoplan
env:
REGISTRY_USERNAME: ${{ secrets.GOVOPLAN_REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.GOVOPLAN_REGISTRY_TOKEN }}
run: |
test -n "$REGISTRY_USERNAME"
test -n "$REGISTRY_TOKEN"
printf '%s' "$REGISTRY_TOKEN" | docker login git.add-ideas.de --username "$REGISTRY_USERNAME" --password-stdin
docker buildx create --name govoplan-runtime --use
- name: Build and publish architecture images
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
PYTHON_IMAGE: ${{ inputs.python_image }}
NGINX_IMAGE: ${{ inputs.nginx_image }}
run: |
COMPOSITION_SHA256="$(sha256sum runtime-output/context-amd64/composition.json | cut -d' ' -f1)"
for ARCH in amd64 arm64; do
docker buildx build --platform "linux/$ARCH" --push \
--file tools/release/runtime/Dockerfile.api \
--build-arg "PYTHON_IMAGE=$PYTHON_IMAGE" \
--build-arg "GOVOPLAN_RELEASE_VERSION=$VERSION" \
--build-arg "GOVOPLAN_COMPOSITION_SHA256=$COMPOSITION_SHA256" \
--tag "git.add-ideas.de/govoplan/runtime-api:$VERSION-$ARCH" \
"runtime-output/context-$ARCH"
docker buildx build --platform "linux/$ARCH" --push \
--file tools/release/runtime/Dockerfile.web \
--build-arg "NGINX_IMAGE=$NGINX_IMAGE" \
--build-arg "GOVOPLAN_RELEASE_VERSION=$VERSION" \
--build-arg "GOVOPLAN_COMPOSITION_SHA256=$COMPOSITION_SHA256" \
--tag "git.add-ideas.de/govoplan/runtime-web:$VERSION-$ARCH" \
"runtime-output/context-$ARCH"
done
docker buildx imagetools create \
--tag "git.add-ideas.de/govoplan/runtime-api:$VERSION" \
"git.add-ideas.de/govoplan/runtime-api:$VERSION-amd64" \
"git.add-ideas.de/govoplan/runtime-api:$VERSION-arm64"
docker buildx imagetools create \
--tag "git.add-ideas.de/govoplan/runtime-web:$VERSION" \
"git.add-ideas.de/govoplan/runtime-web:$VERSION-amd64" \
"git.add-ideas.de/govoplan/runtime-web:$VERSION-arm64"
docker buildx imagetools inspect "git.add-ideas.de/govoplan/runtime-api:$VERSION" --raw > runtime-output/api-index.json
docker buildx imagetools inspect "git.add-ideas.de/govoplan/runtime-web:$VERSION" --raw > runtime-output/web-index.json
API_DIGEST="sha256:$(sha256sum runtime-output/api-index.json | cut -d' ' -f1)"
WEB_DIGEST="sha256:$(sha256sum runtime-output/web-index.json | cut -d' ' -f1)"
python tools/release/resolve-oci-platforms.py --repository git.add-ideas.de/govoplan/runtime-api --index-digest "$API_DIGEST" --index runtime-output/api-index.json --output runtime-output/api-metadata.json
python tools/release/resolve-oci-platforms.py --repository git.add-ideas.de/govoplan/runtime-web --index-digest "$WEB_DIGEST" --index runtime-output/web-index.json --output runtime-output/web-metadata.json
- name: Resolve managed dependency platform images
working-directory: govoplan
env:
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
REDIS_IMAGE: ${{ inputs.redis_image }}
run: |
docker buildx imagetools inspect "$POSTGRES_IMAGE" --raw > runtime-output/postgres-index.json
docker buildx imagetools inspect "$REDIS_IMAGE" --raw > runtime-output/redis-index.json
python tools/release/resolve-oci-platforms.py \
--repository "${POSTGRES_IMAGE%@*}" \
--index-digest "${POSTGRES_IMAGE##*@}" \
--index runtime-output/postgres-index.json \
--output runtime-output/postgres-metadata.json
python tools/release/resolve-oci-platforms.py \
--repository "${REDIS_IMAGE%@*}" \
--index-digest "${REDIS_IMAGE##*@}" \
--index runtime-output/redis-index.json \
--output runtime-output/redis-metadata.json
- name: Register arm64 execution for runtime smoke
working-directory: govoplan
env:
BINFMT_IMAGE: ${{ inputs.binfmt_image }}
run: docker run --privileged --rm "$BINFMT_IMAGE" --install arm64
- name: Exercise amd64 and arm64 runtime images
working-directory: govoplan
run: |
for ARCH in amd64 arm64; do
.runtime-build/bin/python tools/checks/runtime-image-smoke.py \
--api-metadata runtime-output/api-metadata.json \
--web-metadata runtime-output/web-metadata.json \
--postgres-metadata runtime-output/postgres-metadata.json \
--redis-metadata runtime-output/redis-metadata.json \
--platform "linux/$ARCH" \
--output "runtime-output/evidence/runtime-smoke-$ARCH.json"
done
- name: Generate and sign distribution evidence
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ gitea.sha }}
SIGNING_KEY: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY }}
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
TRUSTED_KEYRING: ${{ secrets.RUNTIME_DISTRIBUTION_KEYRING }}
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
REDIS_IMAGE: ${{ inputs.redis_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
GARAGE_IMAGE: ${{ inputs.garage_image }}
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
run: |
test -n "$SIGNING_KEY"
test -n "$SIGNING_KEY_ID"
test -n "$TRUSTED_KEYRING"
printf '%s\n' "$SIGNING_KEY" > runtime-output/signing-key.pem
printf '%s\n' "$TRUSTED_KEYRING" > runtime-output/distribution-keyring.json
chmod 600 runtime-output/signing-key.pem
ARTIFACT_BASE="https://git.add-ideas.de/GovOPlaN/govoplan/releases/download/v$VERSION"
python tools/release/finalize-runtime-distribution.py \
--composition runtime-output/context-amd64/composition.json \
--api-metadata runtime-output/api-metadata.json \
--web-metadata runtime-output/web-metadata.json \
--deployer runtime-output/govoplan-deploy.pyz \
--deployer-url "$ARTIFACT_BASE/govoplan-deploy.pyz" \
--package-lock runtime-output/package-artifacts.lock.json \
--artifact-base-url "$ARTIFACT_BASE" \
--source-commit "$SOURCE_COMMIT" \
--version "$VERSION" \
--sequence "$(date -u +%Y%m%d%H%M)" \
--dependency "postgres=$POSTGRES_IMAGE" \
--dependency "redis=$REDIS_IMAGE" \
--dependency "load_balancer=$LOAD_BALANCER_IMAGE" \
--dependency "managed_ingress=$MANAGED_INGRESS_IMAGE" \
--dependency "garage=$GARAGE_IMAGE" \
--dependency "test_mail=$TEST_MAIL_IMAGE" \
--output-directory runtime-output/evidence \
--descriptor runtime-output/distribution-descriptor.json
.runtime-build/bin/python tools/release/generate-runtime-distribution.py \
--descriptor runtime-output/distribution-descriptor.json \
--signing-key "$SIGNING_KEY_ID=runtime-output/signing-key.pem" \
--output runtime-output/distribution-manifest.json
openssl pkeyutl -sign -inkey runtime-output/signing-key.pem -rawin \
-in runtime-output/govoplan-deploy.pyz \
-out runtime-output/govoplan-deploy.pyz.sig
(cd runtime-output && sha256sum govoplan-deploy.pyz > govoplan-deploy.pyz.sha256)
(cd runtime-output && sha256sum distribution-manifest.json > distribution-manifest.json.sha256)
rm runtime-output/signing-key.pem
- name: Verify the published bundle contract with the zipapp
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
run: |
(cd runtime-output && sha256sum --check govoplan-deploy.pyz.sha256)
(cd runtime-output && sha256sum --check distribution-manifest.json.sha256)
.runtime-build/bin/python - <<'PY'
import json
import os
from pathlib import Path
keyring = json.loads(
Path("runtime-output/distribution-keyring.json").read_text(encoding="utf-8")
)
key_id = os.environ["SIGNING_KEY_ID"]
matches = [item for item in keyring["keys"] if item.get("key_id") == key_id]
if len(matches) != 1 or matches[0].get("status") != "active":
raise SystemExit("runtime signing key is not uniquely active in the keyring")
Path("runtime-output/runtime-release-public.pem").write_text(
matches[0]["public_key_pem"], encoding="utf-8"
)
PY
openssl pkeyutl -verify -pubin \
-inkey runtime-output/runtime-release-public.pem -rawin \
-in runtime-output/govoplan-deploy.pyz \
-sigfile runtime-output/govoplan-deploy.pyz.sig
cp runtime-output/govoplan-deploy.pyz runtime-output/govoplan-deploy.tampered.pyz
printf '\0' >> runtime-output/govoplan-deploy.tampered.pyz
if openssl pkeyutl -verify -pubin \
-inkey runtime-output/runtime-release-public.pem -rawin \
-in runtime-output/govoplan-deploy.tampered.pyz \
-sigfile runtime-output/govoplan-deploy.pyz.sig >/dev/null 2>&1; then
echo "Tampered deployment bootstrap unexpectedly verified" >&2
exit 1
fi
MANIFEST_SHA256="$(cut -d' ' -f1 runtime-output/distribution-manifest.json.sha256)"
python runtime-output/govoplan-deploy.pyz init \
--directory runtime-output/acceptance-install \
--non-interactive --module-set base
python runtime-output/govoplan-deploy.pyz verify-release \
--directory runtime-output/acceptance-install \
--manifest runtime-output/distribution-manifest.json \
--manifest-sha256 "$MANIFEST_SHA256" \
--trusted-keyring runtime-output/distribution-keyring.json \
--adopt
- name: Exercise the managed ingress boundary
working-directory: govoplan
env:
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
run: >-
python tools/checks/managed-ingress-drill.py
--caddy-image "$MANAGED_INGRESS_IMAGE"
--load-balancer-image "$LOAD_BALANCER_IMAGE"
--probe-image "$(jq -r '.platforms["linux/amd64"]' runtime-output/api-metadata.json)"
- name: Publish immutable Gitea release assets
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ gitea.sha }}
GITEA_RELEASE_TOKEN: ${{ secrets.GOVOPLAN_RELEASE_TOKEN }}
run: |
python tools/release/publish-runtime-release.py \
--tag "v$VERSION" \
--target-commit "$SOURCE_COMMIT" \
--title "GovOPlaN v$VERSION runtime distribution" \
--asset runtime-output/govoplan-deploy.pyz \
--asset runtime-output/govoplan-deploy.pyz.sig \
--asset runtime-output/govoplan-deploy.pyz.sha256 \
--asset runtime-output/distribution-manifest.json \
--asset runtime-output/distribution-manifest.json.sha256 \
--asset runtime-output/distribution-keyring.json \
--asset runtime-output/context-amd64/composition.json \
--asset runtime-output/release-packages.json \
--asset runtime-output/package-artifacts.lock.json \
--asset runtime-output/requirements-release.packages.txt \
--asset runtime-output/evidence/api-sbom.cdx.json \
--asset runtime-output/evidence/web-sbom.cdx.json \
--asset runtime-output/evidence/api-provenance.json \
--asset runtime-output/evidence/web-provenance.json \
--asset runtime-output/evidence/runtime-smoke-amd64.json \
--asset runtime-output/evidence/runtime-smoke-arm64.json
@@ -0,0 +1,44 @@
name: Runtime Ingress Drill
on:
workflow_dispatch:
inputs:
caddy_image:
description: Digest-pinned Caddy image
required: true
type: string
load_balancer_image:
description: Digest-pinned HAProxy image
required: true
type: string
probe_image:
description: Digest-pinned amd64 GovOPlaN API image
required: true
type: string
jobs:
managed-ingress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
path: govoplan
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Authenticate runtime image pull
env:
REGISTRY_USERNAME: ${{ secrets.GOVOPLAN_REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.GOVOPLAN_REGISTRY_TOKEN }}
run: echo "$REGISTRY_TOKEN" | docker login git.add-ideas.de --username "$REGISTRY_USERNAME" --password-stdin
- name: Exercise the managed ingress boundary
working-directory: govoplan
env:
CADDY_IMAGE: ${{ inputs.caddy_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
PROBE_IMAGE: ${{ inputs.probe_image }}
run: >-
python tools/checks/managed-ingress-drill.py
--caddy-image "$CADDY_IMAGE"
--load-balancer-image "$LOAD_BALANCER_IMAGE"
--probe-image "$PROBE_IMAGE"
+14 -19
View File
@@ -1,7 +1,8 @@
name: Security Audit
permissions: read-all
on:
pull_request:
push:
branches:
- main
@@ -13,37 +14,31 @@ jobs:
security-audit:
runs-on: ubuntu-latest
env:
SECURITY_AUDIT_MODE: ci
SECURITY_AUDIT_MODE: full
SECURITY_AUDIT_SCOPE: govoplan
SECURITY_AUDIT_FAIL_ON_FINDINGS: "0"
SECURITY_AUDIT_REQUIRE_TOOLS: "1"
GIT_CONFIG_COUNT: "2"
GIT_CONFIG_KEY_0: url.https://git.add-ideas.de/GovOPlaN/govoplan.insteadOf
GIT_CONFIG_VALUE_0: git@git.add-ideas.de:GovOPlaN/govoplan
GIT_CONFIG_KEY_1: url.https://git.add-ideas.de/GovOPlaN/govoplan.insteadOf
GIT_CONFIG_VALUE_1: ssh://git@git.add-ideas.de/GovOPlaN/govoplan
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
path: govoplan
- name: Configure SSH for repository bootstrap
env:
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh repository bootstrap."
exit 1
fi
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Bootstrap GovOPlaN repositories
working-directory: govoplan
run: python tools/repo/bootstrap-repositories.py --parent ..
- name: Run security audit
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Run whole-system security audit
working-directory: govoplan
run: tools/checks/security-audit/run.sh --mode "$SECURITY_AUDIT_MODE" --scope "$SECURITY_AUDIT_SCOPE" --reports-dir audit-reports
- name: Upload audit reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
with:
name: security-audit-reports
path: govoplan/audit-reports
+8
View File
@@ -4,7 +4,15 @@
.ruff_cache/
.venv/
runtime/
!tools/release/runtime/
tools/release/runtime/*
!tools/release/runtime/Dockerfile.api
!tools/release/runtime/Dockerfile.web
!tools/release/runtime/nginx.conf
__pycache__/
build/
dist/
*.egg-info/
audit-reports/
coverage/
htmlcov/
-1
View File
@@ -3,7 +3,6 @@ title = "GovOPlaN secret scanning"
[allowlist]
description = "Repository examples and documented development placeholders"
regexes = [
'''dev-multimailer-api-key''',
'''example(\.invalid|\.org|\.com)''',
'''postgresql(\+psycopg)?://[^:@/\s]+:\*\*\*@''',
'''<redacted>''',
+21
View File
@@ -0,0 +1,21 @@
# GovOPlaN Workspace Codex Guide
## Scope
This repository coordinates the GovOPlaN workspace, release catalog, shared
checks, development environment, and cross-repository automation. Business and
platform behavior remains owned by the corresponding module repository.
## Documentation Contract
- Treat documentation as part of every behavior change. Update the owning module's manifest-driven `DocumentationTopic` contributions for each affected user and administrator workflow, setting, permission, limitation, and operational consequence.
- Keep feature content in the owning module. The optional `govoplan-docs` module projects module contributions and must not import feature internals.
- Every module manifest must provide a static user and administrator baseline, even when `documentation_providers` add configured-state details.
- Run `tools/checks/check-manifest-shapes.py` after module behavior or manifest changes. Run `tools/checks/check-focused.sh` for cross-module changes.
## Working Rules
- Treat Gitea issues as the canonical backlog and state log.
- Preserve optional module boundaries and use Core contracts or capabilities for integrations.
- Prefer targeted checks before full workspace scans.
- Do not start persistent development servers unless requested.
+142 -3
View File
@@ -4,6 +4,12 @@
**Repository type:** system (meta).
<!-- govoplan-repository-type:end -->
[![Module Matrix](https://git.add-ideas.de/GovOPlaN/govoplan/actions/workflows/module-matrix.yml/badge.svg?branch=main)](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=module-matrix.yml&actor=0&status=0)
[![Release Integration](https://git.add-ideas.de/GovOPlaN/govoplan/actions/workflows/release-integration.yml/badge.svg?branch=main)](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=release-integration.yml&actor=0&status=0)
[![Deployment Installer](https://git.add-ideas.de/GovOPlaN/govoplan/actions/workflows/deployment-installer.yml/badge.svg?branch=main)](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=deployment-installer.yml&actor=0&status=0)
[![Dependency Audit](https://git.add-ideas.de/GovOPlaN/govoplan/actions/workflows/dependency-audit.yml/badge.svg?branch=main)](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=dependency-audit.yml&actor=0&status=0)
[![Security Audit](https://git.add-ideas.de/GovOPlaN/govoplan/actions/workflows/security-audit.yml/badge.svg?branch=main)](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=security-audit.yml&actor=0&status=0)
This is the GovOPlaN meta repository. It is the operator entry point for
whole-product development, release orchestration, repository bootstrap, and
system-level Docker composition.
@@ -17,8 +23,7 @@ Create the whole-product development virtualenv in this meta repository:
```sh
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install -r requirements-dev.txt
./.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-dev.txt --python ./.venv/bin/python --upgrade-pip
```
The meta venv is the default Python environment for launch, check, release, and
@@ -31,6 +36,22 @@ Start the development stack through the meta repository:
./tools/launch/launch-dev.sh
```
Open the WebUI in a browser after launch only when explicitly requested:
```sh
GOVOPLAN_OPEN_BROWSER=1 ./tools/launch/launch-dev.sh
```
Limit backend reload triggers during focused module work without changing the
enabled module graph:
```sh
GOVOPLAN_BACKEND_RELOAD_MODULES=calendar,campaign ./tools/launch/launch-dev.sh
```
Set `GOVOPLAN_BACKEND_RELOAD_MODULES=none` to watch only core/config sources.
Leaving it unset keeps the broad default and watches all enabled modules.
Start the shared development PostgreSQL service:
```sh
@@ -49,12 +70,69 @@ Clone missing repositories listed in `repositories.json`:
./tools/repo/bootstrap-repositories.py
```
Gitea Actions jobs bootstrap the registered repositories over HTTPS and reuse
only the checkout job's short-lived authentication header. If registered
modules are private, allow the meta repository read access under
`GovOPlaN -> Settings -> Actions -> General -> Cross-Repository Access`; no
long-lived personal token is stored by the workflow or bootstrap tool.
Update generated repository type notes in all READMEs:
```sh
./tools/repo/update-repository-type-notes.py
```
Regenerate the human-readable repository link index:
```sh
./tools/repo/generate-repository-index.py
```
Synchronize the Python environment after package metadata changes:
```sh
./.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-dev.txt --python ./.venv/bin/python
```
Run the static cross-repository module contract check:
```sh
./tools/checks/check-contracts.sh
```
Require backend, manifest, frontend, lockfile, and release-composition versions
to agree before a release:
```sh
./.venv/bin/python tools/checks/check-version-alignment.py --release-composition
```
Generate the CycloneDX dependency inventory from a resolved release environment:
```sh
./.venv/bin/python tools/release/generate-release-sbom.py --python ./.venv/bin/python
```
Synchronize module package workflows and inspect the registry release contract:
```sh
./.venv/bin/python tools/repo/sync-module-package-workflows.py --check
./.venv/bin/python tools/release/generate-release-package-set.py \
--output /tmp/govoplan-release-packages.json
```
Package publication, exact artifact locking, and the optional `govoplan`
developer meta-package are documented in
[Package Registry Releases](docs/PACKAGE_REGISTRY_RELEASES.md).
For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release
commit timestamp (or pass an explicit timezone-qualified `--timestamp`):
```sh
SOURCE_DATE_EPOCH="$(git -C ../govoplan-core show -s --format=%ct HEAD)" \
./.venv/bin/python tools/release/generate-release-sbom.py --python ./.venv/bin/python
```
Run the consolidated focused verification suite:
```sh
@@ -76,6 +154,29 @@ Run installer rollback drills:
Release, catalog, Gitea, security-audit, and cross-repository maintenance
commands should also be called from this repository through `tools/`.
Start the local release console:
```sh
./.venv/bin/python tools/release/release-console.py
```
Create and validate a private, declarative installation bundle:
```sh
./.venv/bin/python tools/deployment/govoplan-deploy.py init \
--directory ~/.local/share/govoplan/installations/default
./.venv/bin/python tools/deployment/govoplan-deploy.py doctor \
--directory ~/.local/share/govoplan/installations/default
```
The current executable slice and remaining production gates are documented in
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
Same-host replica balancing and the multi-host promotion boundary are documented
in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
The recovery state machine, migration rollback boundary, and required restore
drills are documented in
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
## Configuration
The repository root `.env.example` is the self-hosted operator template for a
@@ -89,7 +190,45 @@ such as `~/.config/gitea/gitea.env` and be passed with `--env-file`.
The repository categories are documented in
`docs/REPOSITORY_STRUCTURE.md`. The machine-readable list lives in
`repositories.json`.
`repositories.json`; the clickable human-readable index is
`docs/REPOSITORY_INDEX.md`.
Meta ownership and module install/contract boundaries are documented in
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`.
Frontend layout principles for module pages are documented in
`docs/FRONTEND_LAYOUT_PRINCIPLES.md`.
The provider-neutral datasource boundary and reusable Dataflow/Workflow graph
contract are documented in
`docs/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md`.
The cross-product destination, stakeholder visions, configuration archetypes,
connected outcome stories, and capability horizons are documented in
the [Connected Governance Platform Roadmap](docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md).
The reconciled institutional semantics, source-authority modes, module layers,
candidate Mandates/Services/Parties/Decisions boundaries, and migration
sequence are documented in the
[Institutional Governance Target Architecture](docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
The selected Campaign-to-Postbox-to-data-to-collaboration implementation path,
including stage gates and shared documentation expectations, is in the
[Reference Journey Program](docs/REFERENCE_JOURNEY_PROGRAM.md).
The administrator journey from Core-only bootstrap through online module
installation, scale-out, and reversible environment promotion is defined in
[System Administrator Lifecycle User Story](docs/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
The corresponding host deployment compiler, managed/external component choices,
reconfiguration semantics, and safe Web update boundary are defined in
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
The concrete replica, worker-node, load-balancer, and shared-state topology is
defined in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
Durable deployment journals, Core recovery evidence, and the distinction
between pre-migration configuration restore and post-migration forward recovery
are defined in
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
The first Campaign-centric capability and infrastructure fit assessment is in
`docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md`. Its rerun tooling can collect and
verify a bounded installed composition; target, provider and production claims
remain separate, expiring attestations signed by independently scoped proof
authorities. The operational issuance, target-run, recovery-measurement, key
custody, and promotion-gate procedure is in
[Target Maturity Evidence Runbook](docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
# GovOPlaN Docker
+12 -1
View File
@@ -80,6 +80,12 @@ cd /mnt/DATA/git/govoplan
tools/launch/launch-dev.sh
```
The launcher does not open a browser by default. To opt in:
```bash
GOVOPLAN_OPEN_BROWSER=1 tools/launch/launch-dev.sh
```
To force the old SQLite fallback for a disposable local run:
```bash
@@ -111,7 +117,12 @@ tools/checks/postgres-integration-check.py \
```
`--reset-schema` drops and recreates the `public` schema before every module
set. Use it only against this disposable database.
set. Use it only against this disposable database. Before those permutations,
the check runs the Files credential-retirement atomicity proof in random,
test-owned schemas. The proof does not modify `public` and fails the release
gate if its PostgreSQL or full-stack dependencies are unavailable. The
`--skip-retirement-atomicity` option is only for bounded migration/smoke
diagnosis; do not use it for release evidence.
Stop the testbed:
+18 -1
View File
@@ -16,10 +16,27 @@ DATABASE_URL=postgresql+psycopg://govoplan:govoplan-dev@127.0.0.1:55433/govoplan
GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan
REDIS_URL=redis://127.0.0.1:56379/0
CELERY_ENABLED=true
CELERY_QUEUES=send_email,append_sent,default
CELERY_QUEUES=send_email,append_sent,notifications,calendar,default
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=true
GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES=16777216
GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES=536870912
GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST=
GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST=
GOVOPLAN_HTTP_MAX_REQUEST_BODY_BYTES=536870912
GOVOPLAN_HTTP_HSTS_SECONDS=0
AUTH_LOGIN_THROTTLE_ENABLED=true
AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10
AUTH_LOGIN_THROTTLE_CLIENT_LIMIT=100
AUTH_LOGIN_THROTTLE_WINDOW_SECONDS=900
AUTH_LOGIN_THROTTLE_REDIS_RETRY_SECONDS=30
ENABLED_MODULES=tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,docs,ops
CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:5173
GOVOPLAN_TRUSTED_HOSTS=127.0.0.1,localhost,testserver
FORWARDED_ALLOW_IPS=127.0.0.1
AUTH_COOKIE_SECURE=false
FILE_STORAGE_BACKEND=local
FILE_STORAGE_LOCAL_ROOT=runtime/production-like/files
+4 -3
View File
@@ -1,7 +1,7 @@
# Production-Like Development Profile
This profile runs the shared services that production depends on while keeping
API, worker, and WebUI code in the editable local repositories.
API, worker, scheduler, and WebUI code in the editable local repositories.
It provides:
@@ -10,6 +10,7 @@ It provides:
- explicit `ENABLED_MODULES`
- local durable file storage under `runtime/production-like/files`
- a Celery worker process using the same queues as the API
- a Celery beat process for durable retry and recovery schedules
Start it from the meta repository:
@@ -37,7 +38,7 @@ password changes:
cp dev/production-like/.env.example dev/production-like/.env
```
The API and worker use:
The API, worker, and scheduler use:
```text
DATABASE_URL=postgresql+psycopg://govoplan:govoplan-dev@127.0.0.1:55433/govoplan
@@ -45,7 +46,7 @@ REDIS_URL=redis://127.0.0.1:56379/0
CELERY_ENABLED=true
```
Stop the launched API/WebUI/worker with `Ctrl+C`. The PostgreSQL and Redis
Stop the launched API/WebUI/worker/scheduler with `Ctrl+C`. The PostgreSQL and Redis
containers keep running by default so the next launch is fast. To stop them too:
```bash
+5
View File
@@ -7,6 +7,11 @@ Current shared profiles:
- `govoplan/dev/postgres`
- `govoplan/dev/production-like`
The generated whole-product Compose profile is owned by
`tools/deployment/govoplan-deploy.py`. It renders a deployment-specific
`compose.json` from a versioned installation specification; generated files and
secrets remain outside the repository.
Module-specific Docker test beds remain in their owning repositories:
- `govoplan-campaign/dev/mail-testbed`
+133
View File
@@ -0,0 +1,133 @@
# Backup And Restore Evidence
## Boundary
`govoplan-deploy` verifies backup and restore evidence; it does not receive
database, object-store, KMS, or orchestrator administration credentials and it
does not create the backup. A provider-owned backup controller creates one
coordinated recovery point, a separate drill runner restores it into an
isolated target, and an evidence authority signs the resulting receipt.
The application containers receive only a sanitized projection: evidence,
recovery-point and drill identifiers, hashes, timestamps, component count, and
measured RPO/RTO. Artifact locations, provider credentials, encryption-key
references, the public trust keyring, and private signing keys remain in the
deployment/evidence boundary.
The machine-readable contracts are:
- [`backup-evidence.schema.json`](backup-evidence.schema.json);
- [`backup-evidence-keyring.schema.json`](backup-evidence-keyring.schema.json).
One evidence document is bound to the installation id, deployment profile,
topology subject, exact signed release manifest, image digests, and composition
digest. It covers PostgreSQL, objects, protected configuration, and recoverable
key custody at one recovery point. It contains references, never key material.
## Production Sequence
1. Establish the provider snapshot, application quiesce, or transaction
boundary and retain a hash of its fencing token.
2. Capture PostgreSQL, object storage, protected deployment configuration, and
key-custody state within five minutes of that recovery point.
3. Restore all four components into a target isolated from production write
endpoints and production queues.
4. Start the exact immutable release named in the evidence, verify migration
heads, verify a deterministic manifest of representative object hashes, and
execute the documented semantic journey checks.
5. Record actual data loss and elapsed recovery as measured RPO and RTO. A
measured RPO above the declared objective invalidates the evidence.
6. Sign the canonical receipt using an evidence-authority Ed25519 key held
outside the application and deployment host. During key rotation, include
both accepted signatures.
7. Transfer the evidence SHA-256 through an independent approved channel, then
verify and adopt it on the deployment host.
Provider automation can sign and validate an unsigned receipt with:
```sh
python tools/deployment/sign-backup-evidence.py \
--input unsigned-backup-evidence.json \
--output backup-evidence.json \
--trusted-keyring backup-evidence-keyring.json \
--signing-key backup-authority-2026=/run/keys/backup-authority.pem
```
The private key file must be owner-only. The tool refuses an unexpected key
type, an inactive/untrusted signer, malformed or partial evidence, stale
recovery points, failed drill checks, mismatched releases, and non-canonical
output.
Adopt the result using the independently obtained digest:
```sh
python3 govoplan-deploy.pyz verify-backup \
--directory /srv/govoplan/default \
--evidence ./backup-evidence.json \
--evidence-sha256 "$APPROVED_BACKUP_EVIDENCE_SHA256" \
--trusted-keyring ./backup-evidence-keyring.json \
--adopt
```
Evidence is fresh for at most 24 hours and may declare an earlier expiry. Every
self-hosted release identity change is conservatively treated as a migration
boundary. `doctor`, Compose `apply`, and `render-kubernetes` fail closed when
fresh evidence for the previously applied immutable release is unavailable.
Compose verifies once before changing runtime state and again after API/worker
quiescing immediately before migration. The exported Kubernetes migration Job
is generated only after verification and is annotated with the sanitized
evidence digest, recovery-point id, and drill id.
## Provider Runbooks
### PostgreSQL
Use a managed transaction-consistent snapshot or a base backup plus retained
WAL sufficient to reconstruct the declared point. Record the provider,
protected artifact reference and digest, snapshot identity, and PostgreSQL LSN.
The restore drill must connect only to the isolated database and must compare
the resulting migration-head digest with the release expectation.
### Object Storage
Use provider snapshots/versioning or an immutable object copy. Build a sorted
manifest containing object key, version, size, and content digest, then record
its digest, object count, total bytes, provider version identity, and protected
artifact reference. Verify representative objects from every owning module
after restore. Single-node managed Garage is persistent but not highly
available; copy its coordinated recovery material to an independent failure
domain.
### Configuration And Key Custody
Back up the private installation bundle and external secret-manager bindings as
an encrypted artifact. Record only its reference and digest. For KMS/HSM/vault
state, record the provider keyset reference, version, and a successful
recoverability assertion. Never put a key, recovery share, token, password, or
credential-bearing URL in evidence. The isolated drill must prove that the
restored release can decrypt representative protected content without
exporting the key material into the report.
## Ownership And Retention
The deployment owner approves the RPO/RTO objectives. State-service owners
operate backup capture and restoration. Module owners define representative
objects and semantic checks. Security owns evidence-authority keys and
revocation. Operations schedules drills and retains sanitized status.
Retain backup artifacts for the approved legal/operational period and at least
through the release's rollback window. Retain signed evidence, drill reports,
and deletion receipts for the audit period. Disposal must remove every backup
copy and provider version according to policy, then revoke or retire references
without deleting the audit receipt. Cryptographic erasure is valid only when
key-destruction evidence and provider-copy coverage are independently proven.
## Failure Handling
Missing components, component-time skew, stale or expired evidence, revocation,
signature/key mismatch, changed stored files, release mismatch, failed semantic
checks, or an RPO breach block migration. The deployment journal records the
rejection without private provider details. If migration has not started, the
operator may supply fresh evidence and retry. Once migration starts, recovery
is explicitly forward-only until the verified coordinated recovery point is
restored with its matching release.
+666
View File
@@ -0,0 +1,666 @@
# GovOPlaN Capability and IT-Infrastructure Fit Assessment
## Assessment record
| Field | Value |
| --- | --- |
| Assessment ID | `campaign-reference-2026-07-22` |
| Schema | `govoplan.fit-assessment/0.1.0` |
| Assessed on | 2026-07-22 |
| Product snapshot | Live Ed25519-signed stable catalog sequence `202607220843`: Core `v0.1.13` and Campaign `v0.1.10` |
| Configuration basis | Root `.env.example` and the production-like development profile |
| Scope | Campaign-centric internal pilot and small-production candidate |
| Explicitly postponed | Workflow and workflow-driven user stories |
| Machine-readable companion | [`capability-fit-current.json`](capability-fit-current.json) |
| Input schema | [`capability-fit.schema.json`](capability-fit.schema.json) |
**Snapshot notice:** this assessment remains valid only for the pinned
2026-07-22 composition above. Workflow Engine, the optional Workflow editor,
Datasources, Dataflow, Search, encryption contracts, and other later main-branch
work must not be inferred into this evidence record. The current product
direction and implemented-state reconciliation are documented separately in
the
[Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
This is a fit assessment, not a production approval or security certification.
It deliberately does not infer implementation from a repository, issue, or
manifest existing. A conclusion needs code plus a route/contract, test,
operational drill, configured example, or explicit evidence that the capability
is absent.
The release baseline below is reproducible at its tagged source and package
references. Later main-branch commits do not become release capabilities merely
because they are committed, pushed, or code-tested. This assessment therefore
distinguishes code-tested, package-integrated, target-tested, and
production-approved evidence explicitly.
## Controlled status vocabulary
| Status | Meaning |
| --- | --- |
| `verified` | The stated capability is implemented and directly exercised by evidence appropriate to the stated scope. Conditions and target-environment proof may still remain. |
| `available_unconfigured` | An implementation and supporting evidence exist, but the target endpoint, credentials, policy, or deployment component has not been configured and exercised. |
| `partial` | A useful subset exists, but one or more material parts of the stated journey or operational requirement are missing or unproved. |
| `scaffold` | Contracts, models, routes, or UI structure exist, but the end-to-end capability is not yet usable. |
| `external_system` | GovOPlaN expects the deployment or another system to supply this capability; integration requirements must still be assessed. |
| `planned` | The capability is represented only by a concept, backlog item, or design direction. |
| `not_fit` | Evidence shows that the current composition cannot meet the stated requirement. |
| `not_assessed` | The requirement or target environment is not sufficiently known to reach a conclusion. |
Status alone is not enough. Every row also states its evidence scope and
conditions. For example, a unit-tested SMTP adapter is
`available_unconfigured`, not `verified` for an organization's mail system.
## Executive conclusion
GovOPlaN is a credible candidate for a controlled, Campaign-centric internal
pilot using local accounts, PostgreSQL, durable single-node file storage, and a
non-production SMTP/IMAP service. The module contract, Campaign authoring and
validation paths, managed attachments, mail-profile boundary, local audit
records, and operator status surface all have direct code/test evidence.
The signed catalog resolves the earlier source/package reproducibility gap, but
it does not make the composition production-approved. The production-like
profile intentionally runs only PostgreSQL and Redis in containers; API,
WebUI, worker, and scheduler processes still run from editable source trees. A
target deployment must supply TLS termination, process supervision, secret
injection, monitoring, backup storage, and recovery procedures. The open
[Core backup/restore issue #29](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29)
is a production gate. Identity federation, external audit export, and a tested
disaster-recovery plan are also not complete.
The current recommendation is therefore:
- **Pilot:** fit with conditions and a bounded proof of concept.
- **Small production:** partial fit; do not approve until the proof checks and
operational gates below pass against the signed release and target
environment.
- **Workflow:** planned and postponed; it is not part of either recommended
composition in this assessment.
## Pinned composition
The configured reference composition comes from the meta repository's
`ENABLED_MODULES`. Versions and refs below are pinned by the live stable catalog;
commits are the commits peeled from those annotated tags.
| Module | Tagged source commit | Catalog version/ref | Role in this assessment |
| --- | --- | --- | --- |
| Core runtime | `govoplan-core@d487726f4d2c` | `0.1.13` / `v0.1.13` | API, registry, migration orchestration, shared WebUI, sessions and kernel contracts |
| `tenancy` | `govoplan-tenancy@efbec827616b` | `0.1.8` / `v0.1.8` | tenant context and lifecycle |
| `organizations` | `govoplan-organizations@39c081c4fb8f` | `0.1.8` / `v0.1.8` | organization model |
| `identity` | `govoplan-identity@7a1710af896f` | `0.1.8` / `v0.1.8` | normalized internal identity directory |
| `access` | `govoplan-access@f1d64d247e12` | `0.1.11` / `v0.1.11` | local authentication, sessions, API keys and RBAC |
| `admin` | `govoplan-admin@11ecf362a36d` | `0.1.8` / `v0.1.8` | administration surfaces |
| `dashboard` | `govoplan-dashboard@4b960ad37f0d` | `0.1.8` / `v0.1.8` | module-aware home surface |
| `policy` | `govoplan-policy@1063622d311a` | `0.1.9` / `v0.1.9` | policy explanation/configuration boundary |
| `audit` | `govoplan-audit@d3d2c60d7dc1` | `0.1.8` / `v0.1.8` | database audit records and retrying audit outbox |
| `campaigns` | `govoplan-campaign@735e874bd03c` | `0.1.10` / `v0.1.10` | Campaign authoring, build, delivery control and reporting |
| `files` | `govoplan-files@2b34f6e30578` | `0.1.9` / `v0.1.9` | managed files and Campaign attachments |
| `mail` | `govoplan-mail@3e2302909022` | `0.1.10` / `v0.1.10` | SMTP/IMAP profiles and transports |
| `calendar` | `govoplan-calendar@9bcf41bb1fbb` | `0.1.8` / `v0.1.8` | optional calendar; not needed by the Campaign pilot |
| `docs` | `govoplan-docs@be52b716caed` | `0.1.10` / `v0.1.10` | configured-system documentation |
| `ops` | `govoplan-ops@341773a4ff8a` | `0.1.8` / `v0.1.8` | readiness and deployment-profile visibility |
Stable catalog sequence `202607220843` is valid, signed by trusted key
`release-key-1`, and pins matching Python and WebUI refs where applicable. This
proves release source/package selection and provenance, not behavior in a target
environment. No configuration revision/package is pinned yet, so deployment
configuration remains a separate reproducibility and promotion gate. The static
contract scan found 43 interface contracts, 33 providers, and no contract errors
in the current workspace. That proves the scanned manifest graph is internally
consistent; it does not prove every provider or production journey.
`govoplan-addresses@93dddbb8c52a` (`0.1.9` / `v0.1.9`) is an optional catalogued
extension for reusable recipient sources. It is not enabled in the pinned root
profile and must be added explicitly when the pilot needs address lists or
CardDAV.
## Recommended scenarios
### Campaign pilot
Use one internal tenant or office, controlled operators, local GovOPlaN accounts,
and non-critical recipient volumes. Enable:
```text
tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,docs,ops
```
Add `addresses` only when reusable address books/lists or CardDAV are in scope.
Leave Calendar and Workflow out unless the pilot has an explicit journey for
them. The minimum topology is one supervised API process, one built WebUI, one
PostgreSQL database, durable local file storage, and one Redis/Celery worker when
asynchronous delivery is enabled. Use a dedicated non-production SMTP/IMAP
account and a restricted recipient allow-list.
### Small-production candidate
Use a reverse proxy/TLS endpoint, built WebUI assets, separately supervised API
and worker processes, PostgreSQL with measured backup/restore, Redis with
persistence, and durable shared or object storage. Run one scheduler only when a
selected module needs scheduled recovery. Multiple scheduler replicas require
leader election or an external lock, which is not established by this report.
This topology is a recommendation from the [Ops scalability profiles](https://git.add-ideas.de/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md),
not a currently shipped production Compose/Kubernetes/systemd package.
## Functional capability matrix
| Capability | Status | Evidence and scope | Conditions, gaps, or exclusions |
| --- | --- | --- | --- |
| Module-aware API/WebUI composition | `verified` | Core registry tests cover installed manifests, route contribution, missing modules, and module permutations; the static contract scan passed; stable catalog sequence `202607220843` has a valid trusted signature and matching package refs. | Package integration is verified; repeat the module-permutation checks on the installed target composition. |
| Local tenants, accounts, sessions, API keys and RBAC | `verified` | Access/tenancy manifests, auth dependency tests, cookie/CSRF API smoke tests, and role/permission contracts. | External identity providers are not included in this conclusion. |
| Campaign draft authoring, validation, recipient import, build, execution snapshot and mock send | `verified` | Core API smoke tests exercise create/validate/build/mock-send, frozen delivery configuration, policy gates, job deltas and reconciliation; Campaign-focused tests passed; Campaign `v0.1.10` and Core `v0.1.13` are package-integrated in the signed stable catalog. | Code and package integration are verified; usability and target-provider acceptance remain separate. |
| Managed Campaign attachments and local file storage | `verified` | Files capability/route tests, Campaign attachment-build tests, and local-storage backend code. | The deployment must provide a durable path and include files in database/storage backup plans. |
| SMTP send, IMAP append and mailbox/profile policy | `available_unconfigured` | Mail adapters, encrypted profile model, helper tests, Campaign mail-policy smoke tests, and a GreenMail-oriented runbook/testbed. | No target mail server, TLS chain, throttling, sender policy, bounce/reply process, or real delivery receipt was exercised here. |
| Delivery retry, uncertainty and reconciliation | `partial` | Campaign API smoke tests include worker-loss/outcome-unknown reconciliation and frozen job evidence; those paths are present in catalogued Campaign `v0.1.10`. | A target-provider failure drill and operational alerting are still required; package integration does not prove provider behavior. |
| Reusable address lists and Campaign recipient-source integration | `available_unconfigured` | Address list/recipient capability and Campaign optional lookup tests passed; manifests expose optional interfaces. | `addresses` is not in the pinned profile. Configure permissions and data governance before use. |
| CardDAV address synchronization | `available_unconfigured` | Discovery, preview/conflict, two-way create/update/delete and disconnect tests passed in the current Addresses workspace. | Requires target-server interoperability, credentials, CA/TLS, rate-limit, conflict and restore drills. |
| File connectors (WebDAV/Nextcloud/Seafile/SMB/S3 import) | `partial` | Provider descriptors and browse/import helper tests exist; S3 connector tests passed. | Coverage varies by provider and optional dependency. No target content system was exercised; connector egress and provenance policy need review. |
| Local audit log and governed-event retry outbox | `verified` | Audit module contract plus enqueue/dispatch/failure-for-retry tests passed. | Central audit/SIEM export, retention enforcement and operational monitoring are not verified. |
| Configured-system documentation and Ops status pages | `verified` | Docs/Ops manifests contribute protected routes and WebUI packages; Ops code checks database, Redis, workers, storage and deployment-security settings. | This does not replace external monitoring or a target runbook. |
| Calendar/CalDAV integration | `partial` | Calendar `v0.1.8` supplies the catalogued storage/sync foundation. The durable external-write outbox, worker recovery, reconciliation, and retention work is committed, tested, and pushed on Calendar `main` after that tag. | The post-tag outbox work is remote-integrated source, not local-only WIP, but it is not in the signed stable package baseline and has not passed a target CalDAV drill. Bulk synchronized-calendar migration semantics remain separate work. |
| External LDAP/AD, OIDC/SAML or SCIM identity integration | `scaffold` | IDM owns normalized assignment APIs and documents connector boundaries. | Provider connectors, login callback flow and target directory reconciliation are not an implemented end-to-end capability. Use local accounts for this pilot. |
| Export-control/embargo-list screening | `planned` | Product-level [GovOPlaN #12](https://git.add-ideas.de/GovOPlaN/govoplan/issues/12) defines the consumer-independent user story. | No screening provider, list provenance, matching policy, review flow or legal evidence exists in this composition. |
| Workflow-driven journeys and views | `planned` | Workflow contracts/concepts exist outside this assessment. | Explicitly postponed. Do not include Workflow in pilot or production claims from this report. |
## Infrastructure matrix
| Component | Status | Current evidence | Target requirement / gap |
| --- | --- | --- | --- |
| WebUI and API | `verified` | FastAPI app, module routes, shared WebUI host, health endpoint, dev smoke and module-permutation tests; the catalog pins matching Core/Campaign backend and WebUI refs. | Materialize and supervise immutable artifacts in the target; no production image/service bundle is supplied here. |
| Background workers | `available_unconfigured` | Celery app, Campaign queues, Redis configuration and production-like launcher. | Run target broker/worker heartbeat and failure drills; split queues when provider limits or load justify it. |
| Scheduler | `partial` | A separately launched Celery beat process exists; Calendar recovery scheduling is committed and pushed after the catalogued `v0.1.8` tag. | The Calendar recovery slice is not stable-package-integrated. Keep single-instance until leader election/locking is proved; add process supervision and missed-schedule alerts. |
| PostgreSQL | `verified` | Production target in settings/operator docs, migration tracks and a disposable integration-check harness. | Target HA, patching, connection limits, WAL/archive policy and restore time are external deployment decisions. |
| Redis/queues | `available_unconfigured` | Redis-backed Celery configuration, health ping and append-only production-like container. | Target availability, persistence, eviction, authentication/TLS and queue-loss behavior were not assessed. |
| Local file storage | `verified` | Local backend, configured root, fallback-root tests and Ops writability check. | Suitable only for a durable single-node/shared path with backup; it blocks independent API scaling when node-local. |
| S3-compatible object storage | `partial` | S3 backend and connector code plus mocked browse/import tests. | Exercise the chosen service, credentials, CA, versioning, lifecycle, multipart/size behavior, restore and consistency expectations. |
| Reverse proxy and TLS | `external_system` | Core validates explicit CORS and secure-cookie posture; Ops reports unsafe production settings. | Deployment must provide certificates, proxy/header policy, request limits, logs and renewal monitoring. |
| Local identity and authorization | `verified` | Password/session/API-key/RBAC capabilities and tests. | Establish account lifecycle, MFA expectation, protected bootstrap and break-glass procedure. |
| Federated identity | `scaffold` | IDM boundary and assignment APIs. | Select and implement/test the actual OIDC/SAML/LDAP/SCIM path before requiring federation. |
| Application secret encryption | `verified` | Stable `MASTER_KEY_B64` contract and encrypted mail-credential paths. | Rotation and loss-recovery procedure need target validation. |
| Secret store and injection | `external_system` | Environment/file references are supported and templates avoid populated secrets. | Choose Vault/KMS/Kubernetes/systemd/environment mechanism, restrict access, rotate credentials and prevent secret exposure in logs/backups. |
| SMTP/IMAP and other connectors | `available_unconfigured` | Protocol adapters and development testbeds. | Target endpoints, egress, DNS, CA, throttling, service accounts and data-processing terms remain environment-specific. |
| Health/readiness | `verified` | `/health`, protected `/health/details`, and Ops checks for DB, Redis, workers, storage, maintenance and cookie/CORS posture. | Add external probes and distinguish liveness from dependency readiness for the chosen orchestrator. |
| Metrics, logs and alerting | `partial` | Correlation IDs, slow-request/query metrics in logs, worker inspection and operator status are implemented. | No bundled metrics exporter, log collector, dashboards, queue-depth alerts, pager route or SLO is verified. |
| Audit | `partial` | Local audit tables and retry outbox are verified. | Retention, tamper-evident export, privileged access review and SIEM integration are unproved. |
| Backup and restore | `partial` | Operator guide and installer hooks describe `pg_dump`/`pg_restore`; SQLite and simulated installer rollback drills exist. | [Core #29](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29) remains open. No target PostgreSQL + files + secrets restore drill or measured RTO/RPO exists. |
| Disaster recovery | `not_assessed` | The Ops guide asks for RPO/RTO and restore drills. | No agreed RPO/RTO, off-site copy, failover topology, dependency recovery order, communications plan or exercise evidence was supplied. |
The scalability and sizing documentation delivered the documentation portions
of [Core #217](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/217) and
[Core #219](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/219).
[Core #28](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/28) records the
operator-documentation slice. These closed tickets are evidence of documented
models, not evidence that an organization's production environment has passed
them.
## Trust, data-flow and network boundaries
| Boundary / flow | Data and trust | Required controls |
| --- | --- | --- |
| Browser -> reverse proxy -> WebUI/API | Session and CSRF cookies, campaign content, recipient personal data and managed files cross the public/client boundary. | HTTPS, exact origins, secure cookies, request/body limits, tenant/RBAC enforcement, security headers and access logs. |
| API -> PostgreSQL | Tenants, identities, permissions, campaign drafts/snapshots/jobs, connector metadata and audit evidence enter the primary trusted data store. | Dedicated DB identity, encrypted transport where networked, least privilege, migration control, backup/restore and retention. |
| API/worker -> file/object storage | Campaign attachments and generated evidence may contain personal or confidential content. | Private buckets/paths, encryption, scoped credentials, malware/content policy where required, lifecycle and coordinated restore. |
| API -> Redis -> worker | Queue messages and job identifiers cross from request processing to an asynchronous trust zone. PostgreSQL remains the durable business-state authority. | Private/authenticated broker, bounded payloads, idempotent claims, queue monitoring and worker isolation. |
| Worker -> SMTP/IMAP | Recipient addresses, message bodies and attachments leave GovOPlaN; IMAP append stores a sent copy externally. | Approved service account, TLS/CA policy, recipient/sender policy, rate limits, outcome reconciliation and disclosure/legal basis. |
| Connector worker -> CardDAV/WebDAV/Seafile/SMB/S3/CalDAV | Address data, files or calendars cross an organizational/system boundary in both directions depending on connector mode. | Explicit direction, scoped credentials, endpoint allow-list, provenance, conflict policy, retry/reconciliation and target interoperability test. |
| Operator -> installer/catalog/migration plane | A highly privileged process can mutate packages, schemas and desired module state. | Separate operator identity, signed/pinned catalogs, maintenance mode, immutable run records, backups, rollback checks and restricted network access. |
| API/audit -> monitoring or SIEM | Operational metadata and potentially personal audit context may leave GovOPlaN. | Data minimization, retention/access policy, authenticated transport, integrity and documented processor/location. No sink is verified today. |
Typical network requirements are inbound HTTPS to the deployment proxy and
private connectivity from API/workers to PostgreSQL and Redis. Outbound access
may be required to SMTP submission, IMAP, HTTPS-based connectors/object storage,
DNS, NTP, certificate validation, monitoring and the signed module catalog.
Ports and destinations must come from the selected infrastructure; common
defaults such as PostgreSQL 5432, Redis 6379, SMTP 465/587 and IMAP 993 are not
an allow-list.
## Known assumptions, gaps and risks
Facts:
- The production-like profile is a development validation composition, not a
production deployment artifact.
- The assessed package baseline is the trusted, signed stable catalog sequence
`202607220843`; it pins Core `v0.1.13` and Campaign `v0.1.10`.
- Workflow is postponed.
- The default composition does not enable Addresses.
- Health/readiness checks exist; an external monitoring stack does not.
Assumptions that require confirmation:
- The pilot can use local accounts and one internal tenant/office.
- A controlled non-production SMTP/IMAP account and safe recipients are
available.
- Campaign volume is below the measured limits of a single worker and database.
- Local durable storage is acceptable for the pilot and all recipient data has
an approved legal basis and retention policy.
Highest risks:
1. A signed, reproducible package selection can still be installed or configured
incorrectly and has not been accepted in the target environment.
2. Real SMTP/IMAP behavior, throttling and ambiguous outcomes have not been
proven against the target provider.
3. Backup/restore and disaster recovery are not demonstrated across database,
files, keys and deployment configuration.
4. External identity, monitoring, secret-store and reverse-proxy controls are
deployment gaps rather than GovOPlaN-delivered components.
5. Post-tag Calendar work is committed and pushed but remains outside the signed
stable package baseline; branch integration must not be mistaken for a
release.
6. Data classification, retention, recipient consent/legal basis and external
disclosure rules are not assessed for a concrete organization.
## Proof checks before promotion
1. Materialize the signed catalog into an isolated installation and rerun the
contract, migration, module-permutation, and focused acceptance gates against
the installed artifacts.
2. Run a target-like Campaign from import through validation, attachment build,
queue, SMTP acceptance, IMAP append, reporting and audit using safe data.
3. Drill transient SMTP failure, accepted-but-local-commit-lost uncertainty,
worker restart, Redis interruption and manual reconciliation without a
duplicate send.
4. Restore PostgreSQL, managed files, configuration and encrypted credentials
into an isolated environment; measure RPO and RTO.
5. Validate proxy/TLS, CORS, cookies, headers, body limits, account bootstrap,
maintenance access and secret redaction.
6. Measure representative recipient/file volume, queue age, database growth,
send rate and provider throttling; set capacity and alert thresholds.
7. Confirm audit/retention/privacy/legal requirements and any SIEM or archive
export before using production personal data.
## Reusable assessment questionnaire
Answers should contain no secrets and no unnecessary personal data. Each answer
must be marked `answered`, `assumed`, `not_applicable`, or `not_assessed` and may
link evidence.
### Scope and outcomes
- What outcome and reference journey must GovOPlaN support?
- Which users, roles, tenants, organization units and delegated functions take
part?
- Which journey steps are mandatory, optional, manual, external, or explicitly
postponed?
- What constitutes pilot success and production acceptance?
### Data, privacy, records and policy
- Which data classes enter database, files, messages, calendars, addresses,
logs and audit evidence?
- What are the legal basis, purpose, minimization, access, residency, retention,
deletion, archive and legal-hold requirements?
- Which external systems/processors receive data, and in which jurisdictions?
- Which decisions require four-eyes approval, explainability or immutable
evidence?
### Identity and integrations
- Are local accounts acceptable, or are OIDC, SAML, LDAP/AD or SCIM mandatory?
- What are the MFA, joiner/mover/leaver, service-account and break-glass rules?
- Which SMTP/IMAP, file/DMS, CardDAV/CalDAV, ERP, API or event endpoints are in
scope? Record protocol/version, direction, auth, CA, rate limit and owner.
- Which integrations may be unavailable, and what degraded/manual behavior is
acceptable?
### Workload and growth
- Tenants, named/active/concurrent users and peak requests?
- Campaigns per period, recipients per Campaign, send window, attachment sizes,
import size and retry peak?
- Managed files, database and audit volume now and over the retention window?
- Connector batches, queue depth/age, scheduled jobs and external rate limits?
### Availability, hosting and operations
- Required service hours, planned maintenance, availability, RPO and RTO?
- Hosting, network zones, egress/proxy/DNS/NTP/CA, data-residency and
air-gap constraints?
- Who operates PostgreSQL, Redis, storage, TLS, identity, secrets, monitoring,
backups and incident response?
- What deployment, patch, migration, rollback, restore and DR drills must pass?
### Procurement and decisions
- Required open-source/license, support, accessibility, security, certification,
interoperability and procurement conditions?
- Which requirements are blockers, accepted risks, residual risks or later
roadmap work?
- Who owns each decision, evidence item and next review date?
For every capability and infrastructure conclusion, record the requirement,
status from the controlled vocabulary, evidence scope, evidence links,
assumptions, gaps, risks, recommendation and proof check. Reassessment must pin a
new composition and review any row whose code, evidence, configuration or target
requirement changed.
### Repeatable release rerun
The release-aware rerun tool validates the machine-readable assessment against
its JSON Schema, verifies the catalog's Ed25519 signature against an
independently pinned local trust keyring, and separately verifies the canonical
hash of the published or candidate keyring pinned by the signed catalog. The
downloaded keyring is therefore never its own sole trust root. The tool compares
catalog sequence and module versions and—when local checkouts are available—checks
annotated release tags against assessed commits. For repositories represented in
signed `release.selected_units`, both the peeled commit and annotated tag-object
ID must match exactly; re-annotating an unchanged commit requires review.
Release drift produces a machine-readable list of affected capability and
infrastructure conclusions instead of silently carrying their prior status
forward:
```bash
./.venv/bin/python tools/assessments/capability-fit.py \
--public \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json
```
Use `--catalog CATALOG --keyring PUBLISHED_OR_CANDIDATE_KEYRING
--trusted-keyring PINNED_LOCAL_KEYRING` for an offline or release-candidate
rerun. `GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE` may provide the
independently managed local trust-root path instead. During key rotation this
local file may contain both current and `next` keys within their validity
windows. Only `active` and `next` entries are trusted; unknown statuses,
duplicate key IDs and malformed structured keyrings fail closed, while revoked,
disabled and retired entries are ignored. Do not establish this trust file by
downloading it in the same rerun. Public JSON responses are bounded to 16 MiB.
Use `--json` or `--output PATH` for automation. Evidence and report files are
written through a bounded same-directory atomic replacement, with mode `0600`
and file and directory `fsync`. All parent directories must already exist, no
parent component may be a symlink, and an existing symlink or non-regular output
target is rejected.
Exit status `0` means the assessed release metadata is current, `2` means
explicit review is required, and `1` means the schema or trust checks are
blocked. The machine report distinguishes independent signature trust,
published-keyring hash agreement, and local-tag proof. Local-tag proof is only
marked checked when every composition entry is available for comparison and was
attempted. A successful rerun proves only the assessment schema, signed catalog
metadata, published-keyring integrity and optional local tag provenance; it does
not prove installed artifacts, target providers, target infrastructure, or
production fitness. Those remain separate proof checks above.
### Installed-composition evidence
The same rerun can inspect the Python environment in which it executes and bind
that observation to the assessment and signed catalog:
```bash
./.venv/bin/python tools/assessments/capability-fit.py \
--public \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--collect-installed-evidence /var/tmp/govoplan-installed-evidence.json \
--output /var/tmp/govoplan-fit-review.json
```
The collector follows the strict version `0.4.0`
[`installed-composition-evidence.schema.json`](installed-composition-evidence.schema.json)
contract. It enumerates all installed distributions whose normalized name starts
with `govoplan-`, compares the enabled assessed package and module-manifest
versions, and identifies missing, duplicate and extra GovOPlaN distributions.
Those differences produce stable review targets for the affected composition
entries and evidence-backed conclusions.
For each distribution, the collector also:
- reads the installed wheel `RECORD` file directly, with a 4 MiB metadata bound,
rather than relying on an interpreted distribution file list; missing files,
duplicate or malformed declarations, mismatched declared sizes and hashes all
prevent a `verified` result;
- verifies every supported SHA-256 wheel-payload declaration in that local
metadata, within fixed limits of 256 GovOPlaN distributions, 10,000 files and
512 MiB hashed per distribution, 50,000 files and 2 GiB hashed for one
collection, and 64 MiB for any single file;
- accepts package-owned data-file targets below the installation prefix and
declared `console_scripts`, while rejecting other traversal, undeclared
scripts, paths outside the prefix and leaf symlinks before opening a file;
- reports pip-generated, unhashed `__pycache__/*.pyc` entries separately only
when their derived source is a hashed payload entry, including generated
files below a confined package-owned runtime-data prefix;
- labels every PEP 610 observation with the explicit
`local-pep610-metadata` basis and distinguishes coherent declared Git commits
and archive hashes from editable installs, local directories,
package-index/unknown origins and malformed metadata;
- compares a non-editable VCS commit with the assessment commit and, when one is
present, the signed catalog `selected_units` commit;
- derives two content identities from bytes it actually verifies: an
install-stable identity for immutable wheel-declared files and a full
installed-RECORD identity, while retaining only SHA-256 digests and counts;
- records only package/module identifiers, versions, bounded counters, hashes
and stable error codes. It never writes direct URLs, paths, hostnames,
usernames, exception text or file contents.
`RECORD` `verified` means complete matching coverage of the wheel payload
declared by the local metadata, not every runtime byte and not a release-origin
binding. The proof scope exposes the aggregate count of generated unhashed
bytecode; runtime activation remains unchecked. `RECORD` agreement proves that
payload files agree with installed metadata, but does not make self-consistent
metadata a trusted release origin. Similarly, a version string is not artifact
integrity. PEP 610 VCS metadata is accepted only for
`vcs: git`, a full 4064 hexadecimal commit, one mutually exclusive provenance
form and a coherent bounded URL shape. Editable and directory-backed installs
remain mutable even when their small editable-install `RECORD` is valid. Archive
or index artifacts without a hash anchored by the assessed release remain
unanchored. A matching declaration is reported under
`installed_source_provenance` as local consistency only, with
`release_origin_bound: false` until the separate origin proof succeeds. A signed
catalog can contain `release.artifacts` identities computed directly from built,
bounded wheel files. Wheels with installer-transformed scripts or headers are
marked `requires_installer_receipt`; catalog metadata alone cannot attest those
post-install bytes. A role-scoped installer receipt instead binds the exact
installed-evidence digest, full installed payload identities, catalog archive
digests and canonical signed-catalog digest. The tool never accepts an installed
hash merely because the installed evidence asserted it.
Only evidence produced in-process by `--collect-installed-evidence` is a local
observation, and it must be no more than five minutes old (with at most 30
seconds of future clock skew) at the selected verification time. Use
`--installed-evidence PATH` to compare evidence collected in a separate
environment. Imported JSON without a valid independent installer receipt
carries a blocker: it can identify differences but cannot establish checked or
valid installed proof. A receipt signed by a separately provisioned installer
authority authenticates that exact imported evidence across the process
boundary. The proof scope records observation mode, collection/evaluation time,
receipt authentication, freshness, and whether evaluation used current time or
an explicit historical override. Evidence input and output are bounded to 4
MiB. Collection loads the `govoplan.modules` entry-point factories in order to
read module IDs and manifest versions; that executes installed GovOPlaN manifest
code. Run it only inside the installation being assessed and with the same
isolation expected for other installed-artifact acceptance checks. This
collector does not observe
which modules a running service activated, migrations, configuration, health,
or reference-journey behavior. Runtime activation therefore remains an explicit
unchecked boundary.
### Reference-readiness, provider and production proof boundary
Installed evidence cannot establish target acceptance, accessibility, privacy,
security, operations, recovery, an external provider, or production use. These
scopes use a separate, expiring
[`capability-fit-boundary-evidence.schema.json`](capability-fit-boundary-evidence.schema.json)
bundle. The bundle is bound to the assessment ID, assessment release and exact
installed-evidence SHA-256 digest. It contains only opaque subject/control/result
IDs and content hashes, not endpoints, credentials, people or raw result files.
Boundary evidence is accepted only when at least one Ed25519 signature validates
against a separately provisioned
[`capability-fit-proof-authority-keyring.schema.json`](capability-fit-proof-authority-keyring.schema.json).
Each authority key explicitly lists the scopes it may attest. Target,
accessibility, privacy, security, operations, recovery, and provider claims use
`passed` or `failed`; production claims use `approved` or `rejected`.
One claim per scope, unique control/artifact IDs, `issued_at < expires_at`, current
validity and exact digest binding are mandatory. Any schema, binding, time,
signature or authority blocker leaves every supplied boundary claim unchecked;
one malformed authority member or invalid validity interval invalidates the
supplied authority set as a whole. An authorized negative result is checked but
requires review only after every prerequisite, including installed
release-origin binding, is established.
Signatures cover UTF-8 JSON with the `signatures` member omitted, object keys
sorted, compact `,`/`:` separators and non-ASCII characters escaped, matching
the tool's deterministic canonicalization.
`tools/assessments/boundary-evidence.py` is the bounded issuance path. It
accepts a private target-run manifest conforming to
[`capability-fit-boundary-run.schema.json`](capability-fit-boundary-run.schema.json),
hashes each retained result file without following a final-component symlink,
and excludes all paths and raw results from the signed receipt. Issuance is
refused unless an independently trusted catalog, exact installed payload,
signed installer receipt, and role-scoped installer authority already pass.
Every claim must be covered by a supplied Ed25519 private key whose public key
is authorized for the full proof interval; catalog and installer key reuse is
rejected. The command immediately verifies its own result and atomically writes
both the proof and a sanitized review. The complete operator procedure and
recovery measurement definition are in
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
```bash
./.venv/bin/python tools/assessments/capability-fit.py \
--public \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--installed-evidence /srv/govoplan/evidence/installed.json \
--boundary-evidence /srv/govoplan/evidence/target-proof.json \
--boundary-authority-keyring /srv/govoplan/trust/proof-authorities.json \
--expected-external-provider-subject provider-production
```
Promotion automation must opt into its required boundaries. Add
`--require-reference-readiness` to require all six target scopes,
`--require-external-provider-proof` when the product depends on a provider, and
`--require-production-approval` for production admission. These switches turn
missing, expired, revoked, mismatched, negative, or otherwise unchecked claims
into a blocking exit status rather than merely reporting them as an unproven
boundary.
With `--installed-evidence`, this command performs comparison and proof-binding
diagnostics. Without an installer receipt, the imported document remains
unsigned, so neither it nor the boundary claim becomes accepted proof. Direct
in-process collection can match catalog-anchored artifact identities for wheels
without installer-transformed files. The installer-receipt flow below
authenticates a durable cross-process observation and is required for transformed
script/header payloads.
Issue a receipt only in the installation process performing the live collection.
The command refuses evidence supplied from a file and first validates the
assessment, independently trusted signed catalog, composition and RECORD bytes:
```bash
./.venv/bin/python tools/assessments/installer-receipt.py \
--assessment /srv/govoplan/assessment.json \
--catalog /srv/govoplan/catalogs/stable.json \
--keyring /srv/govoplan/catalogs/keyring.json \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--receipt-id install:production:20260722 \
--signing-key installer-production=/run/secrets/installer-ed25519.pem \
--python-artifact govoplan-core=/srv/govoplan/staged/govoplan_core-0.1.13-py3-none-any.whl \
--python-artifact govoplan-campaign=/srv/govoplan/staged/govoplan_campaign-0.1.10-py3-none-any.whl \
--evidence-output /srv/govoplan/evidence/installed.json \
--receipt-output /srv/govoplan/evidence/installer-receipt.json
```
Repeat `--python-artifact PACKAGE=/exact/consumed.whl` for every enabled
GovOPlaN distribution. The issuer reopens each exact wheel, verifies its archive
and payload identities against the signed catalog, and refuses a different
build with the same name and version. Receipt issuance must follow collection
within five minutes, and live verification must still fall inside that bounded
window; retain the pair for a reproducible historical review at its explicit
verification time.
Verify that durable pair with its separately managed trust root:
```bash
./.venv/bin/python tools/assessments/capability-fit.py \
--catalog /srv/govoplan/catalogs/stable.json \
--keyring /srv/govoplan/catalogs/keyring.json \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--installed-evidence /srv/govoplan/evidence/installed.json \
--installer-receipt /srv/govoplan/evidence/installer-receipt.json \
--installer-authority-keyring /srv/govoplan/trust/installer-authorities.json
```
Target-environment and production-approval claims must use the assessment's
`deployment_profile.id` as `subject_id`. External-provider claims require the
operator to supply a bounded opaque expected subject with
`--expected-external-provider-subject`; without it, such a claim remains
unchecked and blocks. Expected and observed IDs are retained in proof scope.
Accessibility, privacy, security, operations, and recovery claims use the same
deployment subject. The report emits a cumulative `reference_readiness` verdict
only when all six required scopes are checked and positive. This verdict remains
separate from production approval and from provider-specific acceptance.
Both authority keyrings are governance trust roots. Installer receipt keys use
the strict
[`installer-receipt-authority-keyring.schema.json`](installer-receipt-authority-keyring.schema.json)
contract and may attest only `installed_release_origin`; their public material
must not be reused by catalog or boundary-proof authorities. Do not download or
generate them from the proof bundle being checked. The checker rejects
proof-authority public keys reused by either the published or independently
trusted catalog keyring, and rejects the same proof public-key material assigned
to multiple authority IDs. A malformed key, an invalid or empty validity
interval, or ambiguous key
identity invalidates the supplied authority set. Authority `not_after` is
exclusive. A target operator or approver must validate the referenced
drill/result artifacts before signing. The rerun verifies the
attestation and its bindings; it does not fetch or reinterpret those artifacts.
`--verification-time` exists only for reproducible historical review. Supplying
it always marks the machine and human report as a **HISTORICAL override**; callers
cannot relabel it as current. Live admission must omit it and use the actual
current time.
No boundary bundle or production authority has been supplied for this current
assessment. Reference-readiness, provider, and production proof therefore
remain explicitly unchecked rather than inferred from the local GreenMail
journey, source tests, or signed release metadata.
## Evidence used in this slice
- [Production-like profile](../dev/production-like/README.md) and
[Compose dependencies](../dev/production-like/docker-compose.yml)
- [Module contracts and install boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
- [Core deployment operator guide](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/DEPLOYMENT_OPERATOR_GUIDE.md)
- [Ops scalability profiles](https://git.add-ideas.de/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md)
- Actual module manifests in the pinned repositories and the static contract
checker in this meta repository
- Core module-system/API smoke/auth/install-config tests, plus focused Campaign,
Files, Mail, Audit and Addresses tests
- [Campaign delivery runbook](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/src/branch/main/docs/CAMPAIGN_DELIVERY_RUNBOOK.md)
- [Live signed stable catalog](https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json)
and [published keyring](https://govoplan.add-ideas.de/catalogs/v1/keyring.json),
verified against a separately provisioned local trust keyring
- Annotated source tags `govoplan-core/v0.1.13` and
`govoplan-campaign/v0.1.10`, including their catalogued Python and WebUI refs
- Installed-composition, boundary-proof and independently scoped proof-authority
schemas plus their deterministic review tests; no current target or production
proof bundle is asserted
Checks retained from the first assessment slice against its then-current
workspace:
- static manifest/interface contract scan: 43 contracts, 29 providers, no issues
- selected Core module-composition, auth/CSRF, health, Campaign journey,
reconciliation and install-configuration tests: 14 passed
- Campaign tests: 14 passed
- Files tests: 14 passed
- Mail tests: 22 passed
- Audit tests: 5 passed
- Addresses tests: 14 passed (one non-failing SQLite resource warning)
- JSON Schema validation of the machine-readable assessment: passed
Refresh checks on 2026-07-22:
- live stable catalog sequence `202607220843`: valid Ed25519 signature trusted
through `release-key-1`, no validator warnings
- current static contract scan: 43 modules, 33 providers, 19 requirements, no
contract errors
- catalog-selected commits: Core `d487726f4d2c` at `v0.1.13` and Campaign
`735e874bd03c` at `v0.1.10`; local and remote release refs agree
- Campaign's immutable `v0.1.10` tag object and peeled commit exactly match the
remote tag and catalog record; Calendar durable outbox work is committed and
pushed after catalogued `v0.1.8`
- JSON Schema validation of the refreshed machine-readable assessment: passed
These checks are evidence for the rows above; they are not a substitute for the
installed-release and target-environment proof checks.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,90 @@
# Datasource And Definition Graph Architecture
## Two-Layer Data Boundary
GovOPlaN separates governed data identity from external acquisition:
| Layer | Owner | Responsibilities |
| --- | --- | --- |
| Datasource layer | `govoplan-datasources` | Governed data/register catalogue, tenant visibility, live/cached/static mode, source authority, staging, immutable materializations, frozen states, schema, quality/freshness policy, institutional provenance, dependencies, and bounded reads |
| Connector layer | `govoplan-connectors` and protocol/provider modules | External protocols, endpoints, connection profiles, credentials, discovery, provider maturity/authority support, health, source-side filtering, query pushdown, and effect reconciliation |
Connectors publish versioned datasource origins. Datasources registers those
origins and presents one stable capability to Dataflow, Workflow, Reporting,
Risk Compliance, and other consumers. Consumers must not import connector
implementations or retain credentials.
The initial provider path is:
1. Connectors imports a bounded JSON/CSV snapshot and exposes it as an origin.
2. Datasources registers it as live or cached, or accepts a direct static upload
through staging.
3. Cached refreshes and static promotions append immutable materializations.
4. Any datasource may expose a frozen state for reproducible execution evidence.
5. Dataflow stores an opaque datasource reference, state policy, and expected
fingerprint.
6. A pinned Dataflow run may publish a complete bounded result as a new
immutable materialization through an idempotent Datasources capability.
Database, REST/HTTP, LDAP/directory, managed file, watched-directory, feed, and
stream providers fit behind the same origin contract. Provider-specific
configuration remains in Connectors.
## Shared Definition Graph
Core owns domain-neutral graph primitives:
- nodes, typed ports, edges, and configuration field descriptors;
- node libraries and category labels;
- graph size, connectivity, cycle, and node-count constraints;
- shared backend validation and frontend connection checks.
Domain modules own their semantics:
- Dataflow provides load, combine, filter, transform, and output nodes. Its
graph is acyclic and has one output.
- Workflow Engine provides trigger, activity, review, decision, wait,
module-action, Dataflow, and outcome semantics. It permits governed loops and
has exactly one trigger plus one or more outcomes. The optional Workflow
module supplies the editor over the same native graph/BPMN language.
This division permits a shared editor shell without making Workflow a special
kind of Dataflow or leaking either module into Core.
## Current Implementation
- Core graph and datasource contracts are versioned at `0.1.0`.
- Workflow Engine owns tenant-isolated definitions, immutable revisions,
activation pinning, module-contributed versioned baselines, runtime instances,
governed action/effect execution, retries, waits, and reconciliation. The
optional Workflow module exposes the reusable native BPMN graph editor.
- Datasources exposes catalogue, origins, staging, promotion, preview,
materialization history, refresh, freeze, retirement, and producer
publication APIs.
- Datasources WebUI exposes all current lifecycle views.
- Connectors adapts existing tabular snapshots to datasource origins.
- Dataflow consumes only Datasources catalogue/lifecycle capabilities and can
request current, live, or latest-frozen state.
- Dataflow exposes typed graph/IR, registry-driven validation/execution/SQL
compilation, expressions and reusable subflows, a pinned run-lifecycle
capability, production worker boundary, and Run/Publish surface. Runs record
lineage and intermediate artifacts and publish only complete bounded results.
- The focused composition check proves Connector origin -> Datasource ->
pinned Dataflow run -> frozen published materialization, including replay.
## Next Slices
1. Add the provider declaration and source-authority binding used consistently
by Connectors, Datasources, configuration packages, Ops, and Docs.
2. Add typed datasource owner/steward, legal/purpose, quality/freshness,
classification, correction, service/process, and downstream dependency
metadata under
[Datasources #6](https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/6).
3. Add SQL database and governed REST origin providers with credential-envelope
references and bounded pushdown.
4. Add managed-file and directory origins.
5. Complete datasource quality rules, schema compatibility policy, retention, and
promotion approvals.
6. Complete scheduled/event/API/chained Dataflow trigger governance, reusable
template inheritance, Reporting publication, human reconciliation transforms,
and target resource/recovery evidence for large runs.
+77
View File
@@ -0,0 +1,77 @@
# GovOPlaN Frontend Layout Principles
GovOPlaN modules should choose their page layout by the kind of work the user is
doing, not by the repository that owns the feature.
These concise layout choices are one canonical input to the broader
[`INTERFACE_PATTERN_LANGUAGE.md`](INTERFACE_PATTERN_LANGUAGE.md). The current
route and rollout evidence lives in
[`INTERFACE_SURFACE_INVENTORY.md`](INTERFACE_SURFACE_INVENTORY.md).
## Structured Data Directories
Use a full-available-space workspace for structured data directories: files,
addresses, calendars, records, mailboxes, document stores, and similar domains
where the primary task is browsing, selecting, filtering, inspecting, and acting
on related objects.
Principles:
- The module route should use the full available content area.
- Do not add a separate page heading row above the main workspace.
- Prefer persistent navigation panes, such as tree panels, source panels, folder
panels, calendar list panels, or mailbox folder panels.
- Keep collection navigation and collection-level actions close to the relevant
pane header.
- In a list-detail workspace such as Scheduling, keep related lists stacked in
the left pane and use the remaining main pane for view/create/edit. A single
Add action stays in the relevant list-pane header and opens the common main
editor; it does not create an additional menu or launcher.
- Use bounded widths for navigation/list panes and let the main detail/content
pane take the remaining space.
- Keep filtering controls inside the pane they affect.
- Use overlays, toasts, or floating alerts for transient messages so the
workspace height does not change.
This pattern is appropriate when the user is working inside one coherent data
domain and needs spatial continuity.
## Workflow And Configuration Surfaces
Use the standard heading/menu/card visual language for workflow structures,
settings, administration, dashboards, and pages that collect essentially
unrelated areas.
Principles:
- A page heading and subnavigation are appropriate when the page explains a
task, workflow stage, or administrative area.
- Cards are appropriate for repeated independent panels, settings groups,
summaries, and dashboard widgets.
- Collapsible panels and segmented controls are appropriate when a dense
configuration area needs controlled disclosure.
- A collapsible card whose sole content is a table gives that table the full
available card body; avoid nested cards, duplicate padding, inner max-widths,
and nested scrolling.
- Avoid forcing workflow/configuration pages into a file-explorer style unless
the primary interaction is genuinely directory browsing.
This pattern is appropriate when the user is comparing or configuring separate
concerns rather than navigating one structured object space.
## Shared Components
Reusable layout components belong in `govoplan-core` WebUI. Modules may consume
shared components from core, but must not import another module's private UI
components directly.
When a module-specific component becomes generally useful, promote it to core
with a parameterized API before reusing it elsewhere.
Non-self-explanatory fields use Core `FieldLabel`; documented omissions must
name their accessible-label source. Explicit Discard and dirty navigation use
the same Core unsaved-changes dialog. Table action sets retain unavailable row
actions as disabled controls and reserve empty-state slots so Add remains
aligned. Use central feedback/dialog components; `window.alert` is not an
authorized product surface unless a product-owner-approved exception is first
recorded in the Core decision ledger.
+4 -3
View File
@@ -8,11 +8,11 @@ The same pattern is reusable outside GovOPlaN for any project where Codex works
The repository contains Gitea issue templates in `.gitea/ISSUE_TEMPLATE`, a pull request template in `.gitea/PULL_REQUEST_TEMPLATE.md`, and the label taxonomy in `docs/gitea-labels.json`.
The scripts infer this repository from `origin` (`git@git.add-ideas.de:add-ideas/govoplan.git`). Override inference when needed:
The scripts infer this repository from `origin` (`git@git.add-ideas.de:GovOPlaN/govoplan.git`). Override inference when needed:
```bash
export GITEA_URL=https://git.add-ideas.de
export GITEA_OWNER=add-ideas
export GITEA_OWNER=GovOPlaN
export GITEA_REPO=govoplan
export GITEA_TOKEN=...
```
@@ -23,7 +23,7 @@ The API scripts also read `GITEA_*` values from the target repository's `.env` f
GITEA_TOKEN=...
# Optional if origin inference is not enough:
GITEA_URL=https://git.add-ideas.de
GITEA_OWNER=add-ideas
GITEA_OWNER=GovOPlaN
GITEA_REPO=govoplan
```
@@ -140,6 +140,7 @@ Use one `type/*` label:
- `type/bug`
- `type/feature`
- `type/user-story`
- `type/task`
- `type/debt`
- `type/docs`
@@ -0,0 +1,555 @@
# Installation And Deployment Architecture
## Goal
A supported GovOPlaN installation starts with one downloaded, verified
bootstrap artifact. The administrator answers a bounded set of questions and
receives a working base system. Re-running the same tool repairs or
reconfigures that installation instead of creating unrelated state.
The canonical product journey remains
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
This document defines the deployer boundary and the first executable slice.
## First Executable Slice
`tools/deployment/govoplan-deploy.py` is a standard-library-only deployment
compiler and reconciler. It can be tested without installing GovOPlaN itself.
It currently supports:
- evaluation and self-hosted profiles;
- managed or external PostgreSQL;
- managed, external, or evaluation-only disabled Redis;
- disabled mail, an external relay declaration, or an evaluation-only
GreenMail service;
- durable local file storage, managed single-node Garage S3, or external
S3-compatible storage;
- an explicit HAProxy service that load-balances configured WebUI and API
replicas without access to the Docker socket;
- declarative API, WebUI, and worker replica counts while keeping migrations
and the scheduler singleton;
- Core, base, or full initial module selections;
- deterministic Compose JSON accepted by Compose v2;
- generated secrets stored in a private `0600` file;
- service-specific environment allowlists so infrastructure containers do not
receive unrelated application credentials;
- plan, render, doctor, status, apply, Kubernetes export, operation history,
and bounded recovery commands;
- an installation lock, migration-before-start ordering, readiness polling,
and an applied-state receipt;
- a durable hash-chained deployment journal captured before runtime mutation;
- PostgreSQL advisory serialization for Core and module migrations;
- runtime initialization that waits for exact configured migration heads
without mutating schema;
- runtime node registration, heartbeats, drain state, and a fenced scheduler;
- idempotent reconfiguration that preserves generated secrets;
- a keyed environment fingerprint that detects private binding changes without
writing secret values to plans or receipts;
- host CPU, memory, disk, entropy, architecture, Docker daemon, Compose,
listen-port, and external endpoint preflight checks;
- service removal without implicit data-volume deletion.
Create a local evaluation bundle:
```sh
./.venv/bin/python tools/deployment/govoplan-deploy.py init \
--directory /tmp/govoplan-evaluation \
--profile evaluation \
--postgres managed \
--redis managed \
--storage garage \
--mail test-mail \
--api-replicas 2 \
--web-replicas 2 \
--worker-replicas 2 \
--module-set base
```
Inspect the generated intent and host requirements:
```sh
./.venv/bin/python tools/deployment/govoplan-deploy.py doctor \
--directory /tmp/govoplan-evaluation
```
Change a component without rotating existing generated secrets:
```sh
./.venv/bin/python tools/deployment/govoplan-deploy.py configure \
--directory /tmp/govoplan-evaluation \
--redis external \
--redis-url 'rediss://:password@redis.example.org:6379/0'
```
The private installation directory contains:
| File | Purpose |
| --- | --- |
| `installation.json` | Versioned, non-secret desired state |
| `secrets.env` | Deployment-local secrets and external service bindings |
| `compose.json` | Deterministic generated Compose definition |
| `garage.toml` | Non-secret managed Garage server configuration |
| `load-balancer.cfg` | Non-secret HAProxy WebUI/API discovery configuration |
| `Caddyfile` | Non-secret managed-ingress route and ACME policy |
| `existing-proxy.json` | Exact upstream, trusted-source, header, and health contract for an operator-owned proxy |
| `plan.json` | Latest desired-state diff and readiness findings |
| `receipt.json` | Last successfully applied immutable identities |
| `distribution-manifest.json` | Canonical signed runtime/image selection adopted by the installer |
| `distribution-keyring.json` | Explicitly installed public trust anchor for runtime releases |
| `backup-evidence.json` | Signed provider-neutral coordinated backup and isolated-restore receipt |
| `backup-keyring.json` | Explicit public trust anchor for backup evidence authorities |
| `backup-verification.json` | Sanitized local verification/adoption receipt |
| `applied-state/` | Checksum-verified snapshot of the last healthy deployment bundle |
| `operations/<id>/` | Private hash-chained deployment progress and recovery evidence |
| `kubernetes.json` | Optional stateless multi-host Kubernetes export |
| `.deployment.lock` | Same-host operation exclusion |
The specification contract is
[`installation-spec.schema.json`](installation-spec.schema.json).
Build the same dependency-free tool as one downloadable artifact:
```sh
./.venv/bin/python tools/deployment/build-deployer-zipapp.py \
--output /tmp/govoplan-deploy.pyz
python /tmp/govoplan-deploy.pyz --help
```
To exercise reconciliation with locally available evaluation images:
```sh
python /tmp/govoplan-deploy.pyz init \
--non-interactive \
--directory /tmp/govoplan-evaluation \
--profile evaluation \
--api-image local/govoplan-api:test \
--web-image local/govoplan-web:test
python /tmp/govoplan-deploy.pyz apply \
--directory /tmp/govoplan-evaluation \
--allow-unverified-images \
--skip-pull
```
Those images must already contain the selected module set. The override exists
only to exercise local orchestration before release artifacts exist; it is
rejected for `self-hosted`.
## Runtime Distribution Boundary
The protected `Runtime Distribution` workflow builds GovOPlaN wheels first,
resolves architecture-specific third-party wheels into offline wheelhouses, and
then assembles the API images with `pip --no-index`. The target host never
clones Git repositories and neither runtime image performs network package
installation. Separate amd64/arm64 API and WebUI images are joined into OCI
indexes and run as non-root identities. The release assets include CycloneDX
application SBOMs, SLSA-style provenance, exact composition evidence, the
single-file deployer, its detached Ed25519 signature, and a signed, expiring
distribution manifest. Evidence generation and signing run through the
workflow's isolated release Python environment so their cryptographic tooling
is explicit and independent of packages preinstalled in the Actions runner.
The API image points Core at the migration scripts installed from the verified
wheel under `/opt/govoplan/runtime/govoplan_core_runtime`; migrations therefore
do not depend on a source checkout or the build host's Python installation
scheme.
Before publication, the exact amd64 and arm64 image manifests each run release
migrations against the pinned PostgreSQL image, reach API and WebUI readiness
as non-root/read-only processes, and complete a task through the pinned Redis
image and packaged worker. Sanitized per-platform smoke receipts are retained
as immutable release assets.
PostgreSQL and Redis indexes are resolved to untagged platform-child digests
before each smoke run. This keeps the evidence architecture-specific and
avoids retargeting one local Docker tag between incompatible platforms.
The CI host registers arm64 execution with an explicitly supplied,
digest-pinned `tonistiigi/binfmt` image immediately before the smoke. This
privileged helper is confined to the release runner and is never part of a
GovOPlaN target deployment or its runtime image set.
Because QEMU user-mode execution triggers Redis's arm64 host-kernel COW guard,
the arm64 smoke suppresses only `ARM64-COW-BUG` while persistence, snapshots,
and append-only files are disabled. Target Redis services never inherit this
test-only option.
The smoke also proves a bounded post-migration table contract and aborts as
soon as a required container exits, rather than allowing a dead process to
consume the full readiness timeout.
Ingress acceptance streams generated configuration into Docker-managed
volumes before starting the read-only containers. It therefore also works when
an Actions job reaches a host or remote Docker daemon through a mounted socket;
the drill never assumes that a job-container path is visible to that daemon.
The drill allocates explicit loopback-only host ports and verifies Docker's
host binding configuration, avoiding daemon-specific random-port shorthand
behavior. Because an Actions job and deployment containers may be Docker
siblings, functional HTTP/TLS checks run from the digest-pinned API image on
the deployment network instead of assuming the Docker host is job-local.
The dispatch-only `Runtime Ingress Drill` workflow exposes the same bounded
check independently so ingress changes can be diagnosed before an immutable
runtime publication; it accepts only digest-pinned Caddy, HAProxy, and API
images and has no push trigger.
The official Caddy binary carries the `NET_BIND_SERVICE` file capability. The
managed-ingress container therefore drops every capability and adds back only
`NET_BIND_SERVICE`; otherwise Linux rejects the binary at `execve` before its
high-port configuration can start. `no-new-privileges`, a read-only root
filesystem, and non-privileged container ports remain enforced.
The bounded setup helper writes only generated public configuration as root so
it can initialize a new volume; the actual HAProxy process retains the image's
non-root identity and runs read-only with all capabilities dropped.
The manifest contract is
[`runtime-distribution-manifest.schema.json`](runtime-distribution-manifest.schema.json),
and its separately distributed trust-anchor contract is
[`runtime-distribution-keyring.schema.json`](runtime-distribution-keyring.schema.json).
Publication is immutable: an existing Gitea release asset must have the same
size and SHA-256 digest or publication fails.
Adopt a downloaded or prefetched release only after obtaining the manifest
digest and trusted keyring through the documented independent channel:
```sh
python3 govoplan-deploy.pyz verify-release \
--directory /srv/govoplan/installation \
--manifest ./distribution-manifest.json \
--manifest-sha256 "$(cut -d' ' -f1 distribution-manifest.json.sha256)" \
--trusted-keyring ./distribution-keyring.json \
--adopt
```
`doctor` and `apply` rehash both stored files, re-run OpenSSL Ed25519
verification, enforce channel/expiry/revocation, compare every selected image,
and prove that all enabled module ids occur in the signed image composition.
An offline image index can bind prefetched OCI archives to the same exact image
references and archive hashes; mutable tags or incomplete bundles are rejected.
## Current Production Gates
The first immutable production-distribution baseline is published as
[`v0.1.14`](https://git.add-ideas.de/GovOPlaN/govoplan/releases/tag/v0.1.14)
from source commit `1f039dd39c1ce2672f4978c8abc6dff862ef1445`. Runtime
Distribution [run #459](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/459)
proved migrations, schema compatibility, non-root API/Web readiness, and worker
delivery/shutdown on both `linux/amd64` and `linux/arm64`. Its signed manifest
has SHA-256
`d703267e01855dee63200cb20921c91c3f95fbff550c8ca76e9a35cba3f69109`
and pins these runtime indexes:
- API: `git.add-ideas.de/govoplan/runtime-api@sha256:197ed01790986f2bc927eaa5d8348fa118702e5d2dc05feb851fc2643c23764a`
- WebUI: `git.add-ideas.de/govoplan/runtime-web@sha256:e936cca124f1fad29a067834cf17627d4c236410fdc3fa129e0ccb26b8193812`
The signed bootstrap has SHA-256
`1ff946fba82b0895d153b23352d06e30fe18388450dfd37fed6fb9912310efc5`
and key id `runtime-distribution-2026-01`. The managed-ingress boundary passed
the same publication run and the independently dispatchable Runtime Ingress
Drill [run #458](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/458).
Every later release must renew this evidence; the following target-specific
gates remain:
1. **First administrator.** Production needs a one-time, restricted enrollment
identity. The development bootstrap must not be enabled in production.
2. **Image/module composition.** The deployer enforces the signed
composition. A selected module not shipped by that release cannot be
enabled.
3. **Deployment agent.** Web updates need a separate privileged reconciler with
a typed command allowlist. The API and browser must never receive the Docker
socket or arbitrary shell access.
4. **Target reachability evidence.** Managed Caddy ingress and the
existing-proxy contract are implemented. A production claim still requires
running `doctor` from the target host after public DNS/firewall changes and
retaining public TLS/readiness evidence for that deployment.
`apply --allow-unverified-images` is therefore restricted to the evaluation
profile. It explicitly acknowledges both mutable image identities and
unverified image/module composition. It is a local test escape hatch, not a
production setting.
## Component Choices
### PostgreSQL
`managed` creates a persistent PostgreSQL container and private generated
credentials. `external` requires an explicit `DATABASE_URL`; switching from
managed to external cannot reuse the old `postgres` Docker hostname
accidentally.
Interactive entry hides external URLs because they commonly contain
credentials. For unattended automation, provide them through a protected
operator mechanism and avoid storing secret-bearing flags in shell history.
Production policy should support external managed databases and local managed
PostgreSQL equally at the application boundary. Backup, point-in-time recovery,
high availability, and major-version upgrades remain deployment properties.
### Redis
`managed` creates an authenticated, append-only Redis container. `external`
requires an explicit `REDIS_URL`. `disabled` is evaluation-only and disables
workers while recording the single-process login-throttle risk acknowledgement.
`doctor` performs a bounded TCP connection check for external PostgreSQL,
Redis, and S3 endpoints. This verifies DNS, routing, and that the port accepts a
connection; it is not an authentication or semantic health check.
Production base installations include Redis because durable queues, distributed
throttling, notifications, scheduled work, and transactional event delivery
must survive API restarts.
### Mail
The first slice distinguishes:
- `disabled`;
- `external-relay`, which records the infrastructure decision but leaves Mail
server/credential creation as a visible post-install task;
- `test-mail`, an evaluation-only GreenMail service.
A bundled production mail server is intentionally not a default. Operating one
requires DNS, reverse DNS, TLS, DKIM, SPF, DMARC, reputation, abuse handling,
queue monitoring, and upgrade policy. A later profile may support an
operator-selected MTA/relay, but it must expose these requirements rather than
presenting a container as a complete mail service.
### File Storage
`local` uses a durable Compose volume and is appropriate for one-host
installations. `garage` provisions Garage 2.3 in its supported single-node
bootstrap mode, generates a private application key and bucket, and connects
the Files S3 backend to the exact installer-owned internal endpoint. The
managed trust marker cannot authorize another S3 host.
Garage metadata and object data use separate persistent volumes. `s3` requires
an external endpoint, region, access key, secret key, and bucket values.
Self-hosted external S3 endpoints must be clean HTTPS origins. The generated
runtime explicitly sets `FILE_STORAGE_S3_ENDPOINT_TRUSTED=true` for that
operator-selected endpoint. The trust flag is not accepted for local storage
and cannot be combined with installer-managed Garage trust.
Local storage must be included in backup and restore drills. Horizontal API or
worker scale-out requires shared/object storage. The managed Garage profile is
persistent but has no data redundancy; availability-sensitive installations
must use a tested multi-node Garage cluster or another external S3 service.
### Load Balancing And Replicas
The generated Compose topology publishes only `load-balancer` for local or
existing-proxy profiles. With managed ingress, only Caddy publishes host ports
and HAProxy remains private. HAProxy uses
Docker DNS service discovery to distribute public traffic across WebUI replicas
and WebUI API proxy traffic across API replicas. The WebUI and API services do
not publish host ports. HAProxy has no Docker socket and discovers only the
bounded replica slots rendered into `load-balancer.cfg`.
Replica counts are desired state:
```sh
./.venv/bin/python tools/deployment/govoplan-deploy.py configure \
--directory /tmp/govoplan-evaluation \
--api-replicas 3 \
--web-replicas 2 \
--worker-replicas 4
./.venv/bin/python tools/deployment/govoplan-deploy.py apply \
--directory /tmp/govoplan-evaluation
```
Workers are queue consumers, so they are scaled through Redis rather than put
behind an HTTP load balancer. Migrations are serialized with a deployment-wide
PostgreSQL advisory lock. The Celery scheduler is run under a renewable,
fencing-token lease. Multiple API replicas are rejected when Redis is disabled
because distributed throttling and queued work cannot then be shared correctly.
### Public Ingress And TLS
A self-hosted installation is fail-closed until one of these boundaries is
selected:
- `existing-proxy` publishes HAProxy at `listen.address:listen.port` and emits
`existing-proxy.json`. The operator-owned proxy must use the recorded host,
upstream, and health paths. Only the exact CIDRs listed with repeated
`--trusted-proxy-cidr` values may supply `X-Forwarded-*` headers. Public
proxy addresses must be `/32` or `/128`; private ranges are limited to `/24`
or narrower for IPv4 and `/64` or narrower for IPv6.
- `managed` publishes Caddy on the selected HTTP/HTTPS ports, redirects HTTP to
HTTPS, obtains and renews certificates through ACME, and keeps certificate
material exclusively in the private `caddy-data` and `caddy-config` volumes.
The application containers receive no ACME account or TLS private keys.
Example existing-proxy configuration:
```sh
python govoplan-deploy.py configure \
--directory /srv/govoplan \
--ingress existing-proxy \
--trusted-proxy-cidr 172.20.0.7/32
```
Example managed configuration:
```sh
python govoplan-deploy.py configure \
--directory /srv/govoplan \
--ingress managed \
--acme-email operator@example.org
```
Before managed ingress starts, public A/AAAA records must resolve to the target
and inbound TCP 80/443 must reach it. Existing-proxy mode additionally requires
the public proxy and valid certificate to be reachable before apply. After a
successful receipt, `doctor` reports DNS resolution, certificate validity and
remaining lifetime, public `/health/ready`, and the private HAProxy/WebUI path
as separate checks. Reconfiguration retains the certificate volumes; bundle
rollback never deletes or exposes their contents. Include both Caddy volumes
in coordinated backup and restore evidence.
This is same-host scaling. Docker Compose uses a bridge network and does not
place containers on another machine. See
[Scaling And Multi-Host Deployment](SCALING_AND_MULTI_HOST_DEPLOYMENT.md) for
the supported topology and promotion path.
## Reconfiguration Semantics
`installation.json` is desired state. `receipt.json` is the last successfully
applied state. `plan` compares their canonical hashes and service sets.
- Adding a managed component creates its service and persistent volume.
- Removing a component removes its service container on apply.
- Volumes are retained by default; deleting data requires a separate,
deliberately destructive workflow.
- Existing generated credentials are retained unless an explicit future rotate
operation is requested.
- Private configuration changes are represented by a keyed fingerprint in the
plan and receipt; plaintext values are never copied there.
- Managed-to-external transitions require the new endpoint in the same
operation.
- Migrations run as a one-shot service before API/worker replacement.
- API, worker, and scheduler start commands wait for exact configured migration
heads; only the migration command is permitted to change schema.
- API and worker replicas register their software/module composition and
heartbeat in PostgreSQL. Ops can request and cancel a node drain.
- The first upgrade from a direct WebUI host port stops that legacy WebUI
container immediately before HAProxy claims the same endpoint.
- Health must recover before a new receipt and applied-state snapshot are
committed.
Every apply operation is journalled before image pulls or runtime mutation. A
failure before migration may restore a verified previous bundle. Once migration
starts, recovery is forward-only unless an independently verified database
backup is restored. See
[Recovery And Rollback Guarantees](RECOVERY_AND_ROLLBACK_GUARANTEES.md).
Production updates still need operator/provider-created coordinated backup and
restore evidence, a database compatibility declaration, and a
deployment-specific drain policy. The deployer now verifies and enforces the
signed evidence before migration, but does not manufacture backups or receive
provider administration credentials. See
[Backup And Restore Evidence](BACKUP_AND_RESTORE_EVIDENCE.md).
## Stateless Kubernetes Runtime
`render-kubernetes` exports the application tier for a standard orchestrator.
It requires external PostgreSQL, Redis, and S3 and emits no stateful service or
secret value:
```sh
python tools/deployment/govoplan-deploy.py render-kubernetes \
--directory /srv/govoplan/default \
--namespace govoplan \
--secret-name govoplan-runtime
```
The output includes a release-specific migration Job, database-head wait init
containers, API readiness/liveness probes, rolling Deployments, Services, Pod
disruption budgets, a tokenless ServiceAccount, and one fenced scheduler. Apply
the named Secret through the cluster's secret manager and review ingress proxy
CIDRs before deployment. A release-changing export requires adopted backup
evidence and carries only its sanitized digest and identifiers as Job
annotations. Detailed rollout and scaling rules live in
[Scaling And Multi-Host Deployment](SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
## Recovery Commands
List durable deployment operations:
```sh
python tools/deployment/govoplan-deploy.py operations \
--directory /srv/govoplan/default
```
Recover a selected failed operation after reviewing its stage evidence:
```sh
python tools/deployment/govoplan-deploy.py recover \
--directory /srv/govoplan/default \
--operation-id <operation-id>
```
The command reports whether it restored the pre-migration applied bundle,
requires forward recovery, or needs manual intervention. Add `--apply` only
after that decision has been reviewed.
## Web Update Boundary
The intended update path is:
1. Ops reads the non-secret installation receipt and reports management mode,
current release, component health, and update availability.
2. An authorized administrator asks Core to create a typed deployment request,
for example `reconcile_release` or `rollback_release`.
3. Core persists the reviewed immutable plan, actor, expected current receipt,
and idempotency key.
4. A separately deployed, narrow deployment agent claims the request.
5. The agent verifies signatures/digests, acquires a fenced deployment lock,
backs up, pulls, migrates, reconciles, probes health, and writes evidence.
6. Ops presents durable progress and the resulting receipt.
The agent owns container-runtime access. It accepts no command strings from the
browser and has no domain-data permissions. Installations managed by Kubernetes,
systemd, or another external orchestrator expose read-only status and an export
of the reviewed update recipe instead of a non-functional update button.
## Distribution Workflow
The downloadable entry point is a reproducible release asset: sorted source
paths, fixed ZIP metadata, fixed compression settings, and identical source
bytes produce an identical zipapp regardless of checkout timestamps. Obtain the
zipapp, detached signature, checksum, and trusted public keyring through
independently authenticated paths before execution:
```sh
curl --proto '=https' --tlsv1.2 --fail --location \
https://git.add-ideas.de/GovOPlaN/govoplan/releases/download/vX.Y.Z/govoplan-deploy.pyz \
--output govoplan-deploy.pyz
sha256sum --check govoplan-deploy.pyz.sha256
python3 - <<'PY'
import json
from pathlib import Path
keyring = json.loads(Path("distribution-keyring.json").read_text())
active = [key for key in keyring["keys"] if key["status"] == "active"]
if len(active) != 1:
raise SystemExit("expected exactly one active runtime release key")
Path("runtime-release-public.pem").write_text(active[0]["public_key_pem"])
PY
openssl pkeyutl -verify -pubin -inkey runtime-release-public.pem -rawin \
-in govoplan-deploy.pyz -sigfile govoplan-deploy.pyz.sig
python3 govoplan-deploy.pyz init
```
The zipapp has no GovOPlaN package dependency. It accepts a bounded HTTPS
manifest or a prefetched file, requires an independently supplied SHA-256
digest and explicit trusted keyring, and executes OpenSSL with a fixed argument
vector for Ed25519 verification. It never evaluates downloaded shell text or
accepts an arbitrary command string.
## Verification
Run the focused tests:
```sh
./.venv/bin/python -m unittest -v tests.test_deployment_installer
```
The tests cover signed release adoption, tamper/expiry/revocation/unknown-key
rejection, architecture composition, offline image integrity, profile
restrictions, secret persistence, external endpoint
requirements, managed Garage bootstrap, S3 policy, replica validation, HAProxy
discovery configuration, Compose service selection, secret non-disclosure,
service-specific environment isolation, private file modes, external endpoint
preflight, first-plan generation, apply ordering, receipt idempotency,
hash-chained recovery journals, migration recovery boundaries, and stateless
Kubernetes rendering.
@@ -0,0 +1,579 @@
# Institutional Governance Target Architecture
## Status and sources
This document is the accepted architectural reconciliation of two product
concepts prepared outside the repositories:
- `govoplan_concept_dev.md`
- `software_big_picture.md`
The source concepts describe GovOPlaN as an operational governance platform for
public institutions. This document merges that direction with the implemented
platform state as of 2026-08-01. It is the canonical repository version of the
direction. Gitea issues remain the source of truth for delivery state.
Read this together with:
- [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
- [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md)
- [Module Contracts and Install Boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
- [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md)
- [Capability and Infrastructure Fit](CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
- [Core Module Architecture](../../govoplan-core/docs/MODULE_ARCHITECTURE.md)
- [Core External References and Integration Maturity](../../govoplan-core/docs/EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md)
- [Core Action, Effect, and Automation Layer](../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md)
## Decision
GovOPlaN is a configurable **institutional governance and operations layer** for
public institutions. It should model the institution, coordinate its work,
connect its specialist systems, and preserve why and under whose authority an
action occurred.
GovOPlaN is not intended to become one universal ERP, DMS, groupware suite,
workflow editor, or specialist procedure. It should own the governance concepts
that must remain understandable across those systems and support native,
external, mirrored, synchronized, overlay, and link-only operation explicitly.
This changes product emphasis, not the modular architecture:
1. The current kernel and optional-module model remains.
2. Existing domain owners keep their data and behavior.
3. Cross-module semantics become explicit, versioned contracts.
4. Successful compositions become product and sector packages, not forks or
monolithic replacement applications.
5. Repository creation follows a proof threshold; a noun in the information
model does not automatically require a module.
## What recent work already supersedes
The source concepts predate several implemented foundations. These items are
accepted as the current baseline and must not be reopened as greenfield work.
| Concept requirement | Reconciled current state |
| --- | --- |
| Slim kernel plus installable modules | Implemented through entry-point discovery, `ModuleManifest`, migrations, capabilities, interfaces, WebUI contributions, and permutation checks. |
| Versioned cross-module contracts | Implemented through named interface ranges, capability protocols, static workspace graph checks, activation validation, and release checks. |
| Separate headless workflow runtime and editor | Implemented as `govoplan-workflow-engine` and optional `govoplan-workflow`. Module-owned workflow baselines are versioned and reconciled without replacing local overrides. |
| Provider-neutral external references | Implemented in Core with stable external identity and cumulative integration maturity from discovery through replacement. |
| Governed asynchronous effects | Implemented foundations include the action/effect contract, transactional platform event outbox, module outboxes, idempotency, outcome-unknown states, reconciliation, and worker health. Coverage still varies by provider. |
| Acting identity, function assignment, mandate, and ownership recovery | Implemented foundations span Identity, Organizations, IDM, Access, Mandates, generic ownership transfer/recovery, and audit provenance. Effective competence now resolves through a tenant-bound Mandate capability. |
| Governed data foundations | Connectors, Datasources, Dataflow, Reporting, and Search now exist. Datasources already provides live/cached/static modes, staging, immutable materializations, and publication contracts. |
| Task-focused projections and configured documentation | Views, view-surface declarations, configurable dashboards, and manifest-driven user/admin documentation exist. Rollout and content depth remain incremental. |
| Encryption as an optional capability | Core defines provider-neutral contracts; `govoplan-identity-trust` persists public device keys, key epochs and assurance evidence; and `govoplan-encryption` persists opaque vault/key lifecycle, versioned protection envelopes, quorum recovery authorization, outcome-unknown reconciliation, and disable preflight. A bundled local AES-256-GCM server-envelope provider now stores wrapped key material and supports Files/Postbox protection, rotation, revocation, destruction, rewrap, tamper detection, and fail-closed restore behavior. It is explicitly neither E2EE nor a certified KMS/HSM. |
| Search without mandatory OpenSearch | PostgreSQL-backed, permission-aware search and module provider contracts exist; OpenSearch remains an optional adapter. |
| Scale-out and recovery architecture | Stateless API/worker, shared database/object storage, event delivery, deployment, and recovery contracts are documented and partly exercised. Production profiles and drills remain active work. |
The institutional semantics, provider declaration gate, and first product
compositions described here are now implemented. Subsequent work is
**product depth and stronger maturity evidence**, not another runtime rewrite
or an unimplemented architecture boundary.
## Implementation status (2026-08-01)
The architecture contract is implemented as a bounded, executable vertical
slice. The portfolio declarations and provider governance gates apply to the
whole workspace, while the four semantic domains whose repository thresholds
were proven now have independent persistent owners:
| Area | Implemented state | Remaining rollout |
| --- | --- | --- |
| Module portfolio metadata | Core validates versioned architecture layer/kind, maturity evidence, known limits, ownership boundaries, authority modes, reference packages, target-tested providers, and migration/upgrade/recovery/security/operations documentation. | Complete for all 62 source manifests. Focused and release checks enforce `--require-architecture`; a new module cannot enter the workspace without truthful declaration and repository-local evidence. |
| External providers | Core validates provider objects/field groups, operations, integration maturity, source authority, bounded reads, freshness/health, idempotency, conflicts, outcome-unknown handling, evidence, correction, reconciliation, outage, classification, purpose, retention, and secret handling. Addresses/CardDAV, Files remote storage, Mail SMTP/IMAP, Calendar CalDAV/ICS/Graph/EWS, and Connectors tabular/sanctions providers declare the contract and tenant-bounded secret-free runtime state. | Registry validation rejects any declared external provider without a sanitized state provider. Future adapters must cross the same gate before activation. |
| Institutional context | Core provides versioned temporal, actor/representation, institution/unit/function/task/mandate/jurisdiction/service/case/party/work-item/workflow/approval/decision/record, legal-basis, evidence, information-governance, external-source, presentation, and geographic references. Events, automation actions, audit records, and the transactional Audit outbox preserve the envelope. | Owning modules must progressively require the relevant subset for consequential operations. |
| Semantic provider contracts | Provider-neutral DTOs and protocols cover Mandate resolution, versioned Service definitions, procedure Parties/representation, and formal Decisions. `govoplan-mandates`, `govoplan-services`, `govoplan-parties`, and `govoplan-decisions` now persist immutable revisions behind those contracts with tenant isolation, bounded reads, replay safety, OCC, migrations, uninstall guards, permissions, APIs, capability documentation, and recovery documentation. | The owners are deliberately headless. Procedure-specific UI remains with consuming modules. |
| Formal-outcome proof | Committee persists bodies, meetings, agenda items, minutes, lifecycle events, and an optional protected local Decision projection. Voting separately owns immutable ballot definitions, frozen electorates, recorded casting/replacement, deterministic tally, certification, challenge, annulment, and provider-backed assurance profiles. Committee consumes `voting.ballots` and retains only deliberation linkage and verified aggregate outcome. A bundled `local_confidential` reference provider encrypts server-readable casts outside native ballot rows and exposes only receipts plus aggregate evidence to Committee. | Native recorded ballots remain reconstructable, and the reference confidential provider is neither secret nor certified: its server can decrypt casts while tallying. Secret/electronic-ballot protocol selection, custody, legal acceptance, independent review, and target evidence remain explicit product decisions. |
| Service-to-case proof | Services owns the persistent exact definitions consumed by Portal discovery and Cases intake. Forms owns immutable multi-page/conditional/localized schemas, accessibility assessment and package fragments. Forms Runtime owns definition-aware drafts, validation, submission receipts, status/evidence history, and durable native Case/Workflow handoffs with intent-before-effect and outcome-unknown reconciliation. Portal delegates URL, Case, Form, or Workflow launch to an installed owner while retaining exact Service/Form provenance. | Anonymous intake and concrete attachment/signature providers remain product depth. Portal and Runtime fail closed and explain any absent launcher, target capability, or provider prerequisite. |
| Generic approvals and process execution | Approvals persists exact-subject chains, delegation, separation of duties, quorum, signatures-as-evidence, escalation, OCC, and replay-safe decisions. Campaign proves an exact-version delivery gate. Workflow Engine owns immutable definitions/instances plus API, schedule, event and parent triggers, durable timer/event waits, scale-out claims, current-authority rechecks, and idempotent starts independently of the optional editor. | Policy-authored Approval template selection, concrete signature providers, cron adapters, and broader BPMN execution profiles are product/provider depth on explicit contracts. |
| Device trust and content protection | Identity Trust separates public device keys, epochs, assurance and key-access decisions from login and Access. Encryption separates resource ownership from opaque provider key custody, versioned envelopes, migration evidence, quorum recovery authorization and uninstall proof. Its local server-envelope provider and Files/Postbox adapters prove ciphertext persistence, integrity, rotation/rewrap and fail-closed key loss without leaking plaintext keys across the capability boundary. | Reviewed KMS/HSM/client providers, more owner adapters, target backup/restore/key-loss drills, and E2EE interoperability/certification remain required before stronger deployment claims. |
| Procedure-party proof | Parties persists effective procedure roles, frozen contact snapshots, and representation powers. Existing powers cannot disappear or be silently rewritten; explicit OCC-guarded revocation is required. Cases resolves the provider capability and excludes expired/revoked authority from downstream delivery. | Procedure modules still decide which contextual fields and actions to present. |
| Integrated institutional journey | The executable `product.service-to-decision` fixture uses real SQL-backed Services, Cases, Parties, Mandates, Committee, and Decisions providers. It carries one exact Service version through persisted Case intake, representation and frozen delivery authority, effective Mandate resolution, a body/meeting/agendum/vote/minute sequence, a persisted formal Decision, confirmed Postbox effect, Audit/record evidence, remedy/review, and protected reconstruction. A second executable path proves Portal to exact Form revision, persisted submission, and idempotent replay. | This is architecture and composition evidence. Signed, release-bound target accessibility, privacy, security, operator, delivery-provider, and recovery-drill evidence is still required before the package may claim `reference_ready`. |
| Governed data catalogue | Datasources stores typed governance metadata, exposes bounded tenant-scoped filters and update APIs/UI, carries governance through staging, and snapshots it into immutable materializations. Reporting now persists immutable dataset, semantic-model, report, quality-plan, saved-view, and schedule revisions; executes typed semantic queries with quality gates, access checks, replay, pivoting, export/import assessment, and provenance; and exposes the governed analytical WebUI. | Rich dependency/impact traversal, additional expression functions, and policy-specific field visibility can grow on the established contracts without moving connector, transformation, or source ownership. |
| Portfolio and change governance | Projects now persists tenant-safe, immutable portfolio/project/milestone revisions with OCC, replay, lifecycle rules, restricted memberships, Search ACL indexing, outcomes, benefits, dependencies, capacity assumptions, change impact, and institutional references. Its WebUI exposes the planning catalogue and core planning fields. | Advanced planning structures already accepted by the API can receive deeper specialized editors without creating a second Policy, Reporting, Resources, or Goals owner. |
| Product/package governance | Signed configuration packages distinguish reference, product, sector, deployment, and integration classes; preserve parent/evidence provenance; prevent derived packages from loosening constraints; and preflight provider authority, maturity, exact binding, health, freshness, and recovery expectations. Executable product manifests now exist for governed communication and governed data/assurance and are checked in the module matrix. | Both artifacts deliberately remain product-class until target, accessibility, privacy, security, operations, and recovery evidence justifies reference readiness. |
| Projection and release | Platform metadata, signed module catalogs, release synthesis, Ops, and role-aware Docs retain and display architecture/provider declarations. Module-owned state providers add bounded configured/active, authority, health, freshness, conflict, recovery, and observation state; ordinary-user Docs omits binding detail. Static checks validate evidence paths, and the WebUI build verifies consuming types. | Runtime-state adoption and broader portfolio presentation follow truthful provider declaration rollout. |
The implementation deliberately keeps shared reference contracts in Core and
domain tables in their owners. It does not claim unsupported release maturity:
the four extracted owners and package remain `vertical_slice`/`product` until
target evidence supports a stronger claim. Gitea remains authoritative for
feature depth beyond this architecture contract.
## Target capability layers
The layers describe ownership and dependency direction. They are not navigation
groups and do not imply that every installation exposes every module.
| Layer | Responsibility | Current owners and declared directions |
| --- | --- | --- |
| 0. Runtime and meta | Composition, release, migrations, shared contracts, operations, deployment | Core, meta repository, Admin, Ops |
| 1. Institutional foundation | Institution, tenant, identity, organization, function, authority, access, trust | Tenancy, Identity, Organizations, IDM, Access, Identity Trust, Encryption, Mandates |
| 2. Governance and accountability | Policy, audit, risk, control, explainability, configured projection | Policy, Audit, Risk Compliance, Docs, Views, Search, Decisions |
| 3. Human work and procedure | Intake, cases, tasks, approvals, process execution and editing | Services, Forms, Forms Runtime, Cases, Parties, Tasks, Approvals, Workflow Engine, Workflow, Tickets |
| 4. Communication and participation | Delivery, participation, scheduling, channels, consultation | Portal, Postbox, Notifications, Mail, Campaign, Calendar, Scheduling, Poll, Appointments, Booking, Consultation, Committee, Addresses, Distribution Lists |
| 5. Content, records, and evidence | Managed content, templates, records, knowledge, disclosure | Files, Templates, DMS, Records, Wiki, Transparency, Certificates |
| 6. Data, reporting, and integration | Source access, staging, transformation, search, analytics, protocols | Connectors, Datasources, Dataflow, Reporting, Dashboard, REST, SOAP, XOE/V, XTA/OSCI, FIT-Connect, XRechnung, ERP adapters |
| 7. Domain capabilities | Reusable public-sector subject matter | Projects, Procurement, Contracts, Grants, Resources, Assets, Facilities, Learning, Payments, Ledger, Permits, Inspections, Evaluation, Helpdesk |
| 8. Product and sector packages | Versioned compositions, terminology, forms, processes, controls, reports, integration profiles | Signed configuration packages and reference packages; not runtime modules by default |
## Canonical institutional semantics
The connected model must keep these concepts distinct even where one UI
combines them.
| Concept | Canonical answer | Owner or direction |
| --- | --- | --- |
| Institution and tenant | In which governed installation and tenant does work occur? | Tenancy and Organizations |
| Organization and unit | Where is responsibility situated? | Organizations |
| Function | Which named organizational responsibility can an incumbent hold? | Organizations |
| Identity and account | Who is the person or machine, and through which account do they act? | Identity and Access |
| Function assignment | Who holds or represents a function, for which interval and source? | IDM |
| Role and permission | What application behavior may the acting principal perform? | Access, constrained by Policy |
| Mandate and jurisdiction | Why is an institution, unit, or function competent to act on this subject, territory, population, or interval? | Mandates |
| Service | What governed promise can an institution offer, to whom, under which prerequisites, evidence, channel, deadline, and responsibility? | Services; Portal presents it |
| Case | Which concrete administrative matter is being handled? | Cases |
| Party | In what procedural capacity does a person or organization participate, and who may represent or receive for it? | Parties; Identity/Organizations remain the subject owners |
| Work item | What must a responsible actor do next? | Tasks and domain modules |
| Workflow | How is work coordinated, including waits, human hand-offs, and governed actions? | Workflow Engine; Workflow is the optional editor |
| Approval | Has a proposed action passed a configured review or separation-of-duties gate? | Approvals |
| Decision | What formal institutional outcome was reached, by which competent authority, on which facts, rules, evidence, reasoning, and review path? | Decisions |
| Evidence and record | What proves the input, state, action, effect, correction, and retained institutional memory? | Domain owner, Files/DMS/Records, and Audit |
### Extracted semantic modules
Four horizontal concepts passed the repository proof threshold. Their Core
DTOs and provider protocols remain neutral; their persistent data, lifecycle,
security, APIs, migrations, and recovery behavior now live in independent
repositories.
#### Mandates
Mandates should own public or internal tasks, jurisdiction, responsibility,
decision/signature authority, legal or organizational basis, and effective
history. Organizations continues to own structures and functions; IDM owns
incumbency; Access owns permissions; Policy owns constraints.
`govoplan-mandates` answers: *Was this function competent to act for this case
at the relevant time, and on what basis?* Its resolver evaluates effective
time, task, authority, unit, function, jurisdiction, subject, conflicts, legal
basis, and evidence deterministically. Missing or ambiguous authority fails
closed.
#### Services
Services should own versioned service definitions: audience, prerequisites,
legal basis, evidence, fees, deadlines, channels, responsible unit/function,
jurisdiction, forms, case/workflow/result bindings, remedies, service levels,
and publication status. Portal presents and starts services but should not own
their institutional definition.
`govoplan-services` now owns those exact versioned definitions. Portal is the
first presentation consumer and Cases freezes the selected revision into its
intake context. Availability is an independent capability so publication does
not imply that all runtime prerequisites are satisfied.
#### Parties
Parties should own procedure-local roles and relationships: applicant,
respondent, beneficiary, representative, joint applicant, delivery recipient,
power or authority to represent, and permitted/preferred channels for the
matter. Identity answers who the subject is; Organizations answers which
institutional unit it is; Addresses owns contact points; Parties answers how
the subject participates here.
`govoplan-parties` owns the shared effective-dated lifecycle. Cases retains a
bounded compatibility projection only when the module is absent; that fallback
contains no representation lifecycle and cannot silently become a second
authority source.
#### Decisions
Decisions should own formal outcomes: subject, type, competent authority,
facts, evidence, applicable rule versions, reasoning, operative result,
conditions, effect, delivery/publication, remedy/review, correction, revocation,
and links to observed effects. Approvals own review gates; Poll owns response
collection; Committee owns deliberation, meetings, and votes; Workflow owns
coordination.
`govoplan-decisions` owns the persistent lifecycle and protected reconstruction
surface. Committee supplies deliberation context and records through the
provider capability. Consumers retain exact Decision references without
gaining table access.
## Source authority and integration maturity
Two independent dimensions must be recorded. They must not be collapsed into a
single `sync` flag.
### Source-authority mode
| Mode | Meaning |
| --- | --- |
| `native_authoritative` | GovOPlaN owns the authoritative object and lifecycle. |
| `external_authoritative` | The external system owns the object; GovOPlaN reads or acts through it. |
| `external_mirror` | The external system is authoritative and GovOPlaN keeps a governed local projection or immutable snapshots. |
| `governed_sync` | Both sides may change supported fields under explicit conflict and reconciliation rules. |
| `governance_overlay` | GovOPlaN owns policy, responsibility, evidence, or coordination around an externally executed object. |
| `linked_reference` | GovOPlaN keeps only a stable link and minimal display/provenance metadata. |
Authority may be declared per tenant, organization, service, object type,
object, field group, or process step. A broad default must not hide a narrower
override.
### Integration maturity
The implemented maturity ladder remains `discover`, `link`, `search`, `read`,
`publish`, `synchronize`, `migrate`, and `replace`. Maturity says what an
adapter can do. Source-authority mode says who owns truth in a particular
configuration. For example, a connector may support `synchronize`, while a
tenant deliberately configures it as `external_mirror`.
### Provider declaration
Every provider that reads or causes external effects must declare:
- owned object and field groups;
- supported source-authority modes and integration maturity;
- read, write, delete, search, preview, and dry-run operations;
- revision/concurrency tokens, freshness, health, and bounded-read limits;
- idempotency, retry, timeout, conflict, and outcome-unknown behavior;
- evidence, audit, correction, rollback/compensation, and reconciliation paths;
- degraded and outage behavior;
- classification, purpose, retention, and secret-handling requirements.
The common provider declaration composes the external-reference, action/effect,
connector-lifecycle, capability, operational-check, and documentation
contracts. Core, release tooling, Ops, Docs, and configuration-package
preflight validate it; Registry refuses to activate a declared external
provider without bounded, sanitized runtime state.
## Cross-cutting contracts
The following contracts are mandatory for consequential domain objects. They
should be shared reference DTOs and provider protocols, not shared domain
tables in Core.
1. **Time and history:** valid-from/to, recorded-at, superseded-at, revision,
change reason, and stable identity.
2. **Actor and representation:** real account/identity, system or service
account, represented account/function/party, delegation or power, and
mandate reference.
3. **Institutional context:** tenant, institution, organization unit, function,
task/mandate, jurisdiction, service, case, and decision references.
4. **Legal and policy basis:** typed, versioned references to rules,
obligations, policies, exceptions, and the effective decision source.
5. **Requested and observed effect:** intent, approval, dispatch, possible
execution, confirmation, reconciliation, correction, and terminal evidence.
6. **Evidence and provenance:** source, version, checksum, derivation,
responsible actor, timestamps, and inspection links.
7. **Information governance:** classification, purpose, legal basis, retention,
hold, minimization, and disclosure state.
8. **External source:** system/profile/object identity, authority mode,
maturity, version, freshness, health, and conflict state.
9. **Presentation:** language, accessibility, channel, explanation, and
configured availability.
Existing contracts already cover substantial parts of items 1, 2, 5, 6, 8,
and 9. New work should extend those contracts instead of creating parallel DTO
families.
## Existing module direction changes
### Datasources becomes the governed data and register catalogue
The implemented live/cached/static, staging, immutable materialization, and
publication model includes typed governance metadata for owner/steward,
authoritative source and authority mode, legal basis and purpose, semantic
definition, quality and freshness policy, classification, transfer agreement,
correction process, affected services/processes, and dependent flows,
reports, controls, and decisions. Connector credentials and protocol behavior
remain outside Datasources.
### Projects grows into portfolio and change governance
The Projects boundary already includes portfolios and goals. Extend it through
versioned objectives/outcomes, dependencies, capacity, benefits, change impact,
and links to mandates, services, risks, contracts, resources, and indicators.
Do not create a separate Goals module before more than one domain proves an
independent goal lifecycle.
### Reporting becomes evidence-backed institutional measurement
Every report, measure, and indicator should explain the institutional question
or obligation it serves, owner, source/materialization and flow revision,
freshness/quality, calculation version, visibility/purpose limits, publication,
and decisions or actions that consumed it. Reporting owns presentation and
execution; source and transformation owners retain their domains.
### Risk Compliance becomes the horizontal assurance model
Sanctions screening remains a complete vertical slice. The broader reusable
model is:
```text
Obligation -> governed object -> risk -> control -> evidence -> finding -> measure -> effectiveness review
```
Risk Compliance now persists that effective-dated, immutable-revision assurance
graph, exposes bounded tenant-safe traversal/search/editing, and projects each
completed sanctions run into it idempotently. Policy
owns enforceable rules and decisions; Audit owns immutable event evidence;
domain modules own the governed objects and corrective actions.
### Connectors exposes authority and effect behavior
Connector direction (`consume`, `publish`, `bidirectional`) remains useful but
is not enough. Profiles and bindings need the source-authority mode and
provider declaration above. ERP remains an integration family: finance,
workforce, procurement, asset, or other domain modules own semantics while
connectors own transport and source interaction.
### Geography starts as a reference contract
Before adding a `govoplan-geo` module, define a common reference shape for
coordinates, geometry, administrative area, address/location, CRS, source,
accuracy, validity, and external GIS identity. Create a repository only when
GovOPlaN must own spatial datasets, topology, or independent geospatial
lifecycles rather than link to an external GIS.
## Product and sector packages
A module says what capability can exist. A product package says how capabilities
work together for a bounded outcome. A sector package specializes vocabulary,
forms, rules, process baselines, controls, reports, and integration profiles
without forking the platform.
The signed configuration-package mechanism distinguishes:
- **reference package:** tested composition proving a journey and its recovery
behavior;
- **product package:** reusable operating capability such as governed
communication, service-to-decision, procurement/contracts, or governed BI;
- **sector package:** institutional specialization such as municipality,
university/research, ministry/program, regulator, grants authority, or
committee/council;
- **deployment profile:** supported infrastructure and operational topology;
- **integration profile:** supported set of external systems, authority modes,
bindings, and health expectations.
Packages may require modules and capabilities, but package definitions remain
configuration and evidence. They do not gain access to module-owned tables.
## Module portfolio metadata
Repository category is not capability maturity. The runtime manifest, release
catalog, Docs projection, and meta repository inventory use one
machine-readable declaration with at least:
- architecture layer and module kind;
- lifecycle/maturity claim: `concept`, `scaffold`, `vertical_slice`,
`reference_ready`, `supported`, or `lts`;
- evidence supporting the claim and known limits;
- supported source-authority modes;
- owned and explicitly non-owned concepts;
- provided/required capabilities and interfaces;
- reference packages and target-tested providers;
- migration, upgrade, recovery, security, and operations documentation.
Maturity is a release claim and must be checked against evidence. A manifest
must not become “supported” merely because a maintainer changes one string.
Create a repository only when the capability has distinct data ownership,
independent installability, technical assets, a security/lifecycle profile, a
release reason, more than one consumer or a proven reference process, and tests
that justify the boundary. Otherwise use a shared DTO, provider capability,
submodule, configuration fragment, package, or profile.
## Implemented migration sequence
### 0. Align the portfolio and contracts - complete
- This reconciliation is canonical in the meta repository and mirrored to the
Gitea wiki.
- All 62 source manifests carry validated evidence-based architecture metadata.
- External-reference, action/effect, operational-health, ownership, policy,
audit, and documentation primitives compose into one enforced provider
declaration and sanitized runtime-state contract.
- Institutional context, legal basis, evidence, presentation, external source,
information governance, temporal revision, and geo references are shared
Core DTOs rather than shared domain tables.
### 1. Prove responsibility and formal outcome - complete
- Mandate and Decision contracts, deterministic resolution, lifecycle
transitions, persistence providers, APIs, permissions, migrations, recovery,
and tests are implemented.
- Committee and the SQL-backed institutional fixture prove effective-time
authority, persisted meeting/agendum/vote/minute context, approval context,
reasoning, evidence, observed effect, correction/revision rules, protected
reconstruction, and review references.
- The independent Mandates and Decisions repositories were created only after
persistence and reuse passed the repository threshold.
### 2. Separate service and party semantics - complete
- Portal remains the presentation surface while Services owns reusable,
versioned definitions and explainable availability.
- Parties owns procedure roles, contact snapshots, and append-only
representation/revocation authority; Cases consumes the common resolver.
- `product.service-to-decision` proves both through a portable administrative
service composition.
- Forms owns immutable, versioned schemas while Forms Runtime owns drafts,
server validation, submission receipts, status/evidence history, and exact
Service/Form provenance. Portal delegates Form launch through the runtime
capability and fails closed when it is unavailable.
### 3. Complete governed data, portfolio, and assurance - vertical slices complete
- Datasources carries typed governance through staging and immutable
materializations, with bounded catalogue filters and dependency references.
- Reporting owns immutable semantic definitions, safe execution, quality gates,
provenance, schedules, saved views, pivoting, and export/import assessment
without taking source or transformation ownership.
- Risk Compliance persists the horizontal obligation/risk/control/evidence/
finding/measure graph and projects sanctions runs idempotently.
- Projects persists portfolio/outcome/change-governance revisions as a
consuming domain without becoming a second policy or reporting engine.
### 4. Package repeatable public-sector outcomes - complete at product maturity
- Governed communication, governed data/assurance, and service-to-decision are
portable product package manifests with repository-local evidence.
- Package preflight enforces module, capability, provider authority, health,
freshness, and recovery expectations without cross-module table access.
- Sector and `reference_ready` claims remain gated on target-environment,
recovery, accessibility, privacy, security, and operator evidence. This is a
maturity gate, not missing architecture implementation.
## What remains after the executable architecture slice
The remaining work is not another Core or cross-module architecture rewrite.
It falls into two explicitly different categories, neither of which can be
truthfully completed by adding generic platform code:
1. **Concrete provider packages:** the Committee ballot adapter contract is
complete, but a real secret/electronic ballot provider requires a selected
protocol and product decisions for voter eligibility, custody, secrecy,
recount, challenge, retention, and operational assurance. Equivalent future
adapters must satisfy the declared provider and recovery gates.
Provider selection and certification are tracked in
[Committee #1](https://git.add-ideas.de/GovOPlaN/govoplan-committee/issues/1).
2. **Target-produced maturity evidence:** `reference_ready`, `supported`, and
`lts` cannot be generated from source code. An exact release and deployment
must produce signed, expiring accessibility, privacy, security, operator,
provider, backup/restore, rollback, and recovery-drill evidence. The verifier
and schemas are implemented; the actual claims require those real runs.
A bounded issuer now hashes retained reports, checks role-scoped signing
authority and exact installed-release origin, emits sanitized signed
receipts, verifies them immediately, and exposes admission-enforcing CLI
gates. The real pinned-release evidence run is tracked in
[GovOPlaN #37](https://git.add-ideas.de/GovOPlaN/govoplan/issues/37).
Forms and Forms Runtime no longer constitute an architecture gap. Conditional
multi-page/localized authoring, package-fragment import, and durable native
Case/Workflow handoffs are implemented. Remaining depth is limited to
anonymous/public identity profiles, concrete file/signature providers, and
additional handoff target adapters. Those use the implemented immutable
definition, runtime, policy, evidence, service-launch, and domain-owner
boundaries rather than requiring another split. Public/provider decisions stay
tracked in Forms Runtime
[#2](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/2) and
[#3](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/3).
Approvals, Voting, Workflow trigger/wait dispatch, Identity Trust, and
Encryption now likewise have repository owners, neutral Core contracts,
persistence, migrations, recovery/disable semantics, documentation and focused
tests. Their remaining tickets concern concrete providers, deeper adapters and
target evidence, not an unresolved institutional architecture boundary.
Everything else described as architecture in this document now has a
repository owner, versioned contract, bounded implementation, migration and
recovery boundary where state exists, documentation, and executable evidence.
Further work in those modules is product breadth, UX depth, provider adoption,
and evidence renewal.
## Delivery tracking
The completed cross-repository architecture epic is
[GovOPlaN #29](https://git.add-ideas.de/GovOPlaN/govoplan/issues/29).
Its implementation work packages and resulting owners are:
- [Core #279](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/279):
validated module architecture and provider authority declarations;
- [Core #280](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/280):
shared institutional-context and governed reference primitives;
- [GovOPlaN #30](https://git.add-ideas.de/GovOPlaN/govoplan/issues/30) and
[govoplan-mandates](https://git.add-ideas.de/GovOPlaN/govoplan-mandates):
Mandates semantics and persistent resolver;
- [GovOPlaN #31](https://git.add-ideas.de/GovOPlaN/govoplan/issues/31) and
[govoplan-services](https://git.add-ideas.de/GovOPlaN/govoplan-services):
Services semantics, catalogue, and availability;
- [GovOPlaN #32](https://git.add-ideas.de/GovOPlaN/govoplan/issues/32) and
[govoplan-parties](https://git.add-ideas.de/GovOPlaN/govoplan-parties):
Parties and representation semantics and resolver;
- [GovOPlaN #33](https://git.add-ideas.de/GovOPlaN/govoplan/issues/33) and
[govoplan-decisions](https://git.add-ideas.de/GovOPlaN/govoplan-decisions):
formal Decisions semantics and registry;
- [Datasources #6](https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/6):
governed data/register catalogue;
- [Risk Compliance #7](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance/issues/7):
horizontal assurance graph;
- [GovOPlaN #34](https://git.add-ideas.de/GovOPlaN/govoplan/issues/34):
product and sector package classes; and
- [Docs #19](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/19):
configured architecture, maturity, and source-authority explanations;
- [Forms #2](https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/2):
immutable reusable definitions and the designer surface; and
- [Forms Runtime #1](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/1):
definition-aware submissions and Portal service launch;
- [Forms #3](https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/3) and
[Forms Runtime #4](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/4):
conditional/localized definition depth and governed native handoffs;
- [Approvals #1](https://git.add-ideas.de/GovOPlaN/govoplan-approvals/issues/1)
and [Campaign #22](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/22):
generic exact-subject approval chains and one consequential delivery gate;
- `govoplan-voting`: governed recorded ballots plus fail-closed provider-backed
assurance profiles consumed by Committee; and
- Identity Trust #1 and Encryption #1-#3: public device trust, provider-neutral
key/protection lifecycle, recovery authorization and disable proof, plus a
bounded local server-envelope provider and Files/Postbox fixtures; external
KMS/HSM/client-provider conformance remains separately gated.
Existing Projects #1, Reporting #4, Portal #1, Cases #1, Datasources #1,
Risk Compliance #2, GovOPlaN #14, and GovOPlaN #19 carry product-depth and
reference-readiness work instead of duplicating the completed architecture
contract.
## Completion evidence
The architecture direction is established by the following executable and
machine-enforced evidence:
- the `product.service-to-decision` composition and SQL-backed golden fixture
retain institutional context from service entry through a persisted case,
party, authority/work context, committee deliberation, decision, observed
communication effect, minute/record, and review references;
- the Portal/Form journey retains the exact published Service and Form
revisions through persisted draft state, validates on the server, and returns
the same submission on an idempotent launch replay;
- the system can answer who acted, for whom, in which function, under which
mandate and jurisdiction, using which rule and evidence versions;
- every implemented external binding declares authority mode, maturity,
operations, health, freshness, conflict, and recovery behavior, and Registry
rejects a declaration without sanitized runtime state;
- every material report or decision can be reconstructed from governed source
and transformation versions;
- product/package manifests are portable without cross-module table access or
code forks, while future sector packages inherit the same signed-package
constraints; and
- documentation and Ops explain the configured composition and its limits to
users, administrators, operators, and auditors.
These criteria complete the architecture contract at `vertical_slice` and
`product` maturity. They do not waive the separately enforced evidence needed
for a module or package to claim `reference_ready`, `supported`, or `lts`.
The capability-fit verifier now computes that cumulative readiness gate from
independently signed, expiring claims bound to the exact assessed release,
installed payload, deployment subject, controls, and artifact hashes. Actual
target runs and recovery drills remain operator-produced evidence.
+479
View File
@@ -0,0 +1,479 @@
# GovOPlaN Interface Pattern Language
This document is the cross-repository pattern language for GovOPlaN user
interfaces. It turns the existing ethical doctrine, binding UI/UX decisions,
layout principles, and module boundary into a common composition and review
grammar. It does not replace those sources.
The companion [interface surface inventory](INTERFACE_SURFACE_INVENTORY.md)
records which surfaces the current code contributes and where each surface
enters the rollout.
## Source Of Truth And Precedence
Use the narrowest owning document when changing a rule:
1. `govoplan-core/docs/INTERFACE_ETHICS_AND_DESIGN_DOCTRINE.md` owns why a
consequential interface must preserve context, decision, consequence,
responsibility, contestability, and traceability.
2. `govoplan-core/docs/UI_UX_DECISION_LEDGER.md` owns accepted product decisions
such as progressive disclosure, adaptive forms, blocker language, guided
operations, and the platform theme contract.
3. `docs/FRONTEND_LAYOUT_PRINCIPLES.md` owns the high-level choice between a
full-space structured-data workspace and a heading/menu/card workflow or
configuration surface.
4. `govoplan-core/docs/MODULE_ARCHITECTURE.md` owns the shell, route, navigation,
UI-capability, and shared-component boundaries.
5. This document owns the common pattern names, placement grammar, wording and
state conventions, focused-view composition, and definition of done across
those sources.
If two rules appear to conflict, do not create a third local convention. Record
the conflict in the owning decision ledger, resolve it there, and update the
affected patterns and surfaces together.
## Product Contract
GovOPlaN should feel calm because it shows what is relevant to the task, not
because it hides authority, risk, or evidence. Every surface follows these
rules:
- Start with the user's current object and task.
- Show common actions before advanced controls.
- Keep context, status, problems, and the next action spatially connected.
- Make consequential effects explicit before execution and observed effects
inspectable afterwards.
- Treat permissions, policy, privacy, and module availability as behavior, not
decoration.
- Keep optional modules optional. Compose through core route and UI-capability
contracts, never sibling-private components.
- Use the centrally exported core components wherever a matching contract
exists. A module-local replacement is not an implementation choice: it is a
product exception that requires explicit product-owner authorization.
- Preserve a stable way back to the containing object and the broader system.
- Do not let navigation, selection, or a view switch imply consent.
## Surface Archetypes
Choose an archetype from the task, then specialize it for the domain. A route
may contain more than one bounded archetype, but it should have one dominant
one.
| Archetype | Use when | Standard anatomy | Do not use when |
| --- | --- | --- | --- |
| Directory or explorer | Users browse hierarchical collections such as files, mailboxes, calendars, addresses, or records. | Collection/source pane, collection actions, filter in the pane it affects, main list/content pane, optional detail pane. | The content is a set of unrelated settings or workflow stages. |
| List-detail workspace | Users repeatedly find objects, inspect one, and act without losing list context. | Search/filter/list, persistent selected-object context, detail/actions, stable selection and URL. | A single guided operation is the primary task. |
| Focused task view | Only a bounded composition is needed to complete one task. | Task identity and reason, selected object, necessary module regions/actions, progress or status, obvious exit to the full system. | Hiding a surface would obscure a consequence, blocker, or required evidence. |
| Create or edit | Users change one coherent object state. | Adaptive typed form, field-level validation, advanced section, save/cancel, unsaved-change guard. | Discovery-heavy setup or a broad consequential change needs staged review. |
| Guided setup or import | The user must discover, upload, map, test, or preflight before a safe result exists. | Named steps, current progress, preserved inputs, validation/problem list, review, resumable completion where work is durable. | An ordinary edit can be understood as one coherent form. |
| Review or decision | A person must inspect evidence and deliberately approve, reject, send, publish, or otherwise commit. | Decision context, evidence/problems, consequence and reversibility, authority/provenance, explicit action, resulting record. | The interaction is passive inspection. |
| Monitoring, progress, or report | Users observe asynchronous or aggregate state and intervene when needed. | Summary, filters, durable job/item state, last update, retry/reconcile/intervention, detail and evidence. | A toast is sufficient for a short, non-durable local action. |
| Administration or configuration | Users compare and configure separate concerns. | Heading and scope, grouped subnavigation, overview/list plus selected details, adaptive editor or guided risky operation, effective policy and source. | The primary task is browsing one structured object space. |
| Dashboard | Users need a task-oriented starting point across modules. | Prioritized actionable widgets, scoped status, clear destination per widget, explicit refresh/staleness. | It merely duplicates every module navigation item or metric. |
| Public service or entry | An unauthenticated or external participant starts or resumes a service. | Service identity, eligibility/context, privacy and evidence expectations, accessible form/task, save/resume or handoff. | The actor is performing internal administration. |
## Placement Grammar
### Shell And Navigation
- The global title bar and rail belong to core. A module contributes routes,
navigation metadata, and explicit UI capabilities; it does not reproduce the
shell.
- Global navigation answers "which service area?" Local subnavigation answers
"which stable facet of this object or area?" A progress indicator answers
"where am I in this operation?" Do not use those three controls
interchangeably.
- Keep the current tenant, actor, object, and selected version or scope stable
across local navigation. Guard unsaved work before navigation.
- Permission and capability filtering happens before view composition. A
missing menu item is not evidence that an actor lacks backend access, and a
visible item is never authorization by itself.
- Route, selection, panel, and view changes must not execute consequential
actions.
### Page And Workspace
- Structured directories use the full available content space and persistent
panes. They do not add a decorative heading row that reduces working height.
- Workflow, configuration, dashboard, and explanatory pages may use a heading.
The heading names the task or scoped object and contains only route-level
actions.
- Put a collection-wide create action in the heading of the collection it
affects. Use a short, specific label such as `Add` when the heading already
names the object. Do not duplicate that action in a permanently visible side
panel. A side panel used as the creation surface appears for creation and is
otherwise absent or returns to its documented non-creation purpose.
- Put filters beside the list or pane they affect. Put bulk actions immediately
above or beside the current selection. Put object actions with the object
detail, not in the global title bar.
- Full-page create and edit surfaces put their persistent action cluster in the
upper-right of the page heading. `Discard` comes before `Save …`, with the
primary save action at the far right. Keep both controls in the same place
across validation, loading, and saved states; guard unsaved work when the
user discards or navigates away. Explicit Discard and dirty in-application
navigation use the same central unsaved-changes dialog and registered
save/discard callbacks. A browser-controlled tab/window unload warning is the
only unavoidable different surface.
- A page or panel has one visually primary action for its current state. Put
secondary actions beside it. Separate destructive actions and name their real
effect.
- Dialog actions use a stable footer: cancel/back first, then the primary action
at the end. Header and footer stay fixed while long bodies scroll.
- Use cards for independent groups, summaries, and settings blocks. Do not wrap
every region in a card or nest cards merely to create spacing.
- When a collapsible card contains one table and no other content, the table
uses the card's full available width and body height. The card/table region
owns overflow; do not add an inner max-width, decorative wrapper, duplicate
padding, or nested scroll container that reduces the working area.
- Row actions in tables are icon-only controls in a stable rightmost action
column. Order them by intent: inspect/open, edit, copy/duplicate,
transfer/share/download, retry/restore, then remove/delete last. Omit actions
only when they are structurally irrelevant to the entire table. An action
that belongs to the table but is unavailable for one row remains in its
normal position and is disabled; when the reason is not obvious from row
state, provide it through the central focusable disabled-action explanation.
It does not disappear. Every icon has a translated accessible name and matching tooltip.
Separate destructive actions visually, and use a named confirmation/review
surface when the consequence cannot be understood from the icon and row
context. In an empty editable table, place Add in the same left-most action
slot it occupies in a populated row and reserve the remaining slots so the
column geometry does not move.
- Transient feedback should not shift the workspace. Durable failures, partial
results, and blockers remain attached to the affected item or operation.
### Detail And Explanation
- Keep the selected object's identity and material status visible while its
detail changes.
- Every non-self-explanatory field uses the central `FieldLabel`; short field
help sits with that label. A field without `FieldLabel` is an explicit
documented omission whose register names the field, rationale, and accessible
label source. Users may hide inline help markers with their persisted
interface preference; the visible/accessibility label and validation remain.
Longer "Why?", policy source,
diagnostics, or provenance belongs in an expandable area, detail panel, or
review step.
- Empty space is not an error. An empty state states what is empty, why that can
happen, and the permitted next action. Do not show creation actions to actors
who cannot create.
## Visual Grammar
- Core owns the appearance contract and shared CSS tokens. Modules use core
colors, spacing, radii, shadows, focus treatment, status colors, and disabled
treatment; module CSS may specialize layout only.
- Establish hierarchy through spacing, typography, grouping, and placement
before adding borders or color.
- Color never carries status or required action alone. Pair it with text and,
where useful, an icon.
- Icons support recognition but do not replace accessible names. Use the core
icon-name mapping for navigation.
- Keep list columns, tree indentation, headers, dialog dimensions, and action
positions stable as content changes.
- Density is a user preference, not a license to remove labels, focus targets,
explanations, or consequences.
- Respect system/light/dark themes and reduced-motion preferences through the
core contract. Do not build module-local theme systems.
## Wording Grammar
Use the same noun for the same domain object in navigation, headings, fields,
actions, states, API-facing explanations, and documentation. Prefer the most
specific user-facing noun: "Recipients" rather than "Data", "Delivery job"
rather than "Process", and "Mail profile" rather than "Configuration" when
that is what the user is acting on.
Actions use a verb plus the object or consequence:
- Prefer `Save campaign`, `Review messages`, `Queue delivery`, `Retry failed
deliveries`, or `Delete calendar`.
- Avoid `Submit`, `OK`, `Continue`, or `Execute` when a more precise action is
available.
- Use `Continue` only when it advances a reversible guided flow without
committing the final effect.
- Do not say `Undo` when the system can only cancel future work, create a
correction, supersede a record, or request retraction.
State text describes observed state, not optimism. Use stable shared terms where
they fit: `Draft`, `Ready for review`, `Blocked`, `Queued`, `Running`,
`Retry scheduled`, `Partially completed`, `Completed`, `Failed`, and
`Cancelled`. Domain-specific states may refine these terms but should not give a
shared term a contradictory meaning.
Blocked and failed actions use the structured language from DUE-005:
- what is unavailable or failed
- why, in plain language
- what must happen next
- who can do it
- where to go
- optional technical details behind deliberate disclosure
Errors should identify the affected object and whether saved state or external
effects may already exist. Never expose raw exception text as the only user
message.
Use the central dialog, confirmation, alert, and attached-error components for
feedback. `window.alert` and the global `alert` function are prohibited. A
genuinely unavoidable exception requires explicit product-owner authorization
and an entry in the Core alert exception register before implementation.
## State Contract
Every surface implements the states it can reach; it does not render a blank
region while waiting or collapse distinct outcomes into a generic error.
| State | Required treatment |
| --- | --- |
| Loading | Keep the stable shell and context visible. Name what is loading; preserve usable prior data when safe. |
| Empty | State the scope and reason, then show only permitted next actions. |
| Validation problem | Attach the problem to the field/item and provide a navigable summary when problems span regions or steps. |
| Permission denied | Name the unavailable action or object, the required actor/role where safe, and a valid exit. Do not leak protected data. |
| Capability unavailable | Distinguish not installed, disabled, not configured, unhealthy, and not permitted when the actor may know. Give the responsible actor and target. |
| Offline or unreachable | Preserve local context and unsaved input, show last-known/stale state, and offer a safe retry. Do not present network absence as an authentication failure. |
| Stale or conflicting | Show which data changed, preserve both values where feasible, and offer reload, merge, or explicit overwrite according to policy. |
| Partial result | Show completed and incomplete effects separately. Never label a partial operation successful without qualification. |
| Asynchronous work | Show durable job identity, queued/running/retry/block/final state, last update, progress if meaningful, and leave/return behavior. |
| Success | State the resulting object/effect and provide its evidence or destination. Use a transient toast only when the result is already visible and durable elsewhere. |
| Destructive or corrective action | Preview scope, downstream effects, reversibility limit, evidence, and required confirmation or approval. |
Long-running or external work must expose retry and reconciliation as observable
states. The UI must not imply that a request and its external effect were one
atomic success when an outbox, worker, or remote system sits between them.
## Consequence And Provenance
Before an action affects records, rights, policy, retention, communications,
money, external systems, or workflow state, its action surface must answer the
decision-surface questions in the interface doctrine. At minimum show:
- affected object and scope
- acting identity or system actor and relevant authority
- immediate and possible downstream effects
- whether the operation is reversible, cancellable, corrective, or final
- blockers and their resolution path
- audit/evidence that will be created
- effective policy or configuration source when it changes the decision
After execution, users must be able to reach the command/job, observed effects,
policy result, failures, retries, reconciliation outcome, actor, time, and source
data that explain the result. Provenance may be quiet by default, but it must not
be absent.
Privacy follows the same rule: lists, previews, logs, notifications, and
diagnostics show only the personal or secret data needed for the actor's task.
Redaction must be explicit enough that users do not mistake a redacted value for
missing source data.
## Focused Views
A focused view is a declarative UI composition for a task. It selects the
routes, local regions, navigation entries, and actions relevant to that task
after installed-module, capability, permission, and policy filtering. It does
not change backend authorization or domain state.
A view definition must be able to explain:
- its stable identifier, label, and task purpose
- the current object/scope and default destination
- included navigation and contributed regions/actions, with deterministic order
- the visible escape to the containing module and full system
- why the view is active and how the user may switch when switching is allowed
- what happens to unsaved work when entering, leaving, or switching views
Focused views follow these invariants:
- Do not hide a blocker, material consequence, provenance, or required review
merely to make the screen quieter.
- Preserve the global tenant/actor context and provide an obvious exit.
- Filter unavailable contributions without leaving broken separators, empty
groups, or dead destinations.
- A manual or automatic switch is navigation, not consent. Guard unsaved work
and never execute a domain action as a side effect.
- Display why a non-manual default was selected, for example a role or task
default, without exposing protected policy details.
- Treat an unknown or invalid view as a recoverable fallback to the normal
module surface.
When more than one source proposes a focused view, use this precedence:
1. a manual view pinned for the current user session
2. a current-task suggestion, including a future workflow-step suggestion
3. the user's saved default
4. the role or tenant default
5. the normal full interface
Show the active source and an escape to the full interface. A task or workflow
suggestion is never an authorization change and never locks the user into the
composition; tenant policy may constrain which views are selectable, but it
must not hide required evidence or remove that escape. Workflow implementation
is explicitly postponed and is not a prerequisite for defining, manually
selecting, testing, or piloting focused views. A future workflow module may
request a view through a core contract; it must not own the view composition
implementation.
## Accessibility And Responsive Contract
- Every action is reachable and operable by keyboard in a logical order.
- Use semantic headings, landmarks, labels, tables/lists, and native controls
before adding ARIA. Icon-only controls need stable accessible names.
- Focus is visible. Dialogs trap focus, announce their name, and return focus to
the control that opened them. Validation moves or links focus to the first
relevant problem without losing the problem summary.
- Loading, saved, failed, queued, progress, and externally updated states are
announced without repeatedly interrupting the user.
- Disabled primary actions need a focusable explanation; a pointer-only tooltip
is insufficient.
- Do not rely on color, hover, drag-and-drop, pointer precision, or animation as
the only interaction. Provide keyboard and explicit-control equivalents.
- At narrow widths and high zoom, preserve task order and action access. Collapse
secondary panes into an explicit drawer/step and never move a destructive
action into the primary position.
- Honor reduced motion. Avoid motion that implies progress when the operation is
merely waiting.
- Truncation has an accessible full-value path. Personal or secret values remain
redacted according to permission and policy in that path.
## Component Ownership
Core already exports shell, navigation, access-boundary, form, dialog, loading,
status, policy/provenance, blocker, review, table, tree, message-display, and
unsaved-change primitives. These centrally exported components are mandatory
across GovOPlaN wherever their contract covers the interaction. In particular,
use the core `Card` for logical sections, `DataGrid` for tabular collections and
row actions, and `ToggleSwitch` (the standard Toggle control) for boolean
settings. Styling a native element or a module-local component to imitate one
of these controls is duplication, not reuse.
A route or domain composition assembled from central primitives is not a custom
control. Any new reusable UI control, presentation primitive, or module-local
substitute is a custom component and requires explicit product-owner
authorization before implementation. Record the authorization in the owning
decision or issue together with:
- the narrowly defined purpose and consumers
- why no central component or composition satisfies the need
- the exact scope in which the exception may be used
- its accessibility, state, theme, and test contract
- whether it should remain domain-specific or later become a core component
An authorized custom component serves only that specific purpose. It must not
duplicate, fork, restyle into a substitute for, or silently broaden beyond a
central component. Code review convenience, an existing local implementation,
or a small visual difference is not authorization. When core gains the required
contract, migrate the exception unless the product owner explicitly retains it.
Do not promote a component only because two screens look similar. Promote it to
`@govoplan/core-webui` after a second consumer or a clear platform contract has
proved shared behavior, accessibility, state, and extension needs. Modules own
domain composition, wording, and policy semantics; core owns generic contracts
and appearance.
### Scheduling Request Composition Reference
The Scheduling request surface is the first explicit reference composition for
these rules:
- The persistent left panel contains `My scheduling requests` and `Scheduling
requests for me` as two stacked lists. It preserves list context like mailbox
folders but does not invent folders.
- The left pane's `Scheduling requests` heading owns one short `Add` action. It
opens a new request in the right pane without an extra menu or duplicate
launcher.
- The right main pane is the stable view/create/edit surface. Selecting a list
item opens its details; Add opens the same editor composition used for edit.
- `Basic information`, `Calendar integration`, `Candidate slots`, and
`Participants` are logical sections rendered with the central `Card`.
- Privacy and participation behavior is a separate settings `Card`; dependent
number/password fields are disclosed by their central `ToggleSwitch`.
- Candidate slots and participants are row collections rendered with the
central `DataGrid`, including its stable action column.
- Calendar integration is a boolean choice rendered with the central
`ToggleSwitch`; dependent calendar controls are disclosed only when enabled.
- Each participant is one structured row containing name, email address, and
ordered row actions. An address-parsing text area is not the ordinary editor;
parsing pasted address lists belongs only in an explicitly designed bulk
import flow.
- View mode shows participation statistics and only state-valid quick actions.
Scheduling owns those current domain actions; a future Workflow module may
coordinate them through stable action contracts but is not a runtime
dependency of the surface.
Apply the underlying placement and component rules to equivalent collection and
create/edit surfaces throughout the system; the Scheduling domain names are an
example, not a module-local convention.
## Test Expectations
For every changed surface, select tests from each applicable layer:
- route and permission tests: module enabled/disabled permutations, route guard,
contribution filtering, fallback, and direct-link behavior
- behavior tests: primary task, validation, unsaved-change guard, confirmation,
retry/reconcile, partial result, and leave/return behavior
- accessibility tests: semantic names/roles, keyboard order, focus entry/return,
live-state announcements, and non-color status meaning
- state tests: loading, empty, denied, capability-missing, stale/conflict,
offline, partial, success, and destructive/corrective outcomes as applicable
- composition tests: absent optional module, duplicate/unknown contribution,
deterministic ordering, and focused-view fallback
- presentation checks: supported widths/zoom, light/dark/system theme, reduced
motion, comfortable/compact density, and long translated text
- i18n checks: user-facing strings owned by the rendering package and structural
audits passing
- visual regression: useful for geometry and hierarchy after behavior and
accessibility assertions exist; never the only evidence
If the current harness cannot automate a required check, record the gap and the
manual evidence in the owning issue. "Not tested" is an inventory state, not a
reason to infer that a pattern is satisfied.
## Definition Of Done For A Surface
- The dominant task and archetype are recorded in the inventory.
- Placement, action hierarchy, wording, and every reachable state follow this
pattern language or an explicit ledger exception.
- Permission, capability, privacy, and redaction behavior are verified.
- Consequence, reversibility, authority, evidence, and provenance are present
where applicable.
- Keyboard, focus, announcement, responsive, theme, density, motion, and i18n
behavior are covered in proportion to the surface.
- Optional modules remain optional and no sibling-private UI import was added.
- Every matching central component is reused. Any custom-component exception
has recorded product-owner authorization, narrow scope, rationale, and tests,
and does not duplicate a central component.
- Behavioral/accessibility evidence is linked from the rollout matrix and issue.
- Configured-system help can reach the applicable pattern or reference topic
when [Docs #15](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/15)
supplies that experience.
## First Pilot: Campaign
[Campaign #74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74)
is the first full-domain audit and migration. It should prove patterns before
generic extraction:
- [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) and
[#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73): stable,
accessible preview and attachment-detail overlays
- [#63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63): review
stages, outcomes, blockers, and intervention vocabulary
- [#62](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/62): explicit
synchronous/asynchronous send mode and durable delivery progress
- [#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65): one
coherent report filtering and count-affordance model
- [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35): guided
first-campaign entry
These slices do not depend on the Workflow runtime. Campaign's current
domain-owned review/send state is enough to prove layout, wording, focused-view,
progress, intervention, and evidence patterns.
## Revision Procedure
1. Record a changed product decision in the core UI/UX decision ledger.
2. Update the relevant pattern here without copying the full owning doctrine.
3. Update the surface inventory and rollout owner/issues.
4. Change shared components only where the proven contract belongs to core.
5. Migrate and test affected module surfaces.
6. Publish configured-system pattern/reference help through the Docs module.
+369
View File
@@ -0,0 +1,369 @@
# GovOPlaN Interface Surface Inventory And Rollout
This is the initial evidence inventory for the product-wide interface pattern
language. It records code contributions, not an assertion that every listed
surface is complete, enabled in a deployment, usable, or compliant.
The applicable design contract is
[`INTERFACE_PATTERN_LANGUAGE.md`](INTERFACE_PATTERN_LANGUAGE.md).
## Snapshot And Method
The source-derived inventory command is documented in
[`PLATFORM_CONTROL_PLANE.md`](PLATFORM_CONTROL_PLANE.md). It produces
machine-readable field, label, translation, route, API-reference, and module
manifest evidence. This hand-maintained document remains the reviewed product
interpretation and rollout ledger; generated evidence does not replace it.
Snapshot refreshed: 2026-08-03.
The generated snapshot contains 65 module manifests, 35 WebUI-contributing
repositories, 40 statically declared module routes, 1,156 UI fields, and 836
backend endpoints. All backend endpoints are classified and no stale endpoint
declarations were found. The 234 endpoints without a static WebUI reference are
kept visible as review evidence; they may intentionally serve workers, public
clients, connectors, or external integrations.
Evidence was read from tracked Git `HEAD` in the local GovOPlaN checkouts:
- core routes and fallback behavior in `govoplan-core/webui/src/App.tsx`
- every present `govoplan-*/webui/src/module.ts`
- the matching backend `src/*/backend/manifest.py`
- named UI capabilities and contribution identifiers in each `module.ts`
- Campaign nested routes in its tracked `CampaignWorkspace.tsx` and
`SectionSidebar.tsx`
Tracked commits are used as the integrated baseline. Dirty worktree changes
are not treated as delivered behavior. The former Campaign recipient-editor
and preview WIP is integrated in tracked commits; completed work is no longer
described as a local exception.
Runtime visibility remains conditional on the module being packaged and
enabled, server metadata, installed optional capabilities, the authenticated
actor, tenant context, route permission guards, and inner-surface permission
checks. A route in this inventory therefore means "the code contributes this
route when its module is active", not "every user sees it".
Inventory states:
- **Contributed**: a route or named UI capability exists in tracked source.
- **Metadata gap**: frontend runtime source contributes a route but the backend
manifest does not describe the same route/navigation surface.
- **Unreviewed**: the surface has not yet completed the pattern, state,
accessibility, privacy, and consequence audit. This is the default unless an
issue supplies verification evidence.
- **Pilot**: the surface is in the Campaign-first rollout.
## Core Shell Surfaces
| Surface | Owner and code evidence | Audience/access evidence | Primary task and target archetype | Audit / rollout |
| --- | --- | --- | --- | --- |
| Public landing and login | `govoplan-core` `PublicLandingPage`; rendered while no authenticated principal exists | Unauthenticated; maintenance and backend-reachability context are shell inputs | Understand the service and authenticate; public entry | Core shell contract complete under Core #227: semantic entry/login, uniform reachable/offline/maintenance feedback, keyboard focus, responsive layout and privacy-safe pre-authentication state |
| Session/bootstrap state | `govoplan-core` `App.tsx` and `AppShell` | All browser sessions during bootstrap | Understand that session/platform state is loading; state contract | Core shell contract complete: loading, unreachable, maintenance, authentication-required and module-load failure states use shared status/alert boundaries without erasing the shell |
| `/` authenticated redirect | `govoplan-core` chooses the first visible navigation destination | Authenticated; result depends on visible nav contributions | Enter the actor's first accessible service area; navigation behavior, not a content page | Core route/module-permutation contract complete; permission, module, View and fallback filtering precede navigation and do not execute a domain action |
| `/dashboard` fallback | `govoplan-core` `DashboardPage` only when the Dashboard module is absent | Authenticated; no route-specific scope in core | Cross-module starting point; dashboard | Core fallback and Dashboard module permutations complete; fallback remains usable without the optional Dashboard module |
| `/settings` | `govoplan-core` `SettingsPage` | Authenticated; contributed sections and integrations filter internally | Profile, UI/workspace preference, local connection, and user-scoped integration settings; configuration | Core-owned pattern migration complete in [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225), commit `fa32cca` |
| Shell chrome | `AppShell`, `Titlebar`, `IconRail`, `BreadcrumbBar`, `HelpMenu`, language menu, unsaved-change provider | Public/authenticated variants; nav filtered later | Tenant/actor context, global navigation, help, language, session and maintenance state | Core shell contract complete under Core #227/#225 and Views #2: semantic global controls, scroll-safe rail, visible maintenance state, guarded navigation, configured Docs fallback, optional Search, responsive/theme/i18n checks and module permutations |
## Direct Module Route Contributions
The access column summarizes only the route-level declaration in `module.ts`.
Inner APIs and controls may impose additional checks. Public and compatibility
routes are called out explicitly because they do not have the same manifest
semantics as authenticated navigation routes.
| Routes | Owner | Route-level access | Primary archetype | Migration issue |
| --- | --- | --- | --- | --- |
| `/admin` | Access | Any declared administration/read scope | Administration/configuration host | Access pattern migration complete in [Access #19](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/19), commit `1409dbf`; shared host contract complete in [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225) |
| `/address-book` | Addresses | `addresses:contact:read` | Governed source directory, contact/list detail, external-provider operation, governance facts, and reversible correction | Addresses pattern migration complete in [Addresses #23](https://git.add-ideas.de/GovOPlaN/govoplan-addresses/issues/23), commit `f9a7185` |
| `/approvals` | Approvals | `approvals:workspace:read` | Work queue/guided decision | Approvals pattern migration complete in [Approvals #3](https://git.add-ideas.de/GovOPlaN/govoplan-approvals/issues/3), commit `24e9559` |
| `/calendar` | Calendar | `calendar:event:read` | Full-height calendar workspace with filterable collection/agenda sidebar, continuous and bounded date views, guarded VEVENT and source editors, synchronized-source status, durable outbox recovery, and destructive remote-move evidence | Calendar pattern migration complete in [Calendar #22](https://git.add-ideas.de/GovOPlaN/govoplan-calendar/issues/22), commit `d7fd944` |
| `/campaigns`, `/campaigns/:campaignId/*`, `/campaigns/queue`, `/campaigns/reports` | Campaign | Campaign read/report/control scopes | List-detail, guided review, monitoring, reporting | Campaign pattern pilot complete in [Campaign #74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74); bounded product features such as watched-folder policy remain independently tracked |
| `/operator` | Campaign | Campaign read plus queue/control scope | Compatibility redirect to `/campaigns/queue` | Campaign #74 complete; redirect remains declared for saved links and is retired under the compatibility policy rather than through the UI migration |
| `/cases`, `/cases/:caseId` | Cases | `cases:case:read` | Governed case directory and detail workspace with guarded OCC lifecycle editor, provider-owned references, immutable timeline/history, and confirmed object-access editor | Cases pattern migration complete in [Cases #4](https://git.add-ideas.de/GovOPlaN/govoplan-cases/issues/4), commit `43b4cc8` |
| `/committee` | Committee | `committee:workspace:read` | Governed workspace | Committee pattern migration complete in [Committee #2](https://git.add-ideas.de/GovOPlaN/govoplan-committee/issues/2), commit `e64af30` |
| `/dashboard` | Dashboard | No route-specific scope | View-specific personal workspace with module/permission-filtered widget library, guarded four-column composition, nested widget settings, server/browser fallback, and optimistic layout persistence | Dashboard pattern migration complete in [Dashboard #3](https://git.add-ideas.de/GovOPlaN/govoplan-dashboard/issues/3), commit `da3947f` |
| `/dataflow` | Dataflow | Pipeline read/admin | Governed library, guarded graph/constrained-SQL definition editor, typed node inspector, bounded intermediate preview, automation triggers, and durable run/deployment evidence | Dataflow pattern migration complete in [Dataflow #20](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/20), commit `109ddcd` |
| `/datasources` | Datasources | Catalogue read/source admin | Governed catalogue, staging preflight, optional-origin directory, authority editor, and immutable evidence | Datasources pattern migration complete in [Datasources #7](https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/7), commit `6406ce7` |
| `/distribution-lists` | Distribution Lists | List read/write/admin | Governed directory, immutable-revision editor, expansion preview, and evidence register | Distribution Lists pattern migration complete in [Distribution Lists #8](https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists/issues/8), commit `6cdd804` |
| `/docs` | Docs | Documentation or settings read | Documentation/reference | Configured-system workflow/reference/pattern help complete in [Docs #15](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/15), commit `abe2f78` |
| `/files` | Files | `files:file:read` | Directory/explorer | Files pattern migration complete in [Files #42](https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/42), commit `d8ae506` |
| `/forms` | Forms | `forms:definition:read` | Definition library/editor | Forms pattern migration complete in [Forms #4](https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/4), commit `e505536` |
| `/forms-runtime`, `/forms-runtime/:instanceId` | Forms Runtime | Participate or workspace read | Guided form execution | Forms Runtime pattern migration complete in [Forms Runtime #5](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/5), commit `07dd35b` |
| `/idm` | IDM | Assignment, function-change, relationship, or organization scopes | Directory/governed change | IDM pattern migration complete in [IDM #12](https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/12), commit `d864317` |
| `/mail`, `/mail/bounces` | Mail | Mailbox or bounce read/manage | Directory/explorer, operational evidence | Mail pattern migration complete in [Mail #20](https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/20), commit `7844d9c` |
| `/notifications` | Notifications | `notifications:notification:read` | Inbox/list-detail with guarded recipient state, confirmed local cancellation/dispatch, and sanitized delivery evidence | Notifications pattern migration complete in [Notifications #4](https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/4), commit `ad6a31f` |
| `/ops` | Ops | Operations or settings read | Monitoring/evidence with contextual run, drain, readiness-blocker, and recovery guidance | Ops pattern migration complete in [Ops #4](https://git.add-ideas.de/GovOPlaN/govoplan-ops/issues/4), commit `2b32643` |
| `/organizations` | Organizations | Model/unit/function or settings read | Directory/hierarchy editor | Organizations pattern migration complete in [Organizations #7](https://git.add-ideas.de/GovOPlaN/govoplan-organizations/issues/7), commit `97acfcb` |
| `/portal` | Portal | `portal:service:read` | Explained service directory and governed handoff | Portal pattern migration complete in [Portal #2](https://git.add-ideas.de/GovOPlaN/govoplan-portal/issues/2); durable evidence in `govoplan-portal/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/postbox` | Postbox | `postbox:postbox:read` | Inbox/list-detail | Postbox pattern migration complete in [Postbox #26](https://git.add-ideas.de/GovOPlaN/govoplan-postbox/issues/26), commit `a97eb3b` |
| `/projects` | Projects | `projects:project:read` | Revisioned list-detail/project workspace | Projects pattern migration complete in [Projects #2](https://git.add-ideas.de/GovOPlaN/govoplan-projects/issues/2); durable evidence in `govoplan-projects/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/reporting`, `/reports` | Reporting | `reporting:definition:read` | Governed report catalogue, analytical workspace and evidence | Reporting pattern migration complete in [Reporting #8](https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/8); durable evidence in `govoplan-reporting/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/risk-compliance` | Risk Compliance | Workspace or sanctions read | Immutable source evidence, version-pinned screening, list-detail review, and revisioned assurance graph with explicit blockers and consequences | Risk Compliance pattern migration complete in [Risk Compliance #8](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance/issues/8), commit `24d80a6` |
| `/scheduling` | Scheduling | `scheduling:schedule:read` | List-detail/guided decision | Scheduling pattern migration complete in [Scheduling #8](https://git.add-ideas.de/GovOPlaN/govoplan-scheduling/issues/8), commit `c17cbda` |
| `/scheduling/public/:requestId/:token` | Scheduling | Public signed token | Public participation | Scheduling #8 complete in `c17cbda` |
| `/search` | Search | `search:result:read` | Keyboard-first global/context overlay and full results fallback | Search pattern migration complete in [Search #4](https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/4); durable evidence in `govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/templates` | Templates | Template read/write/publish/render/admin | Governed library, immutable-revision editor, compatibility preview, and render evidence | Templates pattern migration complete in [Templates #5](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/5), commit `72fafa2` |
| `/voting` | Voting | `voting:ballot:read` | Governed ballot workspace | Voting pattern migration complete in [Voting #1](https://git.add-ideas.de/GovOPlaN/govoplan-voting/issues/1), commit `2625990` |
| `/workflow` | Workflow | Definition read or instance admin | Native BPMN editor, governed revision actions and execution evidence | Workflow pattern migration complete in [Workflow #15](https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/15); durable evidence in `govoplan-workflow/docs/INTERFACE_PATTERN_MIGRATION.md` |
## Final Module Closure Evidence
The final five module-owned work packages complete the 2026-08-03 rollout
snapshot. Their module documents are the durable detailed inventories; the
table below records the cross-product closure evidence.
| Owner | Dominant archetype and consequential boundary | Focused evidence |
| --- | --- | --- |
| Workflow | Definition list-detail plus specialized native BPMN editor; save/activate/archive/delete/reset and instance transitions remain revisioned, confirmed, and Engine-owned | Shared dialogs/status/alerts/help, dirty-navigation guard, keyboard palette insertion, edge inspector alternative, responsive/reduced-motion contract, TypeScript and focused structure test |
| Search | Focus-contained global/context overlay plus URL-stable full results; filters only narrow permission-aware source results | F3/Ctrl/Cmd+K, listbox keyboard navigation, provider-partial diagnostics, shared controls/help, narrow layout and focused overlay/interface tests |
| Reporting | Three-region governed analytical workspace; runs, schedules, exports and publications retain purpose, permission, source and policy provenance | Shared grid/dialog/status/help, keyboard-explainable Run blockers, responsive task order, provider/semantic backend tests and focused interface test |
| Projects | Revisioned list-detail planning workspace; visibility and saves are ACL/OCC-governed and retain a change reason | Shared dialog/status/help/field labels, save errors attached to the editor, semantic list controls, responsive/focus contract and focused interface test |
| Portal | Explained service directory and exact-revision provider handoff; Portal never owns the launched case/form/workflow effect | Shared status/alert/toggle/help/blocker controls, stable disabled Open action with actor/action/destination, guarded navigation, responsive layout and focused interface test |
Future WebUI modules and newly added routes are not grandfathered by this
snapshot. They must meet the same surface definition of done in their owning
feature issue and pass the source/runtime inventory gates; they do not reopen
this finite migration program unless the pattern contract itself changes.
## Manifest And Runtime Route Alignment
Backend route metadata lets operators, Docs, release tooling, and remote bundle
loading reason about the configured interface without executing module UI code.
`module.ts` remains the executable local route/render source. Missing metadata
is recorded here as an evidence gap; this inventory does not infer whether each
gap is intentional.
The generated comparison is aligned for all authenticated canonical routes.
Two deliberate exceptions remain visible:
- Campaign contributes `/operator` as a compatibility redirect for saved View
projections; its canonical and manifest-declared destination is
`/campaigns/queue`.
- Scheduling contributes `/scheduling/public/:requestId/:token` through the
separate `publicRoutes` contract. Authenticated manifest routes intentionally
do not describe public signed-token entry points yet.
Admin, Audit, Policy, Tenancy, and Views contribute composed administration or
settings surfaces rather than direct routes. Their migration issues are
[Admin #8](https://git.add-ideas.de/GovOPlaN/govoplan-admin/issues/8),
[Audit #8](https://git.add-ideas.de/GovOPlaN/govoplan-audit/issues/8),
[Policy #11](https://git.add-ideas.de/GovOPlaN/govoplan-policy/issues/11),
[Tenancy #6](https://git.add-ideas.de/GovOPlaN/govoplan-tenancy/issues/6), and
[Views #2](https://git.add-ideas.de/GovOPlaN/govoplan-views/issues/2).
Release evidence must continue to run the generated inventory and manifest
shape checks so new executable routes, public routes, aliases, and composed
surfaces cannot silently diverge from their declared metadata.
## Composed Surfaces And Extension Points
These surfaces are active only when the host and contributing modules are
enabled and the actor passes the declared filters.
| Host surface | Contributor and evidence | Contributed regions/actions | Pattern implication | Audit |
| --- | --- | --- | --- | --- |
| `/admin` | Access host (`AdminPage`) | System tenants/users/roles, tenant users/groups/roles/API keys/settings, function-role mappings, user/group mail and file connector scopes | One stable admin information architecture must contain both host-owned and contributed sections | Pattern migration, contextual help, explained permission/protection states, optional-module blockers, localization, and focused evidence complete in Access #19 (`1409dbf`); Core #225 shared host contract complete |
| `/admin` | `govoplan-admin` `admin.sections` | Overview; system settings; configuration changes; configuration packages; role/group templates; module management | Configuration, guided operations, review/preflight, consequence | Pattern migration, contextual help, explained permission/protection/applicability states, guarded consequential actions, localization, and focused evidence complete in Admin #8 (`d428f33`) |
| `/admin` | `govoplan-tenancy` `admin.sections` | System tenant registry and active-tenant settings | Administration directory, effective configuration, lifecycle consequence | Pattern migration, contextual help, explained permission/lifecycle/system-policy states, dirty-state guards, localization, and focused evidence complete in Tenancy #6 (`e76fe16`) |
| `/admin` | `govoplan-audit` `admin.sections` | System audit; tenant audit | Evidence/provenance and reporting | Pattern migration, localized evidence projection, contextual help, and focused tests complete in Audit #8 (`6d3fcc1`) |
| `/admin` | `govoplan-files` `admin.sections` and `files.connectors` | System and tenant file connections plus scoped connector managers used by Access | Adaptive configuration, discovery/test, policy and credentials | Pattern migration, contextual help, blocker explanations and focused evidence complete in Files #42 (`d8ae506`) |
| `/admin` | `govoplan-organizations` `admin.sections` | Tenant organization settings | Configuration/list-detail | Pattern migration, tenant-owned provenance, contextual help, guarded settings/editor drafts, explained permission states, localization and focused evidence complete in Organizations #7 (`97acfcb`) |
| `/admin` | `govoplan-policy` `admin.sections` | System, tenant, group, and user retention | Effective value, source/provenance, consequential configuration | Pattern migration complete in Policy #11 (`f964ed7`) with Core editor contract `fa32cca` |
| `/admin` and `/settings` | `govoplan-mail` `mail.profiles` | System/tenant/group/user mail profile and policy managers | Same server/credential/policy grammar as file connectors | Pattern migration, contextual help, policy/target/permission blockers and focused evidence complete in Mail #20 (`7844d9c`; shared test-reason contract Core `2d0551a`) |
| `/settings` | Core host | Profile; interface; workspace; local connection | Personal configuration with adaptive forms and immediate feedback | Pattern migration complete in Core #225 (`fa32cca`) |
| `/settings` | Files and Mail named capabilities | User-scoped file connections and mail profiles/policy | Optional integration regions disappear cleanly when capability absent | Files #42, Mail #20 and Core #225 complete |
| `/admin` and `/settings` | `govoplan-views` `admin.sections`, `settings.sections`, and `views.runtime` | System/tenant definition and assignment editors, personal/group editors, global selector | Versioned presentation projection with inheritance, lockout safeguards, optional directory targets, and no authorization effect | Pattern migration, contextual help, localized selector/editor, guarded drafts, explained inherited/permission/capability states, and focused evidence complete in Views #2 (`c125f33`) |
| `/settings` | `govoplan-notifications` `settings.sections` | Notification preferences | Personal configuration | Pattern migration, contextual help, permission/target explanation, typed toggles and focused evidence complete in Notifications #4 (`ad6a31f`) |
| `/dashboard` | Dashboard host and `dashboard.widgets` | Installed-modules widget; Ops health widget when Ops contributes it | Widget ordering, staleness, permissions, destination behavior | Pattern migration, view-aware composition, keyboard/drag alternatives, responsive packing, module filtering and focused evidence complete in Dashboard #3 (`da3947f`) |
| `/organizations` | IDM `organizations.functionActions` | Action leading to assignment view filtered by IDM scopes | Cross-module context action through explicit capability | IDM pattern migration complete in IDM #12 (`d864317`) |
| Campaign attachments/import | Files `files.fileExplorer` | Folder tree, managed chooser, file listing/pattern resolution/sharing | Optional domain composition without sibling-private imports | Campaign #74 pilot complete; watched-folder and duplicate-attachment product policy remain independent Campaign #60/#61 features |
| Campaign review/send | Mail runtime `mail.devMailbox` | Mock-mail verification when backend advertises runtime capability | Optional review stage with unavailable/optional states | Explicit intervention and review-progress vocabulary delivered in Campaign #63; send modes/progress delivered in #62/#79 |
Other named capability exports (`files.connectors`, `organizations.functionPicker`,
and mail profile validation) are contracts consumed inside the composed surfaces
above; they are not independent routes.
## Core Configuration Surface Map
Core #225 now supplies and verifies the platform-owned configuration contract.
The durable Core inventory is
`govoplan-core/docs/INTERFACE_PATTERN_MIGRATION.md`.
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
| --- | --- | --- | --- | --- |
| `/settings` (`SettingsPage`) | Change personal profile, interface/workspace preferences, or local development connection | Two-zone typed settings workspace | Changes are user-scoped; save and test actions distinguish clean, busy, and active states | Contextual help, unsaved guard, typed controls and keyboard-explainable disabled actions in Core `fa32cca` |
| Reusable credentials (`CredentialEnvelopeManager`) | Compare and configure scoped reusable authentication material | Repeated administration plus adaptive create/edit | Secret values are write-only; permission and missing-owner states block mutation explicitly; deletion can break dependent connections | Actionable blocker, stable row actions, typed references, unsaved guard and shared destructive confirmation |
| Retention (`RetentionPolicyManagement`) | Inspect effective retention and narrow permitted local values | Effective-policy editor | Parent locks, source paths and write authority control whether sensitive evidence can be retained | Typed narrowing controls, source-path help, lock/target/permission blockers and clean/loading/save reasons |
| Shared configuration primitives | Compose module-owned settings without sibling-private imports | Platform behavior contract | Consequence, focus, help, async, confirmation and permission semantics remain consistent | Core component suites, 121 module-system tests and full-product type/build/bundle gates |
No primary Core configuration flow requires raw JSON. Expert JSON remains
limited to diagnostics, interchange, conflict evidence, or read-only inspection.
## Policy Surface Map
Policy #11 verifies the four composed retention sections. The durable
module-level inventory is
`govoplan-policy/docs/INTERFACE_PATTERN_MIGRATION.md`.
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
| --- | --- | --- | --- | --- |
| System retention | Set the instance ceiling and run retention | Effective-policy editor plus destructive operation | An applied run can irreversibly redact/delete retained content; dry-run and applied evidence remain distinct | Core source-path/lock contract, permission and busy reasons, shared confirmation, typed/filterable outcome grid and audit-oriented wording |
| Tenant retention | Narrow the inherited system ceiling | Effective-policy editor | Tenant policy cannot silently loosen its parent | Core typed controls, effective path and parent-lock explanation |
| Group and user retention | Select an authorized target and narrow inherited policy | Targeted effective-policy editor | Selection exposes only bounded account/group labels; no retained content is returned | Delta-backed target loading, retry, missing-target blocker and responsive shared admin composition |
Automated evidence for Policy `f964ed7` comprises 50 backend/manifest tests,
the Policy interface structural gate, 65 manifest-shape checks, and the
full-product TypeScript/Vite build with structural localization, theme and
bundle-budget gates. Policy uses no sibling-private imports.
## Files Surface Map
Files #42 classifies and verifies the complete Files-owned route and composition
boundary. The durable module-level inventory is
`govoplan-files/docs/INTERFACE_PATTERN_MIGRATION.md`.
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
| --- | --- | --- | --- | --- |
| `/files` (`FilesPage`) | Browse spaces/folders and repeatedly act on current content | Full-height directory/explorer | Navigation is low consequence; upload, synchronize, move, copy and share are medium; delete is high | Stable two-pane composition, contextual help, selection/permission/state-specific disabled reasons, shared confirmation and responsive collapse |
| Upload/archive, transfer, rename and connector-import dialogs | Supply, validate and review one bounded change | Adaptive create/edit or guided import | Writes managed content and may resolve conflicts or import untrusted bytes | Shared dialogs/drop zone, bounded archive preflight, conflict review, explicit confirmation and no browser-native confirmation |
| Share/access explanation | Inspect or change who can use a resource | Review/decision | Grants can disclose content; delete/revoke changes access | Shared access explanation, action components and destructive confirmation; backend redaction remains authoritative |
| File connector tree and connection/credential dialogs | Compare and configure external endpoints and reusable credentials | Administration plus adaptive create/edit | Endpoint, secret and capability changes can enable remote access | Shared connection tree/forms/advanced panel, endpoint discovery and login test, unsaved-change guard, read-only deployment provenance and actionable disabled reasons |
| Connector policy card | Narrow effective connector use | Effective-policy editor | Inherited deny/allow rules affect lower scopes | Typed selectors, deny-precedence warning, effective sources, contextual admin help and permission blocker |
| `files.widget.spaces` | See available spaces and enter Files | Dashboard widget | Space/provider names remain permission-filtered | Shared loading, alert and status components; bounded configuration and refresh |
| `files.fileExplorer` capability | Select a governed managed snapshot for another module | Directory chooser | Exact file/version becomes another module's governed input | Capability-only composition, no sibling-private import, stable chooser/confirmation and exact snapshot evidence |
Automated evidence for commit `d8ae506` comprises 104 Files backend tests,
three focused Files WebUI structure tests, the full-product TypeScript/Vite
build, structural localization audit, theme contract and bundle budget. Shared
Dialog and disabled-tooltip behavior provide focus entry/return and
keyboard-reachable explanations; responsive source order is guarded at 1050 px
and 760 px. Secrets are not returned to the WebUI, and JSON remains only an
advanced provider-compatibility escape hatch rather than the primary editor.
## Mail Surface Map
Mail #20 classifies and verifies the complete Mail-owned route and composition
boundary. The durable module-level inventory is
`govoplan-mail/docs/INTERFACE_PATTERN_MIGRATION.md`.
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
| --- | --- | --- | --- | --- |
| `/mail` (`MailboxPage`) | Browse an authorized provider mailbox without changing it | Full-height directory/explorer | Message metadata and content are private; every provider read is bounded and non-mutating | Stable three-pane composition, contextual help, explicit no-profile blocker, refresh reasons, keyboard rows, paging and responsive collapse |
| Mail profile tree and profile/server/credential dialogs | Compare and configure reusable transport identities | Administration plus guided/adaptive create/edit | Endpoint and credential changes can enable external effects | Shared connection tree/dialog/stage rail/forms, focused hierarchy editors, unsaved guard, connection tests, permission/target blockers and disabled-save reasons |
| Mail policy card | Narrow profile visibility, lower-scope definitions and transport/address patterns | Effective-policy editor | Inherited allow/deny rules affect delivery and lower scopes | Typed selectors and controls, effective source path, lock/read-only blocker, dirty-save state and contextual admin help |
| `/mail/bounces` watcher table | Configure and explicitly scan bounded IMAP evidence sources | Operational administration | Provider access changes durable source cursors and evidence | Shared grid/status/loading/alerts, actionable no-profile and busy states, field help and stable row actions |
| `/mail/bounces` observations and watcher removal | Review sanitized delivery outcomes or stop future scans | Evidence/reporting plus destructive confirmation | Recipient diagnostics are sensitive; watcher removal retains existing evidence | Bounded sanitized rows and shared confirmation with retained-evidence consequence |
| `mail.profiles` and reference-selector capabilities | Select/validate Mail-owned transport from another module | Governed capability composition | A selected identity can perform external effects | Stable references, Mail-owned authorization/secret resolution, no sibling-private imports and clean optional absence |
Automated evidence for Mail commit `7844d9c` and Core commit `2d0551a`
comprises 114 Mail backend tests, Mail's focused UI/model/structure suite, the
Core shared mail-component suite, 65 manifest-shape checks and the full-product
TypeScript/Vite build with structural localization, theme and bundle-budget
gates. Shared Dialog and disabled-tooltip behavior provides focus containment,
return and keyboard-reachable explanations. Responsive source order is guarded
at 1250 px, 900 px and 760 px. Passwords remain write-only, mailbox responses
are bounded, and bounce evidence excludes raw provider messages.
## Campaign Pilot Surface Map
Campaign is detailed first because it exercises almost every archetype. The
recipient-data editor is now consolidated into the `recipients` section on
remote `main`; [Campaign #67](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/67)
records the accepted and verified integration boundary.
Campaign already consumes core primitives including `ModuleSubnav`, `Card`,
`PageTitle`, `Button`, `LoadingFrame`, `DismissibleAlert`, `FormField`,
`StatusBadge`, `MetricCard`, `DataGrid`, `TableActionGroup`, `Dialog`,
`ConfirmDialog`, `FileDropZone`, `MessageDisplayPanel`, policy components,
access/module capabilities, and unsaved-navigation guards. Reuse alone does not
prove that the composition or states satisfy the pattern.
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Known issue / rollout |
| --- | --- | --- | --- | --- |
| Campaign list (`CampaignListPage`) | Find, compare, create, open | List-detail entry | Campaign lifecycle/status and creation | #74 and guided entry [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) complete |
| Overview (`CampaignOverviewPage`) | Understand/edit campaign identity, version, access, lifecycle | Object overview plus adaptive edit | Lock/archive/delete/access changes expose consequence, reversibility, owner/access and lifecycle evidence | #74 complete; lifecycle policy is independently extended in Campaign #26 |
| Fields (`CampaignFieldsPage`) | Define recipient/template field schema | Structured editor | Schema changes can invalidate recipient/template data | #74 complete |
| Attachments/files (`AttachmentsDataPage`, `AttachmentRulesOverlay`) | Select sources and attachment/ZIP rules | Directory chooser plus adaptive rule editor | Missing or mismatched files affect built messages | #74 and attachment-detail [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) complete |
| Recipients (`RecipientDataPage`) | Select/import/map/edit recipients, address fields and per-recipient values/files | Import/mapping plus list-detail editor | Personal data, validation, bulk activation, file links | Consolidated editor #67, guided entry #35 and #74 audit complete; independent bulk action #68 remains product scope |
| Template (`TemplateDataPage`, placeholder/expression dialogs) | Author subject/body and preview substitutions | Adaptive editor plus stable preview | Generated communication content and unresolved expressions | #74 and stable overlay [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) complete |
| Mail settings (`MailSettingsPage` settings view) | Select/configure campaign mail transport | Adaptive configuration | Credentials, SMTP/IMAP destinations, test outcomes | #74 and Core #225 shared mail pattern complete; final credential hierarchy remains Mail #10 |
| Campaign settings (`GlobalSettingsPage` settings view) | Configure campaign behavior | Adaptive configuration | Can alter validation/build/send behavior | #74 complete |
| Mail policy (`MailSettingsPage` policy view) | Inspect/override effective mail policy | Effective policy/provenance editor | Inheritance and locks affect allowed delivery | #74 and Core #225 effective-policy pattern complete |
| Campaign policy (`GlobalSettingsPage` policy view) | Inspect/override campaign policy | Effective policy/provenance editor | Inheritance, actor authority, and blocked edits | #74 and Core #225 effective-policy pattern complete |
| Review/send (`ReviewSendPage`) | Validate, build, mock-test, confirm/send, inspect results | Guided review/decision plus durable progress | External communication, bounded synchronous execution, persisted queue mode, partial effects, retries, evidence | Interventions #63, send/progress #62/#79 and #74 wording/accessibility audit complete |
| Message and attachment detail overlays | Inspect one built/mock message and its attachment links | Stable detail/review dialog | Personal data, exact outbound content, reviewed state | Delivered and verified in [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) and [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) |
| Campaign report (`CampaignReportPage`) | Filter and inspect delivery outcomes | Reporting/list-detail | Partial, failed, explicitly excluded/skipped, SMTP/IMAP outcomes and retries | Server-owned filtering and counts delivered in [#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65) with the full-result DataGrid contract from [Core #263](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/263); excluded semantics in [#66](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/66) |
| Audit (`CampaignAuditPage`) | Reach campaign evidence/history | Explained provenance handoff | Campaign emits platform evidence; Audit owns reading, retention and bundles | #74 complete as an explicit Audit handoff; object-scoped projection may follow Audit #3 without a sibling-private import |
| JSON (`CampaignJsonView`) | Inspect/download expert representation | Advanced diagnostics/reference | Full authorized configuration may contain personal data but no inline transport secrets | #74 privacy audit complete with explicit sensitivity warning and campaign-read boundary |
| Create wizard (`CreateWizard`) | Seed a campaign through basics, sender, fields, recipients, template, attachments, review, send | Guided setup | Current steps mix creation and later consequential delivery; completion semantics need audit | Guided first campaign [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) |
| Review/send wizard routes | Focus the canonical review or send stage | Guided review | Thin wrappers render the same `ReviewSendPage` with a stable initial stage; no parallel workflow state exists | #74 inventory decision complete |
| Operator queue (`OperatorQueuePage`) | Monitor jobs and intervene | Monitoring/work queue | Campaign/version/job identity, historical active-version discovery, fixed action positions, authority-aware disabled states, exact non-overlapping queue counts, server-paged jobs, bounded refresh, retry/queue/reconcile per version, campaign-wide pause/resume/cancel, and leave/return progress | Durable controls #78 and #74 wording/accessibility audit complete |
| Aggregate reports (`AggregateReportsPage`) | Compare cross-campaign delivery outcomes | Privacy-preserving aggregate reporting | Tenant/campaign ACL, deployment/tenant small-cell policy, complementary and overlapping-cell suppression, explicit denominator, and no recipient detail/diagnostics/export/drill-down | Separate aggregate-reader surface delivered in [#80](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/80); not parity with the permission-gated per-campaign detail report |
The five review stages currently named in code are `Validate and inspect`,
`Build and review`, `Mock send and verify`, `Confirm and send`, and `Delivery
results`. Campaign #63 owns the intervention and status vocabulary; Workflow is
not required to define or implement it.
## Repositories Without A WebUI Package
The generated manifest snapshot reports no WebUI package for:
`govoplan-assets`, `govoplan-booking`, `govoplan-certificates`,
`govoplan-connectors`, `govoplan-consultation`, `govoplan-contracts`,
`govoplan-decisions`, `govoplan-encryption`, `govoplan-evaluation`,
`govoplan-facilities`, `govoplan-grants`, `govoplan-helpdesk`,
`govoplan-identity`, `govoplan-identity-trust`, `govoplan-inspections`,
`govoplan-learning`, `govoplan-mandates`, `govoplan-parties`,
`govoplan-permits`, `govoplan-poll`, `govoplan-procurement`,
`govoplan-records`, `govoplan-resources`, `govoplan-rest`,
`govoplan-services`, `govoplan-soap`, `govoplan-tickets`,
`govoplan-transparency`, `govoplan-wiki`, and `govoplan-workflow-engine`.
Tenancy does provide composed administration surfaces despite having no direct
route. This section is only negative package evidence; connector-only,
capability-only, runtime-only, and backend-only modules may intentionally remain
headless. A new WebUI should be created only for a concrete user task, not to
make every module symmetrical.
## Rollout Matrix
| Order | Scope | Current evidence | Target | Owner / issue | Verification gate | Status |
| --- | --- | --- | --- | --- | --- | --- |
| 0 | Product grammar and route inventory | Doctrine, ledger, layout rules, module contract, current route sources | One reconciled pattern language and evidence inventory | Meta [#11](https://git.add-ideas.de/GovOPlaN/govoplan/issues/11) | Reviewed route/component inventory, module documents, manifest shapes and focused contracts | Complete 2026-08-03 |
| 1 | Campaign baseline integration | Recipient-editor WIP and tracker state have been reconciled with remote `main` | Integrated, testable baseline before migration claims | Campaign #67 and tracker cleanup | Backend and focused WebUI suites; issue evidence | Complete 2026-07-22 |
| 2 | Campaign previews/details | Stable shared dialog with bounded scrolling and fixed responsive preview workspace | Stable header/body/footer, accessible long-content detail | Campaign #59 and #73 | Review-preview and overlay structure tests | Complete 2026-07-22 |
| 3 | Campaign review/interventions | Five domain-owned stages use central blocker and guided-review primitives; validation/build warnings name action, actor, and destination; hard blockers, individual review, and group review remain distinct; reviewed/remaining counts survive reload through build-bound review evidence | Clear stages, outcomes, blockers, next actor/action, reviewed evidence | Campaign #63 | `reviewProgress` state tests, shared-component structure contract, TypeScript build, configured-system help topic, and Campaign documentation tests | Complete 2026-08-03 (`d635f3a`; Core primitives and contextual help `b823a22`) |
| 4 | Campaign send/progress | A hard deployment ceiling bounds synchronous delivery; the selected synchronous, worker-queue, or database-queue mode is explicit and persisted; progress and recovery survive navigation; immediate-send response and audit evidence are allowlisted | Pre-send mode/consequence plus durable leave/return progress, retry and reconciliation without recipient/provider leakage | Campaign #62 and #79 | Boundary/concurrency/preflight, async selection, persisted mode, sanitized response/audit, partial/failure/retry and reload/return tests | Complete 2026-07-22 (`7e16603`, `60efd1c`, `62a6879`, `b0282eb`, `f095a3e`) |
| 5 | Campaign report filtering | Core DataGrid distinguishes client/full-result from server-owned queries; Campaign applies filter/sort/count before pagination and synchronizes count shortcuts with the grid query | One shared server-owned status/list/filter/count model | Campaign #65 and Core #263 | DataGrid contract/build tests plus exact shortcut/query/filter/count and large-result behavior | Complete 2026-07-22 (`e6062fe`, `cece71d`, `aa4ec66`, `4eb651c`) |
| 6 | Campaign operator recovery | A durable campaign/version queue page exposes historical work, exact non-overlapping state counts, persisted mode, permission-safe controls, server-paged job evidence, bounded refresh and active-state recovery | Fixed-position actions, disabled explanations, leave/return state, version-scoped retry/queue/reconcile and explicit campaign-wide pause/resume/cancel | Campaign #78 | Queue model/structure, historical-version, permission, paging, recovery-control, stale-response and delta tests | Complete 2026-07-22 (`21f3014`, `99d44ee`, `735e874`) |
| 7 | Campaign aggregate reports | A separate aggregate-reader projection and UI expose only policy-suppressed business totals with a stable status domain | Explicit denominator and exclusions, deployment floor plus tenant-strengthened small-cell threshold, complementary and overlapping-cell suppression, no detail/export/diagnostics | Campaign #80 | Aggregate query, cross-metric suppression, route/role/ACL, stable filter and UI structure tests | Complete 2026-07-22 (`06125cc`, `fc36aee`, `8ee87b7`, `ac3329c`, `1225802`) |
| 8 | Campaign excluded outcomes | Excluded build rows become explicit skipped transport outcomes and remain protected from queue/cancel/retry ambiguity | One durable source-to-job-to-report meaning with guarded historical normalization | Campaign #66 | Builder/persistence, migration, query/count, queue-control and report-explanation tests | Complete 2026-07-22 (`7229fb8`) |
| 9 | Guided first campaign | Eight-stage creation flow persists current step/draft and hands off to ordinary review/delivery preparation | Task-oriented entry that hands off clearly to normal editing/review | Campaign #35 | First-run flow, resume/back, partial validation, immutable-history and optional-module behavior, no implicit send | Complete 2026-07-30 |
| 10 | Prove/extract generic primitives | Shared consequence, focus, help, blocker, unsaved-change, confirmation, connection-tree and effective-policy contracts now have Core and multiple module consumers | Keep Core behavior-only and leave domain composition in owning modules | Core #225 plus bounded follow-ups | Core behavior/accessibility tests and module-permutation tests | Complete 2026-08-03 (`fa32cca`; Files `d8ae506`; Mail `7844d9c`) |
| 11 | Configured-system pattern help | Role/config-aware workflow, reference, pattern, and system topics are projected by Docs; shared route, field, blocker, and action links resolve to configured Docs or the hosted fallback | Stable configured-system guidance without feature-to-Docs imports | Docs #15 | Docs suite, shared component tests, Campaign review tests, 46 module permutations, full-product bundle budget | Complete 2026-08-03 (Docs `abe2f78`; Core `b823a22`; Campaign `d635f3a`) |
| 12 | Admin/configuration family | Core host/settings/credential/retention contracts, shared primitives, module lifecycle, Files, Mail, Policy, Access, Admin, Tenancy, Views, and Organizations are integrated and verified | Continue the same consequence/provenance grammar only through bounded module-owned migrations | Core #225 and module children | Per-surface state/accessibility/consequence evidence | Core #225 complete `fa32cca`; Access `1409dbf`; Files `d8ae506`; Mail `7844d9c`; Policy `f964ed7`; Admin `d428f33`; Tenancy `e76fe16`; Views `c125f33`; Organizations `97acfcb` |
| 13 | Remaining module surfaces | 33 bounded module-owned issues cover every WebUI contributor not already tracked by Campaign #74 or completed Docs #15 | Per-module audit and migration, ordered by user task and consequence rather than a bulk rewrite | Issues linked in the direct-route and composed-surface sections | Module-focused tests, manifest shapes, contextual Docs, and applicable definition-of-done gates | Complete: prior 28 recorded commits plus Workflow #15, Search #4, Reporting #8, Projects #2 and Portal #2 verified 2026-08-03 |
| 14 | Manifest/runtime alignment | Authenticated canonical routes align; public signed-token and compatibility routes are explicit exceptions | Stable declarations reconcile with source and any effective runtime module combination | [Meta #25](https://git.add-ideas.de/GovOPlaN/govoplan/issues/25) | Strict duplicate/stale/undeclared declaration CI, per-module digests, and authorized read-only runtime inventory | Complete 2026-08-04 |
Workflow remains outside this rollout matrix because it has its own runtime and
editor workstream, not because it is postponed. Focused views can be specified,
manually selected, and tested through core composition contracts today.
Workflow steps may activate those views through the same contract without
changing the proven surface patterns.
## Inventory Maintenance
When a route, nav item, named UI capability, host section, or Campaign workspace
surface changes:
1. Update the owner, evidence, task, archetype, and consequence here.
2. Link the implementation issue and verification evidence.
3. Keep "unreviewed" until state, permission/privacy, consequence/provenance,
accessibility, responsive, theme, i18n, and applicable async behavior have
been checked.
4. Re-scan both `module.ts` and the backend manifest; do not infer one from the
other.
5. Recreate the inventory from a clean release lockfile before using it as
release evidence.
+83
View File
@@ -0,0 +1,83 @@
# Meta Repository Scan
Scan date: 2026-07-13.
This scan checked local repositories under `/mnt/DATA/git` listed in
`repositories.json`.
## Repository Inventory
`repositories.json` already lists every checked-out GovOPlaN repository.
Checked-out repositories not listed in `repositories.json`: none.
Repositories listed in `repositories.json` but not checked out locally: none.
The human-readable link index is `docs/REPOSITORY_INDEX.md`; the JSON file
remains the machine-readable source of truth.
## Meta-Owned Content
These items are correctly owned by the meta repository:
- `.gitea/workflows`: cross-repository CI and audit workflows.
- `.gitleaks.toml`: whole-workspace secret scan policy.
- `audit-reports`: whole-workspace audit output.
- `dev/postgres`: shared development PostgreSQL service.
- `dev/production-like`: production-like product validation composition.
- `tools/checks`: whole-product checks and audit launchers.
- `tools/gitea`: issue/wiki/label/backlog tooling.
- `tools/launch`: product launch entry points.
- `tools/release`: release catalog, lock, push, and release console tooling.
- `tools/repo`: repository bootstrap, status, and metadata tooling.
## Module-Local Content That Should Stay Local
These paths look operational, but they are tied to one module's protocol or
transport behavior and should stay in the owning repository for now:
- `govoplan-campaign/dev/mail-testbed`: mail transport testbed for campaign.
- `govoplan-files/dev/connectors`: WebDAV, Nextcloud, and SMB connector smoke
environment for files.
The meta repo can later wrap these with aggregate commands, but the test-bed
definitions should remain close to the module code unless they become a shared
product deployment profile.
## Generated Or Local-Only Content
These should not move to the meta repo; they should be ignored or cleaned:
- `__pycache__` directories under `tools/gitea` and module test beds.
- `.venv` and `.ruff_cache` directories.
- Populated `.env` files such as `govoplan-campaign/dev/mail-testbed/.env`.
## Repo-Local Workflow Files
Most module repositories have `.gitea/ISSUE_TEMPLATE` installed. These are
repo-local generated copies of the shared workflow templates and are fine to
keep in each repository.
Repositories currently missing a `.gitea` directory in the local checkout:
- `govoplan-dashboard`
- `govoplan-evaluation`
- `govoplan-poll`
- `govoplan-rest`
- `govoplan-soap`
If those repositories should use the shared issue templates, run the meta repo
installer from `govoplan/tools/gitea/gitea-install-workflow.py`.
## Do Not Move
The following stay in each module repository:
- `pyproject.toml` and package dependency metadata.
- Backend manifests and migrations.
- Module-owned tests.
- Module READMEs and module-specific docs.
- WebUI package manifests and module frontend source.
Those files describe or implement the module itself. The meta repo should
catalog and orchestrate them, not become the owner of module code.
+123
View File
@@ -0,0 +1,123 @@
# Module Contracts and Install Boundaries
## Why Some Changes Require `pip install`
GovOPlaN discovers runtime modules through Python package entry points in the
`govoplan.modules` group. Core reads those entry points with
`importlib.metadata.entry_points()`, imports the configured manifest factory, and
then builds the module registry from the returned `ModuleManifest`.
That means the Python environment must know that a package exists before core can
discover it.
In development, `requirements-dev.txt` installs modules with `-e
../govoplan-module`. With editable installs:
- normal Python source changes are picked up after the process reloads;
- manifest code changes are picked up after the process reloads;
- new imports inside an already installed package are picked up after reload.
`pip install` is still needed when package metadata changes:
- a repository was not installed in the environment before;
- a `pyproject.toml` entry point is added, renamed, or removed;
- package dependencies change;
- optional extras change;
- package names or import roots change;
- console scripts or other installed metadata change;
- release installs need a different tag, wheel, or source ref.
The same principle applies to WebUI packages: source changes are local during
development, but `package.json` dependency or export changes require an install
step so the consuming app sees the correct package metadata.
## Current Contract Mechanism
Modules already announce contracts through `ModuleManifest`:
- `dependencies`
- `optional_dependencies`
- `required_capabilities`
- `optional_capabilities`
- `provides_interfaces`
- `requires_interfaces`
- `capability_factories`
- permissions and role templates
- route factories, migrations, docs, lifecycle hooks, and frontend metadata
Core validates the active manifest graph when it builds the registry. Release
tooling can inspect those manifests to calculate compatibility and migration
impact.
## What Core Can and Cannot Pick Up Automatically
Core can pick up contract changes automatically after reload when the changed
module package is already installed and importable.
Core cannot discover a new module or new entry point that has not been installed
into the environment, because there is no distribution metadata to enumerate.
Core also cannot notify every module about a contract change at edit time by
itself. The runtime registry is built from installed/importable packages. The
right place for cross-module announcement is the meta repo tooling and CI:
- scan manifests across all repositories;
- build an impact graph from provided/required interfaces and capabilities;
- run affected module tests;
- post Gitea issue/release notes for affected modules;
- block releases when a required interface is missing or incompatible.
## Mitigation Strategy
Use three layers:
1. Editable development environment.
Keep `requirements-dev.txt` in the meta repo as the one workspace installer.
Source edits then need process reloads, not repeated full installs.
2. Versioned runtime contracts.
Keep adding and tightening `provides_interfaces`, `requires_interfaces`, and
capability protocols. Treat interface names and versions as public module
contracts.
3. Meta-level contract audit.
The meta repo statically reads `src/**/backend/manifest.py` files across all
repositories and validates provided/required interface ranges without
importing the packages. CI blocks missing or incompatible required
interfaces before release installs are attempted.
Run the static graph check with:
```sh
./tools/checks/check-contracts.sh
```
Use `--json` when the release console or another automation needs structured
provider/consumer impact data.
## Practical Rule
Do not run `pip install` for every code edit. Run it when package metadata or the
set of installed packages changes.
For normal development:
```sh
./.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-dev.txt --python ./.venv/bin/python
```
Then restart the server when Python code or manifests change. The development
launcher runs this helper automatically by default; set
`GOVOPLAN_AUTO_SYNC_PYTHON=0` to disable that preflight.
For release validation:
```sh
./.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python ./.venv/bin/python
./.venv/bin/python -m pip install -r requirements-release-tests.txt
```
That install is necessary because the release environment intentionally resolves
tagged package refs, not local editable source trees. The second requirements
file contains only the harness needed to execute tests from those immutable
source tags; it is not part of the deployable release dependency set.
+184
View File
@@ -0,0 +1,184 @@
# Package Registry Releases
GovOPlaN publishes reusable module artifacts through Gitea's native PyPI and
npm registries. These packages improve developer installation, release
resolution, cacheability, and artifact inspection. They do not replace the
signed runtime distribution: the signed manifest and digest-pinned OCI images
remain the production deployment authority.
## Publication boundary
Every repository with a `pyproject.toml` contains
`.gitea/workflows/module-package-release.yml`. The meta repository owns the
canonical template and installs it with:
```bash
python tools/repo/sync-module-package-workflows.py --write
python tools/repo/sync-module-package-workflows.py --check
```
The workflow runs for `v*` tags and may be dispatched manually for an existing
tag. The organization preflight verifies that every package repository protects
the `v*` namespace. Before building, the workflow itself verifies that:
- the tagged commit is contained in `main`;
- the tag, Python project version, and optional WebUI package version agree;
- package names remain in the `govoplan-*` and `@govoplan/*-webui` namespaces.
The workflow binds the repository explicitly from the Gitea Actions context.
Do not rely on GitHub-compatible environment variables being injected by the
runner image; Gitea runners may expose only the context values. Gitea 1.24 job
tokens cannot read repository tag-protection settings, so package jobs must not
receive a broad administrator token merely to repeat the organization preflight.
Run the following before the first publication and after repository or tag-rule
changes:
```bash
python tools/gitea/gitea-configure-package-releases.py
```
Preview and dispatch the exact wheel/WebUI versions selected by the developer
meta-package with:
```bash
python tools/gitea/gitea-dispatch-package-set.py \
--env-file ~/.config/gitea/gitea.env
python tools/gitea/gitea-dispatch-package-set.py \
--env-file ~/.config/gitea/gitea.env \
--apply
```
The dispatcher reads exact versions from `packages/govoplan-meta/pyproject.toml`,
inspects the selected tag to determine whether a WebUI package is expected,
skips complete registry pairs and does not duplicate an active workflow. Use
`--repository govoplan-core` for a bounded dispatch or `--verify-existing` to
rebuild and hash-verify versions already present in both registries.
For coordinated lockstep tags, `push-release-tag.sh` pushes module tags first,
Core next, and the meta tag last. This is a dependency guarantee for a
single-capacity Actions runner: the developer package cannot run before its
exact Core and module versions have entered the queue.
The same release entry point first validates the migration graph, then records
the reviewed current Alembic heads under the target release version and reruns
the strict migration audit before it changes package versions, commits, or
tags. The default preflight intentionally does not require those heads to exist
in the previous release baseline. A failed candidate-baseline check therefore
cannot produce a protected package release.
The source gate validates `pyproject.toml`, the module version declaration
(`MODULE_VERSION` or the top-level `ModuleManifest.version`), public package
`__version__`, and WebUI metadata before creating tags. Release-tag artifact
checks run only after the candidate tags and immutable WebUI lock have been
created locally.
Release-lock regeneration resolves a fresh immutable lock from the reviewed
candidate manifests; it does not seed resolution from the previous release
lock. This prevents removed transitive packages and stale peer metadata from
blocking or contaminating the new release. Candidate resolution also uses an
isolated temporary npm cache, so a locally replaced tag cannot reuse metadata
from a failed, unpushed release attempt.
Modules that retain the same WebUI package identity in both a root publish
manifest and `webui/package.json` use the WebUI manifest as the canonical peer
contract. The coordinated release synchronizes `peerDependencies` and
`peerDependenciesMeta` into the publish manifest before creating the module
tag, then synchronizes each lockfile root from the final package metadata. A
distinct root package remains independent.
It builds one wheel and, where applicable, one npm tarball. The workflow records
the source tag, source commit, filename, size, and SHA-256 in
`package-artifacts.json` before publishing. Gitea rejects a second upload of the
same package version, so correction requires a new version rather than artifact
replacement.
A retry after partial publication is safe. Before upload, the workflow reads the
native package registry file record and compares its SHA-256 with the artifact
rebuilt from the protected tag. An exact existing artifact is skipped; a
same-version artifact with another digest or an unexpected file set fails
closed. This permits a failed npm publication to resume without weakening
package immutability or accepting `--skip-existing` blindly.
The npm tarball is always published through an explicit local `./dist/...`
path. Without that prefix, npm may interpret a relative tarball name as a Git
package shorthand before it ever contacts the configured registry.
Published WebUI packages contain registry-compatible dependencies only. The
workflow converts an internal dependency pinned to a protected `vX.Y.Z` Git tag
into the exact `X.Y.Z` registry version and rejects unresolved `file:` or Git
dependencies. Repository development metadata may therefore keep local or Git
references without leaking them into the published package contract.
Historical `add-ideas` and current `GovOPlaN` organization URLs are accepted
for immutable tagged releases; both normalize to the same exact registry
dependency and no branch or unversioned Git reference is accepted.
## One-time Gitea setup
Protect `v*` tags in every package repository and the meta repository. Allow
only the `Owners` team to create or delete those tags.
```bash
set -a
. ~/.config/gitea/gitea.env
set +a
python tools/gitea/gitea-configure-package-releases.py --apply
```
Create a dedicated personal access token with only `write:package` scope and
store these organization-level Actions secrets on `GovOPlaN`:
- `GOVOPLAN_PACKAGE_USERNAME`: account owning the package token;
- `GOVOPLAN_PACKAGE_TOKEN`: dedicated package-write token.
Do not use an administrator or general release token. Gitea 1.24 does not grant
package publication to the automatic Actions job token. Organization secrets
allow the same least-privilege credential to serve every module workflow.
## Exact release consumption
`tools/release/generate-release-package-set.py` translates the reviewed Git
source refs in `requirements-release.txt` into an exact registry package set.
It resolves each version tag to its commit and verifies the package metadata in
that tag.
`tools/release/resolve-package-artifacts.py` then downloads exactly those wheel
and WebUI versions from Gitea. It reads the identity embedded in every wheel and
npm tarball, rejects missing, duplicate, unexpected, or oversized artifacts,
and writes `package-artifacts.lock.json` with SHA-256 values and npm integrity
values. Credentials are accepted only through environment variables and are
never written to the lock. Python resolution ignores ambient pip configuration
and extra indexes for GovOPlaN roots, preventing an internal package name from
being selected from an undeclared registry.
The runtime distribution workflow uses the verified wheelhouse directly and
installs module WebUI tarballs only after matching them to the lock. It publishes
the package set, package lock, and hash-locked requirements as release assets.
The package-lock SHA-256 is part of the signed distribution manifest. Runtime
finalization also requires the lock's package versions and hashes to match the
wheel composition embedded in the images. OCI assembly remains network-free
after package and third-party dependency resolution.
The source refs remain in the module catalog for source provenance and release
planning. Production installation consumes the signed runtime images rather
than invoking `pip`, `npm`, or Git on the target host.
## Developer meta-package
`packages/govoplan-meta` builds the optional `govoplan` package. Its default
dependencies mirror the reviewed runtime roots; `govoplan[full]` adds all
currently packageable workspace modules. Regenerate it after changing release
requirements or package versions:
```bash
python tools/release/generate-developer-meta-package.py
python tools/release/generate-developer-meta-package.py --check
```
`push-release-tag.sh` performs this synchronization before release commits and
tags. The meta-package is for editable/developer setup and composition tests. It
does not enable modules, apply migrations, provision services, or establish
backup and recovery evidence.
Generic Packages are intentionally not used. Add that transport only when a
consumer needs an artifact format unsupported by PyPI, npm, Gitea Releases, or
the OCI registry.
+201
View File
@@ -0,0 +1,201 @@
# Platform Control Plane And Self-Description
## Objective
GovOPlaN should be able to describe its installed structure without becoming a
self-modifying application. The platform model is a declarative control plane:
module manifests, UI contributions, schemas, policy provenance, runtime
capabilities, and generated source evidence describe what can be configured.
Ordinary administrators edit validated data through those contracts; they do
not edit Python, TypeScript, routes, or database code from the product UI.
This distinction provides the requested overview while preserving reviewable
releases, module boundaries, migrations, and security controls.
## Canonical Sources
| Concern | Canonical source |
| --- | --- |
| Installed modules and dependency graph | Runtime `ModuleManifest` registry |
| Backend routes | Registered FastAPI application; Python AST is build-time evidence |
| Frontend routes and navigation | `PlatformWebModule` contributions |
| View-filterable regions | Versioned `viewSurfaces` declarations |
| Admin sections and module settings | `admin.sections`, including `moduleId`, `kind`, scope group, permission guards, and surface ID |
| User settings | `settings.sections` and core settings schemas |
| Labels and translations | Generated translation catalogs plus source usage |
| Fields and help coverage | Shared form components plus generated TypeScript AST inventory |
| API use by the WebUI | Typed API clients plus generated static reference inventory |
| Stable platform interface IDs | Typed manifest/WebUI declarations plus line-independent source anchors for low-level controls |
| Effective configuration | Owning module data plus Policy provenance |
Runtime introspection is authoritative for an installed system. Static source
inventory is authoritative evidence for a checkout or release candidate. The
two should be compared in CI and by Ops, not conflated.
## Generated Inventory
Run:
```bash
cd /mnt/DATA/git/govoplan
./.venv/bin/python tools/inventory/platform-interface-inventory.py
```
The command writes:
- `audit-reports/platform-inventory/platform-interface-inventory.json`
- `audit-reports/platform-inventory/platform-interface-inventory.md`
Use `--strict` for the combined translation, endpoint, and declaration audit.
Use `--strict-declarations` for duplicate/stale/undeclared interface checks
without making existing translation coverage a release blocker. Use
`--strict-endpoints` in the endpoint-surface CI gate so unrelated translation
catalog work cannot disable route classification enforcement. Both strict modes
require every backend endpoint without a statically visible WebUI path to have
an exact entry in
`tools/inventory/endpoint-surface-declarations.json`. The registry is keyed by
repository, HTTP method, and canonical version-independent path. It accepts:
- `ui_reachable`: a mounted router, generic action, or provider path hides the
reference from static extraction;
- `intentionally_headless`: a capability/API is deliberately consumed without
its own UI;
- `public_integration`: a documented public or interoperability endpoint;
- `worker_internal`: a worker, scheduler, reconciliation, or monitoring path;
- `compatibility`: a retained transition endpoint with a current replacement;
- `missing_ui`: a real UI gap, which must include a Gitea tracking issue;
- `removable`: a reviewed dead endpoint pending removal.
Strict mode also rejects declarations that no longer match source. When an
endpoint is added, changed, or removed, update its declaration in the same
change. Do not classify an endpoint from a string mismatch alone: first check
mounted prefixes, dynamic action paths, public clients, worker use, and
capability consumers.
It combines:
1. loaded module manifests
2. TypeScript AST extraction of fields, label attributes, visible text,
translations, frontend routes, navigation, capabilities, and API references
3. Python AST extraction of FastAPI route decorators and router prefixes
4. normalized runtime declarations from every loaded `ModuleManifest`
The declaration set covers routes, navigation, View surfaces, fields, actions,
help references, translations, admin/settings sections, widgets, search
objects, permissions, provided interfaces, and backend capabilities. Typed
module contributions keep their declared IDs. Shared controls may declare
`interfaceId` and `helpTopicId`; otherwise the extractor assigns a deterministic
source anchor based on repository, file, component context, control type, and
semantic label rather than a line number. The JSON records which identity
source was used.
The JSON includes exact repository, file, and line evidence. A missing-help
entry is a review candidate because dynamic parent components may supply help.
A backend route without a static frontend reference is also a review candidate:
public APIs, workers, callbacks, health checks, connectors, and dynamic URL
assembly are valid explanations.
The module matrix enforces endpoint and interface declarations with
`--strict-endpoints --strict-declarations`.
Combined `--strict` additionally fails when used translation keys are absent
from generated locale catalogs. Help-text findings remain review candidates
rather than a release gate because dynamic parent components can supply help.
## Runtime Comparison
Core exposes a sanitized read-only catalog at
`GET /api/v1/platform/interface-catalog`. Access requires
`admin:module:read` or `system:settings:read`. Tenant module entitlements are
applied before serialization, so the response describes only the effective
installed combination. It contains IDs, paths, authorization metadata,
versions, counts, and canonical digests; it excludes factories, callbacks,
credentials, and mutable runtime state.
Capture and compare a running installation:
```bash
curl --fail --silent \
-H "Authorization: Bearer $GOVOPLAN_ACCESS_TOKEN" \
"$GOVOPLAN_URL/api/v1/platform/interface-catalog" \
> /tmp/govoplan-runtime-interface.json
./.venv/bin/python tools/inventory/platform-interface-inventory.py \
--runtime-snapshot /tmp/govoplan-runtime-interface.json \
--strict-declarations \
--strict-endpoints
```
The comparison accepts any installed subset. Every module present in the
runtime response must have the same contract version, module version, and
declaration digest as the static release inventory. Unknown, duplicate, or
mismatched runtime modules fail strict declaration mode.
## Admin Information Architecture
The Admin host uses a tree because system, tenant, group, user, and module
settings form a hierarchy rather than one flat list. Every contributed section
can identify:
- its owning `moduleId`
- whether it is `management` or `settings`
- its system/tenant/group/user scope group
- an optional future `parentId`
- permission and View visibility requirements
Existing panels remain their own render owners. The tree only changes discovery
and grouping. A later embedded-settings contract may add named slots inside an
owning page; it must not allow one module to import another module's private
component.
## Navigation And Workflow
The intended maximum visible navigation stack is:
1. global shell context
2. one task/object navigation surface
3. one workflow stage surface when a workflow is active
Workflow instance pages should reuse the Campaign stage language: clear stage
state, optional/skipped/blocked semantics, partial progress, and a stable current
step. Workflow definition pages remain graph editors. Views may activate a
focused workflow view that suppresses unrelated shell and module surfaces while
retaining an explicit way out.
Nested module submenus should not be added merely because a data hierarchy
exists. Prefer a tree inside configuration/directory surfaces, tabs for sibling
views, and the workflow stage rail for ordered work.
## Safe Meta-Configuration
The platform can eventually render many configuration editors from versioned
JSON Schema and UI Schema supplied by modules. Generated editors remain bounded
by:
- explicit typed schemas and migrations
- module-owned validation and preview
- Policy locks and provenance
- permission and View filtering
- preflight, consequence, and rollback information
- auditable apply operations
Custom code, new routes, arbitrary SQL, and executable workflow nodes remain
release artifacts. Modeling them as ordinary configuration would create an
unreviewed code-execution and migration channel.
## Enforced Contract
1. Public WebUI routes and View surfaces must reconcile with runtime manifest
metadata; stale runtime routes and source-only public surfaces fail CI.
2. Duplicate stable IDs fail CI. Shared controls support explicit field/action
and help-topic identities; fallback anchors remain visible review evidence.
3. Every statically unreferenced backend endpoint has an exact reviewed
consumer classification, and stale classifications fail CI.
4. Runtime module combinations can be compared exactly with static release
evidence through versioned per-module digests.
5. Runtime introspection is authorized, tenant-filtered, and read-only. It is
safe for Ops/Docs projection but is not a generic configuration or code
mutation channel.
Generated JSON and Markdown remain build/audit artifacts. Do not hand-edit or
use them as a backlog; change the owning manifest, typed WebUI contribution,
translation/help declaration, or exact endpoint classification instead.
+182
View File
@@ -0,0 +1,182 @@
# Production Target And Independent Evidence Handoff
This runbook identifies the external inputs needed to finish
[GovOPlaN #27](https://git.add-ideas.de/GovOPlaN/govoplan/issues/27) and
[GovOPlaN #37](https://git.add-ideas.de/GovOPlaN/govoplan/issues/37). The
repository can render, inspect and sign evidence for a target, but it cannot
manufacture an independent failure domain or an independent approval authority.
## GovOPlaN #27: real two-node target
The bounded acceptance target is two independently schedulable worker nodes.
The API and WebUI must each have ready replicas on both nodes, all Deployments
must be available, the active module composition and software versions must be
consistent, every configured queue must have a worker, and the database
connection budget must pass. The validation then deletes one ready API pod and
requires replacement without an observed readiness outage.
Two virtual machines on different physical hosts or availability zones meet the
failure-domain intent. Two containers, VMs or Kubernetes nodes on one physical
host are useful development targets but do not close #27. A two-worker cluster
also does not prove control-plane high availability. For a self-managed
production cluster, use three control-plane nodes plus at least two workers; a
managed control plane plus two workers is the shorter path.
### What the target owner must provide
Provide these through a secure handoff, not an issue, chat message or Git:
1. A kubeconfig path with access to the target, for example
`~/.config/govoplan/targets/<target>.kubeconfig`, mode `0600`.
2. A stable installation ID, public HTTPS hostname, namespace, ingress class and
TLS-secret or certificate-manager arrangement.
3. Two independently schedulable workers and permission to place API and WebUI
replicas on both.
4. External, logically shared PostgreSQL, Redis and S3 endpoints with trusted
CA material and network reachability from every worker. Do not co-locate the
only copies of these services on the two workers used for the failure drill.
5. The six runtime secret values required by the generated manifest:
`MASTER_KEY_B64`, `DATABASE_URL`, `GOVOPLAN_DATABASE_URL_PGTOOLS`,
`REDIS_URL`, `FILE_STORAGE_S3_ACCESS_KEY_ID` and
`FILE_STORAGE_S3_SECRET_ACCESS_KEY`.
6. A short-lived GovOPlaN API key limited to `ops:operations:read`, supplied in
`GOVOPLAN_OPS_API_KEY` only for evidence collection.
7. An approved drill window and permission to delete one API pod.
If no Kubernetes target exists, provide hostnames/IP addresses for the machines,
an SSH user and key path, the internal/external DNS plan, and the permitted
firewall ports. Those inputs are sufficient to provision a k3s target. They are
not sufficient to claim control-plane HA unless three control-plane failure
domains are present.
### Separate deployment and evidence authorities
The deployment identity may create and update the namespace, Secret,
ConfigMap, Deployments, Services, Jobs, PodDisruptionBudgets and Ingress. The
evidence collector only needs:
- cluster scope: `get` and `list` for `nodes`;
- target namespace: `get` and `list` for `pods` and `deployments`;
- target namespace during the approved drill: `delete` for `pods`.
Use separate kubeconfig contexts or service accounts when the same person does
not hold both roles.
### Render, apply and verify
Use the signed, digest-pinned installation bundle selected for the target:
```bash
export KUBECONFIG="$HOME/.config/govoplan/targets/<target>.kubeconfig"
python tools/deployment/govoplan-deploy.py render-kubernetes \
--directory /srv/govoplan/<installation-id> \
--namespace govoplan \
--secret-name govoplan-runtime \
--tls-secret-name govoplan-tls \
--ingress-class-name nginx \
--output /srv/govoplan/<installation-id>/kubernetes.json
kubectl apply -f /srv/govoplan/<installation-id>/kubernetes.json
kubectl -n govoplan wait --for=condition=available deployment --all --timeout=10m
export GOVOPLAN_OPS_API_KEY="$(cat /run/secrets/govoplan-ops-evidence-key)"
python tools/deployment/govoplan-deploy.py verify-kubernetes \
--directory /srv/govoplan/<installation-id> \
--namespace govoplan \
--exercise-api-pod-loss \
--output /srv/govoplan/<installation-id>/evidence/kubernetes-multi-host.json
unset GOVOPLAN_OPS_API_KEY
```
The verifier emits sanitized JSON and exits nonzero if the topology, runtime,
queue, connection-budget or pod-loss checks fail. Preserve the private cluster
logs and manifest alongside the sanitized result in the controlled evidence
store.
## GovOPlaN #37: controlled signed target evidence
Yes, collection, review and signing can run in containers. A container provides
repeatability and process isolation; it does not create independent authority.
The production approver must control a different private key from the target
operator/assessor and must review the evidence before signing the
`production_approval` scope.
Use at least these three key boundaries:
1. **Installer authority:** signs installed-release-origin receipts only.
2. **Target assessment authority:** signs the permitted target, accessibility,
privacy, security, operations and recovery scopes.
3. **Production approval authority:** independently signs only
`production_approval` after reviewing the other evidence.
Do not reuse release-catalog keys for any of these roles. Keep private Ed25519
keys outside Git, Gitea, GovOPlaN application storage and chat. Publish only the
public keyrings. The proof issuer already rejects key reuse across release,
installer and proof trust domains.
### Generate independently held keys
Each authority runs this command in its own `0700` directory. The generator
refuses existing output paths and writes both files as `0600`:
```bash
install -d -m 0700 "$HOME/.config/govoplan/authority-keys"
python tools/assessments/generate-authority-keypair.py \
--purpose proof \
--key-id authority:target-2026 \
--scope target_environment \
--scope accessibility \
--scope privacy \
--scope security \
--scope operations \
--scope recovery \
--private-key "$HOME/.config/govoplan/authority-keys/target-2026.pem" \
--keyring "$HOME/.config/govoplan/authority-keys/target-2026-public.json"
```
The independent production approver generates another key with only
`--scope production_approval`. An installer authority uses `--purpose installer`
and no `--scope`. Merge public key entries into the separately controlled
keyrings only after the responsible authorities verify fingerprints out of
band.
### Container boundary
Use two one-shot jobs or containers:
- **Collector/assessor:** network access, read-only source and trust mounts,
read/write private evidence output, and the narrowly scoped kubeconfig. It
must not receive the production-approval private key.
- **Production approver:** `--network none`, read-only assessment/evidence/trust
mounts, a read-only secret mount containing only the approval key, and a
separate output mount. It must not receive deployment credentials.
Build or select the assessment image by digest and record that digest in the
evidence log. A representative runtime shape is:
```bash
docker run --rm --network none --read-only --tmpfs /tmp \
--user "$(id -u):$(id -g)" \
--mount type=bind,src="$PWD/evidence",dst=/evidence,readonly \
--mount type=bind,src="$PWD/trust",dst=/trust,readonly \
--mount type=bind,src="$HOME/.config/govoplan/authority-keys",dst=/run/keys,readonly \
--mount type=bind,src="$PWD/approved",dst=/output \
<assessment-image>@sha256:<digest> \
<assessment command>
```
The current evidence commands and required scopes are documented in
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
The final proof must cover `target_environment`, `accessibility`, `privacy`,
`security`, `operations`, `recovery` and independent `production_approval`, and
must bind to the verified installed composition and installer receipt.
## Completion boundary
#27 can close after the real target produces a passing pod-loss result. #37 can
close after an independently approved, schema-valid proof is generated for that
same installed composition and the public authority keyrings, proof and private
evidence custody references are recorded. Neither issue should close from a
single-host simulation or a self-approved signature.
+155
View File
@@ -0,0 +1,155 @@
# Recovery And Rollback Guarantees
## Principle
GovOPlaN must prove recovery claims with durable state recorded before and
after side effects. A failed operation is not automatically rolled back merely
because the previous application image still exists. Database schema and
external effects may make release rollback unsafe.
Core therefore distinguishes five recovery modes:
| Mode | Meaning |
| --- | --- |
| `atomic` | One database transaction either commits or rolls back. No external effect is claimed. |
| `compensation` | Durable evidence identifies explicit inverse actions for completed effects. |
| `snapshot_restore` | A separately verified backup reference and restore procedure exist. |
| `forward_recovery` | Repair or resume the current version; reverting code/configuration is not claimed safe. |
| `irreversible` | No automated recovery is claimed and an approval reference is mandatory. |
An operation plan must include verification steps. Compensation requires named
compensation steps, snapshot restore requires a verified backup reference,
forward recovery requires repair steps, and irreversible work requires explicit
approval.
## Core Recovery Ledger
Core stores recovery operations and append-only, hash-chained checkpoints in
PostgreSQL. The contract provides:
- installation/module/resource identity;
- an idempotency key bound to a canonical request hash;
- recovery mode, preconditions, verification steps, and references;
- optional runtime lease holder and fencing token;
- explicit planned, prepared, running, recovery-required, recovering,
succeeded, recovered, failed, outcome-unknown, and manual-intervention states;
- an evidence-chain head and sequence count;
- rejection of plaintext secrets in metadata or evidence.
Preparation cannot succeed without durable precondition evidence. A non-atomic
operation cannot hide a partial effect by transitioning directly from running
to failed. Success and recovery require explicit verification evidence with at
least one check. The ledger verifies its hash chain before evidence is trusted.
This is a platform contract, not an assertion that every existing module
operation has adopted it. Module operations with external or multi-resource
effects must be migrated to the ledger before claiming these guarantees.
The owning-module inventory and adoption state are maintained in
[Recovery Ledger Adoption](RECOVERY_LEDGER_ADOPTION.md); CI validates the
machine-readable inventory so newly identified boundaries cannot disappear from
the backlog silently.
## Deployment Journal
Every `govoplan-deploy apply` begins an operation journal before it pulls images
or mutates runtime state. The private installation directory records:
```text
operations/<operation-id>/operation.json
operations/<operation-id>/before/
applied-state/
```
Each stage is hash-chained. The previous applied bundle is copied with per-file
SHA-256 evidence. Applied state is replaced atomically after health verification;
an interrupted replacement restores its previous directory.
New journals also bind the complete desired deployment plan, snapshot
availability, failure summary, recovery mode, and terminal status to the
evidence chain. Recovery verifies every snapshot entry and checksum before it
changes any live bundle file, then replaces each live file atomically. A crash
between file replacements is recoverable by rerunning the same idempotent
recovery command under the deployment lock.
Inspect operations:
```sh
python tools/deployment/govoplan-deploy.py operations \
--directory /srv/govoplan/default
```
Recover the latest failed operation, or provide its identifier:
```sh
python tools/deployment/govoplan-deploy.py recover \
--directory /srv/govoplan/default \
--operation-id 20260801T120000Z-1234abcd
```
Add `--apply` only after reviewing the reported action.
## Migration Boundary
Before database migration starts, a failed deployment with a verified prior
applied snapshot may restore its prior release/configuration bundle and
reconcile that desired state.
As soon as migration starts, the journal permanently changes to
`forward_recovery`. It will not restore old application configuration because
old code may not understand the new schema. Recovery then means one of:
1. fix and re-run the current release;
2. deploy a newer compatible repair release;
3. restore a separately verified, coordinated database/object/key backup and
then deploy the matching release.
The deployment tool does not create that backup. It does verify an externally
produced, signed evidence contract covering PostgreSQL, objects, protected
configuration, and key custody at one recovery point plus an isolated restore
drill. A self-hosted release change cannot reach the migration command or be
exported as a Kubernetes migration Job until fresh evidence bound to the
previous immutable release has been adopted. Compose verifies it again after
runtime quiescing. See
[Backup And Restore Evidence](BACKUP_AND_RESTORE_EVIDENCE.md) for the contract,
provider runbooks, RPO/RTO ownership, retention, and disposal rules.
## Scaled Nodes
Recovery actions must be safe across replicas:
- drain affected API and worker nodes before incompatible changes;
- use the deployment-wide PostgreSQL advisory lock for schema migration;
- use distributed leases and fencing tokens for singleton or externally
visible effects;
- use idempotency keys for retried commands and jobs;
- retain shared object keys and database references until deletion succeeds;
- classify uncertain external outcomes instead of retrying blindly;
- verify the exact software/module composition after replacement.
Campaign generated-message objects now follow this model: object writes are
compensated when a build fails before database commit, workers verify stored
size and digest before delivery, and retention keeps the database reference
when storage deletion fails. A hard process loss between object creation and
database commit can still leave an orphan object; an inventory reconciler is a
separate operational slice and must use the build-specific object prefix.
## Required Drills
Record evidence for at least these scenarios before production acceptance:
1. Kill an API replica and verify traffic continues without session loss.
2. Drain and replace a worker while work is queued and while one job is active.
3. Start two migration jobs and verify only one mutates schema.
4. Kill the fenced scheduler and verify one replacement acquires a higher
fencing token.
5. Fail deployment before migration and restore the prior applied bundle.
6. Fail deployment after migration and verify old configuration is not
restored.
7. Restore PostgreSQL, object storage, and encryption keys to one coordinated
recovery point and verify representative object hashes.
8. Interrupt object storage during Campaign build and retention and verify
compensation/reference-preservation behavior.
9. Tamper with a deployment or Core recovery checkpoint and verify chain
validation rejects it.
No runbook, status badge, or green health endpoint substitutes for a dated,
repeatable restore drill against the actual deployment topology.
+89
View File
@@ -0,0 +1,89 @@
# Recovery Ledger Adoption
The Core recovery ledger is a platform primitive, not automatic protection for
module-owned effects. The canonical, machine-checked inventory is
[`recovery-operation-inventory.json`](recovery-operation-inventory.json).
## Classification Rules
- Use `atomic` only when every mutation commits in one database transaction and
no external effect occurs.
- Use `compensation` when every completed effect has a bounded, verifiable
inverse action. A best-effort delete is not proof of compensation.
- Use `snapshot_restore` only with fresh, signed backup evidence that covers all
affected state services at one recovery point.
- Use `forward_recovery` for provider acceptance, queue publication, cursor
advancement, and other effects that may be resumable but cannot safely be
undone.
- Use `irreversible` for approved purge or destruction where no automated
recovery is claimed.
One feature may cross more than one boundary. Module installation is
compensatable before schema migration, forward-only after migration starts, and
snapshot-restorable for an approved destructive retirement. Mail submission is
forward recovery because losing the response after provider acceptance must not
cause an automatic resend.
## Adoption Order
1. Campaign build is the reference implementation for a database plus object
storage operation. Its operation reserves a build-specific object prefix,
persists request and precondition evidence before writes, records the final
object manifest, and verifies database/object state before success.
2. Campaign delivery and Mail provider effects adopt outcome-unknown semantics
without weakening their existing provider-specific idempotency records.
3. Files applies the same contract to uploads, purge, integrity reconciliation,
and writable connector synchronization.
4. Connectors, Dataflow, and Workflow Engine consume the contract at their
registry/capability boundaries so optional providers remain optional.
5. Core module lifecycle uses the ledger in addition to, not instead of, signed
deployment and backup evidence.
Every fenced operation uses a process incarnation and distributed lease. A
stale process cannot append a checkpoint or report success. An expired operation
is claimed for recovery through an explicit takeover that preserves the prior
fence in the checkpoint chain; it is never resumed as a normal retry.
Connectors read-only sanctions and feed acquisitions are adopted: source
revision/cursor and dry-run evidence are recorded before provider I/O, while
the immutable snapshot and terminal checkpoint commit atomically. The generic
external-mutation contract is conformance-tested but remains `planned` until a
production connector actually publishes, updates, or deletes provider state.
Dataflow runs are adopted. Database-only execution uses one atomic terminal
commit for the run projection and recovery checkpoint. Output publication uses
forward recovery: source and output digests are checkpointed before dispatch,
a conclusive provider result commits with the run projection, and an expired
or failed attempt after dispatch becomes `outcome_unknown`. A stale attempt may
be retried only when its durable boundary proves dispatch had not started.
Workflow Engine is adopted at both declared boundaries. Instance workers,
trigger deliveries, and timer resumptions use process-bound distributed fences.
Every module-action invocation records the pinned definition, input, preview,
authority, provider-idempotency, and action-contract hashes before dispatch.
Conclusive results commit with the Workflow projection. A lost acknowledgement,
invalid result, or unannounced non-atomic effect becomes `outcome_unknown` and
cannot be retried until evidence confirms either that the effect occurred or is
absent. Linked Dataflow uncertainty blocks the Workflow without duplicating
Dataflow's recovery authority.
Core module lifecycle is adopted at four boundaries. Installer recovery is
prepared before snapshots so a full database restore preserves the attempted
operation. Pre-migration package changes use compensation, migrated changes use
forward recovery, destructive retirement requires a hashed and restore-checked
snapshot, and live graph changes restore the prior registry when no migration
ran. A deployment-wide database fence serializes these effects; any unresolved
predecessor blocks a differently keyed retry until explicit reconciliation.
Supervised installs become successful only after restart and health evidence is
recorded.
## Operator Contract
Ops lists non-terminal and manual-intervention operations. Operators must verify
the checkpoint chain before trusting evidence, distinguish `outcome_unknown`
from rejection, and use the owning module's documented reconciliation action.
No evidence payload may contain credentials or resolved secrets.
The parent adoption issue remains open until all inventory rows are adopted and
the module matrix proves crash, retry, stale-fence, tamper, and optional-module
behavior for each consequential path.
+460
View File
@@ -0,0 +1,460 @@
# GovOPlaN Reference Journey Program
## Status
This is the selected product-development sequence, originally chosen on
2026-07-21 and reconciled with the implemented platform on 2026-07-31. It turns
the long-term connected-platform roadmap into five demonstrable journeys.
Workflow Engine and the optional Workflow editor now exist, but they are used
by a stage only when its package explicitly composes and proves them; Workflow
is not an automatic dependency of every journey.
The institutional semantics and source-authority model applied to these stages
are defined in the
[Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
The stages are ordered, but they are not monolithic releases. Each stage is
delivered as small, reviewable, green increments and is complete only when its
user journey, failure behavior, documentation, and operator evidence work in a
pinned composition.
## Why this sequence
The sequence grows one connected product rather than advancing repositories in
isolation:
```text
Campaign reference
-> function-bound Postbox delivery
-> data-backed templates and reports
-> governed analytical data products
-> collaborative document lifecycle
```
Each stage reuses the preceding contracts:
- Campaign proves governed recipient selection, files, profiles, preview,
durable delivery, retry, reconciliation, audit, and role-specific UI.
- Postbox generalizes delivery from an email address to an institutional
responsibility that survives personnel changes.
- Templates and Reporting turn governed source data into reproducible outputs
and provide a safe launch boundary for specialist systems such as HIS.
- The BI path scales those source, transformation, quality, lineage, and report
contracts from one document to reusable analytical data products.
- DMS collaboration turns generated or uploaded artifacts into governed living
documents without confusing storage, editing, approval, or records
ownership.
## Continuous foundation lane
This lane is not a sixth product stage. It advances only as far as the next
journey needs and supplies contracts shared by all five stages.
1. **Governed integrations.** Connector profiles declare endpoint, authority,
source-of-truth mode, capabilities, health, limits, credential references,
and lifecycle. Network destinations are resolved and pinned at connection
time; private-network access is deployment-controlled and unpinned private
transports fail closed. Deleting a connector or provider immediately
deletes every secret it owns and records a non-secret audit event in the
same lifecycle action; an unavailable external secret provider blocks the
deletion or retirement rather than leaving an unaudited orphan.
2. **External effects.** Requested action, durable command, observed effect,
unknown outcome, retry, reconciliation, compensation/correction, and manual
intervention remain distinct and auditable.
3. **Institutional identity.** Identity owns subjects, Organizations owns units
and functions, IDM owns identity-to-function assignments and upstream
synchronization, and Access owns permission and acting-context decisions.
4. **Stable composition.** Modules cooperate through versioned Core-mediated
capabilities and typed references. Optional sibling modules are never
imported as implementation dependencies.
5. **Evidence and provenance.** Every imported datum, generated artifact,
delivery, transformation, and later correction can identify its source,
version, actor or system trigger, policy, and relevant execution evidence.
6. **Focused experience.** Central components and the interface pattern
language are mandatory. Views present only what the current task and actor
need while preserving an explainable escape to the configured full system.
7. **Adaptive documentation.** The owning module contributes canonical subject
matter; GovOPlaN Docs composes it for the installed modules, enabled
configuration, role, route, and task.
8. **Release proof.** Version alignment, clean install/upgrade, module
permutations, backup/restore, degraded-provider behavior, security checks,
and target-environment acceptance gate product claims.
9. **Artifact and infrastructure egress.** Tenant connectors use the pinned
connector boundary. Installer artifacts use a separate deployment-owned
trust path: approved origins, bounded pinned download or operator prefetch,
signature/checksum verification, safe extraction, and offline package-manager
execution. Database, broker, cache, and worker channels are constrained by
deployment network policy and authenticated transport rather than treated as
tenant connector profiles.
## Documentation contract for every reference stage
Documentation is one connected knowledge model rendered through different
perspectives, not separately maintained manuals that drift.
Every demonstrated journey provides:
- **Task guidance:** “I want to …” instructions using visible UI wording.
- **Process view:** actors, inputs, states, decisions, handoffs, exceptions,
completion, correction, and retention.
- **Concept view:** domain language and ownership boundaries.
- **Administrator view:** permissions, policies, profiles, configuration,
module requirements, migrations, and audit effects.
- **Operator view:** health, queues, retries, reconciliation, backup/restore,
incident handling, and target-provider checks.
- **Integration view:** capabilities, DTOs, APIs, events, idempotency,
compatibility, deep-link/launch contracts, and example payloads.
- **Security, privacy, and audit view:** threats, disclosure rules, secrets,
provenance, evidence, retention, and destructive actions.
- **Acceptance view:** runnable examples, expected results, failure injection,
and release gates.
The Docs module selects and links these views according to installed
capabilities and actor context. Feature repositories remain the source of
truth for their behavior; Docs owns indexing, conditions, safe disclosure, and
presentation. Generated API/contract reference may supplement, but never
replace, the maintained behavioral explanation.
## Stage 1: Campaign as the demonstration module
### Product promise
> As a communications team, we can compose a campaign from governed recipient
> data, managed files, reusable mail configuration, and policy; preview the
> exact effect; authorize delivery; and resolve every recipient to a known or
> explicitly uncertain outcome without exposing transport internals to normal
> readers.
Campaign is the first reference composition, not an all-purpose mail client or
contact database.
### Ownership and composition
- Campaign owns campaign intent, content, campaign-local recipient snapshots,
personalization, execution state, and delivery evidence.
- Mail owns reusable profiles, credentials, protocol adapters, profile policy,
send/append execution, and provider-facing diagnostics. Campaign persists
only a stable Mail profile reference, never SMTP/IMAP settings or secrets.
- Files owns stored objects, provider connectors, file policy, and provenance.
Campaign persists stable attachment references and frozen execution evidence.
- Addresses or another directory may supply recipients, but Campaign freezes
the execution-time recipient and source evidence needed to explain the send.
- Policy, Access, Audit, Docs, Notifications, Organizations, Identity, IDM, and
Postbox contribute only through declared capabilities when installed.
### Implementation slices
1. Enforce the Mail-profile-only boundary and reject inline transport material
on authoring, import, update, build, and delivery paths. Legacy records are
preserved but fail closed until explicitly migrated.
2. Finish the central-component UI pass and consistent task-focused author,
reviewer, operator, and reader surfaces. Normal readers receive business
state, not paths, backend keys, worker claims, or raw provider diagnostics.
3. Prove test, single-send, resend, batch, append-to-Sent, retry,
reconciliation, quarantine, and manual intervention semantics against a
target SMTP/IMAP environment.
4. Complete adaptive Campaign, Mail, and Files documentation under the shared
documentation contract, including runnable reference campaigns and failure
drills.
5. Package the composition with sample data, policies, roles, configured views,
preflight, health checks, and pinned compatible versions.
### Gate
A clean installation can run the maintained examples through target providers;
no Campaign payload contains transport credentials; worker/provider failures do
not cause blind duplicate delivery; each supported role sees an appropriate
surface and explanation; install, upgrade, restore, and release evidence are
repeatable.
## Stage 2: Postbox and delivery to institutional responsibility
### Product promise
> As a sender, I can deliver a message to a named function in an organizational
> unit without knowing which individual currently performs it. As the current
> function holder or authorized delegate, I can discover, read, and act on that
> message; when assignments change, access follows current responsibility while
> the delivery and access history remains explainable.
### Ownership and composition
- Postbox owns addressable in-platform postboxes, messages, bindings,
participants, attachment references, access-sensitive events, and retention
inputs.
- Organizations defines units and functions. Identity defines subjects. IDM
assigns identities to functions and reconciles upstream directories. Access
resolves roles, delegations, acting context, and permissions.
- Mail is an optional external channel bridge. A Postbox is not an SMTP/IMAP
account and is never owned by one login credential.
- Campaign consumes a generic delivery-target capability. It may target an
email recipient or a function-bound Postbox without importing either
module's internals.
- A Postbox exists independently of its current holders. Vacancy, one
incumbent, several incumbents, and time-bounded delegation are valid states;
none turns the Postbox into a personal account.
- The E2EE target uses per-content keys and function/postbox key epochs.
Assignment grants device-bound access to policy-selected history, while
hand-over, revocation, and delegation expiry rotate or withdraw future key
access without claiming to erase plaintext already obtained.
- Content and signed manifests are immutable. Correction or replacement is a
linked new object/version, never silent substitution.
### Implementation slices
1. Stabilize Postbox manifest, permissions, DTOs, migrations, directory,
binding administration, access decisions, message create/list/read, and
audit events.
2. Define a typed delivery target and receipt contract for a Postbox id or an
organization-unit/function address. Resolution never copies current holders
into permanent mailbox ownership.
3. Complete the OrganizationsIdentityIDMAccess function-assignment and
delegation path, including provenance and compatibility migration from
legacy projections.
4. Define the E2EE key-grant/epoch profile for vacancy, multiple incumbents,
hand-over, history access, time-bounded delegation, recovery, and audit.
5. Add Campaign delivery to a function-bound Postbox, file attachment
references, optional external-mail bridge, and notification attention
signals through capabilities.
6. Document reassignment, delegation, vacancy, expired assignment, unavailable
optional module, retention, and future E2EE limitations honestly.
### Gate
A campaign message can be delivered once to a function-bound Postbox; an
authorized holder can act in an explicit context; a reassignment changes future
access without moving or rewriting the message. A vacant Postbox remains
addressable, retains the delivery, and visibly reports that no holder can
decrypt or act until assignment; multiple incumbents and delegates receive
independent key/access evidence. Delivery, key access, and later corrections
remain auditable.
## Stage 3: Templates, reports, data sources, and deep launches
### Product promise
> As a staff member arriving from a specialist system such as HIS, I can open a
> GovOPlaN report or document task with a governed source context already
> selected, review the effective data and template version, generate the
> document, and return or link the immutable result without re-entering data.
### Ownership and composition
- Templates owns reusable definitions, versions, merge schemas, localization,
render profiles, preview, and rendering.
- Reporting owns report definitions, parameters, curated data selection,
execution history, dashboards, sharing, scheduling, publication, and export.
- Connectors own protocol-specific access. A governed data-source catalog owns
connection profiles, source contracts, freshness, and health if repeated
use proves that a separate module is warranted.
- Files stores generated bytes. DMS owns a generated artifact once it becomes a
governed living document. Records owns later retention/archive semantics.
### Safe launch and generation contract
1. The source program opens a stable GovOPlaN route with an opaque, short-lived,
single-purpose launch reference—not credentials, arbitrary SQL, or trusted
personal data in the URL.
2. GovOPlaN authenticates the actor, authorizes the requested report/template
and source object, resolves the launch context server-side, and shows source,
freshness, purpose, and any blockers.
3. A curated connector/read-model capability returns a versioned input snapshot
that satisfies the template or report schema.
4. Generation records template/report definition version, parameters, input
snapshot or reproducible source references, transformation version, output
checksum, actor, and policy.
5. The result is stored or handed back through an explicit callback/reference
contract. Retries are idempotent; expired or replayed launch references fail
safely.
### Implementation slices
1. Implement the first Templates manifest, version/schema DTOs, safe preview,
deterministic render, package fragments, and Files-backed output.
2. Implement the first Reporting manifest, parameter/source DTOs, one
module-owned read model, execution evidence, and downloadable export.
3. Define the shared data-source/profile and launch-context contracts with one
read-only target connector and a mock HIS-style launch producer.
4. Deliver one end-to-end reference document and one analytical report from the
same governed source contract.
5. Add role-specific UI/docs, source freshness and validation states, deep-link
integration examples, and target-system acceptance tests.
### Gate
A user can follow a signed or server-side launch reference from a target system
to a prefilled GovOPlaN task, verify the source and freshness, render an output,
and reproduce why that exact artifact was produced. The browser cannot inject
arbitrary queries or transport secrets, and authorization is re-evaluated in
GovOPlaN.
## Stage 4: Governed BI and reporting
### Product promise
> As an institutional analyst, I can combine approved operational sources into
> versioned, quality-checked analytical data products, define transparent
> measures and reports, drill from aggregates where policy permits, and explain
> the source, transformation, effective organizational hierarchy, and reporting
> date behind every result.
### Lessons taken from SuperX
SuperX is useful inspiration because it combines many university sources in a
modular data warehouse, loads and transforms them on a schedule, keeps
historical or reporting-date views, supplies prepared subject modules, and
serves different aggregation levels through a common reporting surface. Its
official overview also emphasizes a lower operational-data layer, validation
logs, configurable load routines, organizational hierarchies, and transparent
report calculations. See the
[SuperX project overview](https://www.superx-projekt.de/__index.htm),
[administration and module architecture](https://www.superx-projekt.de/doku/kern_modul/admin/__index.htm),
and [cross-institution indicator module](https://www.superx-projekt.de/doku/kenn_modul/benutzer/__index.htm).
GovOPlaN should adopt the principles, not clone the implementation. Its distinct
value is connecting analytical results to governed work, responsibility,
evidence, policy, and correction across independently installable modules.
### Architecture layers
1. **Source catalog:** governed profiles, owners, purpose, schema/version,
classification, credentials, health, extraction mode, and source-of-truth
declaration.
2. **Ingestion and staging:** immutable run evidence, source snapshots or
watermarks, schema-drift detection, quarantine, replay, and personal-data
minimization.
3. **Transformation and quality:** versioned transformations, dependencies,
tests, validation findings, correction policy, and lineage. Silent repair of
source facts is not acceptable.
4. **Semantic products:** documented dimensions, measures, official key
mappings, organizational hierarchies, time/stichtag semantics, ownership,
access policy, and version.
5. **Consumption:** Reporting definitions, dashboards, drill-down, scheduled
outputs, APIs, open-data handoffs, and template generation.
6. **Operations:** scheduling, backfill, freshness objectives, observability,
cost/resource limits, retention, backup/restore, promotion between systems,
and reproducible release recipes.
### Selected first subject-area direction
- Start with read-only HIS and CampusOnline source profiles and produce the
institution's internal student-statistics data product plus reports derived
from it.
- Model reference/effective date separately from extraction, validation, and
freeze dates. For example, a semester state may describe `1 September`, be
accepted and frozen on `12 December`, and remain the reproducible source for
that semester's official statistics. Later corrections create a superseding
version; they never rewrite the frozen state silently.
- Make a graphical, typed data-flow editor the primary analyst surface. Nodes
represent governed sources/snapshots, validation, mapping, transformation,
aggregation, a list report/data product, template rendering, and publication.
The graph is versioned, testable, previewable, acyclic, and shows schema,
lineage, quality findings, policy, and pinned upstream versions.
- A list report may become a governed source for another flow. This enables
`source + transformations/aggregation -> list report/data product -> template
-> publishable report` without copying the calculation or losing lineage.
### Implementation slices
1. Bound the first HIS/CampusOnline student-statistics dataset and internal
report; define its legal/policy basis, source owners, official keys,
reference/freeze semantics, privacy level, and acceptance calculation.
2. Generalize the Stage 3 source profile into catalog, ingestion-run, staged
dataset, validation, transformation, lineage, and data-product contracts.
3. Implement one scheduled load and explicit frozen-state lifecycle with
quarantine/replay and a fully transparent transformation into the internal
student-statistics product.
4. Implement the first graphical typed flow editor over those proven nodes,
including version/diff, validation, preview, cycle prevention, and pinned
upstream product versions.
5. Serve the product through Reporting with policy-aware aggregate and
drill-down behavior; export reproducible evidence and calculation metadata.
6. Chain one list report as a source into a template-backed publishable report.
7. Package mappings, transformations, validations, reports, documentation, and
tests so they can be promoted from development to test to production.
### Gate
One internal student-statistics product can be rebuilt from declared HIS and/or
CampusOnline snapshots, preserves its reference and freeze dates, passes
explicit quality tests, explains every measure and mapping, enforces disclosure
policy, survives schema/freshness failures visibly, and produces the same
accepted list and template-backed report across promoted environments.
## Stage 5: Collaborative document editing
### Product promise
> As an authorized group, we can turn an uploaded or generated artifact into a
> shared document, edit it concurrently or through controlled check-out,
> comment, review, approve, compare, and recover versions, then freeze the
> accepted rendition as evidence or a record without losing who changed what.
### Ownership and composition
- Files owns bytes, checksums, storage providers, upload/download, and low-level
file permissions.
- DMS owns document identity, versions, renditions, editing sessions, locks,
comments, reviews, approvals, signatures, external DMS references, and
document-level evidence.
- A collaboration connector owns provider-specific Collabora, OnlyOffice,
Nextcloud/OpenDesk, or another office-suite protocol. DMS owns the session
lifecycle and authorization even when editing occurs externally.
- Templates may create the first version. Cases, Campaign, Postbox, Reporting,
and Workflow may reference documents. Records owns classification, legal
hold, archive handoff, and disposal after the document enters that lifecycle.
### Implementation slices
1. Implement DMS document/version/reference DTOs and one Files-backed version
path, including optimistic concurrency, checksums, locks, and audit.
2. Add comments, review requests, approval state, immutable accepted
renditions, comparison, and recovery of prior versions.
3. Define a provider-neutral editing-session capability with short-lived
grants, callback authentication, save idempotency, health, and reconciliation.
4. Integrate one external collaborative editor, including loss of provider,
concurrent save, stale callback, permission revocation, and restore tests.
5. Connect generated reports/templates, Postbox/Campaign sharing, focused
document views, configured docs, and Records handoff.
### Gate
Multiple authorized actors can edit a document without silent lost updates;
every accepted version has stable content and provenance; provider callbacks
cannot bypass current authorization; degraded or ambiguous saves are visible
and reconcilable; the approved rendition can enter records without making Files
or the external editor the document-lifecycle owner.
## Program-wide acceptance and pause rules
- Do not start a broad generic platform abstraction until two concrete stages
demonstrate the repeated contract.
- Do not make Workflow a dependency of these stages. Manual transitions,
explicit actions, and focused views should become stable providers that a
later transition engine can coordinate.
- Do not treat a remote system as trusted merely because it supplied a deep
link, callback, webhook, file, identity, or data row.
- Do not claim a stage complete from local unit tests. Use pinned composition,
target integration, failure drills, adaptive docs, and operator evidence.
- A later stage may prototype contracts while the preceding gate is being
proven, but it may not redefine an owning module's boundary by convenience.
## Decisions intentionally deferred
The sequence itself is selected. These bounded choices remain for the stage
that first needs them:
- first target SMTP/IMAP, file/directory, identity, and deployment profile;
- first external IDM source and identity conflict/disable policy;
- first trusted Postbox assurance/recovery/history-access profile and
external-mail bridge;
- first HIS or other launch producer and callback/reference contract;
- exact HIS/CampusOnline source endpoints, student-statistics keys and accepted
calculation, freeze/correction policy, privacy profile, and permitted
drill-down level;
- datasource provider selection and quality/promotion policy; the architecture
now separates `govoplan-datasources` lifecycle from `govoplan-connectors`
acquisition and `govoplan-dataflow` transformation;
- first collaborative editor/provider and whether the first UX is concurrent
editing, controlled check-out, or both; and
- first Records/archive target and approval/signature assurance level.
+587
View File
@@ -0,0 +1,587 @@
# GovOPlaN Release Console
The release console is a local operator tool for planning and executing
GovOPlaN releases. It belongs to the `govoplan` meta repository because it works
across all local checkouts, release scripts, module manifests, migration audits,
catalog files, Git state, and signing keys.
The current implementation has a read-only dashboard, preview-only legacy
controls, and a bounded durable executor for release steps whose inputs and
effects can be verified safely:
- inspect repositories from `repositories.json`
- show dirty, ahead, behind, missing, no-HEAD, and tag state
- show local package catalog and keyring state
- optionally run release/dev migration audits
- propose next actions without executing them
- compare local catalog/keyring JSON with the published channel and public
keyring when online checks are enabled
- configure target versions per release unit in the web UI
- select the repositories that should advance through checkboxes
- generate dry-run selective release plans for independently versioned packages
- freeze a selective plan as a durable, resumable local release run
- durably preflight a creation-time-bound repository, create its annotated tag,
and publish its branch/tag pair atomically
- deterministically update recognized package/manifest version declarations and
commit only the receipt-bound metadata paths
- order selected module providers before consumers, create module tags before
Core, regenerate Core's selected WebUI release lock, and re-run alignment
before any remote push
- build selected Python wheels and generate a private, signed, receipt-bound
catalog candidate
- publish that exact candidate through a verified website commit and immutable
tag after explicit confirmation
- install selected candidate wheels into a private no-network/no-dependency
target and verify their installed metadata against the frozen plan
Start it from the meta repository:
```sh
./.venv/bin/python tools/release/release-console.py
```
The launcher accepts only a numeric loopback address, binds to `127.0.0.1` by
default, always creates a fresh API token, and prints that token only in the URL
fragment. Open that URL in a browser on the same machine. A deliberately
tokenless embedded app is read-only: non-GET `/api/` requests fail with `403`.
Non-loopback operation needs a separately deployed authenticated TLS boundary;
the local launcher will not expose the mutation API that way.
Run durable release mutation only against an operator-private workspace.
Repository roots, every path ancestor, critical and nested Git metadata, and
tracked worktree files must be owned by the console process UID (or root where
appropriate) and must not be group/world writable. Symlink/special Git
metadata, object alternates, and grafts are rejected. The console pins
`/usr/bin/git`, `/usr/bin/ssh`, a fixed system `PATH`, disabled hooks, isolated
Git configuration, and no replace objects. Shared or `nfsnobody`-owned
checkouts remain usable for read-only planning, but every durable executor
fails closed there; clone the registered origins into a private workspace
before releasing.
The runtime itself is part of the authority boundary. Durable run creation
verifies the meta checkout, release/check tooling, repository registry, Python
environment, loaded `govoplan_core` and cryptography packages, and Git/SSH
executables. It then binds a clean meta-repository HEAD and named branch that
exactly match the registered `origin`. Every execution and reconciliation
rechecks that receipt. Permission checks cannot prove that code was safe before
it entered a writable tree, so release from a fresh operator-private clone or a
separately verified installed console artifact.
The web UI starts with a repository table. Each repository can be checked
independently and assigned its own target version. Repositories without version
metadata remain visible so they can be planned as initial releases. `Build Plan`
shows the dry-run commands for the selected rows, and `Generate Candidate`
creates a signed catalog candidate that advances only selected repositories that
already have a catalog entry.
The full-width **Release Workflow** guide projects the server state into seven
operator phases: Inspect, Targets, Validate, Source, Package, Publish, and
Verify. It does not maintain a second workflow state. Completed, current,
blocked, locked, and unavailable phases are derived from the dashboard,
selective plan, and durable run record. The next-action panel opens the exact
section or durable step that needs attention. Changing a channel, target
version, repository selection, or release gate detaches the browser from the
current run and invalidates the draft plan; the persisted run remains available
from the saved-run selector. Problems in unselected repositories remain visible
as workspace notices but do not lock an unrelated release; the selective plan
is the authority for blockers in the selected repository set.
Installation verification is an explicit durable step after catalog
publication. It verifies the exact candidate receipt, installs every selected
Python wheel into a temporary target with network and dependency resolution
disabled, and compares installed names and versions with the frozen plan.
Deployment startup, database upgrades, and module-combination smoke tests remain
release-integration CI gates; the console does not represent its local wheel
check as a production deployment.
`Build Plan` also returns structured release-gate findings for each selected
repository. The plan names the recommended next action and gives an explicit
remediation for source-version, lockfile, Core WebUI composition, Git state, and
worktree findings. A target version that differs from internally consistent
source metadata becomes a bounded `UPDATE` step. Unsupported, missing, or
internally inconsistent declarations remain a blocker with exact remediation.
`source_preflight_ready` means that plan-visible source gates pass or have a
bounded deterministic mutation; the non-mutating `Preview Tag + Publish`
remains mandatory for remote, manifest, and immutable-tag checks.
## Durable release runs
The **Durable Run State** card turns the current repository/version selection
into a versioned local run record. The server rebuilds the selective plan and
requires the plan to resolve exactly the requested repositories and target
versions; the browser cannot submit or replace the plan snapshot. The input and
plan are then immutable and covered by a canonical SHA-256 integrity digest.
Every executable repository step also carries its creation-time full HEAD,
branch, target tag, a SHA-256 over both the fetch and push URLs of `origin`, and
a SHA-256 over bounded dirty-path names and bytes. Both URLs must exactly equal
the remote registered in `repositories.json`; a changed HEAD, branch, worktree,
remote, push URL, or metadata byte requires a new run or explicit
interrupted-step reconciliation rather than silently retargeting the frozen
compatibility decision.
The complete record also has a checksum so a valid-looking manual edit to its
mutable state fails closed. File permissions remain the authority boundary;
these digests detect accidental or manual corruption, not an attacker who can
replace the private record and recompute its checksums. Changing a target,
channel, or gate input requires a new run.
Creation requires a caller-generated `request_id`. Its SHA-256 fingerprint is
the private, workspace-scoped durable mapping to exactly one run; the raw
identifier is never persisted. Repeating the same identifier with the same
immutable inputs returns that run without rebuilding the plan while it remains
inside the bounded local retention window, even if the live dashboard has
since drifted. Reusing it with different inputs fails closed. The browser keeps
an uncertain create identifier in session storage,
replays it after reload, and selects the known run returned by the server. A
successful create remains shown as saved if only the subsequent list refresh
fails.
Run records survive console restarts, but remain local operator state rather
than a signed release artifact or the system audit log. The default location is
`$XDG_STATE_HOME/govoplan/release-console/workspace-<sha256>/release-runs/`, or
`~/.local/state/...` when `XDG_STATE_HOME` is unset or relative. It is outside
the source checkout so filesystems without enforceable POSIX modes cannot
silently weaken the journal. Newly created state directories use mode `0700`
and records use `0600`. Writes use a cross-process lock, a same-directory
temporary file, `fsync`, atomic replacement, and directory/parent `fsync`.
Symbolic-link paths, untrusted owners or writable ancestry, overly broad record
modes, malformed schemas, unknown fields, invalid state combinations,
oversized files, and digest mismatches fail closed. A bad record is neither
rewritten nor automatically quarantined.
The store retains at most 512 workspace-scoped run records created through the
console. On creation at that limit it removes only the oldest fully completed,
integrity-verified record. Running, planned, attention, blocked, foreign, and
unreadable records are never deleted implicitly; if no completed record is
available, creation fails closed with a retention remediation. Unavailable
records still consume the bound and remain visible in cursor-paginated lists
so corruption cannot be hidden by normal turnover.
The full resolved-workspace SHA-256 is part of both the private storage
namespace and immutable input snapshot. Every list, read, and state transition
checks it. An alternate workspace therefore cannot list or resume another
workspace's runs. This remains true for an embedding/test `run_state_root`
override: the server always appends
`workspace-<full-sha256>/release-runs/` rather than treating the override as a
shared record directory. Durable candidates use its private sibling
`release-candidates/` directory, never a checkout-local runtime path. A corrupt
record from one workspace therefore cannot leak even its identifier or an
integrity error into another workspace.
Each frozen plan step has an explicit `pending`, `running`, `succeeded`,
`failed`, or `interrupted` state. Plan order remains a prerequisite: a later
step is unavailable until earlier steps have succeeded. Exact attempt and
resume/retry/reconciliation request identifiers are fingerprinted so delayed
repetitions remain idempotent for the retained run's lifetime. These fingerprints are
never evicted: the store fails closed before accepting more than 2,048 commands
or 2,048 attempts and asks the operator to create a fresh run. The display
record keeps at most 256 server-generated state events. Events contain only an
enum event type, timestamp, step identifier, and bounded result code—never
commands, process output, confirmation text, credentials, bearer tokens, or
signing material.
Before a step can enter `running`, the store reserves the two command-ledger
slots needed for worst-case recovery. It also projects the serialized record
through start, finish/failure, resume, and the required terminal reconciliation
or read-only retry; the start is rejected unless every required atomic write
fits the record-size bound. An explicit resume consumes one slot and is
accepted only while a persisted attempt is actually running. A mutating attempt
always retains its final `effect_absent` or `effect_succeeded` slot;
`unresolved` may be recorded at most once for that attempt and only when an
additional ledger slot and serialized terminal-write capacity are available.
Read-only interruption and known failure retain the slot and byte capacity
needed to prepare a retry. Capacity exhaustion is therefore detected before
starting an effect rather than stranding an ambiguous attempt.
An explicit resume after a process restart converts every persisted `running`
step to `interrupted`; it never guesses whether an external effect happened.
An interrupted read-only step can be prepared for retry. An interrupted
mutating step remains unavailable until the operator independently reconciles
local and remote state, selects `effect_absent`, `effect_succeeded`, or
`unresolved`, and types `RECONCILE`. `effect_absent` prepares a safe new
attempt, `effect_succeeded` advances the run without repeating the external
effect, and `unresolved` keeps the run blocked. Each outcome emits a bounded,
code-only state event. A known failed attempt can likewise be prepared for
retry. The UI keeps unavailable controls visible and disabled.
Supported executors durably claim the step before invoking an effect. Exact
attempt replays return the recorded outcome and never invoke the executor a
second time. Successful repository preflight, version, commit, Core-bundle,
tag, and push steps persist a bounded repository-state receipt. Version
reconciliation requires aligned declarations in the same commit; commit
reconciliation requires the expected single-parent release commit and only
recognized metadata paths. Tag reconciliation independently requires an
annotated local tag at the frozen HEAD; push reconciliation additionally
requires both the remote annotated tag object and remote branch to match.
Catalog generation persists
only its server-issued opaque candidate ID and canonical catalog SHA-256, then
re-resolves and re-hashes that private candidate before publication.
Repository capabilities are frozen into each plan unit (`python-package`,
`webui-package`, `module-manifest`, `database-migrations`, `documentation`,
`core-release-bundle`, and the universal `git-source`) and determine which
steps appear. Internally aligned version changes are rendered deterministically
from recognized TOML, JSON, lockfile, manifest, and package declarations.
Pre-existing dirty worktrees remain visible but have no commit executor; the
console never absorbs unrelated operator changes.
For mixed releases, module interface providers are ordered before consumers
and Core is tagged last. The durable sequence creates and commits module
metadata, creates local module tags, updates Core's selected WebUI references,
regenerates the release lock against those local tags, commits/tags Core, and
runs a receipt-bound alignment gate before exposing any atomic branch/tag push.
A failed step stops later steps while preserving prior receipts for explicit
retry or reconciliation.
The browser likewise retains the request identifier for an uncertain
resume/retry/reconciliation response and replays it after reload. A successful
replay selects the returned run state. Transport and server failures retain the
identifier; a deterministic `4xx` rejection clears it so a stale command cannot
poison a later attempt.
The run API is covered by the same local console token middleware as every
other `/api/` route:
- `POST /api/release-runs` requires `request_id`, then idempotently rebuilds and
freezes a selective plan only when that creation is not already known.
- `GET /api/release-runs` lists bounded summaries ordered by immutable
`created_at` and run identifier. `next_cursor` advances a stable descending
traversal even while older runs are updated, without offset duplicates or
skips. Unreadable entries
remain a deterministic final section and are therefore reachable through
pagination instead of displacing newer verified runs.
- `GET /api/release-runs/{run_id}` reads and verifies one exact record.
- `POST /api/release-runs/{run_id}/resume` records explicit recovery.
- `POST /api/release-runs/{run_id}/steps/{step_id}/retry` prepares a failed or
read-only interrupted step for another attempt.
- `POST /api/release-runs/{run_id}/steps/{step_id}/reconcile` records a
confirmed observed outcome for an interrupted mutating step.
- `POST /api/release-runs/{run_id}/steps/{step_id}/execute` claims and invokes
only the narrow executor declared by the immutable plan step. Durable release
execution accepts only the registered `origin`, never a caller-selected
remote.
- `POST /api/release-runs/{run_id}/steps/{step_id}/preview` provides the
non-mutating preview for receipt-bound catalog publication.
Run-storage errors are confined to the Durable Release Run section; dashboard
and release-preview collection continue and the workflow guide points to the
bounded storage remediation.
The run record is execution evidence only for a supported step whose durable
claim and bounded result receipt were persisted. The console never infers
success from a button click or process exit alone. An executor exception or a
lost result write leaves the attempt interrupted and non-retriable until
explicit recovery. Catalog publication persists the candidate and keyring
hashes, exact website commit, annotated tag object and peeled commit, branch,
tag name, and registered-origin digest. A successful reconciliation revalidates
the candidate against the trust anchor in the frozen website parent, requires
the exact deterministic catalog/keyring/module blobs and full commit delta, a
sole frozen parent, and matching local and remote branch/tag identities. If any
part cannot be proved, the run remains interrupted and may only be recorded as
`unresolved`.
Dashboard collection is also fail closed. Unreadable Core version metadata or a
malformed module contract is returned as a bounded `collection_errors` entry
with a remediation, marks the dashboard blocked, and becomes a structured
blocker in both full and selective release plans. The console does not silently
omit a contract or turn these source errors into an HTTP 500.
The release-control area above the repository table is read-only and is meant
to become the central release cockpit. It shows:
- local and published channel health
- catalog/keyring drift
- signature and trusted-key status
- published module versions and refs
- local checkout version drift against the catalog
- catalog-declared interface compatibility
The target-version control can generate the next major, minor, or subversion
from the current base version. Manual target input accepts explicit versions
such as `0.2.0` or `0.2.0-alpha1`, but requires the first three version numbers
to move forward.
Plain repository pushes are separate from catalog publication. `Preview Push`
shows the selected repository push commands. `Push Selected` requires `PUSH` in
the repository push confirmation field.
The source release panel retains `Preview Tag + Publish` as a non-mutating
inspection. Its legacy `Create Tags` and `Publish Tags` controls stay visible
but disabled; the corresponding mutation endpoint rejects apply requests.
Creation and atomic branch/tag publication use the `TAG` and `PUBLISH`
confirmations on the durable run steps. The gate requires an aligned target
version, a clean named branch with a HEAD, and a checkout that is not behind. Existing
local or remote tags must resolve to the selected HEAD and are never moved.
The local and remote annotated tag objects must also be identical, not merely
point at the same commit. Before any source tag is created, the console loads
the cross-repository module registry. This release gate also rejects every
user-facing workflow documentation topic that has no scope condition, or has
an alternative condition without `required_scopes` or `any_scopes`.
The catalog workflow panel can also operate on the same selected rows for
generation and preview. Its legacy apply/push controls stay disabled; durable
publication consumes only the candidate receipt recorded by that run:
- `Generate` creates a signed candidate in the operator's private XDG state
directory and records only an opaque candidate ID plus the canonical catalog
SHA-256 in durable run state.
- `Preview` validates a candidate and shows what would be copied into the
website repository.
- `Apply + Website Tag` remains visible but disabled outside a durable run.
- `Push Website Release` remains visible but disabled outside a durable run.
Source release tags belong to Core or module repositories. Website catalog
publication creates a separate catalog tag in the website repository; the UI
names these independently to avoid confusing the two immutable references.
Candidate signing is fail-closed: every Core and module source ref in the
complete post-update catalog must already have the requested annotated tag both
locally and on the configured source remote. Both tags must be the same
annotation object with version-aligned tagged package metadata. Selected tags
must additionally resolve to the selected clean, version-aligned HEAD, and the
signed `selected_units` record captures the peeled commit and tag-object
identifiers. Create and publish the source tags before using `Generate`.
Catalog publication repeats that check for selected units and also verifies
every Core and module source ref in the complete candidate, including entries
preserved from the previous catalog. Historical refs need not be at the current
worktree HEAD, but their local and remote annotated tags must match and their
tagged installable package and module-manifest metadata must declare the catalog
version. New selected releases additionally pass the stricter current-source
alignment gate, including public runtime version declarations. A candidate
cannot be applied or published while any referenced source tag is absent or
inconsistent; the preview and API response list each repository and missing or
invalid ref so the operator can repair the exact source releases first.
Every selected Python release must also supply its exact built wheel. Durable
generation first verifies that the receipt-bound annotated tag is the same
object locally and on the registered origin. It clones an isolated checkout at
the receipt's exact commit and builds from that checkout, never from the mutable
live worktree. Every authenticated base-catalog source is likewise cloned from
its registered origin at an annotated release tag; only selected repositories
contribute synthesized fields. The base catalog and keyring are read as one
authenticated, exact private snapshot before synthesis.
Python wheels are built by a required Bubblewrap worker with no network,
private temporary/home directories, a read-only exact source mount, no host
home or file keys, cleared environment, fixed system tools, and CPU/address
space/process/file-size limits. If a trusted Bubblewrap launcher is unavailable,
generation fails closed. The worker must return one bounded regular wheel; the
console copies it through no-follow descriptors into a fresh private file,
`fsync`s it, then validates its package identity. Generation computes the
archive SHA-256 and an install-stable
payload identity from one bounded, regular-file descriptor and signs those
values into `release.artifacts`. Updating a Python version without a matching
wheel removes the stale identity. A source-only preview can still explain the
gap, but apply, commit, tag, and push fail closed until every selected Python
unit has a matching built-artifact identity. Repositories selected only for a
meta/source tag do not need a Python artifact.
Candidate directories and files are operator-owned `0700`/`0600` state. Before
any later release step consumes one, the executor re-resolves the opaque handle
below the configured root and re-hashes the signed catalog against its persisted
receipt. Shared roots, symlinks, channel path fragments, altered candidates, and
unowned files are rejected.
The current same-host worker is a containment baseline, not the final hostile
build-service boundary. `RLIMIT_FSIZE` is per file, and the worker does not yet
have a size-limited filesystem/cgroup quota, a fresh kernel keyring, or a
seccomp profile denying keyctl/request-key/ptrace/mount operations. A malicious
backend could therefore exhaust scratch disk/inodes or target same-UID kernel
facilities. Closing that denial-of-service/isolation gap requires a dedicated
quota/cgroup worker with a fresh keyring and seccomp policy.
The server-owned wheel builds remain under the private candidate's `artifacts/`
directory. This slice signs their identities but does **not** upload the wheel or
add a download URL to the public catalog; the existing Git `python_ref` is source
provenance and rebuilding it is not an equivalent artifact. Keep the private
candidate until the exact wheels have been transferred through an approved
deployment channel, verified against `archive_sha256`, consumed by the installer,
and covered by its signed receipt. Public artifact transport and receipt-aware
candidate cleanup remain separate release-lifecycle work.
Read-only provenance checks derive a same-host HTTPS Git URL from conventional
SSH remotes when possible, with a non-interactive check of the configured remote
as fallback. Source tag creation and atomic publication always use the explicit
configured Git remote.
The default signing key is
`$HOME/.config/govoplan/release-keys/release-key-1.pem` when no signing key is
entered in the UI. Signing-key files must be regular, owned by the operator, and
inaccessible to group/other users. The browser sends a configured key path only
on the initial execution request; it never persists signing material in session
storage, and request-ID recovery replays no key path.
Generate a selective release plan from the terminal:
```sh
./.venv/bin/python tools/release/release-plan.py \
--repo govoplan-files \
--target-version 0.1.9 \
--channel stable \
--online
```
`--online` compares the local catalog/keyring with the published channel and
published keyring. Use `--remote-tags` only when the plan also needs to check
Git remotes for tag existence; that can be slower across the full repository
set.
Build a signed selective catalog candidate:
```sh
KEY_DIR="$HOME/.config/govoplan/release-keys"
ARTIFACT_DIR="$(mktemp -d)"
../govoplan-files/.venv/bin/python -m pip wheel \
--no-deps \
--no-build-isolation \
--wheel-dir "$ARTIFACT_DIR" \
../govoplan-files
./.venv/bin/python tools/release/release-catalog.py selective \
--repo-version govoplan-files=0.1.9 \
--python-artifact \
"govoplan-files=$ARTIFACT_DIR/govoplan_files-0.1.9-py3-none-any.whl" \
--channel stable \
--catalog-signing-key "release-key-1=$KEY_DIR/release-key-1.pem"
```
This writes candidate catalog/keyring files below
`$XDG_STATE_HOME/govoplan/release-candidates/` (or
`~/.local/state/govoplan/release-candidates/`), generates the browsable module
directory below `modules/`, validates the signed candidate with the module
installer validator, and reports whether the candidate catalog/keyring match the
currently published channel. It does not publish to the website repository.
Regenerate the browsable module directory from an existing catalog/keyring:
```sh
./.venv/bin/python tools/release/release-catalog.py module-directory \
--catalog ../addideas-govoplan-website/public/catalogs/v1/channels/stable.json \
--keyring ../addideas-govoplan-website/public/catalogs/v1/keyring.json \
--output-dir runtime/module-directory-preview \
--channel stable
```
Preview publication of a reviewed candidate:
```sh
CANDIDATE_ROOT="${XDG_STATE_HOME:-$HOME/.local/state}/govoplan/release-candidates"
./.venv/bin/python tools/release/release-catalog.py publish-candidate \
--candidate-dir "$CANDIDATE_ROOT/stable-YYYYMMDD-HHMMSS" \
--channel stable
```
Apply the reviewed candidate into the website repository without pushing:
```sh
./.venv/bin/python tools/release/release-catalog.py publish-candidate \
--candidate-dir "$CANDIDATE_ROOT/stable-YYYYMMDD-HHMMSS" \
--channel stable \
--apply \
--commit \
--tag
```
Push is a separate explicit flag:
```sh
./.venv/bin/python tools/release/release-catalog.py publish-candidate \
--candidate-dir "$CANDIDATE_ROOT/stable-YYYYMMDD-HHMMSS" \
--channel stable \
--apply \
--commit \
--tag \
--push
```
Publication validates the same in-memory catalog object that it writes; it does
not copy a path that can change after validation. On commit, the console reads
the catalog, keyring, and complete module directory back from the immutable Git
tree and requires byte-for-byte equality with those validated objects. Tags and
remote branch updates then reference that exact commit SHA rather than the
mutable worktree `HEAD`.
Published channels are expected below the public catalog base URL:
- `https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json`
- `https://govoplan.add-ideas.de/catalogs/v1/keyring.json`
The console treats channels as release artifacts. A package can advance without
forcing every repository to the same tag, but channel publication must preserve
the unchanged package versions, validate interface compatibility, sign the
updated catalog, and keep the published keyring healthy.
When a selected module exposes a WebUI package, its requested version must also
match Core's `webui/package.release.json` input and the resolved
`package-lock.release.json` entry. The source-tag preflight, selective plan, and
catalog-candidate writer all enforce this composition boundary. Pins for modules
that are not part of the selective release remain unchanged.
Release integration also enforces repository and composition version alignment
and generates a CycloneDX SBOM from the resolved Python environment and the
release WebUI lockfile. Catalog publication should attach that immutable SBOM
and its digest to the corresponding Core/composition release.
Addresses and Notifications have current WebUI source contributions but are
not part of the pinned v0.1.8 WebUI release composition because their v0.1.8
tags predate those packages. They re-enter the release composition only through
new, immutable module tags whose backend, manifest, frontend, and lock metadata
pass the alignment gate.
Candidate publication verifies signatures against the already published
keyring, not against keys supplied only by the candidate. A changed keyring
must have its canonical SHA-256 embedded in the signed catalog. The public
module-directory files are regenerated from that verified catalog and keyring
at publication time; candidate-supplied directory files are never copied as
authoritative provenance.
## Published Module Directory
The target release repository is an online, browsable module directory. The
catalog remains the machine-readable channel entry point, but the published
space should also expose a tree that operators and installations can inspect:
- `/catalogs/v1/channels/<channel>.json` describes the active channel state.
- `/catalogs/v1/keyring.json` publishes trusted release signing keys.
- `/catalogs/v1/modules/<module>/<version>/manifest.json` describes one
published module version, including repository refs, package refs, contracts,
compatibility windows, signatures, and available artifacts.
- `/catalogs/v1/modules/<module>/index.json` lists available versions for
one module.
- `/catalogs/v1/modules/index.json` lists all published modules.
Gitea tags/releases remain the source release anchors. The public GovOPlaN
catalog directory becomes the installation-facing release repository that points
to those anchors and carries structured compatibility information.
Selective catalog candidates now include this directory tree. Publishing a
candidate copies the channel catalog, keyring, and `modules/` tree into the
website repository together.
Selective catalog generation can add a module that is not yet represented in
the source channel. Initial entries are synthesized from the selected
repository's `[project]` metadata, `govoplan.modules` entry point, and runtime
`ModuleManifest`; there is no separate hand-maintained initial-entry list. The
release gate requires the entry point to resolve inside the selected checkout,
exact package/manifest/frontend version agreement, verified source-tag
provenance, a non-conflicting module id, and complete required dependency and
interface closure in the resulting candidate. Classification beyond the
manifest-derived `official` tag remains an explicit later catalog curation
step rather than an inferred business/service label.
## Direction
The console should grow in slices:
1. Read-only dashboard and next-action suggestions.
2. Release plan builder that writes explicit JSON plans.
3. Dry-run executor that shows exact commands and expected file changes.
4. Apply executor for commit, tag, push, artifact build, signing, and catalog
publication.
5. Compatibility planner for manifest contracts and version ranges.
6. Install/test workflow that validates a release from tags or catalog entries.
All mutation must stay explicit: plan first, dry run second, apply only after a
clear confirmation.
+101
View File
@@ -0,0 +1,101 @@
# GovOPlaN Repository Index
Generated from `repositories.json`. Use that JSON file as the machine-readable source of truth; this page is the human-readable link index.
## System
| Repository | Subtype | Local path | Gitea |
| --- | --- | --- | --- |
| `govoplan` | `meta` | `../govoplan` | [govoplan](https://git.add-ideas.de/GovOPlaN/govoplan) |
| `govoplan-core` | `kernel` | `../govoplan-core` | [govoplan-core](https://git.add-ideas.de/GovOPlaN/govoplan-core) |
## Module
| Repository | Subtype | Local path | Gitea |
| --- | --- | --- | --- |
| `govoplan-access` | `platform` | `../govoplan-access` | [govoplan-access](https://git.add-ideas.de/GovOPlaN/govoplan-access) |
| `govoplan-addresses` | `domain` | `../govoplan-addresses` | [govoplan-addresses](https://git.add-ideas.de/GovOPlaN/govoplan-addresses) |
| `govoplan-admin` | `platform` | `../govoplan-admin` | [govoplan-admin](https://git.add-ideas.de/GovOPlaN/govoplan-admin) |
| `govoplan-appointments` | `domain` | `../govoplan-appointments` | [govoplan-appointments](https://git.add-ideas.de/GovOPlaN/govoplan-appointments) |
| `govoplan-approvals` | `domain` | `../govoplan-approvals` | [govoplan-approvals](https://git.add-ideas.de/GovOPlaN/govoplan-approvals) |
| `govoplan-assets` | `domain` | `../govoplan-assets` | [govoplan-assets](https://git.add-ideas.de/GovOPlaN/govoplan-assets) |
| `govoplan-audit` | `platform` | `../govoplan-audit` | [govoplan-audit](https://git.add-ideas.de/GovOPlaN/govoplan-audit) |
| `govoplan-booking` | `domain` | `../govoplan-booking` | [govoplan-booking](https://git.add-ideas.de/GovOPlaN/govoplan-booking) |
| `govoplan-calendar` | `domain` | `../govoplan-calendar` | [govoplan-calendar](https://git.add-ideas.de/GovOPlaN/govoplan-calendar) |
| `govoplan-campaign` | `domain` | `../govoplan-campaign` | [govoplan-campaign](https://git.add-ideas.de/GovOPlaN/govoplan-campaign) |
| `govoplan-cases` | `domain` | `../govoplan-cases` | [govoplan-cases](https://git.add-ideas.de/GovOPlaN/govoplan-cases) |
| `govoplan-certificates` | `domain` | `../govoplan-certificates` | [govoplan-certificates](https://git.add-ideas.de/GovOPlaN/govoplan-certificates) |
| `govoplan-committee` | `domain` | `../govoplan-committee` | [govoplan-committee](https://git.add-ideas.de/GovOPlaN/govoplan-committee) |
| `govoplan-consultation` | `domain` | `../govoplan-consultation` | [govoplan-consultation](https://git.add-ideas.de/GovOPlaN/govoplan-consultation) |
| `govoplan-contracts` | `domain` | `../govoplan-contracts` | [govoplan-contracts](https://git.add-ideas.de/GovOPlaN/govoplan-contracts) |
| `govoplan-dashboard` | `platform` | `../govoplan-dashboard` | [govoplan-dashboard](https://git.add-ideas.de/GovOPlaN/govoplan-dashboard) |
| `govoplan-dataflow` | `platform` | `../govoplan-dataflow` | [govoplan-dataflow](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow) |
| `govoplan-datasources` | `platform` | `../govoplan-datasources` | [govoplan-datasources](https://git.add-ideas.de/GovOPlaN/govoplan-datasources) |
| `govoplan-decisions` | `domain` | `../govoplan-decisions` | [govoplan-decisions](https://git.add-ideas.de/GovOPlaN/govoplan-decisions) |
| `govoplan-dms` | `domain` | `../govoplan-dms` | [govoplan-dms](https://git.add-ideas.de/GovOPlaN/govoplan-dms) |
| `govoplan-dist-lists` | `domain` | `../govoplan-dist-lists` | [govoplan-dist-lists](https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists) |
| `govoplan-docs` | `platform` | `../govoplan-docs` | [govoplan-docs](https://git.add-ideas.de/GovOPlaN/govoplan-docs) |
| `govoplan-encryption` | `platform` | `../govoplan-encryption` | [govoplan-encryption](https://git.add-ideas.de/GovOPlaN/govoplan-encryption) |
| `govoplan-erp` | `domain` | `../govoplan-erp` | [govoplan-erp](https://git.add-ideas.de/GovOPlaN/govoplan-erp) |
| `govoplan-evaluation` | `domain` | `../govoplan-evaluation` | [govoplan-evaluation](https://git.add-ideas.de/GovOPlaN/govoplan-evaluation) |
| `govoplan-facilities` | `domain` | `../govoplan-facilities` | [govoplan-facilities](https://git.add-ideas.de/GovOPlaN/govoplan-facilities) |
| `govoplan-files` | `domain` | `../govoplan-files` | [govoplan-files](https://git.add-ideas.de/GovOPlaN/govoplan-files) |
| `govoplan-forms` | `domain` | `../govoplan-forms` | [govoplan-forms](https://git.add-ideas.de/GovOPlaN/govoplan-forms) |
| `govoplan-forms-runtime` | `platform` | `../govoplan-forms-runtime` | [govoplan-forms-runtime](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime) |
| `govoplan-grants` | `domain` | `../govoplan-grants` | [govoplan-grants](https://git.add-ideas.de/GovOPlaN/govoplan-grants) |
| `govoplan-helpdesk` | `domain` | `../govoplan-helpdesk` | [govoplan-helpdesk](https://git.add-ideas.de/GovOPlaN/govoplan-helpdesk) |
| `govoplan-identity` | `platform` | `../govoplan-identity` | [govoplan-identity](https://git.add-ideas.de/GovOPlaN/govoplan-identity) |
| `govoplan-identity-trust` | `platform` | `../govoplan-identity-trust` | [govoplan-identity-trust](https://git.add-ideas.de/GovOPlaN/govoplan-identity-trust) |
| `govoplan-idm` | `platform` | `../govoplan-idm` | [govoplan-idm](https://git.add-ideas.de/GovOPlaN/govoplan-idm) |
| `govoplan-inspections` | `domain` | `../govoplan-inspections` | [govoplan-inspections](https://git.add-ideas.de/GovOPlaN/govoplan-inspections) |
| `govoplan-learning` | `domain` | `../govoplan-learning` | [govoplan-learning](https://git.add-ideas.de/GovOPlaN/govoplan-learning) |
| `govoplan-ledger` | `domain` | `../govoplan-ledger` | [govoplan-ledger](https://git.add-ideas.de/GovOPlaN/govoplan-ledger) |
| `govoplan-mail` | `domain` | `../govoplan-mail` | [govoplan-mail](https://git.add-ideas.de/GovOPlaN/govoplan-mail) |
| `govoplan-mandates` | `domain` | `../govoplan-mandates` | [govoplan-mandates](https://git.add-ideas.de/GovOPlaN/govoplan-mandates) |
| `govoplan-notifications` | `platform` | `../govoplan-notifications` | [govoplan-notifications](https://git.add-ideas.de/GovOPlaN/govoplan-notifications) |
| `govoplan-ops` | `platform` | `../govoplan-ops` | [govoplan-ops](https://git.add-ideas.de/GovOPlaN/govoplan-ops) |
| `govoplan-organizations` | `platform` | `../govoplan-organizations` | [govoplan-organizations](https://git.add-ideas.de/GovOPlaN/govoplan-organizations) |
| `govoplan-payments` | `domain` | `../govoplan-payments` | [govoplan-payments](https://git.add-ideas.de/GovOPlaN/govoplan-payments) |
| `govoplan-parties` | `domain` | `../govoplan-parties` | [govoplan-parties](https://git.add-ideas.de/GovOPlaN/govoplan-parties) |
| `govoplan-permits` | `domain` | `../govoplan-permits` | [govoplan-permits](https://git.add-ideas.de/GovOPlaN/govoplan-permits) |
| `govoplan-policy` | `platform` | `../govoplan-policy` | [govoplan-policy](https://git.add-ideas.de/GovOPlaN/govoplan-policy) |
| `govoplan-poll` | `domain` | `../govoplan-poll` | [govoplan-poll](https://git.add-ideas.de/GovOPlaN/govoplan-poll) |
| `govoplan-portal` | `domain` | `../govoplan-portal` | [govoplan-portal](https://git.add-ideas.de/GovOPlaN/govoplan-portal) |
| `govoplan-postbox` | `domain` | `../govoplan-postbox` | [govoplan-postbox](https://git.add-ideas.de/GovOPlaN/govoplan-postbox) |
| `govoplan-procurement` | `domain` | `../govoplan-procurement` | [govoplan-procurement](https://git.add-ideas.de/GovOPlaN/govoplan-procurement) |
| `govoplan-projects` | `domain` | `../govoplan-projects` | [govoplan-projects](https://git.add-ideas.de/GovOPlaN/govoplan-projects) |
| `govoplan-records` | `domain` | `../govoplan-records` | [govoplan-records](https://git.add-ideas.de/GovOPlaN/govoplan-records) |
| `govoplan-reporting` | `domain` | `../govoplan-reporting` | [govoplan-reporting](https://git.add-ideas.de/GovOPlaN/govoplan-reporting) |
| `govoplan-resources` | `domain` | `../govoplan-resources` | [govoplan-resources](https://git.add-ideas.de/GovOPlaN/govoplan-resources) |
| `govoplan-risk-compliance` | `domain` | `../govoplan-risk-compliance` | [govoplan-risk-compliance](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance) |
| `govoplan-scheduling` | `domain` | `../govoplan-scheduling` | [govoplan-scheduling](https://git.add-ideas.de/GovOPlaN/govoplan-scheduling) |
| `govoplan-search` | `platform` | `../govoplan-search` | [govoplan-search](https://git.add-ideas.de/GovOPlaN/govoplan-search) |
| `govoplan-services` | `domain` | `../govoplan-services` | [govoplan-services](https://git.add-ideas.de/GovOPlaN/govoplan-services) |
| `govoplan-tasks` | `domain` | `../govoplan-tasks` | [govoplan-tasks](https://git.add-ideas.de/GovOPlaN/govoplan-tasks) |
| `govoplan-templates` | `domain` | `../govoplan-templates` | [govoplan-templates](https://git.add-ideas.de/GovOPlaN/govoplan-templates) |
| `govoplan-tenancy` | `platform` | `../govoplan-tenancy` | [govoplan-tenancy](https://git.add-ideas.de/GovOPlaN/govoplan-tenancy) |
| `govoplan-tickets` | `domain` | `../govoplan-tickets` | [govoplan-tickets](https://git.add-ideas.de/GovOPlaN/govoplan-tickets) |
| `govoplan-transparency` | `domain` | `../govoplan-transparency` | [govoplan-transparency](https://git.add-ideas.de/GovOPlaN/govoplan-transparency) |
| `govoplan-views` | `platform` | `../govoplan-views` | [govoplan-views](https://git.add-ideas.de/GovOPlaN/govoplan-views) |
| `govoplan-voting` | `domain` | `../govoplan-voting` | [govoplan-voting](https://git.add-ideas.de/GovOPlaN/govoplan-voting) |
| `govoplan-wiki` | `domain` | `../govoplan-wiki` | [govoplan-wiki](https://git.add-ideas.de/GovOPlaN/govoplan-wiki) |
| `govoplan-workflow` | `platform` | `../govoplan-workflow` | [govoplan-workflow](https://git.add-ideas.de/GovOPlaN/govoplan-workflow) |
| `govoplan-workflow-engine` | `platform` | `../govoplan-workflow-engine` | [govoplan-workflow-engine](https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine) |
## Connector
| Repository | Subtype | Local path | Gitea |
| --- | --- | --- | --- |
| `govoplan-connectors` | `connector-hub` | `../govoplan-connectors` | [govoplan-connectors](https://git.add-ideas.de/GovOPlaN/govoplan-connectors) |
| `govoplan-fit-connect` | `standard` | `../govoplan-fit-connect` | [govoplan-fit-connect](https://git.add-ideas.de/GovOPlaN/govoplan-fit-connect) |
| `govoplan-rest` | `protocol` | `../govoplan-rest` | [govoplan-rest](https://git.add-ideas.de/GovOPlaN/govoplan-rest) |
| `govoplan-soap` | `protocol` | `../govoplan-soap` | [govoplan-soap](https://git.add-ideas.de/GovOPlaN/govoplan-soap) |
| `govoplan-xoev` | `standard` | `../govoplan-xoev` | [govoplan-xoev](https://git.add-ideas.de/GovOPlaN/govoplan-xoev) |
| `govoplan-xrechnung` | `standard` | `../govoplan-xrechnung` | [govoplan-xrechnung](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung) |
| `govoplan-xta-osci` | `standard` | `../govoplan-xta-osci` | [govoplan-xta-osci](https://git.add-ideas.de/GovOPlaN/govoplan-xta-osci) |
## Website
| Repository | Subtype | Local path | Gitea |
| --- | --- | --- | --- |
| `addideas-govoplan-website` | `public-site` | `../addideas-govoplan-website` | [addideas-govoplan-website](https://git.add-ideas.de/add-ideas/addideas-govoplan-website) |
+7
View File
@@ -76,6 +76,13 @@ one place. If a deployment profile later needs pinned SHAs for every repository,
generate that lock as a release artifact instead of making day-to-day
development depend on submodule updates.
Module release tags also publish wheels and WebUI tarballs to the organization
PyPI/npm registries. The meta release resolves exact versions into a hash-bound
package lock before producing the signed OCI runtime. See
`docs/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
registries are reusable artifact transport; the signed runtime manifest and
digest-pinned images remain production authority.
## Docker Placement
Whole-product Docker and production-like deployment composition belongs in
+259
View File
@@ -0,0 +1,259 @@
# Scaling And Multi-Host Deployment
For the exact external handoff, least-privilege collector permissions and live
two-node acceptance procedure, see
[`PRODUCTION_TARGET_HANDOFF.md`](PRODUCTION_TARGET_HANDOFF.md).
## Implemented Contract
GovOPlaN now supports a stateless application tier backed by logically shared
state services. The runtime roles are independently replaceable API, WebUI,
worker, and scheduler processes. Every replica in one installation must use the
same immutable release composition and the same:
- `GOVOPLAN_INSTALLATION_ID`;
- PostgreSQL database;
- Redis broker and coordination service;
- `MASTER_KEY_B64` and deployment secret references;
- enabled-module graph;
- S3-compatible object-storage namespace.
The application tier must not use node-local durable business data in a
multi-host deployment. Files owns managed file metadata while Core provides the
storage-backend contract. Campaign build artifacts are stored under opaque
object keys and workers read those objects from the shared backend. Temporary
build and materialization directories may remain node-local because they are
discardable.
Core validates three explicit state profiles:
| Profile | Supported shape | Storage rule |
| --- | --- | --- |
| `local` | One API and one worker process for development | Local filesystem permitted. |
| `host-shared` | Multiple processes on one Docker host | A shared host volume is permitted; PostgreSQL and Redis are required. |
| `shared` | Multiple independent hosts | PostgreSQL, Redis, and S3-compatible object storage are required. |
`shared` also requires a stable installation identifier. Module package
mutation is blocked in this profile: build and verify a new immutable release,
then roll the complete cluster to it.
## Same-Host Compose
The generated Compose bundle provides:
```text
client -> TLS proxy -> HAProxy -> WebUI replicas -> HAProxy -> API replicas
API/worker/scheduler -> PostgreSQL
-> Redis
-> local volume, managed Garage, or external S3
```
HAProxy discovers Compose replicas through Docker DNS and performs health-aware
balancing without mounting the Docker socket. This improves concurrency and
permits process replacement, but the Docker host and installer-managed stateful
services remain single failure domains. Generated Compose therefore declares
the `host-shared` state profile even when its shared storage happens to be an
external S3 service. Its API backend checks `/health/ready`, so drain or
coordination loss removes a replica from rotation. Container, load-balancer,
and Kubernetes probes send the configured public host explicitly, keeping
readiness compatible with strict trusted-host validation.
Managed Garage is a convenient single-node S3-compatible service. It is not a
multi-host storage cluster. Use an independently operated Garage cluster or
another S3-compatible service for the `shared` profile.
## Kubernetes Export
The deployment compiler exports a stateless Kubernetes runtime when PostgreSQL,
Redis, and S3 are all external:
```sh
python tools/deployment/govoplan-deploy.py render-kubernetes \
--directory /srv/govoplan/default \
--namespace govoplan \
--secret-name govoplan-runtime \
--tls-secret-name govoplan-tls \
--ingress-class-name nginx \
--output /srv/govoplan/default/kubernetes.json
```
The export contains a Namespace, tokenless ServiceAccount, non-secret
ConfigMap, API/WebUI/worker/scheduler Deployments, Services, Pod disruption
budgets, Ingress, and a release-specific migration Job. It deliberately emits
no Secret values, persistent volume, PostgreSQL, Redis, or object-store
deployment. Export is rejected unless both release images use immutable
`image@sha256:...` references.
Create the named Secret through the cluster's secret-management path. The
command prints the exact required key contract. Review the generated
`FORWARDED_ALLOW_IPS` value and replace it with the exact ingress-proxy network
before production use.
The generated containers run as non-root with a read-only root filesystem and
an ephemeral `/tmp`. Runtime Deployments wait for the exact configured database
migration heads before starting. The API exposes `/health/ready`, which fails
while that API node is draining or cannot prove its runtime-coordination
heartbeat.
## Runtime Coordination
Each API and worker incarnation registers in PostgreSQL with its role, software
version, module-composition hash, queue set, and heartbeat. Ops shows active,
draining, stopped, and stale nodes and compares active counts with configured
replica expectations.
An operator may request or cancel drain from Ops:
- API readiness becomes unavailable on the next heartbeat so the load balancer
stops assigning new requests.
- A worker stops consuming its configured queues and may finish work already
claimed by that process.
- A stale process incarnation cannot overwrite a replacement incarnation's
heartbeat.
- A coordination outage removes API readiness and cancels worker consumers;
the existing incarnation must heartbeat successfully before either resumes.
Singleton work uses PostgreSQL-backed leases with monotonically increasing
fencing tokens. The generated scheduler runs Celery beat through
`govoplan_core.commands.fenced_run`; loss of its lease terminates the child and
returns a distinct failure code. A fenced business operation must validate the
same lease token immediately before committing its effect.
## Release Ordering
Use this order for every multi-replica rollout:
1. Verify immutable image identities, module composition, external state
reachability, backup evidence, and the generated plan.
2. Drain application replicas when the migration compatibility declaration
requires it.
3. Run the release-specific migration Job exactly once. PostgreSQL advisory
locking serializes all Core and module migration tasks across competing
deployment jobs.
4. Let runtime init containers run `wait_for_database`. They wait for exact
configured Alembic heads and never mutate schema.
5. Roll API, workers, scheduler, and WebUI using health-aware replacement.
6. Verify runtime composition, expected replica counts, queue consumers,
object-storage round trips, and recovery status in Ops.
Applying the complete generated manifest is fail-closed: runtime pods remain in
their init phase until the migration Job reaches the expected heads. A second
release may be submitted concurrently, but advisory locking prevents concurrent
schema mutation and each release has a distinct migration Job name.
## Storage Trust Boundary
Installer-managed Garage uses its exact generated endpoint. An arbitrary
external S3 endpoint is accepted only when the deployment explicitly sets
`FILE_STORAGE_S3_ENDPOINT_TRUSTED=true`; that endpoint must be a clean HTTPS
origin without embedded credentials, query, fragment, or path. This is an
operator trust declaration, not a user-controlled connector bypass. Operators
remain responsible for DNS, certificate, network-egress, bucket-policy,
versioning, and lifecycle controls.
## Capacity
- Set `GOVOPLAN_DB_CONNECTION_LIMIT` to the PostgreSQL role's effective
connection limit. The Kubernetes export reserves
`GOVOPLAN_DB_CONNECTION_RESERVE` connections and rejects a topology whose
calculated rolling-update peak would exceed the remainder. The calculation
includes API pools, every Celery parent and prefork child, the scheduler,
migration, and one surge replica per deployment. Role-specific pool and
overflow values are emitted into each workload rather than inherited from one
unconstrained global default.
- Scale workers by queue, with upper bounds based on external provider limits.
`GOVOPLAN_WORKER_POOLS` may contain a JSON list of exact queue owners, for
example:
```json
[
{"name":"delivery","queues":["send_email","append_sent"],"replicas":2,"concurrency":2},
{"name":"platform","queues":["events","workflow","default"],"replicas":2,"concurrency":2}
]
```
Pool replica totals must equal `replicas.worker`, and the pools must cover
`CELERY_QUEUES` exactly without duplicate ownership. Each pool receives its
own Deployment, disruption budget, topology-spread selector, runtime identity,
and declared concurrency.
- Keep one fenced scheduler rather than load-balancing schedulers.
- Increase WebUI replicas for asset/proxy capacity.
- Measure request latency, database query time and locks, active connections,
queue age, retry rate, storage latency, and provider throttling before adding
replicas.
Workers compete for Redis-backed work and are not placed behind a load balancer.
SMTP, IMAP, directory, connector, workflow, dataflow, and reporting queues often
hit external-system limits before host CPU is exhausted.
## What This Does Not Claim
The implemented contract provides stateless runtime placement, shared artifact
access, node visibility, drain controls, migration serialization, and scheduler
fencing. It does not by itself provide:
- a highly available PostgreSQL, Redis, or object-store deployment;
- automatic PostgreSQL/object backup creation or point-in-time recovery;
- autoscaling policy;
- central logs, metrics, traces, or alert routing;
- certificate portability between independently managed ingress providers;
- automatic reconciliation of every possible module side effect;
- a service-level availability guarantee.
The deployer verifies and gates migrations on signed coordinated backup and
isolated-restore evidence, but backup capture and restoration remain owned by
the selected state-service providers. Before claiming high
availability, drill replica loss, rolling replacement, session continuity, job
redelivery, scheduler failover, migration exclusion, object-store outage, and a
coordinated database/object/key restore. Recovery rules and evidence are
defined in [Recovery And Rollback Guarantees](RECOVERY_AND_ROLLBACK_GUARANTEES.md).
## Worker Delivery Evidence
The module-matrix workflow runs `tools/checks/worker-runtime-drill.py` against a
real isolated Redis database. The drill starts supervised Celery worker
processes and records four guarantees without accessing tenant data:
1. a task published through the broker is consumed exactly once;
2. an application retry is delivered again and completes;
3. warm `SIGTERM` lets an in-flight late-ack task complete before shutdown; and
4. loss of a worker after task start causes the unacknowledged task to be
redelivered after the configured visibility timeout.
Run the same drill with the release Python environment and target Redis before
promoting a worker composition. Use a dedicated Redis database, retain the JSON
evidence, and set `CELERY_VISIBILITY_TIMEOUT_SECONDS` above the longest supported
business-task duration. The short visibility timeout used by CI is an isolated
test setting, not a production recommendation.
```bash
GOVOPLAN_WORKER_DRILL_REDIS_URL=redis://redis.example.test:6379/15 \
.venv/bin/python tools/checks/worker-runtime-drill.py \
--output evidence/worker-runtime.json
```
## Live Multi-Host Evidence
After deploying a pinned release on at least two Kubernetes nodes, create an API
key with Ops read scope and run:
```bash
export GOVOPLAN_OPS_API_KEY='...'
python tools/deployment/govoplan-deploy.py verify-kubernetes \
--directory /srv/govoplan/installation \
--namespace govoplan
```
The command fails unless API and WebUI pods are ready on at least two nodes,
all rendered deployments are available, Ops reports a consistent release and
module composition, every declared worker queue is served, and the calculated
database peak remains below its budget. It writes a private, sanitized JSON
record under the installation evidence directory and never retains the API key.
Use `--exercise-api-pod-loss` in an approved drill window to delete one API pod,
observe the public readiness path continuously, and record its replacement.
This proves the bounded stateless-node-loss slice only. Session continuity,
accepted-job redelivery, state-service failover, and coordinated restore remain
separate target exercises whose signed evidence is governed by
`docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md` and GovOPlaN #37.
+84 -9
View File
@@ -33,12 +33,41 @@ cd /mnt/DATA/git/govoplan
tools/checks/security-audit/run.sh --mode full --scope govoplan
```
Reports are written to `audit-reports/`, which is intentionally ignored by git.
When invoked from a Flatpak development environment without a sandbox-local
Docker CLI, the wrapper automatically uses `flatpak-spawn --host docker`. The
host account must still be allowed to open the Docker daemon socket. For a
conventional rootful installation this commonly means membership in the
`docker` group followed by a complete logout/login; that membership is
root-equivalent, so rootless Docker is preferable where the deployment policy
requires a smaller privilege boundary.
The wrapper tags the toolbox image by a fingerprint of the Dockerfile and
`requirements-audit.txt`. If those inputs have not changed, subsequent runs reuse
the existing local image instead of reinstalling all tools. The stable alias is
`govoplan/security-audit:local` unless `SECURITY_AUDIT_IMAGE` is set.
Reports are written to `audit-reports/`, which is intentionally ignored by git.
Each run records tool versions, report checksums, and start/end repository
revision plus worktree fingerprints. A repository change during scanning makes
the run fail so a mixed code snapshot cannot be reported as a valid audit.
Step exit codes are recorded separately from findings: report-only mode may
accept findings, but scanner execution errors and malformed JSON/SARIF reports
always fail the run. The manifest lists the expected, present, and missing
reports for that invocation. Validation and checksums use that explicit set, so
reusing a report directory cannot make stale output look like part of a new run.
It also contains `coverage_status` and structured `scanner_coverage` entries.
Every required scanner is recorded as `no-findings`, `findings`,
`scanner-failure`, or `skipped`; this makes an incomplete local run visible
without treating it as a clean audit.
The wrapper tags the toolbox image by a fingerprint of the Dockerfile,
`requirements-audit.txt`, and the Semgrep smoke-test inputs. If those inputs have
not changed, subsequent runs reuse the existing local image instead of
reinstalling all tools. The stable alias is `govoplan/security-audit:local`
unless `SECURITY_AUDIT_IMAGE` is set.
Semgrep is installed separately in the toolbox image because current Semgrep
packages pin affected Click and MCP versions. The image upgrades both after
Semgrep installation, runs an actual local-rule scan, and audits the final
toolbox Python environment during the image build. The compatibility releases
are pinned exactly to keep the tested override reproducible. Remove either
compatibility override only after Semgrep's own dependency bounds include a
fixed version.
Force a cached rebuild:
@@ -65,9 +94,26 @@ tools/checks/security-audit/run.sh --mode quick --scope current --update --build
- `ci`: quick plus Semgrep public registry rulesets, Trivy, pip-audit, npm audit.
- `full`: ci plus OSV-Scanner, jscpd, Radon, and Xenon.
The Gitea workflow uses `full` mode so its coverage contract includes every
scanner above. Missing scanners fail strict runs and all Actions runs, even
while actual findings remain report-only. Local report-only runs may finish
with missing tools for diagnostics, but their manifest is marked
`coverage_status: incomplete`.
Semgrep and Trivy are invoked with finding-sensitive exit codes. Their exit 1
is therefore a finding under the wrapper contract; higher exit codes, missing
output, invalid JSON/SARIF, and scanner error payloads are execution failures.
Bandit and Ruff security reports are split by source kind. Production code under
`src/` is written to `bandit.json` and `ruff-security.json` and controls strict
mode. Test code under `tests/` is still scanned for visibility, but its findings
are written separately to `bandit-tests.json` and `ruff-security-tests.json` so
fixture passwords, assertions, and temporary paths do not hide the production
baseline.
## Gating
The initial Gitea workflow runs in report-only mode:
The Gitea workflow currently runs findings in report-only mode:
```bash
SECURITY_AUDIT_FAIL_ON_FINDINGS=0
@@ -83,13 +129,13 @@ SECURITY_AUDIT_FAIL_ON_FINDINGS=1
or run locally with:
```bash
tools/checks/security-audit/run.sh --mode ci --scope current --strict
tools/checks/security-audit/run.sh --mode full --scope govoplan --strict
```
## Audit Burndown Workflow
Treat Gitea issues as the active audit state. A full GovOPlaN audit should
produce one tracker issue in `add-ideas/govoplan` and child issues in the
produce one tracker issue in `GovOPlaN/govoplan` and child issues in the
repository that owns each fix.
Use the tracker issue for:
@@ -115,11 +161,35 @@ Keep active implementation status in issues instead of committing generated
audit reports. `audit-reports/` is ignored; quote the report directory and the
important scanner counts in the tracker issue.
The jscpd step is intentionally scoped to application and test source. It
excludes documentation snippets, package manifests, generated translations,
public SVG assets and catalog output, workflow YAML, declarative backend schema
JSON, the generated migration baseline, and mirrored development migration
directories because those reports produce metadata or generated-source
repetition rather than actionable source duplication. Keep exclusions narrow
and create child issues for source-code clusters that cross module ownership or
make behavior harder to change safely.
The 2026-08-02 full-workspace baseline covered 64 repositories and reported
1.82% duplicated lines before those generated-source exclusions. The reviewed
high-value clusters were catalog acceptance persistence, release publication
result assembly, and local WebUI JSON mutation wrappers. Similar Dataflow and
Workflow graph/governance code remains independently owned until its shared
contract is stable enough for Core; a raw similarity score is not grounds for a
module-to-module dependency.
## Image Freshness
The regular `Security Audit` workflow reuses the fingerprinted toolbox image
when the Docker daemon is persistent, which is the normal case for the
self-hosted Gitea runner using the host Docker socket. The separate
self-hosted Gitea runner using the host Docker socket. Trusted push, schedule,
and manual runs scan all registered repositories; authenticated SSH is used
only for the private website repository. The wrapper inspects the Actions job
mount table and forwards only the narrowest writable mount covering the audit
scope; it never inherits the job's Docker socket or unrelated runner mounts.
Pull-request audit runs stay disabled while the audit runner exposes its host
Docker socket: PR-controlled audit code must run on a disposable or rootless
runner without host-socket access. The separate
`Security Audit Toolbox Update` workflow runs weekly with
`SECURITY_AUDIT_UPDATE=1`; it pulls current base images and re-resolves the
allowed tool version ranges into a refreshed local image.
@@ -133,6 +203,11 @@ tools first:
cd /mnt/DATA/git/govoplan
python -m venv .venv
./.venv/bin/python -m pip install -r requirements-audit.txt
./.venv/bin/python -m pip install 'semgrep>=1.140,<2'
./.venv/bin/python -m pip install --upgrade --no-deps 'click==8.3.3'
./.venv/bin/python -m pip install --upgrade --no-deps 'mcp==1.28.1'
./.venv/bin/semgrep scan --metrics=off --config tools/checks/security-audit/semgrep-govoplan.yml tools/checks/check-version-alignment.py
./.venv/bin/pip-audit --progress-spinner off
```
Then install the non-Python tools (`gitleaks`, `trivy`, `osv-scanner`, `jscpd`)
@@ -0,0 +1,202 @@
# System Administrator Lifecycle User Story
## Story
> As a system administrator, I can execute one shell command that downloads a
> verified GovOPlaN distribution and starts a completely configured Core control
> plane without optional modules. In the WebUI I can browse compatible signed
> module releases, select the modules for this installation, and follow every
> download, validation, migration, installation, activation, and health-check
> step. When an update is available, I can review its impact and confirm it.
>
> I can add API or worker replicas on containers or other servers without
> copying mutable local state. I can operate development, test, and production
> systems, transfer a versioned configuration through the WebUI, undo an applied
> configuration version, test an update in one environment, and then apply the
> same immutable update recipe to another environment.
This is a product-level story owned by the GovOPlaN platform rather than by an
individual domain module. It joins installation, module lifecycle, operations,
configuration packages, and release provenance into one administrator journey.
The canonical backlog item is
[GovOPlaN #13](https://git.add-ideas.de/GovOPlaN/govoplan/issues/13).
## Terms
- **Core control plane:** the smallest bootable distribution: Core API, Core
WebUI, PostgreSQL, Redis, installer worker, migration runner, and durable
storage configuration. No optional GovOPlaN module package is installed.
- **Bootstrap administrator:** a single-use, time-limited installation identity
that may access only first-run and module-lifecycle functions. It is retired
when the selected identity/access configuration becomes healthy.
- **Module release:** an immutable, signed package plus manifest, compatibility
contract, migrations, WebUI contribution, checksums, and SBOM references.
- **Configuration revision:** an immutable, schema-versioned export of
non-secret system/module settings, policies, compositions, and secret
references. Secret values are never exported.
- **Update recipe:** the reviewed, immutable plan containing exact Core/module
versions, configuration revision, migration order, preflight results,
maintenance/drain requirements, health checks, and permitted rollback or
forward-recovery actions.
## Acceptance journeys
### One-command first installation
1. The administrator runs one documented command on a supported host.
2. The bootstrapper verifies a signed distribution manifest before executing or
starting downloaded artifacts.
3. It checks container/runtime, ports, storage, entropy, memory, architecture,
and connectivity requirements; generates deployment-local secrets with
restrictive permissions; and never prints them.
4. It starts PostgreSQL, Redis, Core API/WebUI, one installer worker, and the
migration runner. Readiness does not pass until migrations and durable
dependencies are healthy.
5. It prints the local URL and one-time bootstrap credential. Re-running the
command is idempotent and shows or repairs the existing installation rather
than creating another identity or database.
6. No optional module is installed or enabled at this point.
### Module selection, installation, and update
1. Core reads an approved signed catalog and trusted keyring through the shared
outbound-network policy.
2. The WebUI shows available, installed, compatible, blocked, withdrawn, and
update-available releases with their channel, provenance, contracts,
migrations, permissions, configuration requirements, and release notes.
3. Selecting modules produces a dependency/compatibility plan before any
mutation. The administrator can amend the selection or confirm the plan.
4. Installation executes durably in a worker. The UI receives persisted step
state and can reconnect without losing progress.
5. Package signature/checksum, version alignment, contract closure, migration
graph, configuration schema, and health checks are mandatory gates.
6. Remote artifacts are acquired by a deployment-owned downloader that permits
approved origins, pins the validated connection peer, enforces size and time
limits, and verifies signature/checksum before making a local immutable
artifact available. `pip`, `npm`, archive tools, and any compatibility Git
importer then run without unrestricted network access. A restricted-network
profile can satisfy the same contract through operator-prefetched artifacts.
7. A failed or interrupted operation reaches a visible retry, forward-recovery,
rollback, or manual-intervention state. It never reports success merely
because the initiating request returned.
8. An available update follows the same preview and confirmation path. Exact
repeated requests are idempotent.
### Horizontal scaling
1. API, WebUI, installer, scheduler, and ordinary worker roles are stateless
with respect to local container disks. Durable state uses PostgreSQL, Redis,
and configured shared file/object storage.
2. A documented command can add API or queue-specific worker replicas. The
default Compose profile supports local scale-out; an orchestrator profile
supplies equivalent health/readiness probes and rolling replacement.
3. Only one migration or module-lifecycle mutation may own the deployment lock,
while any healthy replica can serve read and normal domain traffic.
4. Workers announce identity, queues, software composition, heartbeat, and
drain state. Operators can see skew and safely retire a replica.
5. Sessions, throttling, idempotency, jobs, installer progress, and scheduled
work remain correct when requests move between replicas.
### Development, test, and production promotion
1. The administrator exports a configuration revision from development through
the WebUI. The package is versioned, checksummed, attributable, and contains
secret references or required-secret declarations, never secret values.
2. Test imports the package into a preview area. Core reports environment-bound
values, missing capabilities/secrets, compatibility changes, and the exact
apply plan.
3. Applying creates a new revision; the previous effective revision remains
addressable. Undo is a new audited revision that restores the earlier
configuration where contracts permit it.
4. After tests and health checks pass, Core emits an update recipe with exact
immutable release and configuration identifiers.
5. Production validates the recipe against its own environment, requires a new
confirmation, and executes the same ordered plan. Environment-specific
secret bindings and endpoints remain local.
6. Promotion never copies tenant/business data implicitly. Data migration,
anonymized fixtures, and backup/restore are separate explicit operations.
## Safety and governance requirements
- Catalogs, artifacts, recipes, and configuration revisions have signatures,
checksums, provenance, expiry/revocation semantics, and audit evidence.
- Catalog and artifact retrieval is not delegated to an unrestricted package
manager. Approved-origin and peer-pinned download, response bounds,
signature/checksum verification, extraction safety, offline installation, and
deployment egress policy form separate defenses.
- The installer has a dedicated narrow authority; normal domain permissions do
not imply host/package-management access.
- The UI distinguishes reversible configuration rollback from database or
package migrations that require forward recovery.
- Cluster-wide mutations use fencing/leases so an expired worker cannot later
commit a stale result.
- Update plans declare availability impact, worker draining, backup/restore
prerequisites, database compatibility windows, and post-change probes.
- Deployment profiles set secure headers, trusted proxies/hosts, body limits,
outbound-network policy, storage, TLS/cookie posture, and observability.
- Release publication requires aligned backend/frontend/manifest versions and
machine-readable dependency/SBOM provenance.
## Implementation slices
Implementation status as of the current source tree:
- Slice 1 has a published production-artifact baseline. Immutable
[`v0.1.14`](https://git.add-ideas.de/GovOPlaN/govoplan/releases/tag/v0.1.14)
binds source commit `1f039dd39c1ce2672f4978c8abc6dff862ef1445`, a signed
one-file deployer, exact API/Web and managed-dependency image digests,
composition, SBOMs, and provenance. Runtime Distribution
[run #459](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/459)
passed migrations, schema checks, non-root API/Web readiness, and worker
delivery/shutdown on both amd64 and arm64. Each future release must renew the
evidence, and a real installation must still produce topology-specific
ingress, failover, backup, and recovery receipts.
- Slice 6 has a working application-tier foundation: state profiles, shared
object storage, runtime node registration/heartbeats/drain, fenced scheduler,
migration serialization, exact-head startup waiting, Ops visibility, and a
Kubernetes export. Production acceptance still requires topology-specific
failover and restore drills.
- The recovery foundation for slices 4 and 5 is implemented as a Core recovery
ledger and deployment operation journal. Automatic database backup and broad
adoption by module-owned external effects remain open work.
1. **Reproducible Core-only distribution.** Publish pinned multi-architecture
images, signed distribution manifest, Core-only Compose profile, bootstrap
preflight, generated secrets, readiness, and idempotent rerun/repair.
2. **First-run control plane.** Add the restricted bootstrap administrator,
one-time enrollment, initial catalog/keyring configuration, and retirement
after durable administrator access is established.
3. **Read-only online module directory.** Move the existing catalog and module
directory contracts into the installed Core WebUI with compatibility,
provenance, release-note, and update-state presentation.
4. **Durable module plan and install.** Reuse the existing installer queue,
locks, signed-package validator, rollback drill, and run evidence behind a
plan/confirm/progress UI. Add initial catalog-entry synthesis and artifact
acquisition where the current release console still assumes local sources.
5. **Safe module update.** Add drain/maintenance coordination, backup gate,
migration compatibility window, reconnectable progress, health verification,
retry/recovery, and update notification.
6. **Stateless replica profile.** Continue module adoption and operational
proof for the implemented role commands, shared-state validation, runtime
registration/drain, fenced scheduler, and Kubernetes application-tier
export. Prove multiple API and worker replicas against the target shared
dependencies.
7. **Configuration revision model.** Define provider export/import schemas,
canonical serialization, secret references, validation/diff, immutable
revision storage, audit, apply, and undo-as-new-revision.
8. **Environment promotion and recipes.** Add source/target fingerprints,
preview, environment bindings, acceptance evidence, exact recipe generation,
signed transfer, and independently confirmed application.
9. **Operational proof.** Exercise interrupted installs, stale locks, unavailable
catalogs, revoked keys, failed migrations, replica loss, configuration undo,
and development-to-test-to-production promotion in release CI and target
drills.
## Explicit non-goals for the first distribution slice
- Shipping optional modules in the Core image.
- Exporting secrets or production business data with configuration.
- Pretending every schema migration can be reversed automatically.
- Building a proprietary orchestrator instead of supporting Compose and a
standard cluster scheduler through the same role/readiness contracts.
- Allowing the browser process to execute arbitrary shell commands.
+157
View File
@@ -0,0 +1,157 @@
# Target Maturity Evidence Runbook
For authority-key generation, container isolation and the concrete inputs that
must be supplied by the target owner and independent production approver, see
[`PRODUCTION_TARGET_HANDOFF.md`](PRODUCTION_TARGET_HANDOFF.md).
This runbook turns retained target-environment results into a sanitized,
signed GovOPlaN capability-fit proof. It does not make a deployment suitable,
certified, supported, or production-approved by itself. The proof records what
independent authorities assessed against one exact installed release.
## Roles and custody
Use separate trust domains for release signing, installation receipts,
boundary assessment, and production approval. A private proof key must be
provisioned outside the assessed application and its matching public key must
already exist in a separately managed
`capability-fit-proof-authority-keyring.schema.json` document. Do not store
private keys, raw reports, credentials, personal data, backup material, or
target endpoints in Git.
Each authority key lists only the scopes that role may attest. At least one
supplied signing key must cover every claim, and the issuer rejects a key that:
- is absent, inactive, expired, or revoked in the authority keyring;
- expires before the proof;
- does not match its independently provisioned public key;
- reuses release-catalog or installer-authority key material.
## Target run
Install one pinned catalog release and issue its installed-composition receipt
with `tools/assessments/installer-receipt.py`. Exercise the actual target
topology, including:
- PostgreSQL and Redis as shared state services;
- shared S3-compatible object storage;
- at least two stateless API replicas and the intended worker topology;
- fenced singleton work, ingress, certificates, proxy headers, and the real
network/trust boundary;
- provider health and freshness for every provider required by the product;
- monitoring, alerting, failure response, accessibility, privacy, and security
controls;
- backup, isolated restore, failed-deployment rollback, and forward recovery.
For the recovery claim, retain the observed recovery point, measured RPO and
RTO, database/object-store consistency result, and semantic reconstruction of
the institutional and Service/Form reference journeys. Measure RPO from the
last acknowledged durable effect that survives recovery and RTO until service
health plus semantic reconstruction pass. Failed runs are evidence too and
must use a negative result.
The private reports stay in the approved evidence store. Give each report an
opaque artifact ID and each evaluated control a versioned opaque control ID.
## Private claim manifest
Create a private manifest conforming to
`capability-fit-boundary-run.schema.json`. Relative artifact paths resolve from
the manifest directory. Paths are read and hashed by the issuer and are never
copied into the signed output.
```json
{
"$schema": "./capability-fit-boundary-run.schema.json",
"schema_version": "0.1.0",
"evidence_kind": "govoplan.capability-fit-boundary-run",
"proof_id": "target:production:20260802",
"expires_at": "2026-09-01T00:00:00Z",
"claims": [
{
"scope": "target_environment",
"result": "passed",
"control_ids": ["topology:shared-state-v1"],
"artifacts": [
{"artifact_id": "target:run-20260802", "path": "private/target.json"}
]
},
{
"scope": "recovery",
"result": "passed",
"control_ids": ["recovery:restore-rollback-v1"],
"artifacts": [
{"artifact_id": "recovery:run-20260802", "path": "private/recovery.json"}
]
}
]
}
```
Reference readiness needs positive `target_environment`, `accessibility`,
`privacy`, `security`, `operations`, and `recovery` claims. Provider acceptance
and production approval are separate scopes. A production-approval authority
must not approve its own unreviewed target run.
## Issue and verify
Issue only while the signed installer observation is current. Repeat
`--signing-key` when multiple independent roles are needed. The command first
verifies the catalog, independent catalog trust root, exact installed payload,
installer receipt, and installer authority. It then hashes artifacts, signs the
sanitized proof, verifies it immediately, and writes both proof and review with
atomic private-file permissions.
```bash
./.venv/bin/python tools/assessments/boundary-evidence.py \
--assessment /srv/govoplan/assessment.json \
--catalog /srv/govoplan/catalogs/stable.json \
--keyring /srv/govoplan/catalogs/keyring.json \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--installed-evidence /srv/govoplan/evidence/installed.json \
--installer-receipt /srv/govoplan/evidence/installer-receipt.json \
--installer-authority-keyring /srv/govoplan/trust/installer-authorities.json \
--claims /srv/govoplan/evidence/private/target-run.json \
--authority-keyring /srv/govoplan/trust/proof-authorities.json \
--signing-key authority-target=/run/secrets/target-proof-ed25519.pem \
--output /srv/govoplan/evidence/target-proof.json \
--review-output /srv/govoplan/evidence/target-proof-review.json
```
Add `--expected-external-provider-subject provider-production` when the claim
manifest contains `external_providers`. This value is an opaque deployment ID,
not a URL or credential.
## Promotion gate
The general verifier can now be made admission-enforcing. These switches return
a blocking exit status when a required claim is absent, expired, negative,
revoked, or bound to another assessment, release, installation, or subject:
```bash
./.venv/bin/python tools/assessments/capability-fit.py \
--catalog /srv/govoplan/catalogs/stable.json \
--keyring /srv/govoplan/catalogs/keyring.json \
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
--installed-evidence /srv/govoplan/evidence/installed.json \
--installer-receipt /srv/govoplan/evidence/installer-receipt.json \
--installer-authority-keyring /srv/govoplan/trust/installer-authorities.json \
--boundary-evidence /srv/govoplan/evidence/target-proof.json \
--boundary-authority-keyring /srv/govoplan/trust/proof-authorities.json \
--require-reference-readiness \
--require-production-approval \
--output /srv/govoplan/evidence/admission-review.json
```
Use `--require-external-provider-proof` as well when the promoted product
requires an external provider. Live admission must not use
`--verification-time`; that switch is only for clearly labelled historical
review.
## Renewal and failure
Renew evidence after release, installed composition, deployment, control, or
provider changes and before expiry. Revoke an authority key immediately after
custody loss and rerun the affected assessment with a new independent key.
Never copy a previous positive claim to a new release. Preserve negative and
superseded receipts according to the approved evidence-retention policy.
+137
View File
@@ -0,0 +1,137 @@
# GovOPlaN Views Architecture
## Purpose
GovOPlaN Views are governed presentation projections for a task,
responsibility, or workflow step. A View can reduce the visible modules,
navigation entries, routes, page sections, and commands to the interface
needed for the current job.
Views are optional. If `govoplan-views` is not installed or enabled, the normal
permission-derived interface remains unchanged.
## Security Boundary
A View is not an authorization mechanism.
- Access, tenant isolation, resource guards, and backend permission checks
remain authoritative.
- A View may hide an interface surface that the actor is otherwise allowed to
use.
- A View can never expose a route, action, tenant, or resource that normal
authorization denies.
- An authorized deep link outside the current View should offer an explicit
temporary escape or View switch. It must not be presented as a permission
denial.
This boundary lets Views improve focus without creating a second, weaker RBAC
system.
## Ownership
Core owns the versioned, module-neutral surface contract and WebUI runtime
hooks. Modules declare stable surfaces and use shared hooks to respect the
effective projection. Modules do not import `govoplan-views`.
`govoplan-views` owns:
- draft and immutable published View revisions
- system, tenant, group, and user assignments
- default, mandatory, and user-selectable Views
- active per-user View state
- effective projection resolution and provenance
- the View editor, preview, validation, and stale-surface diagnostics
Policy optionally owns inherited ceilings and explainable decisions. Workflow
optionally references a pinned View revision for an instance or step and may
narrow it further.
## Surface Contract
Modules announce only useful, semantic surfaces:
- module
- navigation item
- route or workspace
- section or panel
- command or action
Each descriptor has a stable namespaced id, parent id, kind, label, default
visibility, ordering, and dependency metadata where needed. Surface ids are
public module contracts, not CSS selectors, component paths, or arbitrary DOM
fragments.
The first release supports visible or hidden. Read-only states, layout
replacement, visual emphasis, and arbitrary styling are separate concerns and
are deferred.
## Effective Resolution
The effective interface is the intersection of:
1. installed and enabled modules
2. actor permissions and resource access
3. administrator and Policy ceilings
4. an assigned or user-selected View
5. an optional workflow instance or step overlay
Lower scopes and workflow overlays may narrow inherited visibility but cannot
broaden it. Every inherited, locked, hidden, unavailable, or stale choice
should carry provenance that the editor and runtime can explain.
Published View revisions are immutable. Active workflow instances pin the
revision they use. Unknown or retired surface ids produce diagnostics rather
than breaking startup. If no valid effective View can be resolved, the system
uses the last valid projection or the normal authorized interface and reports
the configuration problem to administrators.
## Workflow Behavior
A workflow definition may reference a View for the whole instance or a
particular step. Starting, resuming, or advancing the workflow activates the
appropriate projection. Users can intentionally leave focused mode and return
from an open-work widget or notification without losing workflow state.
Module handoffs carry the workflow and View context through Core contracts.
Workflow does not import the target module or the Views implementation.
## Delivery Order
1. Define the Core surface registry and runtime hooks.
2. Initialize `govoplan-views` and persist versioned definitions.
3. Add assignment, selection, resolution, provenance, and the editor.
4. Add Policy inheritance and administrator ceilings.
5. Add Workflow instance and step activation.
6. Adopt semantic section/action descriptors module by module.
## Implementation Status
Implemented in the initial Views slice:
- Core contract version `1`, stable module/navigation/route identifiers, custom
section/action descriptors, manifest validation, and platform API metadata
- shell navigation, route-boundary, settings, administration, dashboard-widget,
embedded-capability, and organization-action filtering
- `govoplan-views` definitions, immutable revisions, system/tenant/group/user
assignments, user selection, provenance, and stale-surface recovery
- a system and tenant administration editor with unsaved-change protection,
publish/archive controls, assignment management, and server-enforced lockout
prevention
- surface declarations for every currently installed module that contributes a
WebUI, including finer-grained shared administration and settings surfaces
Still intentionally separate:
- Policy-owned inherited ceilings and policy decision provenance
- workflow-instance and workflow-step activation of pinned View revisions
- read-only and layout-replacement projections beyond the version `1`
visible/hidden contract
## Gitea Work Packages
- `govoplan#17`: task-focused Views user story
- `govoplan#16`: initialize and implement `govoplan-views`
- `govoplan-core#271`: versioned surface and runtime contracts
- `govoplan-policy#9`: inheritance, ceilings, and provenance
- `govoplan-workflow#7`: workflow instance and step activation
- `govoplan-workflow#3`: focused workflow mode user story
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://govoplan.add-ideas.de/schemas/backup-evidence-keyring-v1.json",
"title": "GovOPlaN backup evidence trust keyring",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "purpose", "keys"],
"properties": {
"schema_version": { "const": "1" },
"purpose": { "const": "govoplan-backup-evidence" },
"keys": {
"type": "array",
"minItems": 1,
"maxItems": 64,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"key_id",
"algorithm",
"status",
"public_key_pem",
"not_before",
"expires_at"
],
"properties": {
"key_id": { "type": "string", "minLength": 1, "maxLength": 128 },
"algorithm": { "const": "ed25519" },
"status": { "enum": ["active", "retired", "revoked"] },
"public_key_pem": { "type": "string", "minLength": 1, "maxLength": 8192 },
"not_before": { "type": "string", "format": "date-time" },
"expires_at": { "type": "string", "format": "date-time" }
}
}
}
}
}
+255
View File
@@ -0,0 +1,255 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://govoplan.add-ideas.de/schemas/backup-evidence-v1.json",
"title": "GovOPlaN coordinated backup and restore evidence",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"evidence_id",
"installation_id",
"deployment_subject",
"release",
"recovery_point",
"components",
"restore_drill",
"issued_at",
"expires_at",
"revoked",
"signatures"
],
"properties": {
"schema_version": { "const": "1" },
"evidence_id": { "$ref": "#/$defs/token" },
"installation_id": { "$ref": "#/$defs/token" },
"deployment_subject": {
"type": "object",
"additionalProperties": false,
"required": ["profile", "topology", "subject_ref"],
"properties": {
"profile": { "enum": ["evaluation", "self-hosted"] },
"topology": { "$ref": "#/$defs/token" },
"subject_ref": { "$ref": "#/$defs/reference" }
}
},
"release": {
"type": "object",
"additionalProperties": false,
"required": [
"channel",
"version",
"manifest_sha256",
"composition_sha256",
"api_image",
"web_image"
],
"properties": {
"channel": { "$ref": "#/$defs/token" },
"version": { "$ref": "#/$defs/token" },
"manifest_sha256": { "$ref": "#/$defs/sha256" },
"composition_sha256": { "$ref": "#/$defs/sha256" },
"api_image": { "$ref": "#/$defs/digest_image" },
"web_image": { "$ref": "#/$defs/digest_image" }
}
},
"recovery_point": {
"type": "object",
"additionalProperties": false,
"required": ["id", "captured_at", "consistency", "rpo_seconds", "write_fence"],
"properties": {
"id": { "$ref": "#/$defs/token" },
"captured_at": { "type": "string", "format": "date-time" },
"consistency": {
"enum": ["provider-atomic", "application-quiesced", "transaction-consistent"]
},
"rpo_seconds": { "$ref": "#/$defs/duration" },
"write_fence": {
"type": "object",
"additionalProperties": false,
"required": ["mode", "token_sha256", "established_at"],
"properties": {
"mode": {
"enum": ["provider-snapshot", "application-quiesce", "transaction-boundary"]
},
"token_sha256": { "$ref": "#/$defs/sha256" },
"established_at": { "type": "string", "format": "date-time" }
}
}
}
},
"components": {
"type": "object",
"additionalProperties": false,
"required": ["database", "objects", "configuration", "key_custody"],
"properties": {
"database": { "$ref": "#/$defs/database" },
"objects": { "$ref": "#/$defs/objects" },
"configuration": { "$ref": "#/$defs/configuration" },
"key_custody": { "$ref": "#/$defs/key_custody" }
}
},
"restore_drill": {
"type": "object",
"additionalProperties": false,
"required": [
"drill_id",
"recovery_point_id",
"started_at",
"completed_at",
"isolated_target_ref",
"release_manifest_sha256",
"migration_heads_sha256",
"representative_object_manifest_sha256",
"database_verified",
"objects_verified",
"configuration_verified",
"key_custody_verified",
"semantic_checks",
"measured_rpo_seconds",
"measured_rto_seconds",
"evidence_ref"
],
"properties": {
"drill_id": { "$ref": "#/$defs/token" },
"recovery_point_id": { "$ref": "#/$defs/token" },
"started_at": { "type": "string", "format": "date-time" },
"completed_at": { "type": "string", "format": "date-time" },
"isolated_target_ref": { "$ref": "#/$defs/reference" },
"release_manifest_sha256": { "$ref": "#/$defs/sha256" },
"migration_heads_sha256": { "$ref": "#/$defs/sha256" },
"representative_object_manifest_sha256": { "$ref": "#/$defs/sha256" },
"database_verified": { "const": true },
"objects_verified": { "const": true },
"configuration_verified": { "const": true },
"key_custody_verified": { "const": true },
"semantic_checks": {
"type": "array",
"minItems": 1,
"maxItems": 128,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id", "status", "evidence_ref"],
"properties": {
"id": { "$ref": "#/$defs/token" },
"status": { "const": "passed" },
"evidence_ref": { "$ref": "#/$defs/reference" }
}
}
},
"measured_rpo_seconds": { "$ref": "#/$defs/duration" },
"measured_rto_seconds": { "$ref": "#/$defs/duration" },
"evidence_ref": { "$ref": "#/$defs/reference" }
}
},
"issued_at": { "type": "string", "format": "date-time" },
"expires_at": { "type": "string", "format": "date-time" },
"revoked": { "const": false },
"signatures": {
"type": "array",
"minItems": 1,
"maxItems": 16,
"items": { "$ref": "#/$defs/signature" }
}
},
"$defs": {
"token": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"digest_image": {
"type": "string",
"maxLength": 300,
"pattern": "^[^@\\s]+@sha256:[0-9a-f]{64}$"
},
"reference": {
"type": "string",
"minLength": 3,
"maxLength": 2048,
"pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$"
},
"duration": { "type": "integer", "minimum": 0, "maximum": 2592000 },
"protected_key": {
"type": "object",
"properties": {
"protected": { "const": true },
"encryption_key_ref": { "$ref": "#/$defs/reference" },
"captured_at": { "type": "string", "format": "date-time" }
}
},
"database": {
"type": "object",
"additionalProperties": false,
"required": [
"provider", "artifact_ref", "artifact_sha256", "snapshot_id", "lsn",
"protected", "encryption_key_ref", "captured_at"
],
"properties": {
"provider": { "$ref": "#/$defs/token" },
"artifact_ref": { "$ref": "#/$defs/reference" },
"artifact_sha256": { "$ref": "#/$defs/sha256" },
"snapshot_id": { "$ref": "#/$defs/token" },
"lsn": { "type": "string", "minLength": 1, "maxLength": 256 },
"protected": { "const": true },
"encryption_key_ref": { "$ref": "#/$defs/reference" },
"captured_at": { "type": "string", "format": "date-time" }
}
},
"objects": {
"type": "object",
"additionalProperties": false,
"required": [
"provider", "artifact_ref", "manifest_sha256", "version_id",
"object_count", "total_bytes", "protected", "encryption_key_ref", "captured_at"
],
"properties": {
"provider": { "$ref": "#/$defs/token" },
"artifact_ref": { "$ref": "#/$defs/reference" },
"manifest_sha256": { "$ref": "#/$defs/sha256" },
"version_id": { "$ref": "#/$defs/token" },
"object_count": { "type": "integer", "minimum": 0 },
"total_bytes": { "type": "integer", "minimum": 0 },
"protected": { "const": true },
"encryption_key_ref": { "$ref": "#/$defs/reference" },
"captured_at": { "type": "string", "format": "date-time" }
}
},
"configuration": {
"type": "object",
"additionalProperties": false,
"required": ["artifact_ref", "sha256", "protected", "encryption_key_ref", "captured_at"],
"properties": {
"artifact_ref": { "$ref": "#/$defs/reference" },
"sha256": { "$ref": "#/$defs/sha256" },
"protected": { "const": true },
"encryption_key_ref": { "$ref": "#/$defs/reference" },
"captured_at": { "type": "string", "format": "date-time" }
}
},
"key_custody": {
"type": "object",
"additionalProperties": false,
"required": ["provider", "keyset_ref", "keyset_version", "recoverable", "captured_at"],
"properties": {
"provider": { "$ref": "#/$defs/token" },
"keyset_ref": { "$ref": "#/$defs/reference" },
"keyset_version": { "$ref": "#/$defs/token" },
"recoverable": { "const": true },
"captured_at": { "type": "string", "format": "date-time" }
}
},
"signature": {
"type": "object",
"additionalProperties": false,
"required": ["key_id", "algorithm", "value"],
"properties": {
"key_id": { "$ref": "#/$defs/token" },
"algorithm": { "const": "ed25519" },
"value": { "type": "string", "minLength": 1, "maxLength": 256 }
}
}
}
}
@@ -0,0 +1,153 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit-boundary-evidence.schema.json",
"title": "GovOPlaN externally issued capability-fit boundary evidence",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"evidence_kind",
"proof_id",
"assessment_id",
"assessment_release",
"installed_evidence_sha256",
"issued_at",
"expires_at",
"claims",
"signatures"
],
"properties": {
"$schema": {
"type": "string",
"format": "uri-reference"
},
"schema_version": {
"const": "0.1.0"
},
"evidence_kind": {
"const": "govoplan.capability-fit-boundary-proof"
},
"proof_id": {
"$ref": "#/$defs/opaque_id"
},
"assessment_id": {
"$ref": "#/$defs/opaque_id"
},
"assessment_release": {
"$ref": "#/$defs/opaque_id"
},
"installed_evidence_sha256": {
"$ref": "#/$defs/sha256"
},
"issued_at": {
"type": "string",
"format": "date-time"
},
"expires_at": {
"type": "string",
"format": "date-time"
},
"claims": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"items": {
"$ref": "#/$defs/claim"
}
},
"signatures": {
"type": "array",
"minItems": 1,
"maxItems": 16,
"items": {
"$ref": "#/$defs/signature"
}
}
},
"$defs": {
"opaque_id": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"claim": {
"type": "object",
"additionalProperties": false,
"required": ["scope", "result", "subject_id", "control_ids", "artifacts"],
"properties": {
"scope": {
"enum": [
"target_environment",
"external_providers",
"accessibility",
"privacy",
"security",
"operations",
"recovery",
"production_approval"
]
},
"result": {
"enum": ["passed", "failed", "approved", "rejected"]
},
"subject_id": {
"$ref": "#/$defs/opaque_id"
},
"control_ids": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"uniqueItems": true,
"items": {
"$ref": "#/$defs/opaque_id"
}
},
"artifacts": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"items": {
"$ref": "#/$defs/evidence_artifact"
}
}
}
},
"evidence_artifact": {
"type": "object",
"additionalProperties": false,
"required": ["artifact_id", "sha256"],
"properties": {
"artifact_id": {
"$ref": "#/$defs/opaque_id"
},
"sha256": {
"$ref": "#/$defs/sha256"
}
}
},
"signature": {
"type": "object",
"additionalProperties": false,
"required": ["algorithm", "key_id", "value"],
"properties": {
"algorithm": {
"const": "ed25519"
},
"key_id": {
"$ref": "#/$defs/opaque_id"
},
"value": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"contentEncoding": "base64"
}
}
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
}
}
}
@@ -0,0 +1,103 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit-boundary-run.schema.json",
"title": "GovOPlaN private target-run claim manifest",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"evidence_kind",
"proof_id",
"expires_at",
"claims"
],
"properties": {
"$schema": {
"type": "string",
"format": "uri-reference"
},
"schema_version": {
"const": "0.1.0"
},
"evidence_kind": {
"const": "govoplan.capability-fit-boundary-run"
},
"proof_id": {
"$ref": "#/$defs/opaque_id"
},
"expires_at": {
"type": "string",
"format": "date-time"
},
"claims": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"items": {
"$ref": "#/$defs/claim"
}
}
},
"$defs": {
"opaque_id": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"claim": {
"type": "object",
"additionalProperties": false,
"required": ["scope", "result", "control_ids", "artifacts"],
"properties": {
"scope": {
"enum": [
"target_environment",
"external_providers",
"accessibility",
"privacy",
"security",
"operations",
"recovery",
"production_approval"
]
},
"result": {
"enum": ["passed", "failed", "approved", "rejected"]
},
"control_ids": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"uniqueItems": true,
"items": {
"$ref": "#/$defs/opaque_id"
}
},
"artifacts": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"items": {
"$ref": "#/$defs/artifact"
}
}
}
},
"artifact": {
"type": "object",
"additionalProperties": false,
"required": ["artifact_id", "path"],
"properties": {
"artifact_id": {
"$ref": "#/$defs/opaque_id"
},
"path": {
"type": "string",
"minLength": 1,
"maxLength": 4096
}
}
}
}
}
+978
View File
@@ -0,0 +1,978 @@
{
"$schema": "./capability-fit.schema.json",
"schema_version": "0.1.0",
"assessment_id": "campaign-reference-2026-07-22",
"assessed_at": "2026-07-22",
"scope": {
"title": "Campaign-centric internal pilot and small-production candidate",
"reference_journeys": [
"Internal operator authors, validates, builds, queues, sends and reconciles an email Campaign with managed attachments",
"Operator inspects delivery and audit evidence"
],
"postponed": [
"Workflow and workflow-driven user stories"
]
},
"release": {
"kind": "tagged_release",
"ref": "stable-catalog-202607220843",
"meta_commit": "5447299289a1",
"reproducible": true,
"configuration_packages": [],
"notes": [
"The live stable catalog has a valid Ed25519 signature trusted through release-key-1.",
"Core v0.1.13 and Campaign v0.1.10 are tagged and package-integrated; this is not target-environment or production approval.",
"No configuration revision or configuration package is pinned yet."
]
},
"composition": [
{
"module_id": "core",
"repository": "govoplan-core",
"commit": "d487726f4d2c",
"manifest_version": "0.1.13",
"enabled": true,
"dirty": false,
"role": "API, registry, migrations, sessions, kernel contracts and shared WebUI"
},
{
"module_id": "tenancy",
"repository": "govoplan-tenancy",
"commit": "efbec827616b",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Tenant context and lifecycle"
},
{
"module_id": "organizations",
"repository": "govoplan-organizations",
"commit": "39c081c4fb8f",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Organization model"
},
{
"module_id": "identity",
"repository": "govoplan-identity",
"commit": "7a1710af896f",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Normalized internal identity directory"
},
{
"module_id": "access",
"repository": "govoplan-access",
"commit": "f1d64d247e12",
"manifest_version": "0.1.11",
"enabled": true,
"dirty": false,
"role": "Local authentication, sessions, API keys and RBAC"
},
{
"module_id": "admin",
"repository": "govoplan-admin",
"commit": "11ecf362a36d",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Administration surfaces"
},
{
"module_id": "dashboard",
"repository": "govoplan-dashboard",
"commit": "4b960ad37f0d",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Module-aware home surface"
},
{
"module_id": "policy",
"repository": "govoplan-policy",
"commit": "1063622d311a",
"manifest_version": "0.1.9",
"enabled": true,
"dirty": false,
"role": "Policy explanation and configuration boundary"
},
{
"module_id": "audit",
"repository": "govoplan-audit",
"commit": "d3d2c60d7dc1",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Database audit records and retrying audit outbox"
},
{
"module_id": "campaigns",
"repository": "govoplan-campaign",
"commit": "735e874bd03c",
"manifest_version": "0.1.10",
"enabled": true,
"dirty": false,
"role": "Campaign authoring, build, delivery control and reporting"
},
{
"module_id": "files",
"repository": "govoplan-files",
"commit": "2b34f6e30578",
"manifest_version": "0.1.9",
"enabled": true,
"dirty": false,
"role": "Managed files and Campaign attachments"
},
{
"module_id": "mail",
"repository": "govoplan-mail",
"commit": "3e2302909022",
"manifest_version": "0.1.10",
"enabled": true,
"dirty": false,
"role": "SMTP and IMAP profiles and transports"
},
{
"module_id": "calendar",
"repository": "govoplan-calendar",
"commit": "9bcf41bb1fbb",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Optional calendar outside the Campaign pilot minimum"
},
{
"module_id": "docs",
"repository": "govoplan-docs",
"commit": "be52b716caed",
"manifest_version": "0.1.10",
"enabled": true,
"dirty": false,
"role": "Configured-system documentation"
},
{
"module_id": "ops",
"repository": "govoplan-ops",
"commit": "341773a4ff8a",
"manifest_version": "0.1.8",
"enabled": true,
"dirty": false,
"role": "Readiness and deployment-profile visibility"
},
{
"module_id": "addresses",
"repository": "govoplan-addresses",
"commit": "93dddbb8c52a",
"manifest_version": "0.1.9",
"enabled": false,
"dirty": false,
"role": "Optional reusable recipient sources and CardDAV"
}
],
"deployment_profile": {
"id": "production-like-dev",
"status": "partial",
"description": "PostgreSQL and Redis run in containers while API, WebUI, worker and scheduler run from editable source trees.",
"evidence": [
{
"kind": "configuration",
"scope": "current_workspace",
"locator": "govoplan/dev/production-like/docker-compose.yml"
},
{
"kind": "documentation",
"scope": "documented_model",
"locator": "govoplan/dev/production-like/README.md"
}
]
},
"questionnaire": {
"scope_outcomes": [
{
"id": "outcome.reference_journey",
"question": "Which journey is assessed?",
"state": "answered",
"answer": "An internal operator authors, validates, builds, queues, sends and reconciles a Campaign with managed attachments.",
"evidence": []
},
{
"id": "outcome.workflow",
"question": "Is Workflow in scope?",
"state": "answered",
"answer": "No; Workflow is planned and explicitly postponed.",
"evidence": []
}
],
"data_policy": [
{
"id": "data.classification",
"question": "Which data classes and legal bases apply?",
"state": "not_assessed",
"answer": null,
"evidence": []
},
{
"id": "data.retention",
"question": "What retention, deletion, archive and legal-hold rules apply?",
"state": "not_assessed",
"answer": null,
"evidence": []
}
],
"identity_integrations": [
{
"id": "identity.pilot",
"question": "May the pilot use local GovOPlaN accounts?",
"state": "assumed",
"answer": "Yes; federation is outside the verified composition.",
"evidence": []
},
{
"id": "integration.mail",
"question": "Which target SMTP/IMAP service and policy apply?",
"state": "not_assessed",
"answer": null,
"evidence": []
}
],
"workload_growth": [
{
"id": "workload.campaign",
"question": "What are Campaign frequency, recipients per Campaign, send window, import size and attachment volume?",
"state": "not_assessed",
"answer": null,
"evidence": []
},
{
"id": "workload.platform",
"question": "What are tenant, user, concurrency, file, database, queue and audit growth assumptions?",
"state": "not_assessed",
"answer": null,
"evidence": []
}
],
"availability_operations": [
{
"id": "availability.rto_rpo",
"question": "What availability, RPO and RTO are required?",
"state": "not_assessed",
"answer": null,
"evidence": []
},
{
"id": "operations.ownership",
"question": "Who operates database, queue, storage, TLS, secrets, monitoring, backup and incident response?",
"state": "not_assessed",
"answer": null,
"evidence": []
}
],
"procurement_decisions": [
{
"id": "procurement.constraints",
"question": "Which licensing, accessibility, security, certification, support and procurement conditions are mandatory?",
"state": "not_assessed",
"answer": null,
"evidence": []
}
]
},
"capabilities": [
{
"id": "platform.composition",
"requirement": "Compose enabled backend and WebUI modules without hard optional-module dependencies.",
"status": "verified",
"evidence": [
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-core/tests/test_module_system.py"
},
{
"kind": "contract",
"scope": "current_workspace",
"locator": "govoplan/tools/checks/check-contracts.py",
"note": "43 modules, 33 providers, 19 requirements, no issues"
}
],
"conditions": [
"Package integration is verified; repeat checks on the installed target composition."
],
"gaps": [
"No target deployment acceptance is recorded."
],
"risks": [
"A reproducible module graph can still be installed or configured incorrectly."
],
"recommendation": "Use the signed stable catalog and verify the minimal Campaign composition after installation.",
"proof_check": "Run contract, migration, API and WebUI module-permutation gates on the installed release."
},
{
"id": "access.local",
"requirement": "Provide tenant-scoped local accounts, sessions, API keys and RBAC.",
"status": "verified",
"evidence": [
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-access/tests/test_auth_dependencies.py"
},
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-core/tests/test_api_smoke.py#cookie-session-csrf"
}
],
"conditions": [
"Pilot accepts local accounts."
],
"gaps": [
"MFA and federated lifecycle are not part of this conclusion."
],
"risks": [
"Manual account lifecycle may not satisfy production identity policy."
],
"recommendation": "Use controlled local pilot accounts and define break-glass/bootstrap rules.",
"proof_check": "Exercise joiner, role change, suspension and protected-owner recovery."
},
{
"id": "campaign.journey",
"requirement": "Author, validate, build, queue, send, reconcile and report a Campaign with frozen execution evidence.",
"status": "verified",
"evidence": [
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-core/tests/test_api_smoke.py#campaign-create-validate-build-mock-send"
},
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-campaign/tests",
"note": "Campaign v0.1.10 is exactly the catalog-selected tagged source"
},
{
"kind": "configuration",
"scope": "committed_source",
"locator": "https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json#sequence-202607220843",
"note": "Core v0.1.13 and Campaign v0.1.10 have matching catalogued Python and WebUI refs"
}
],
"conditions": [
"This verifies implementation paths, not target-provider delivery."
],
"gaps": [
"Usability and target-provider acceptance remain separate."
],
"risks": [
"Package integration does not prove provider behavior or production operations."
],
"recommendation": "Use the catalogued Campaign release for usability and target-provider acceptance.",
"proof_check": "Run the complete journey with safe data and the target-like mail service."
},
{
"id": "files.managed_attachments",
"requirement": "Store and resolve managed Campaign attachments on durable storage.",
"status": "verified",
"evidence": [
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-files/tests",
"note": "14 tests passed"
},
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-campaign/tests/test_attachment_building.py"
}
],
"conditions": [
"Deployment provides a durable storage root."
],
"gaps": [
"Target backup and restore are not verified."
],
"risks": [
"Node-local storage prevents safe independent API scaling."
],
"recommendation": "Use durable local storage for the pilot and assess object/shared storage before scaling.",
"proof_check": "Back up and restore files together with database references."
},
{
"id": "mail.smtp_imap",
"requirement": "Send Campaign mail through SMTP and optionally append sent messages through IMAP.",
"status": "available_unconfigured",
"evidence": [
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-mail/tests",
"note": "22 tests passed"
},
{
"kind": "documentation",
"scope": "documented_model",
"locator": "govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md"
}
],
"conditions": [
"Use a dedicated non-production service account and safe recipients."
],
"gaps": [
"No target provider, TLS chain, throttling or bounce/reply process was exercised."
],
"risks": [
"Ambiguous provider outcomes can cause duplicate-send risk if reconciled incorrectly."
],
"recommendation": "Run target-like interoperability and failure drills before production use.",
"proof_check": "Prove SMTP acceptance, IMAP append, throttling and outcome reconciliation."
},
{
"id": "addresses.recipient_sources",
"requirement": "Select reusable address lists as Campaign recipient sources.",
"status": "available_unconfigured",
"evidence": [
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-addresses/tests",
"note": "14 tests passed"
}
],
"conditions": [
"Enable the Addresses module explicitly."
],
"gaps": [
"Addresses is disabled in the pinned root profile."
],
"risks": [
"Recipient governance may differ between source data and frozen Campaign evidence."
],
"recommendation": "Enable only when reusable lists are a pilot requirement.",
"proof_check": "Build a Campaign from a source list and verify immutable recipient provenance."
},
{
"id": "audit.local",
"requirement": "Retain tenant/system audit evidence and retry governed audit events.",
"status": "verified",
"evidence": [
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-audit/tests",
"note": "5 tests passed"
}
],
"conditions": [
"Conclusion covers local database evidence only."
],
"gaps": [
"No central sink, retention enforcement or tamper-evident archive is verified."
],
"risks": [
"Local audit evidence may not satisfy organizational records or SIEM requirements."
],
"recommendation": "Define retention and export requirements before production approval.",
"proof_check": "Exercise privileged-event review, retention and any required external export."
},
{
"id": "identity.federation",
"requirement": "Integrate external LDAP/AD, OIDC/SAML or SCIM identity infrastructure.",
"status": "scaffold",
"evidence": [
{
"kind": "documentation",
"scope": "documented_model",
"locator": "govoplan-idm/README.md"
}
],
"conditions": [],
"gaps": [
"No end-to-end provider connector or federated login is verified."
],
"risks": [
"Federation-dependent organizations cannot use the current pilot composition without extra implementation."
],
"recommendation": "Use local pilot accounts or assess and implement the selected provider path.",
"proof_check": "Run provider metadata, login/provisioning, deprovisioning and failure tests."
},
{
"id": "compliance.export_control",
"requirement": "Screen persons and organizations against embargo/sanctions lists with review evidence.",
"status": "planned",
"evidence": [
{
"kind": "issue",
"scope": "documented_model",
"locator": "https://git.add-ideas.de/GovOPlaN/govoplan/issues/12"
}
],
"conditions": [],
"gaps": [
"No provider, list provenance, match policy, review flow or legal evidence exists."
],
"risks": [
"The current composition must not be represented as performing export-control screening."
],
"recommendation": "Keep outside pilot claims until the user story is implemented and legally validated.",
"proof_check": "Validate list ingestion, versioning, matching, false-positive review and audit evidence."
},
{
"id": "workflow",
"requirement": "Orchestrate the journey through Workflow.",
"status": "planned",
"evidence": [
{
"kind": "observation",
"scope": "documented_model",
"locator": "Assessment scope",
"note": "Explicitly postponed"
}
],
"conditions": [],
"gaps": [
"Workflow is outside this assessment."
],
"risks": [
"Including it would overstate the assessed composition."
],
"recommendation": "Do not enable or claim Workflow for this reference pilot.",
"proof_check": "Reassess in a later Workflow-focused composition."
}
],
"infrastructure": [
{
"id": "runtime.web_api",
"requirement": "Serve matching WebUI and API artifacts with health endpoints.",
"status": "verified",
"evidence": [
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-core/tests/test_module_system.py"
},
{
"kind": "route",
"scope": "committed_source",
"locator": "govoplan-core/src/govoplan_core/server/fastapi.py#/health"
}
],
"conditions": [
"Materialize the matching catalogued artifacts in the target."
],
"gaps": [
"No production image or service bundle is supplied by the profile."
],
"risks": [
"Editable source processes are unsuitable as a production artifact."
],
"recommendation": "Install matching catalogued WebUI/API refs and supervise them as immutable artifacts.",
"proof_check": "Deploy the built artifacts and run health/module-route checks."
},
{
"id": "runtime.worker",
"requirement": "Run durable asynchronous Campaign jobs.",
"status": "available_unconfigured",
"evidence": [
{
"kind": "configuration",
"scope": "current_workspace",
"locator": "govoplan/tools/launch/launch-production-like-dev.sh"
}
],
"conditions": [
"Redis and a supervised worker are required when Celery is enabled."
],
"gaps": [
"Target heartbeat, restart and queue-age alerting are not proved."
],
"risks": [
"Queued work can stall silently without monitoring."
],
"recommendation": "Start one worker for the pilot and split queues only after measurement.",
"proof_check": "Interrupt and restart a worker while preserving job/reconciliation safety."
},
{
"id": "runtime.scheduler",
"requirement": "Run periodic recovery and cleanup safely.",
"status": "partial",
"evidence": [
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-calendar/tests/test_outbox.py",
"note": "Committed and pushed after the catalogued Calendar v0.1.8 tag"
}
],
"conditions": [
"Calendar outbox and recovery work is remote-integrated source but not stable-package-integrated."
],
"gaps": [
"No distributed leader election or target supervision is established."
],
"risks": [
"Multiple schedulers can duplicate periodic dispatch without locking."
],
"recommendation": "Omit from the Campaign-only pilot or run one supervised instance.",
"proof_check": "Prove missed-schedule recovery and single-leader behavior."
},
{
"id": "data.postgresql",
"requirement": "Persist application state in PostgreSQL with explicit migrations.",
"status": "verified",
"evidence": [
{
"kind": "configuration",
"scope": "committed_source",
"locator": "govoplan/dev/postgres"
},
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan/tools/checks/postgres-integration-check.py"
}
],
"conditions": [
"Target database remains deployment-owned."
],
"gaps": [
"HA, patching, WAL policy and capacity are not assessed."
],
"risks": [
"A single unprotected database is a system-wide failure point."
],
"recommendation": "Use managed or dedicated PostgreSQL with explicit migration and backup controls.",
"proof_check": "Run migrations and restore a target-like database."
},
{
"id": "queue.redis",
"requirement": "Provide the Celery broker and queue persistence.",
"status": "available_unconfigured",
"evidence": [
{
"kind": "configuration",
"scope": "current_workspace",
"locator": "govoplan/dev/production-like/docker-compose.yml#redis"
}
],
"conditions": [],
"gaps": [
"Authentication, TLS, eviction, HA and queue-loss policy are not assessed."
],
"risks": [
"Broker loss or eviction can delay work even when database business state survives."
],
"recommendation": "Configure private persistent Redis and monitor queue age/depth.",
"proof_check": "Exercise broker interruption and worker recovery."
},
{
"id": "storage.local",
"requirement": "Persist managed files on a durable single-node/shared path.",
"status": "verified",
"evidence": [
{
"kind": "contract",
"scope": "committed_source",
"locator": "govoplan-files/src/govoplan_files/backend/storage/backends.py"
}
],
"conditions": [
"Path is durable, private, writable and backed up."
],
"gaps": [
"Node-local storage cannot support independent API replicas."
],
"risks": [
"Files can be lost or become inconsistent with database state."
],
"recommendation": "Use for a bounded pilot only with coordinated backup.",
"proof_check": "Restore files and verify all database references."
},
{
"id": "storage.object",
"requirement": "Use S3-compatible storage for independently scalable file persistence.",
"status": "partial",
"evidence": [
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-files/tests/test_connector_providers.py"
}
],
"conditions": [],
"gaps": [
"No chosen target service or storage-backend interoperability drill."
],
"risks": [
"Provider semantics, CA or lifecycle mismatch can break file access/retention."
],
"recommendation": "Select and exercise the target object store before horizontal scaling.",
"proof_check": "Upload, retrieve, version, back up and restore representative objects."
},
{
"id": "edge.proxy_tls",
"requirement": "Terminate HTTPS and enforce proxy/security policy.",
"status": "external_system",
"evidence": [
{
"kind": "route",
"scope": "committed_source",
"locator": "govoplan-ops/src/govoplan_ops/backend/api/v1/routes.py#deployment-security"
}
],
"conditions": [],
"gaps": [
"No proxy, certificates, renewal, header or request-limit configuration is shipped here."
],
"risks": [
"Incorrect proxy/cookie/CORS configuration can expose sessions or block legitimate use."
],
"recommendation": "Supply and monitor the edge through the target platform.",
"proof_check": "Run external TLS/header/cookie/CORS and upload-limit tests."
},
{
"id": "security.secret_store",
"requirement": "Inject and rotate master, database, mail and connector secrets.",
"status": "external_system",
"evidence": [
{
"kind": "configuration",
"scope": "committed_source",
"locator": "govoplan/.env.example"
}
],
"conditions": [],
"gaps": [
"No target secret manager or rotation drill is selected."
],
"risks": [
"Loss of the master key makes encrypted credentials unavailable; leakage compromises connectors."
],
"recommendation": "Use target-native secret injection and document rotation/recovery.",
"proof_check": "Rotate a non-production credential and recover from a protected backup."
},
{
"id": "operations.monitoring",
"requirement": "Detect API, database, worker, queue, storage and delivery degradation.",
"status": "partial",
"evidence": [
{
"kind": "route",
"scope": "committed_source",
"locator": "govoplan-ops/src/govoplan_ops/backend/api/v1/routes.py#/ops/readiness"
},
{
"kind": "contract",
"scope": "committed_source",
"locator": "govoplan-core/src/govoplan_core/server/fastapi.py#slow-request-logging"
}
],
"conditions": [],
"gaps": [
"No metrics exporter, log collector, dashboards, alert routes or SLO is verified."
],
"risks": [
"Failures and queue backlog can remain unnoticed."
],
"recommendation": "Integrate external monitoring before small production.",
"proof_check": "Trigger each readiness/delivery failure and verify an actionable alert."
},
{
"id": "operations.backup_restore",
"requirement": "Back up and restore database, files, configuration and keys as a coherent service.",
"status": "partial",
"evidence": [
{
"kind": "documentation",
"scope": "documented_model",
"locator": "govoplan-core/docs/DEPLOYMENT_OPERATOR_GUIDE.md"
},
{
"kind": "issue",
"scope": "documented_model",
"locator": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29"
}
],
"conditions": [],
"gaps": [
"No target full-service restore drill or measured RPO/RTO exists."
],
"risks": [
"Partial restore can produce missing files, unusable secrets or inconsistent evidence."
],
"recommendation": "Treat Core #29 and a target restore drill as a production gate.",
"proof_check": "Restore the whole service into an isolated environment and measure it."
},
{
"id": "operations.disaster_recovery",
"requirement": "Recover the service after site or dependency loss within agreed RPO/RTO.",
"status": "not_assessed",
"evidence": [
{
"kind": "absence",
"scope": "current_workspace",
"locator": "No target DR plan or exercise evidence supplied"
}
],
"conditions": [],
"gaps": [
"RPO/RTO, off-site copies, recovery order, failover, communications and exercise schedule are unknown."
],
"risks": [
"Service and evidence may be unrecoverable after a major incident."
],
"recommendation": "Define and exercise DR before any availability commitment.",
"proof_check": "Run a documented end-to-end recovery exercise."
}
],
"data_flows": [
{
"id": "browser.api",
"from": "User browser",
"to": "Reverse proxy and GovOPlaN WebUI/API",
"data": [
"Session and CSRF cookies",
"Campaign content",
"Recipient personal data",
"Managed files"
],
"trust_boundary": "Client/public to application",
"controls": [
"HTTPS",
"Exact CORS origins",
"Secure cookies",
"Tenant and RBAC enforcement",
"Request limits"
]
},
{
"id": "api.database",
"from": "GovOPlaN API and workers",
"to": "PostgreSQL",
"data": [
"Tenant and identity records",
"Campaign drafts, snapshots and jobs",
"Connector metadata",
"Audit evidence"
],
"trust_boundary": "Application to primary state store",
"controls": [
"Dedicated database identity",
"Private or encrypted transport",
"Migrations",
"Backup and retention"
]
},
{
"id": "api.queue.worker",
"from": "GovOPlaN API",
"to": "Redis and Celery worker",
"data": [
"Job identifiers",
"Queue routing and retry metadata"
],
"trust_boundary": "Request plane to asynchronous processing plane",
"controls": [
"Private authenticated broker",
"Bounded payloads",
"Idempotent claims",
"Queue monitoring"
]
},
{
"id": "worker.mail",
"from": "GovOPlaN Campaign worker",
"to": "External SMTP and IMAP services",
"data": [
"Recipient addresses",
"Message bodies",
"Attachments",
"Sent-message copy"
],
"trust_boundary": "GovOPlaN to external communication provider",
"controls": [
"Scoped service account",
"TLS and CA policy",
"Sender and recipient policy",
"Rate limits",
"Outcome reconciliation"
]
},
{
"id": "worker.connectors",
"from": "GovOPlaN connector worker",
"to": "External address, file, object or calendar service",
"data": [
"Addresses",
"Files and provenance",
"Calendar resources"
],
"trust_boundary": "GovOPlaN to organizational/external content systems",
"controls": [
"Explicit sync direction",
"Scoped credentials",
"Endpoint allow-list",
"Provenance",
"Conflict and reconciliation policy"
]
}
],
"assumptions": [
"The pilot can use local accounts and one internal tenant or office.",
"A dedicated non-production SMTP/IMAP account and safe recipients are available.",
"Pilot load fits one API and one worker until measured otherwise.",
"Durable local storage is acceptable for the pilot."
],
"open_questions": [
"What are the target organization's data classes, legal bases, retention and external-disclosure rules?",
"Which identity, mail, file, address and monitoring systems are mandatory?",
"What are Campaign volume, concurrency, growth, availability, RPO and RTO?",
"Who owns each external runtime component and operational control?",
"Which accessibility, security, support and procurement constraints are mandatory?"
],
"risks": [
{
"id": "risk.reproducibility",
"statement": "The signed package selection is reproducible but has not been accepted as an installed target composition.",
"impact": "Installation or configuration drift can still produce uncertain deployed behavior.",
"treatment": "Materialize the signed catalog in an isolated target and run installed-artifact acceptance gates.",
"owner": null,
"residual_risk": "Module and environment differences still require release-environment verification."
},
{
"id": "risk.delivery_provider",
"statement": "Target SMTP/IMAP behavior and failure modes are unproved.",
"impact": "Failed, delayed or duplicate communication and incomplete evidence.",
"treatment": "Run target-like interoperability, throttling and uncertainty drills.",
"owner": null,
"residual_risk": "External provider outages and ambiguous outcomes remain operational risks."
},
{
"id": "risk.recovery",
"statement": "Backup/restore and disaster recovery are not demonstrated across all state and keys.",
"impact": "Irrecoverable or inconsistent service after loss.",
"treatment": "Complete Core #29 and an isolated full-service restore/DR exercise.",
"owner": null,
"residual_risk": "Recovery time and data loss remain bounded by the selected external infrastructure."
}
],
"recommendations": [
"Proceed only with a controlled internal Campaign pilot after the bounded proof checks pass.",
"Use the minimal composition and enable Addresses only for an explicit reusable-recipient journey.",
"Do not claim Workflow, export-control screening, identity federation or production DR as implemented.",
"Treat installed-release acceptance, target mail proof, monitoring and a coherent restore drill as production gates."
],
"proof_checks": [
"Materialize the signed catalog into an isolated installation and rerun contract, migration and module-permutation gates against the installed artifacts.",
"Collect the isolated installation with the bounded installed-composition evidence contract; require exact enabled package/module versions, complete RECORD verification and immutable provenance anchored to this assessment.",
"Run a safe target-like Campaign through SMTP acceptance, IMAP append, reporting and audit.",
"Drill worker, Redis and ambiguous-delivery failures without duplicate sends.",
"Restore PostgreSQL, managed files, configuration and encrypted credentials and measure RPO/RTO.",
"Validate proxy/TLS, cookies/CORS, account bootstrap, secret redaction, monitoring and alert delivery.",
"Measure representative Campaign/file/queue/database load and external throttling.",
"Require separately issued, expiring and independently scope-authorized evidence before marking target environment, external provider or production approval proof as checked."
]
}
@@ -0,0 +1,80 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit-proof-authority-keyring.schema.json",
"title": "GovOPlaN capability-fit proof authority keyring",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "purpose", "keys"],
"properties": {
"$schema": {
"type": "string",
"format": "uri-reference"
},
"schema_version": {
"const": "0.1.0"
},
"purpose": {
"const": "govoplan.capability-fit-proof-authorities"
},
"keys": {
"type": "array",
"maxItems": 64,
"items": {
"$ref": "#/$defs/key"
}
}
},
"$defs": {
"opaque_id": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"key": {
"type": "object",
"additionalProperties": false,
"required": ["key_id", "status", "public_key", "allowed_scopes"],
"properties": {
"key_id": {
"$ref": "#/$defs/opaque_id"
},
"status": {
"enum": ["active", "next", "revoked", "disabled", "retired"]
},
"public_key": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"contentEncoding": "base64"
},
"allowed_scopes": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"uniqueItems": true,
"items": {
"enum": [
"target_environment",
"external_providers",
"accessibility",
"privacy",
"security",
"operations",
"recovery",
"production_approval"
]
}
},
"not_before": {
"type": "string",
"format": "date-time"
},
"not_after": {
"type": "string",
"format": "date-time"
}
}
}
}
}
+298
View File
@@ -0,0 +1,298 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit.schema.json",
"title": "GovOPlaN capability and infrastructure fit assessment",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"assessment_id",
"assessed_at",
"scope",
"release",
"composition",
"deployment_profile",
"questionnaire",
"capabilities",
"infrastructure",
"data_flows",
"assumptions",
"open_questions",
"risks",
"recommendations",
"proof_checks"
],
"properties": {
"$schema": {
"type": "string",
"format": "uri-reference"
},
"schema_version": {
"const": "0.1.0"
},
"assessment_id": {
"$ref": "#/$defs/non_empty_string"
},
"assessed_at": {
"type": "string",
"format": "date"
},
"scope": {
"type": "object",
"additionalProperties": false,
"required": ["title", "reference_journeys", "postponed"],
"properties": {
"title": { "$ref": "#/$defs/non_empty_string" },
"reference_journeys": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/non_empty_string" }
},
"postponed": {
"type": "array",
"items": { "$ref": "#/$defs/non_empty_string" }
}
}
},
"release": {
"type": "object",
"additionalProperties": false,
"required": ["kind", "ref", "meta_commit", "reproducible", "configuration_packages"],
"properties": {
"kind": {
"enum": ["tagged_release", "release_candidate", "workspace_snapshot"]
},
"ref": { "$ref": "#/$defs/non_empty_string" },
"meta_commit": { "$ref": "#/$defs/non_empty_string" },
"reproducible": { "type": "boolean" },
"configuration_packages": {
"type": "array",
"items": { "$ref": "#/$defs/non_empty_string" }
},
"notes": {
"type": "array",
"items": { "$ref": "#/$defs/non_empty_string" }
}
}
},
"composition": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/module" }
},
"deployment_profile": {
"type": "object",
"additionalProperties": false,
"required": ["id", "status", "description", "evidence"],
"properties": {
"id": { "$ref": "#/$defs/non_empty_string" },
"status": { "$ref": "#/$defs/status" },
"description": { "$ref": "#/$defs/non_empty_string" },
"evidence": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/evidence" }
}
}
},
"questionnaire": {
"type": "object",
"additionalProperties": false,
"required": [
"scope_outcomes",
"data_policy",
"identity_integrations",
"workload_growth",
"availability_operations",
"procurement_decisions"
],
"properties": {
"scope_outcomes": { "$ref": "#/$defs/answers" },
"data_policy": { "$ref": "#/$defs/answers" },
"identity_integrations": { "$ref": "#/$defs/answers" },
"workload_growth": { "$ref": "#/$defs/answers" },
"availability_operations": { "$ref": "#/$defs/answers" },
"procurement_decisions": { "$ref": "#/$defs/answers" }
}
},
"capabilities": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/assessed_item" }
},
"infrastructure": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/assessed_item" }
},
"data_flows": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/data_flow" }
},
"assumptions": { "$ref": "#/$defs/string_list" },
"open_questions": { "$ref": "#/$defs/string_list" },
"risks": {
"type": "array",
"items": { "$ref": "#/$defs/risk" }
},
"recommendations": { "$ref": "#/$defs/string_list" },
"proof_checks": { "$ref": "#/$defs/string_list" }
},
"$defs": {
"non_empty_string": {
"type": "string",
"minLength": 1
},
"string_list": {
"type": "array",
"items": { "$ref": "#/$defs/non_empty_string" }
},
"status": {
"enum": [
"verified",
"available_unconfigured",
"partial",
"scaffold",
"external_system",
"planned",
"not_fit",
"not_assessed"
]
},
"evidence": {
"type": "object",
"additionalProperties": false,
"required": ["kind", "scope", "locator"],
"properties": {
"kind": {
"enum": [
"test",
"route",
"contract",
"drill",
"documentation",
"configuration",
"issue",
"absence",
"observation"
]
},
"scope": {
"enum": [
"committed_source",
"current_workspace",
"documented_model",
"target_environment"
]
},
"locator": { "$ref": "#/$defs/non_empty_string" },
"note": { "type": "string" }
}
},
"module": {
"type": "object",
"additionalProperties": false,
"required": [
"module_id",
"repository",
"commit",
"manifest_version",
"enabled",
"dirty",
"role"
],
"properties": {
"module_id": { "$ref": "#/$defs/non_empty_string" },
"repository": { "$ref": "#/$defs/non_empty_string" },
"commit": {
"type": "string",
"pattern": "^[0-9a-f]{7,40}$"
},
"manifest_version": { "$ref": "#/$defs/non_empty_string" },
"enabled": { "type": "boolean" },
"dirty": { "type": "boolean" },
"role": { "$ref": "#/$defs/non_empty_string" }
}
},
"answers": {
"type": "array",
"items": { "$ref": "#/$defs/answer" }
},
"answer": {
"type": "object",
"additionalProperties": false,
"required": ["id", "question", "state", "answer", "evidence"],
"properties": {
"id": { "$ref": "#/$defs/non_empty_string" },
"question": { "$ref": "#/$defs/non_empty_string" },
"state": {
"enum": ["answered", "assumed", "not_applicable", "not_assessed"]
},
"answer": {
"type": ["string", "array", "null"],
"items": { "type": "string" }
},
"evidence": {
"type": "array",
"items": { "$ref": "#/$defs/evidence" }
}
}
},
"assessed_item": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"requirement",
"status",
"evidence",
"conditions",
"gaps",
"risks",
"recommendation",
"proof_check"
],
"properties": {
"id": { "$ref": "#/$defs/non_empty_string" },
"requirement": { "$ref": "#/$defs/non_empty_string" },
"status": { "$ref": "#/$defs/status" },
"evidence": {
"type": "array",
"items": { "$ref": "#/$defs/evidence" }
},
"conditions": { "$ref": "#/$defs/string_list" },
"gaps": { "$ref": "#/$defs/string_list" },
"risks": { "$ref": "#/$defs/string_list" },
"recommendation": { "type": "string" },
"proof_check": { "type": "string" }
}
},
"data_flow": {
"type": "object",
"additionalProperties": false,
"required": ["id", "from", "to", "data", "trust_boundary", "controls"],
"properties": {
"id": { "$ref": "#/$defs/non_empty_string" },
"from": { "$ref": "#/$defs/non_empty_string" },
"to": { "$ref": "#/$defs/non_empty_string" },
"data": { "$ref": "#/$defs/string_list" },
"trust_boundary": { "$ref": "#/$defs/non_empty_string" },
"controls": { "$ref": "#/$defs/string_list" }
}
},
"risk": {
"type": "object",
"additionalProperties": false,
"required": ["id", "statement", "impact", "treatment", "owner", "residual_risk"],
"properties": {
"id": { "$ref": "#/$defs/non_empty_string" },
"statement": { "$ref": "#/$defs/non_empty_string" },
"impact": { "$ref": "#/$defs/non_empty_string" },
"treatment": { "$ref": "#/$defs/non_empty_string" },
"owner": { "type": ["string", "null"] },
"residual_risk": { "$ref": "#/$defs/non_empty_string" }
}
}
}
}
+138
View File
@@ -11,6 +11,12 @@
"description": "New user-visible behavior or platform capability.",
"exclusive": true
},
{
"name": "type/user-story",
"color": "1d76db",
"description": "End-to-end user journey or real-world process story used to steer product slices.",
"exclusive": true
},
{
"name": "type/task",
"color": "1d76db",
@@ -107,6 +113,12 @@
"description": "GovOPlaN Appointments module behavior or integration.",
"exclusive": false
},
{
"name": "module/approvals",
"color": "d93f0b",
"description": "GovOPlaN Approvals module behavior or integration.",
"exclusive": false
},
{
"name": "module/audit",
"color": "0e8a16",
@@ -137,24 +149,78 @@
"description": "GovOPlaN Connectors module behavior or integration.",
"exclusive": false
},
{
"name": "module/committee",
"color": "5319e7",
"description": "GovOPlaN Committee module behavior or integration.",
"exclusive": false
},
{
"name": "module/core",
"color": "0052cc",
"description": "GovOPlaN core runner, shared primitives, shell, or extension points.",
"exclusive": false
},
{
"name": "module/dashboard",
"color": "1d76db",
"description": "GovOPlaN Dashboard module behavior or integration.",
"exclusive": false
},
{
"name": "module/dataflow",
"color": "1d76db",
"description": "GovOPlaN Dataflow module behavior or integration.",
"exclusive": false
},
{
"name": "module/datasources",
"color": "006b75",
"description": "GovOPlaN governed datasource contracts, catalogs, and integrations.",
"exclusive": false
},
{
"name": "module/dms",
"color": "c5def5",
"description": "GovOPlaN Dms module behavior or integration.",
"exclusive": false
},
{
"name": "module/docs",
"color": "c5def5",
"description": "GovOPlaN Docs module behavior or integration.",
"exclusive": false
},
{
"name": "module/dist-lists",
"color": "0e8a16",
"description": "GovOPlaN Distribution Lists module behavior or integration.",
"exclusive": false
},
{
"name": "module/decisions",
"color": "d93f0b",
"description": "GovOPlaN formal Decisions module behavior or integration.",
"exclusive": false
},
{
"name": "module/erp",
"color": "fef2c0",
"description": "GovOPlaN Erp module behavior or integration.",
"exclusive": false
},
{
"name": "module/encryption",
"color": "b60205",
"description": "GovOPlaN Encryption key custody, cryptographic policy, and E2EE integration.",
"exclusive": false
},
{
"name": "module/evaluation",
"color": "bfdadc",
"description": "GovOPlaN Evaluation module behavior or integration.",
"exclusive": false
},
{
"name": "module/files",
"color": "006b75",
@@ -173,6 +239,18 @@
"description": "GovOPlaN Forms module behavior or integration.",
"exclusive": false
},
{
"name": "module/forms-runtime",
"color": "f9d0c4",
"description": "GovOPlaN Forms Runtime module behavior or integration.",
"exclusive": false
},
{
"name": "module/helpdesk",
"color": "c2e0c6",
"description": "GovOPlaN Helpdesk module behavior or integration.",
"exclusive": false
},
{
"name": "module/identity-trust",
"color": "d4c5f9",
@@ -203,6 +281,12 @@
"description": "GovOPlaN mail module behavior or integration.",
"exclusive": false
},
{
"name": "module/mandates",
"color": "006b75",
"description": "GovOPlaN Mandates, jurisdiction, responsibility, and authority behavior or integration.",
"exclusive": false
},
{
"name": "module/notifications",
"color": "d876e3",
@@ -227,12 +311,30 @@
"description": "GovOPlaN Payments module behavior or integration.",
"exclusive": false
},
{
"name": "module/parties",
"color": "bfd4f2",
"description": "GovOPlaN procedure Parties, representation, and delivery-authority behavior or integration.",
"exclusive": false
},
{
"name": "module/permits",
"color": "fbca04",
"description": "GovOPlaN Permits module behavior or integration.",
"exclusive": false
},
{
"name": "module/policy",
"color": "d93f0b",
"description": "GovOPlaN Policy module behavior or integration.",
"exclusive": false
},
{
"name": "module/poll",
"color": "e4e669",
"description": "GovOPlaN Poll module behavior or integration.",
"exclusive": false
},
{
"name": "module/portal",
"color": "1d76db",
@@ -245,12 +347,24 @@
"description": "GovOPlaN Postbox module behavior or integration.",
"exclusive": false
},
{
"name": "module/projects",
"color": "5319e7",
"description": "GovOPlaN Projects module behavior or integration.",
"exclusive": false
},
{
"name": "module/reporting",
"color": "c2e0c6",
"description": "GovOPlaN Reporting module behavior or integration.",
"exclusive": false
},
{
"name": "module/risk-compliance",
"color": "b60205",
"description": "GovOPlaN Risk Compliance module behavior or integration.",
"exclusive": false
},
{
"name": "module/search",
"color": "bfdadc",
@@ -263,6 +377,12 @@
"description": "GovOPlaN Scheduling module behavior or integration.",
"exclusive": false
},
{
"name": "module/services",
"color": "1d76db",
"description": "GovOPlaN versioned institutional Services behavior or integration.",
"exclusive": false
},
{
"name": "module/tasks",
"color": "c5def5",
@@ -281,6 +401,24 @@
"description": "GovOPlaN Tenancy module behavior or integration.",
"exclusive": false
},
{
"name": "module/tickets",
"color": "0e8a16",
"description": "GovOPlaN Tickets module behavior or integration.",
"exclusive": false
},
{
"name": "module/views",
"color": "c5def5",
"description": "GovOPlaN governed task views, interface projections, and workflow view integration.",
"exclusive": false
},
{
"name": "module/wiki",
"color": "006b75",
"description": "GovOPlaN Wiki module behavior or integration.",
"exclusive": false
},
{
"name": "module/workflow",
"color": "f9d0c4",
+381
View File
@@ -0,0 +1,381 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://govoplan.add-ideas.de/schemas/installation-spec-v1.json",
"title": "GovOPlaN installation specification",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"installation_id",
"profile",
"public_url",
"listen",
"network_subnet",
"release",
"components",
"enabled_modules"
],
"properties": {
"schema_version": {
"const": 1
},
"installation_id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]{1,47}$"
},
"profile": {
"enum": [
"evaluation",
"self-hosted"
]
},
"public_url": {
"type": "string",
"format": "uri",
"pattern": "^https?://"
},
"listen": {
"type": "object",
"additionalProperties": false,
"required": [
"address",
"port"
],
"properties": {
"address": {
"type": "string"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
}
}
},
"network_subnet": {
"type": "string"
},
"release": {
"type": "object",
"additionalProperties": false,
"required": [
"channel",
"version",
"manifest_url",
"manifest_sha256",
"api_image",
"web_image"
],
"properties": {
"channel": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]{1,31}$"
},
"version": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"manifest_url": {
"type": "string"
},
"manifest_sha256": {
"type": "string",
"pattern": "^$|^[0-9a-f]{64}$"
},
"manifest_keyring_sha256": {
"type": "string",
"pattern": "^$|^[0-9a-f]{64}$"
},
"manifest_signature_key_id": {
"type": "string",
"pattern": "^$|^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"composition_sha256": {
"type": "string",
"pattern": "^$|^[0-9a-f]{64}$"
},
"api_image": {
"type": "string",
"minLength": 1,
"maxLength": 300
},
"web_image": {
"type": "string",
"minLength": 1,
"maxLength": 300
}
}
},
"components": {
"type": "object",
"additionalProperties": false,
"required": [
"postgres",
"redis",
"mail",
"storage"
],
"properties": {
"postgres": {
"$ref": "#/$defs/postgres"
},
"redis": {
"$ref": "#/$defs/redis"
},
"mail": {
"$ref": "#/$defs/mail"
},
"storage": {
"type": "object",
"additionalProperties": false,
"required": [
"mode"
],
"properties": {
"mode": {
"enum": [
"local",
"garage",
"s3"
]
},
"image": {
"type": "string",
"maxLength": 300
}
}
},
"load_balancer": {
"$ref": "#/$defs/load_balancer"
}
}
},
"replicas": {
"type": "object",
"additionalProperties": false,
"required": [
"api",
"web",
"worker"
],
"properties": {
"api": {
"type": "integer",
"minimum": 1,
"maximum": 64
},
"web": {
"type": "integer",
"minimum": 1,
"maximum": 64
},
"worker": {
"type": "integer",
"minimum": 0,
"maximum": 128
}
}
},
"ingress": {
"type": "object",
"additionalProperties": false,
"required": [
"mode",
"image",
"trusted_proxy_cidrs",
"http_port",
"https_port",
"acme_email"
],
"properties": {
"mode": {
"enum": [
"local",
"existing-proxy",
"managed",
"unconfigured"
]
},
"image": {
"type": "string",
"maxLength": 300
},
"trusted_proxy_cidrs": {
"type": "array",
"maxItems": 16,
"uniqueItems": true,
"items": {
"type": "string",
"maxLength": 64
}
},
"http_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"https_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"acme_email": {
"type": "string",
"maxLength": 254
}
}
},
"enabled_modules": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{1,63}$"
}
}
},
"$defs": {
"service": {
"type": "object",
"additionalProperties": false,
"required": [
"mode",
"image",
"url_env"
],
"properties": {
"mode": {
"type": "string"
},
"image": {
"type": "string"
},
"url_env": {
"type": "string"
}
}
},
"postgres": {
"allOf": [
{
"$ref": "#/$defs/service"
},
{
"properties": {
"mode": {
"enum": [
"managed",
"external"
]
},
"url_env": {
"const": "DATABASE_URL"
}
}
}
]
},
"redis": {
"allOf": [
{
"$ref": "#/$defs/service"
},
{
"properties": {
"mode": {
"enum": [
"managed",
"external",
"disabled"
]
},
"url_env": {
"const": "REDIS_URL"
}
}
}
]
},
"mail": {
"allOf": [
{
"$ref": "#/$defs/service"
},
{
"properties": {
"mode": {
"enum": [
"disabled",
"external-relay",
"test-mail"
]
},
"url_env": {
"const": ""
}
}
}
]
},
"load_balancer": {
"allOf": [
{
"$ref": "#/$defs/service"
},
{
"properties": {
"mode": {
"const": "managed"
},
"url_env": {
"const": ""
}
}
}
]
}
},
"allOf": [
{
"if": {
"properties": {
"profile": {
"const": "self-hosted"
}
}
},
"then": {
"properties": {
"public_url": {
"pattern": "^https://"
},
"components": {
"properties": {
"redis": {
"properties": {
"mode": {
"enum": [
"managed",
"external"
]
}
}
},
"mail": {
"properties": {
"mode": {
"enum": [
"disabled",
"external-relay"
]
}
}
}
}
}
}
}
}
]
}
@@ -0,0 +1,335 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/installed-composition-evidence.schema.json",
"title": "GovOPlaN installed composition evidence",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"evidence_kind",
"assessment_id",
"assessment_release",
"collected_at",
"scope",
"artifacts",
"collection_issues"
],
"properties": {
"$schema": {
"type": "string",
"format": "uri-reference"
},
"schema_version": {
"const": "0.4.0"
},
"evidence_kind": {
"const": "govoplan.installed-composition"
},
"assessment_id": {
"$ref": "#/$defs/opaque_id"
},
"assessment_release": {
"$ref": "#/$defs/opaque_id"
},
"collected_at": {
"type": "string",
"format": "date-time"
},
"scope": {
"const": "current-python-environment.govoplan-distributions"
},
"artifacts": {
"type": "array",
"maxItems": 256,
"items": {
"$ref": "#/$defs/artifact"
}
},
"collection_issues": {
"type": "array",
"maxItems": 256,
"items": {
"$ref": "#/$defs/collection_issue"
}
}
},
"$defs": {
"opaque_id": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"package_name": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"version": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._+!-]*$"
},
"artifact": {
"type": "object",
"additionalProperties": false,
"required": [
"package_name",
"package_version",
"modules",
"source_provenance",
"record_integrity"
],
"properties": {
"package_name": {
"$ref": "#/$defs/package_name"
},
"package_version": {
"$ref": "#/$defs/version"
},
"modules": {
"type": "array",
"maxItems": 16,
"items": {
"$ref": "#/$defs/module"
}
},
"source_provenance": {
"$ref": "#/$defs/source_provenance"
},
"record_integrity": {
"$ref": "#/$defs/record_integrity"
}
}
},
"module": {
"type": "object",
"additionalProperties": false,
"required": ["module_id", "manifest_version"],
"properties": {
"module_id": {
"$ref": "#/$defs/opaque_id"
},
"manifest_version": {
"$ref": "#/$defs/version"
}
}
},
"source_provenance": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["basis", "kind", "commit"],
"properties": {
"basis": {
"const": "local-pep610-metadata"
},
"kind": {
"const": "vcs-commit"
},
"commit": {
"type": "string",
"pattern": "^[0-9a-f]{40,64}$"
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["basis", "kind", "sha256"],
"properties": {
"basis": {
"const": "local-pep610-metadata"
},
"kind": {
"const": "archive"
},
"sha256": {
"$ref": "#/$defs/sha256"
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["basis", "kind"],
"properties": {
"basis": {
"const": "local-pep610-metadata"
},
"kind": {
"enum": [
"editable-local",
"local-directory",
"index-or-unknown",
"malformed-direct-url"
]
}
}
}
]
},
"record_integrity": {
"type": "object",
"additionalProperties": false,
"required": [
"status",
"hashed_file_count",
"permitted_unhashed_file_count",
"generated_unhashed_file_count",
"unverifiable_file_count",
"missing_file_count",
"mismatched_file_count",
"artifact_payload_identity",
"installed_payload_identity"
],
"properties": {
"status": {
"enum": [
"verified",
"partial",
"mismatch",
"unavailable",
"limit-exceeded"
]
},
"hashed_file_count": {
"$ref": "#/$defs/count"
},
"permitted_unhashed_file_count": {
"$ref": "#/$defs/count"
},
"generated_unhashed_file_count": {
"$ref": "#/$defs/count"
},
"unverifiable_file_count": {
"$ref": "#/$defs/count"
},
"missing_file_count": {
"$ref": "#/$defs/count"
},
"mismatched_file_count": {
"$ref": "#/$defs/count"
},
"artifact_payload_identity": {
"oneOf": [
{ "$ref": "#/$defs/artifact_payload_identity" },
{ "type": "null" }
]
},
"installed_payload_identity": {
"oneOf": [
{ "$ref": "#/$defs/installed_payload_identity" },
{ "type": "null" }
]
}
},
"allOf": [
{
"if": { "properties": { "status": { "const": "verified" } } },
"then": {
"properties": {
"hashed_file_count": { "minimum": 1 },
"permitted_unhashed_file_count": { "minimum": 1 },
"unverifiable_file_count": { "const": 0 },
"missing_file_count": { "const": 0 },
"mismatched_file_count": { "const": 0 },
"artifact_payload_identity": { "$ref": "#/$defs/artifact_payload_identity" },
"installed_payload_identity": { "$ref": "#/$defs/installed_payload_identity" }
}
}
},
{
"if": { "properties": { "status": { "const": "partial" } } },
"then": {
"properties": {
"hashed_file_count": { "minimum": 1 },
"unverifiable_file_count": { "minimum": 1 },
"missing_file_count": { "const": 0 },
"mismatched_file_count": { "const": 0 }
}
}
},
{
"if": { "properties": { "status": { "const": "mismatch" } } },
"then": {
"anyOf": [
{ "properties": { "missing_file_count": { "minimum": 1 } } },
{ "properties": { "mismatched_file_count": { "minimum": 1 } } }
]
}
},
{
"if": { "properties": { "status": { "const": "unavailable" } } },
"then": {
"properties": {
"hashed_file_count": { "const": 0 },
"missing_file_count": { "const": 0 },
"mismatched_file_count": { "const": 0 }
}
}
}
]
},
"collection_issue": {
"type": "object",
"additionalProperties": false,
"required": ["code", "package_name"],
"properties": {
"code": {
"enum": [
"distribution-metadata-invalid",
"duplicate-distribution",
"module-entry-point-load-failed",
"module-entry-point-invalid",
"module-entry-point-limit-exceeded",
"collection-limit-exceeded"
]
},
"package_name": {
"$ref": "#/$defs/package_name"
}
}
},
"artifact_payload_identity": {
"type": "object",
"additionalProperties": false,
"required": ["algorithm", "sha256", "file_count"],
"properties": {
"algorithm": { "const": "govoplan-wheel-declared-payload-v1" },
"sha256": { "$ref": "#/$defs/sha256" },
"file_count": {
"type": "integer",
"minimum": 1,
"maximum": 10000
}
}
},
"installed_payload_identity": {
"type": "object",
"additionalProperties": false,
"required": ["algorithm", "sha256", "file_count"],
"properties": {
"algorithm": { "const": "govoplan-installed-record-payload-v1" },
"sha256": { "$ref": "#/$defs/sha256" },
"file_count": {
"type": "integer",
"minimum": 1,
"maximum": 10000
}
}
},
"count": {
"type": "integer",
"minimum": 0,
"maximum": 1000000
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
}
}
}
@@ -0,0 +1,53 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/installer-receipt-authority-keyring.schema.json",
"title": "GovOPlaN installer receipt authority keyring",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "purpose", "keys"],
"properties": {
"$schema": { "type": "string", "format": "uri-reference" },
"schema_version": { "const": "0.1.0" },
"purpose": { "const": "govoplan.installer-receipt-authorities" },
"keys": {
"type": "array",
"minItems": 1,
"maxItems": 64,
"items": { "$ref": "#/$defs/key" }
}
},
"$defs": {
"opaque_id": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"key": {
"type": "object",
"additionalProperties": false,
"required": ["key_id", "status", "public_key", "allowed_scopes"],
"properties": {
"key_id": { "$ref": "#/$defs/opaque_id" },
"status": {
"enum": ["active", "next", "revoked", "disabled", "retired"]
},
"public_key": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"contentEncoding": "base64"
},
"allowed_scopes": {
"type": "array",
"minItems": 1,
"maxItems": 1,
"uniqueItems": true,
"items": { "const": "installed_release_origin" }
},
"not_before": { "type": "string", "format": "date-time" },
"not_after": { "type": "string", "format": "date-time" }
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/installer-receipt.schema.json",
"title": "GovOPlaN signed installer receipt",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"evidence_kind",
"receipt_id",
"assessment_id",
"assessment_release",
"installed_evidence_sha256",
"catalog",
"issued_at",
"artifacts",
"signatures"
],
"properties": {
"$schema": { "type": "string", "format": "uri-reference" },
"schema_version": { "const": "0.1.0" },
"evidence_kind": { "const": "govoplan.installer-receipt" },
"receipt_id": { "$ref": "#/$defs/opaque_id" },
"assessment_id": { "$ref": "#/$defs/opaque_id" },
"assessment_release": { "$ref": "#/$defs/opaque_id" },
"installed_evidence_sha256": { "$ref": "#/$defs/sha256" },
"catalog": {
"type": "object",
"additionalProperties": false,
"required": ["channel", "sequence", "sha256"],
"properties": {
"channel": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{0,63}$"
},
"sequence": { "type": "integer", "minimum": 1 },
"sha256": { "$ref": "#/$defs/sha256" }
}
},
"issued_at": { "type": "string", "format": "date-time" },
"artifacts": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"items": { "$ref": "#/$defs/artifact" }
},
"signatures": {
"type": "array",
"minItems": 1,
"maxItems": 16,
"items": { "$ref": "#/$defs/signature" }
}
},
"$defs": {
"opaque_id": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
},
"package_name": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"version": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._+!-]*$"
},
"artifact": {
"type": "object",
"additionalProperties": false,
"required": [
"package_name",
"package_version",
"catalog_archive_sha256",
"installed_payload"
],
"properties": {
"package_name": { "$ref": "#/$defs/package_name" },
"package_version": { "$ref": "#/$defs/version" },
"catalog_archive_sha256": { "$ref": "#/$defs/sha256" },
"installed_payload": {
"type": "object",
"additionalProperties": false,
"required": ["algorithm", "sha256", "file_count"],
"properties": {
"algorithm": { "const": "govoplan-installed-record-payload-v1" },
"sha256": { "$ref": "#/$defs/sha256" },
"file_count": {
"type": "integer",
"minimum": 1,
"maximum": 10000
}
}
}
}
},
"signature": {
"type": "object",
"additionalProperties": false,
"required": ["algorithm", "key_id", "value"],
"properties": {
"algorithm": { "const": "ed25519" },
"key_id": { "$ref": "#/$defs/opaque_id" },
"value": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"contentEncoding": "base64"
}
}
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
}
}
}
+195
View File
@@ -0,0 +1,195 @@
{
"schema_version": 1,
"parent_issue": "https://git.add-ideas.de/GovOPlaN/govoplan/issues/36",
"operations": [
{
"id": "campaign.build.publish-artifacts",
"repository": "govoplan-campaign",
"resources": ["postgresql", "object-storage", "templates-capability", "files-capability"],
"mode": "compensation",
"fenced": true,
"adoption": "reference-implementation",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
},
{
"id": "campaign.delivery.external-channels",
"repository": "govoplan-campaign",
"resources": ["postgresql", "queue", "smtp", "imap", "postbox", "print-provider"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
},
{
"id": "campaign.retention.generated-artifacts",
"repository": "govoplan-campaign",
"resources": ["postgresql", "object-storage"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
},
{
"id": "files.upload.finalize",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage", "filesystem-staging"],
"mode": "compensation",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "files.retention.purge",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage", "encryption-key-custody"],
"mode": "irreversible",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "files.integrity.reconcile",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "files.connector.write-sync",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage", "external-connector"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "mail.outbox.smtp-submit",
"repository": "govoplan-mail",
"resources": ["postgresql", "queue", "smtp"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
},
{
"id": "mail.sent.imap-append",
"repository": "govoplan-mail",
"resources": ["postgresql", "imap"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
},
{
"id": "mail.mailbox.imap-mutate",
"repository": "govoplan-mail",
"resources": ["postgresql", "imap"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
},
{
"id": "mail.mailbox.sync-cursor",
"repository": "govoplan-mail",
"resources": ["postgresql", "imap"],
"mode": "atomic",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
},
{
"id": "connectors.sync.read-snapshot",
"repository": "govoplan-connectors",
"resources": ["postgresql", "external-provider"],
"mode": "atomic",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/15"
},
{
"id": "connectors.sync.external-mutation",
"repository": "govoplan-connectors",
"resources": ["postgresql", "queue", "external-provider"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/15"
},
{
"id": "dataflow.run.database-only",
"repository": "govoplan-dataflow",
"resources": ["postgresql"],
"mode": "atomic",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/19"
},
{
"id": "dataflow.run.publish-output",
"repository": "govoplan-dataflow",
"resources": ["postgresql", "queue", "object-storage", "external-sink"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/19"
},
{
"id": "workflow-engine.instance.state-transition",
"repository": "govoplan-workflow-engine",
"resources": ["postgresql"],
"mode": "atomic",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/1"
},
{
"id": "workflow-engine.activity.external-effect",
"repository": "govoplan-workflow-engine",
"resources": ["postgresql", "queue", "module-capability", "external-provider"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/1"
},
{
"id": "core.module-lifecycle.pre-migration",
"repository": "govoplan-core",
"resources": ["postgresql", "package-environment", "webui-bundle", "filesystem"],
"mode": "compensation",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
},
{
"id": "core.module-lifecycle.post-migration",
"repository": "govoplan-core",
"resources": ["postgresql", "package-environment", "webui-bundle", "runtime-nodes"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
},
{
"id": "core.module-retirement.destroy-data",
"repository": "govoplan-core",
"resources": ["postgresql", "object-storage", "package-environment"],
"mode": "snapshot_restore",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
},
{
"id": "core.module-runtime.apply-graph",
"repository": "govoplan-core",
"resources": ["postgresql", "runtime-nodes", "module-registry"],
"mode": "compensation",
"fenced": true,
"adoption": "adopted",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
}
]
}
@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://govoplan.add-ideas.de/schemas/runtime-distribution-keyring-v1.json",
"title": "GovOPlaN runtime distribution trust keyring",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "purpose", "keys"],
"properties": {
"schema_version": { "const": "1" },
"purpose": { "const": "govoplan-runtime-distribution" },
"keys": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"key_id",
"algorithm",
"status",
"public_key_pem",
"not_before",
"expires_at"
],
"properties": {
"key_id": { "type": "string", "minLength": 1, "maxLength": 128 },
"algorithm": { "const": "ed25519" },
"status": { "enum": ["active", "retired", "revoked"] },
"public_key_pem": { "type": "string", "minLength": 1, "maxLength": 8192 },
"not_before": { "type": "string", "format": "date-time" },
"expires_at": { "type": "string", "format": "date-time" }
}
}
}
}
}
@@ -0,0 +1,125 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://govoplan.add-ideas.de/schemas/runtime-distribution-manifest-v1.json",
"title": "GovOPlaN runtime distribution manifest",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"channel",
"sequence",
"version",
"issued_at",
"expires_at",
"revoked",
"deployer",
"package_lock",
"images",
"dependencies",
"composition",
"signatures"
],
"properties": {
"schema_version": { "const": "1" },
"channel": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,63}$" },
"sequence": { "type": "integer", "minimum": 1 },
"version": { "type": "string", "minLength": 1, "maxLength": 128 },
"issued_at": { "type": "string", "format": "date-time" },
"expires_at": { "type": "string", "format": "date-time" },
"revoked": { "const": false },
"deployer": { "$ref": "#/$defs/artifact" },
"package_lock": { "$ref": "#/$defs/artifact" },
"images": {
"type": "object",
"additionalProperties": false,
"required": ["api", "web"],
"properties": {
"api": { "$ref": "#/$defs/image" },
"web": { "$ref": "#/$defs/image" }
}
},
"dependencies": {
"type": "object",
"minProperties": 1,
"propertyNames": { "pattern": "^[a-z][a-z0-9_]{1,63}$" },
"additionalProperties": { "$ref": "#/$defs/imageReference" }
},
"composition": {
"type": "object",
"additionalProperties": false,
"required": ["sha256", "module_ids", "packages"],
"properties": {
"sha256": { "$ref": "#/$defs/sha256" },
"module_ids": {
"type": "array",
"uniqueItems": true,
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,63}$" }
},
"packages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "version", "wheel_sha256"],
"properties": {
"name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
"version": { "type": "string", "minLength": 1, "maxLength": 128 },
"wheel_sha256": { "$ref": "#/$defs/sha256" }
}
}
}
}
},
"signatures": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["key_id", "algorithm", "value"],
"properties": {
"key_id": { "type": "string", "minLength": 1, "maxLength": 128 },
"algorithm": { "const": "ed25519" },
"value": { "type": "string", "minLength": 1, "maxLength": 256 }
}
}
}
},
"$defs": {
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"imageReference": {
"type": "string",
"pattern": "^[^@\\s]+@sha256:[0-9a-f]{64}$",
"maxLength": 300
},
"artifact": {
"type": "object",
"additionalProperties": false,
"required": ["url", "sha256"],
"properties": {
"url": { "type": "string", "format": "uri", "pattern": "^https://" },
"sha256": { "$ref": "#/$defs/sha256" }
}
},
"image": {
"type": "object",
"additionalProperties": false,
"required": ["index", "platforms", "sbom", "provenance"],
"properties": {
"index": { "$ref": "#/$defs/imageReference" },
"platforms": {
"type": "object",
"additionalProperties": false,
"required": ["linux/amd64", "linux/arm64"],
"properties": {
"linux/amd64": { "$ref": "#/$defs/imageReference" },
"linux/arm64": { "$ref": "#/$defs/imageReference" }
}
},
"sbom": { "$ref": "#/$defs/artifact" },
"provenance": { "$ref": "#/$defs/artifact" }
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
# GovOPlaN developer meta-package
`govoplan` is an optional convenience package for local development and
composition tests. The default dependency set matches the reviewed runtime
release roots; `govoplan[full]` adds every packageable module present in the
workspace at generation time.
This package is not a production deployment artifact. Production installations
consume the signed runtime distribution manifest and digest-pinned OCI images.
The package does not enable modules, apply migrations, choose infrastructure,
or replace installation and recovery evidence.
+90
View File
@@ -0,0 +1,90 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "govoplan"
version = "0.1.15"
description = "Developer convenience package for a versioned GovOPlaN composition"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "AGPL-3.0-or-later" }
dependencies = [
"govoplan-core[server]==0.1.15",
"govoplan-tenancy==0.1.15",
"govoplan-organizations==0.1.15",
"govoplan-identity==0.1.15",
"govoplan-idm==0.1.15",
"govoplan-access==0.1.15",
"govoplan-admin==0.1.15",
"govoplan-policy==0.1.15",
"govoplan-audit==0.1.15",
"govoplan-dashboard==0.1.15",
"govoplan-files==0.1.15",
"govoplan-mail==0.1.15",
"govoplan-campaign==0.1.15",
"govoplan-calendar==0.1.15",
"govoplan-docs==0.1.15",
"govoplan-ops==0.1.15",
]
[project.optional-dependencies]
full = [
"govoplan-addresses==0.1.15",
"govoplan-approvals==0.1.15",
"govoplan-assets==0.1.15",
"govoplan-booking==0.1.15",
"govoplan-cases==0.1.15",
"govoplan-certificates==0.1.15",
"govoplan-committee==0.1.15",
"govoplan-connectors==0.1.15",
"govoplan-consultation==0.1.15",
"govoplan-contracts==0.1.15",
"govoplan-dataflow==0.1.15",
"govoplan-datasources==0.1.15",
"govoplan-decisions==0.1.15",
"govoplan-dist-lists==0.1.15",
"govoplan-encryption==0.1.15",
"govoplan-evaluation==0.1.15",
"govoplan-facilities==0.1.15",
"govoplan-forms==0.1.15",
"govoplan-forms-runtime==0.1.15",
"govoplan-grants==0.1.15",
"govoplan-helpdesk==0.1.15",
"govoplan-identity-trust==0.1.15",
"govoplan-inspections==0.1.15",
"govoplan-learning==0.1.15",
"govoplan-mandates==0.1.15",
"govoplan-notifications==0.1.15",
"govoplan-parties==0.1.15",
"govoplan-permits==0.1.15",
"govoplan-poll==0.1.15",
"govoplan-portal==0.1.15",
"govoplan-postbox==0.1.15",
"govoplan-procurement==0.1.15",
"govoplan-projects==0.1.15",
"govoplan-records==0.1.15",
"govoplan-reporting==0.1.15",
"govoplan-resources==0.1.15",
"govoplan-rest==0.1.15",
"govoplan-risk-compliance==0.1.15",
"govoplan-scheduling==0.1.15",
"govoplan-search==0.1.15",
"govoplan-services==0.1.15",
"govoplan-soap==0.1.15",
"govoplan-templates==0.1.15",
"govoplan-tickets==0.1.15",
"govoplan-transparency==0.1.15",
"govoplan-views==0.1.15",
"govoplan-voting==0.1.15",
"govoplan-wiki==0.1.15",
"govoplan-workflow==0.1.15",
"govoplan-workflow-engine==0.1.15",
]
[project.urls]
Repository = "https://git.add-ideas.de/GovOPlaN/govoplan"
Documentation = "https://govoplan.add-ideas.de"
[tool.setuptools.packages.find]
where = ["src"]
@@ -0,0 +1,12 @@
"""Metadata helpers for the optional GovOPlaN developer composition."""
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("govoplan")
except PackageNotFoundError: # pragma: no cover - source checkout only
__version__ = "0+unknown"
__all__ = ["__version__"]
@@ -0,0 +1,32 @@
# Governed Communication Product Package
This package composes Campaign, Mail, Postbox, Files, Templates, Policy, Audit,
and Access into one governed-delivery capability. Modules retain their own
tables and lifecycle. The package declares installation and configuration
expectations; it does not import module internals.
## Reference journey
1. Select governed recipient and attachment data.
2. Build and review an immutable Campaign version.
3. Apply attachment, access, and delivery policy.
4. Deliver through Mail and/or function-bound Postbox targets.
5. Reconcile outcomes, acknowledgements, correction, and reports against Audit
evidence.
## Reference-readiness gates
The artifact remains a `product` package. Promotion to `reference` requires:
- target-environment delivery and reconciliation tests, including an
outcome-unknown transport result;
- backup/restore and interrupted-dispatch recovery evidence;
- security evidence for recipient, attachment, and postbox isolation;
- operator runbooks for queue health, retry, reconciliation, and retirement;
- keyboard, focus, screen-reader, and responsive workflow evidence;
- privacy evidence for minimization, purpose, retention, redaction, and report
access; and
- version-pinned user and administrator documentation.
Optional Notifications, Portal, Reporting, and Workflow Engine integrations do
not change the package boundary when absent.
@@ -0,0 +1,34 @@
{
"package_id": "product.governed-communication",
"name": "Governed Communication",
"version": "0.1.0",
"package_class": "product",
"description": "Compose evidence-backed campaigns, mail and function-bound postboxes without merging their domain ownership.",
"publisher": "GovOPlaN",
"category": "communication",
"license": "AGPL-3.0-or-later",
"required_modules": [
{"module_id": "access"},
{"module_id": "audit"},
{"module_id": "campaign"},
{"module_id": "files"},
{"module_id": "mail"},
{"module_id": "policy"},
{"module_id": "postbox"},
{"module_id": "templates"}
],
"optional_modules": [
{"module_id": "notifications"},
{"module_id": "portal"},
{"module_id": "reporting"},
{"module_id": "workflow_engine"}
],
"evidence": [
{
"kind": "documentation",
"reference": "packages/product/governed-communication/README.md",
"summary": "Defines the package boundary, journey, and reference-readiness gates."
}
],
"tags": ["campaign", "mail", "postbox", "public-sector"]
}
@@ -0,0 +1,41 @@
# Governed Data and Assurance Product Package
This package composes Datasources, Dataflow, Reporting, Search, Risk
Compliance, Policy, Audit, and Access. Connectors may acquire external data,
but Datasources owns the governed catalogue and immutable snapshots; Dataflow
owns transformation definitions and runs; Reporting owns measures and
presentation; Risk Compliance owns controls, findings, and effectiveness
review.
## Reference journey
1. Register a typed datasource with source authority, purpose, classification,
owner, freshness, and correction policy.
2. Acquire or upload an immutable source state.
3. execute a versioned flow and retain intermediate materializations and
provenance;
4. publish a report or decision input against exact source and flow revisions;
5. link obligation, governed object, risk, control, evidence, finding,
corrective measure, and effectiveness review; and
6. search current authorized objects while preserving ownership and access
rechecks.
## Reference-readiness gates
The artifact remains a `product` package. Promotion to `reference` requires:
- a target-tested monthly-data and sanctions-screening fixture with expected
outputs and complete provenance;
- database/object-storage backup and restore plus interrupted-run recovery;
- security evidence for datasource credentials, staged data, intermediate
states, search documents, and assurance references;
- operator runbooks for stale sources, failed runs, index rebuilds, and graph
reconciliation;
- accessibility evidence for the catalogue, graph editors, result inspection,
reports, and assurance graph;
- privacy evidence for minimization, purpose, retention, field visibility, and
aggregate disclosure; and
- version-pinned user and administrator documentation.
Optional Connectors, Files, Notifications, and Workflow Engine integrations
must remain capability-based and absence-safe.
@@ -0,0 +1,34 @@
{
"package_id": "product.governed-data-assurance",
"name": "Governed Data and Assurance",
"version": "0.1.0",
"package_class": "product",
"description": "Compose governed sources, transformations, reports, search, controls and assurance evidence through stable contracts.",
"publisher": "GovOPlaN",
"category": "data-governance",
"license": "AGPL-3.0-or-later",
"required_modules": [
{"module_id": "access"},
{"module_id": "audit"},
{"module_id": "dataflow"},
{"module_id": "datasources"},
{"module_id": "policy"},
{"module_id": "reporting"},
{"module_id": "risk_compliance"},
{"module_id": "search"}
],
"optional_modules": [
{"module_id": "connectors"},
{"module_id": "files"},
{"module_id": "notifications"},
{"module_id": "workflow_engine"}
],
"evidence": [
{
"kind": "documentation",
"reference": "packages/product/governed-data-assurance/README.md",
"summary": "Defines the package boundary, provenance chain, and reference-readiness gates."
}
],
"tags": ["datasources", "dataflow", "reporting", "assurance"]
}
@@ -0,0 +1,42 @@
# Governed Service To Decision
This product package composes independently owned institutional semantics into
one reconstructable administrative journey:
```text
Service discovery -> Case intake -> Party and representation -> Mandate
resolution -> approval/deliberation -> formal Decision -> observed delivery
effect -> record and review references
```
An installed Forms and Forms Runtime pair adds an alternative governed entry
path before case/workflow handoff:
```text
Service discovery -> exact Form revision -> validated draft/submission
-> receipt and handoff evidence -> Case or Workflow owner
```
Services, Cases, Parties, Mandates, Committee, and Decisions retain immutable
provider-owned revisions for the parts they own. Portal, Cases, and Committee
consume capabilities for cross-module semantics only. The package does not
grant cross-module table access and can omit optional presentation, work,
deliberation, delivery, or records modules while retaining explicit references
to externally performed steps.
## Security And Recovery
Every provider is tenant-bound. Missing or conflicting authority fails closed.
Protected Decision content has a separate permission. Writes are replay-safe
and OCC-guarded. Database restore is the semantic-state recovery unit; file and
communication effects remain governed by their owning providers and are linked
through requested/observed effect, evidence, and audit references.
The executable fixture in
`tests/test_institutional_governance_journey.py` proves SQL-backed Service,
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
`tests/test_institutional_service_journey.py` separately proves exact Portal
Form launch, persisted submission provenance, and idempotent replay.
Target-environment accessibility, security, operator, privacy,
delivery-provider, and recovery evidence are still required before this product
package may claim `reference_ready` maturity.
@@ -0,0 +1,59 @@
{
"package_id": "product.service-to-decision",
"name": "Governed Service To Decision",
"version": "0.1.0",
"package_class": "product",
"description": "Carry one exact institutional context from service discovery and case intake through parties, authority, formal outcome, communication evidence, and review.",
"publisher": "GovOPlaN",
"category": "institutional-governance",
"license": "AGPL-3.0-or-later",
"required_modules": [
{"module_id": "audit"},
{"module_id": "cases"},
{"module_id": "decisions"},
{"module_id": "mandates"},
{"module_id": "parties"},
{"module_id": "policy"},
{"module_id": "portal"},
{"module_id": "services"}
],
"required_capabilities": [
"cases.party_context",
"cases.service_intake",
"decisions.registry",
"mandates.resolver",
"parties.resolver",
"portal.service_directory",
"services.availability",
"services.definitions"
],
"optional_modules": [
{"module_id": "approvals"},
{"module_id": "committee"},
{"module_id": "files"},
{"module_id": "forms"},
{"module_id": "forms_runtime"},
{"module_id": "postbox"},
{"module_id": "records"},
{"module_id": "tasks"},
{"module_id": "workflow_engine"}
],
"evidence": [
{
"kind": "documentation",
"reference": "packages/product/service-to-decision/README.md",
"summary": "Defines the package boundary, authority path, recovery contract, and known operational limits."
},
{
"kind": "target_test",
"reference": "tests/test_institutional_governance_journey.py",
"summary": "Executes the SQL-backed provider composition from service discovery through persisted formal Decision reconstruction."
},
{
"kind": "target_test",
"reference": "tests/test_institutional_service_journey.py",
"summary": "Executes Portal delegation from an exact Service revision through an exact immutable Form revision to a replay-safe persisted submission."
}
],
"tags": ["service", "case", "mandate", "party", "decision", "public-sector"]
}
+79 -64
View File
@@ -1,70 +1,85 @@
{
"version": 1,
"organization": "add-ideas",
"organization": "GovOPlaN",
"default_parent": "/mnt/DATA/git",
"repositories": [
{"name": "govoplan", "category": "system", "subtype": "meta", "remote": "git@git.add-ideas.de:add-ideas/govoplan.git", "path": "govoplan"},
{"name": "govoplan-core", "category": "system", "subtype": "kernel", "remote": "git@git.add-ideas.de:add-ideas/govoplan-core.git", "path": "govoplan-core"},
{"name": "govoplan-access", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-access.git", "path": "govoplan-access"},
{"name": "govoplan-addresses", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-addresses.git", "path": "govoplan-addresses"},
{"name": "govoplan-admin", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-admin.git", "path": "govoplan-admin"},
{"name": "govoplan-appointments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-appointments.git", "path": "govoplan-appointments"},
{"name": "govoplan-approvals", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-approvals.git", "path": "govoplan-approvals"},
{"name": "govoplan-assets", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-assets.git", "path": "govoplan-assets"},
{"name": "govoplan-audit", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-audit.git", "path": "govoplan-audit"},
{"name": "govoplan-booking", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-booking.git", "path": "govoplan-booking"},
{"name": "govoplan-calendar", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-calendar.git", "path": "govoplan-calendar"},
{"name": "govoplan-campaign", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-campaign.git", "path": "govoplan-campaign"},
{"name": "govoplan-cases", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-cases.git", "path": "govoplan-cases"},
{"name": "govoplan-certificates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-certificates.git", "path": "govoplan-certificates"},
{"name": "govoplan-committee", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-committee.git", "path": "govoplan-committee"},
{"name": "govoplan-connectors", "category": "connector", "subtype": "connector-hub", "remote": "git@git.add-ideas.de:add-ideas/govoplan-connectors.git", "path": "govoplan-connectors"},
{"name": "govoplan-consultation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-consultation.git", "path": "govoplan-consultation"},
{"name": "govoplan-contracts", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-contracts.git", "path": "govoplan-contracts"},
{"name": "govoplan-dashboard", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-dashboard.git", "path": "govoplan-dashboard"},
{"name": "govoplan-dms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-dms.git", "path": "govoplan-dms"},
{"name": "govoplan-docs", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-docs.git", "path": "govoplan-docs"},
{"name": "govoplan-erp", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-erp.git", "path": "govoplan-erp"},
{"name": "govoplan-facilities", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-facilities.git", "path": "govoplan-facilities"},
{"name": "govoplan-files", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-files.git", "path": "govoplan-files"},
{"name": "govoplan-fit-connect", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-fit-connect.git", "path": "govoplan-fit-connect"},
{"name": "govoplan-forms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-forms.git", "path": "govoplan-forms"},
{"name": "govoplan-forms-runtime", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-forms-runtime.git", "path": "govoplan-forms-runtime"},
{"name": "govoplan-grants", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-grants.git", "path": "govoplan-grants"},
{"name": "govoplan-helpdesk", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-helpdesk.git", "path": "govoplan-helpdesk"},
{"name": "govoplan-identity", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-identity.git", "path": "govoplan-identity"},
{"name": "govoplan-identity-trust", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-identity-trust.git", "path": "govoplan-identity-trust"},
{"name": "govoplan-idm", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-idm.git", "path": "govoplan-idm"},
{"name": "govoplan-inspections", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-inspections.git", "path": "govoplan-inspections"},
{"name": "govoplan-issue-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-issue-reporting.git", "path": "govoplan-issue-reporting"},
{"name": "govoplan-learning", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-learning.git", "path": "govoplan-learning"},
{"name": "govoplan-ledger", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-ledger.git", "path": "govoplan-ledger"},
{"name": "govoplan-mail", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-mail.git", "path": "govoplan-mail"},
{"name": "govoplan-notifications", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-notifications.git", "path": "govoplan-notifications"},
{"name": "govoplan-ops", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-ops.git", "path": "govoplan-ops"},
{"name": "govoplan-organizations", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-organizations.git", "path": "govoplan-organizations"},
{"name": "govoplan-payments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-payments.git", "path": "govoplan-payments"},
{"name": "govoplan-permits", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-permits.git", "path": "govoplan-permits"},
{"name": "govoplan-policy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-policy.git", "path": "govoplan-policy"},
{"name": "govoplan-portal", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-portal.git", "path": "govoplan-portal"},
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-postbox.git", "path": "govoplan-postbox"},
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-procurement.git", "path": "govoplan-procurement"},
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-records.git", "path": "govoplan-records"},
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-reporting.git", "path": "govoplan-reporting"},
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-resources.git", "path": "govoplan-resources"},
{"name": "govoplan-rest", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:add-ideas/govoplan-rest.git", "path": "govoplan-rest"},
{"name": "govoplan-risk-compliance", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-risk-compliance.git", "path": "govoplan-risk-compliance"},
{"name": "govoplan-scheduling", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-scheduling.git", "path": "govoplan-scheduling"},
{"name": "govoplan-search", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-search.git", "path": "govoplan-search"},
{"name": "govoplan-soap", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:add-ideas/govoplan-soap.git", "path": "govoplan-soap"},
{"name": "govoplan-tasks", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tasks.git", "path": "govoplan-tasks"},
{"name": "govoplan-templates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-templates.git", "path": "govoplan-templates"},
{"name": "govoplan-tenancy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tenancy.git", "path": "govoplan-tenancy"},
{"name": "govoplan-transparency", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-transparency.git", "path": "govoplan-transparency"},
{"name": "addideas-govoplan-website", "category": "website", "subtype": "public-site", "remote": "git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git", "path": "addideas-govoplan-website"},
{"name": "govoplan-workflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-workflow.git", "path": "govoplan-workflow"},
{"name": "govoplan-xoev", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xoev.git", "path": "govoplan-xoev"},
{"name": "govoplan-xrechnung", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xrechnung.git", "path": "govoplan-xrechnung"},
{"name": "govoplan-xta-osci", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xta-osci.git", "path": "govoplan-xta-osci"}
{"name": "govoplan", "category": "system", "subtype": "meta", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan.git", "path": "govoplan"},
{"name": "govoplan-core", "category": "system", "subtype": "kernel", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-core.git", "path": "govoplan-core"},
{"name": "govoplan-access", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-access.git", "path": "govoplan-access"},
{"name": "govoplan-addresses", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-addresses.git", "path": "govoplan-addresses"},
{"name": "govoplan-admin", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-admin.git", "path": "govoplan-admin"},
{"name": "govoplan-appointments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-appointments.git", "path": "govoplan-appointments"},
{"name": "govoplan-approvals", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-approvals.git", "path": "govoplan-approvals"},
{"name": "govoplan-assets", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-assets.git", "path": "govoplan-assets"},
{"name": "govoplan-audit", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-audit.git", "path": "govoplan-audit"},
{"name": "govoplan-booking", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-booking.git", "path": "govoplan-booking"},
{"name": "govoplan-calendar", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-calendar.git", "path": "govoplan-calendar"},
{"name": "govoplan-campaign", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-campaign.git", "path": "govoplan-campaign"},
{"name": "govoplan-cases", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-cases.git", "path": "govoplan-cases"},
{"name": "govoplan-certificates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-certificates.git", "path": "govoplan-certificates"},
{"name": "govoplan-committee", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-committee.git", "path": "govoplan-committee"},
{"name": "govoplan-connectors", "category": "connector", "subtype": "connector-hub", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-connectors.git", "path": "govoplan-connectors"},
{"name": "govoplan-consultation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-consultation.git", "path": "govoplan-consultation"},
{"name": "govoplan-contracts", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-contracts.git", "path": "govoplan-contracts"},
{"name": "govoplan-dashboard", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dashboard.git", "path": "govoplan-dashboard"},
{"name": "govoplan-dataflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dataflow.git", "path": "govoplan-dataflow"},
{"name": "govoplan-datasources", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-datasources.git", "path": "govoplan-datasources"},
{"name": "govoplan-decisions", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-decisions.git", "path": "govoplan-decisions"},
{"name": "govoplan-dms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dms.git", "path": "govoplan-dms"},
{"name": "govoplan-dist-lists", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dist-lists.git", "path": "govoplan-dist-lists"},
{"name": "govoplan-docs", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-docs.git", "path": "govoplan-docs"},
{"name": "govoplan-encryption", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-encryption.git", "path": "govoplan-encryption"},
{"name": "govoplan-erp", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-erp.git", "path": "govoplan-erp"},
{"name": "govoplan-evaluation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-evaluation.git", "path": "govoplan-evaluation"},
{"name": "govoplan-facilities", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-facilities.git", "path": "govoplan-facilities"},
{"name": "govoplan-files", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-files.git", "path": "govoplan-files"},
{"name": "govoplan-fit-connect", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-fit-connect.git", "path": "govoplan-fit-connect"},
{"name": "govoplan-forms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-forms.git", "path": "govoplan-forms"},
{"name": "govoplan-forms-runtime", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-forms-runtime.git", "path": "govoplan-forms-runtime"},
{"name": "govoplan-grants", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-grants.git", "path": "govoplan-grants"},
{"name": "govoplan-helpdesk", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-helpdesk.git", "path": "govoplan-helpdesk"},
{"name": "govoplan-identity", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-identity.git", "path": "govoplan-identity"},
{"name": "govoplan-identity-trust", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-identity-trust.git", "path": "govoplan-identity-trust"},
{"name": "govoplan-idm", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-idm.git", "path": "govoplan-idm"},
{"name": "govoplan-inspections", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-inspections.git", "path": "govoplan-inspections"},
{"name": "govoplan-learning", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-learning.git", "path": "govoplan-learning"},
{"name": "govoplan-ledger", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-ledger.git", "path": "govoplan-ledger"},
{"name": "govoplan-mail", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-mail.git", "path": "govoplan-mail"},
{"name": "govoplan-mandates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-mandates.git", "path": "govoplan-mandates"},
{"name": "govoplan-notifications", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-notifications.git", "path": "govoplan-notifications"},
{"name": "govoplan-ops", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-ops.git", "path": "govoplan-ops"},
{"name": "govoplan-organizations", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-organizations.git", "path": "govoplan-organizations"},
{"name": "govoplan-payments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-payments.git", "path": "govoplan-payments"},
{"name": "govoplan-parties", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-parties.git", "path": "govoplan-parties"},
{"name": "govoplan-permits", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-permits.git", "path": "govoplan-permits"},
{"name": "govoplan-policy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-policy.git", "path": "govoplan-policy"},
{"name": "govoplan-poll", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-poll.git", "path": "govoplan-poll"},
{"name": "govoplan-portal", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-portal.git", "path": "govoplan-portal"},
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-postbox.git", "path": "govoplan-postbox"},
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-procurement.git", "path": "govoplan-procurement"},
{"name": "govoplan-projects", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-projects.git", "path": "govoplan-projects"},
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-records.git", "path": "govoplan-records"},
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-reporting.git", "path": "govoplan-reporting"},
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-resources.git", "path": "govoplan-resources"},
{"name": "govoplan-rest", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-rest.git", "path": "govoplan-rest"},
{"name": "govoplan-risk-compliance", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-risk-compliance.git", "path": "govoplan-risk-compliance"},
{"name": "govoplan-scheduling", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-scheduling.git", "path": "govoplan-scheduling"},
{"name": "govoplan-search", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-search.git", "path": "govoplan-search"},
{"name": "govoplan-services", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-services.git", "path": "govoplan-services"},
{"name": "govoplan-soap", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-soap.git", "path": "govoplan-soap"},
{"name": "govoplan-tasks", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-tasks.git", "path": "govoplan-tasks"},
{"name": "govoplan-templates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-templates.git", "path": "govoplan-templates"},
{"name": "govoplan-tenancy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-tenancy.git", "path": "govoplan-tenancy"},
{"name": "govoplan-tickets", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-tickets.git", "path": "govoplan-tickets"},
{"name": "govoplan-transparency", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-transparency.git", "path": "govoplan-transparency"},
{"name": "govoplan-views", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-views.git", "path": "govoplan-views"},
{"name": "govoplan-voting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-voting.git", "path": "govoplan-voting"},
{"name": "govoplan-wiki", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-wiki.git", "path": "govoplan-wiki"},
{"name": "addideas-govoplan-website", "category": "website", "subtype": "public-site", "remote": "git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git", "path": "addideas-govoplan-website", "bootstrap_transport": "registered"},
{"name": "govoplan-workflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-workflow.git", "path": "govoplan-workflow"},
{"name": "govoplan-workflow-engine", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-workflow-engine.git", "path": "govoplan-workflow-engine"},
{"name": "govoplan-xoev", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-xoev.git", "path": "govoplan-xoev"},
{"name": "govoplan-xrechnung", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-xrechnung.git", "path": "govoplan-xrechnung"},
{"name": "govoplan-xta-osci", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-xta-osci.git", "path": "govoplan-xta-osci"}
]
}
+1 -1
View File
@@ -1,4 +1,5 @@
bandit>=1.8,<2
click>=8.3.3
filelock>=3.20.3
idna>=3.15
pip>=26.1.2
@@ -6,5 +7,4 @@ pip-audit>=2.9,<3
python-multipart>=0.0.31
radon>=6,<7
ruff>=0.14,<1
semgrep>=1.140,<2
xenon>=0.9,<1
+34
View File
@@ -7,21 +7,55 @@
-e ../govoplan-tenancy
-e ../govoplan-organizations
-e ../govoplan-identity
-e ../govoplan-idm
-e ../govoplan-access
-e ../govoplan-admin
-e ../govoplan-policy
-e ../govoplan-audit
-e ../govoplan-approvals
-e ../govoplan-dashboard
-e ../govoplan-addresses
-e ../govoplan-dist-lists
-e ../govoplan-templates
-e ../govoplan-files
-e ../govoplan-forms
-e ../govoplan-forms-runtime
-e ../govoplan-mail
-e ../govoplan-campaign
-e ../govoplan-calendar
-e ../govoplan-committee
-e ../govoplan-cases
-e ../govoplan-portal
-e ../govoplan-services
-e ../govoplan-parties
-e ../govoplan-mandates
-e ../govoplan-decisions
-e ../govoplan-connectors
-e ../govoplan-datasources
-e ../govoplan-dataflow
-e ../govoplan-workflow-engine
-e ../govoplan-workflow
-e ../govoplan-views
-e ../govoplan-voting
-e ../govoplan-search
-e ../govoplan-risk-compliance
-e ../govoplan-postbox
-e ../govoplan-poll
-e ../govoplan-scheduling
-e ../govoplan-notifications
-e ../govoplan-evaluation
-e ../govoplan-docs
-e ../govoplan-encryption
-e ../govoplan-identity-trust
-e ../govoplan-ops
httpx==0.28.1
httpx2>=2.5,<3
filelock>=3.20.3
idna>=3.15
jsonschema>=4,<5
pip>=26.1.2
pip-audit>=2.9,<3
pytest>=9.0.3,<10
pygments>=2.20,<3
python-multipart>=0.0.31
ruff>=0.14,<1
+5
View File
@@ -0,0 +1,5 @@
# Test-harness dependencies used against immutable release source tags.
# Keep these separate from requirements-release.txt so they are not part of the
# deployable product dependency set.
pytest>=9.0.3,<10
pygments>=2.20,<3
+17 -18
View File
@@ -1,19 +1,18 @@
# Whole-product release install from tagged module repositories.
# Update GOVOPLAN_RELEASE_TAG together with pyproject/package versions when
# cutting a release.
# Whole-product release install from immutable, independently versioned module tags.
# Only add a module after its referenced tag has been published.
../govoplan-core[server]
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-tenancy.git@v0.1.6
govoplan-organizations @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git@v0.1.6
govoplan-identity @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-identity.git@v0.1.6
govoplan-idm @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-idm.git@v0.1.6
govoplan-access @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git@v0.1.6
govoplan-admin @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git@v0.1.6
govoplan-policy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git@v0.1.6
govoplan-audit @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git@v0.1.6
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git@v0.1.6
govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0.1.6
govoplan-mail @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git@v0.1.6
govoplan-campaign @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git@v0.1.6
govoplan-calendar @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git@v0.1.6
govoplan-docs @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git@v0.1.6
govoplan-ops @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git@v0.1.6
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.15
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.15
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.15
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.15
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.15
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.15
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.15
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.15
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.15
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.15
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.15
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.15
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.15
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.15
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.15
+120
View File
@@ -0,0 +1,120 @@
from __future__ import annotations
import base64
import json
from pathlib import Path
import stat
import subprocess
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from jsonschema import Draft202012Validator, FormatChecker
META_ROOT = Path(__file__).resolve().parents[1]
GENERATOR = META_ROOT / "tools" / "assessments" / "generate-authority-keypair.py"
class AssessmentAuthorityKeypairTests(unittest.TestCase):
def test_generates_schema_valid_scoped_proof_authority(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output_dir = Path(temp_dir)
output_dir.chmod(0o700)
private_path = output_dir / "target.pem"
keyring_path = output_dir / "target.json"
result = subprocess.run(
(
sys.executable,
str(GENERATOR),
"--purpose",
"proof",
"--key-id",
"authority:target-2026",
"--scope",
"target_environment",
"--scope",
"operations",
"--private-key",
str(private_path),
"--keyring",
str(keyring_path),
),
check=False,
capture_output=True,
text=True,
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(0o600, stat.S_IMODE(private_path.stat().st_mode))
self.assertEqual(0o600, stat.S_IMODE(keyring_path.stat().st_mode))
keyring = json.loads(keyring_path.read_text(encoding="utf-8"))
schema = json.loads(
(
META_ROOT
/ "docs"
/ "capability-fit-proof-authority-keyring.schema.json"
).read_text(encoding="utf-8")
)
errors = tuple(
Draft202012Validator(
schema, format_checker=FormatChecker()
).iter_errors(keyring)
)
self.assertEqual((), errors)
self.assertEqual(
["target_environment", "operations"],
keyring["keys"][0]["allowed_scopes"],
)
private_key = serialization.load_pem_private_key(
private_path.read_bytes(), password=None
)
self.assertIsInstance(private_key, Ed25519PrivateKey)
public_key = base64.b64encode(
private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("ascii")
self.assertEqual(public_key, keyring["keys"][0]["public_key"])
def test_installer_authority_uses_fixed_scope_and_refuses_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output_dir = Path(temp_dir)
output_dir.chmod(0o700)
private_path = output_dir / "installer.pem"
keyring_path = output_dir / "installer.json"
command = (
sys.executable,
str(GENERATOR),
"--purpose",
"installer",
"--key-id",
"authority:installer-2026",
"--private-key",
str(private_path),
"--keyring",
str(keyring_path),
)
first = subprocess.run(
command, check=False, capture_output=True, text=True
)
second = subprocess.run(
command, check=False, capture_output=True, text=True
)
self.assertEqual(0, first.returncode, first.stderr)
self.assertNotEqual(0, second.returncode)
keyring = json.loads(keyring_path.read_text(encoding="utf-8"))
self.assertEqual(
["installed_release_origin"],
keyring["keys"][0]["allowed_scopes"],
)
if __name__ == "__main__":
unittest.main()
+430
View File
@@ -0,0 +1,430 @@
from __future__ import annotations
import base64
from contextlib import redirect_stderr, redirect_stdout
from datetime import UTC, datetime, timedelta
import hashlib
import io
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
META_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(META_ROOT / "tools" / "deployment"))
from govoplan_deploy.backup_evidence import verify_backup_evidence # noqa: E402
from govoplan_deploy.bundle import ( # noqa: E402
atomic_write,
bundle_paths,
canonical_json,
read_env,
)
from govoplan_deploy.cli import main as deploy_main # noqa: E402
from govoplan_deploy.distribution import ( # noqa: E402
DistributionError,
canonical_signed_payload,
canonical_json as canonical_distribution_json,
)
from govoplan_deploy.model import default_spec, parse_spec # noqa: E402
from govoplan_deploy.planning import ( # noqa: E402
release_change_requires_backup,
verify_stored_backup_evidence,
)
class BackupEvidenceTests(unittest.TestCase):
def setUp(self) -> None:
self.now = datetime(2026, 8, 3, 12, tzinfo=UTC)
self.private = Ed25519PrivateKey.generate()
public = (
self.private.public_key()
.public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
.decode("ascii")
)
self.keyring = {
"schema_version": "1",
"purpose": "govoplan-backup-evidence",
"keys": [
{
"key_id": "backup-controller-1",
"algorithm": "ed25519",
"status": "active",
"public_key_pem": public,
"not_before": (self.now - timedelta(days=1)).isoformat(),
"expires_at": (self.now + timedelta(days=365)).isoformat(),
}
],
}
self.release = {
"channel": "stable",
"version": "1.2.3",
"manifest_sha256": "a" * 64,
"composition_sha256": "b" * 64,
"api_image": "registry.example/api@sha256:" + "c" * 64,
"web_image": "registry.example/web@sha256:" + "d" * 64,
}
def test_verifies_coordinated_restore_drill_and_release_binding(self) -> None:
summary = verify_backup_evidence(
self._evidence(),
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
now=self.now,
)
self.assertEqual("recovery-1", summary["recovery_point_id"])
self.assertEqual("restore-1", summary["restore_drill_id"])
self.assertEqual("backup-controller-1", summary["signature_key_id"])
def test_tampering_staleness_and_partial_restore_fail_closed(self) -> None:
tampered = self._evidence()
tampered["components"]["objects"]["object_count"] = 999
with self.assertRaisesRegex(DistributionError, "signature verification"):
verify_backup_evidence(
tampered,
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
now=self.now,
)
stale = self._evidence(captured=self.now - timedelta(days=2))
with self.assertRaisesRegex(DistributionError, "stale"):
verify_backup_evidence(
stale,
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
now=self.now,
)
partial = self._evidence()
partial["restore_drill"]["objects_verified"] = False
partial["signatures"] = [self._signature(partial)]
with self.assertRaisesRegex(DistributionError, "objects_verified"):
verify_backup_evidence(
partial,
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
now=self.now,
)
def test_wrong_release_key_purpose_and_component_skew_fail_closed(self) -> None:
wrong_release = dict(self.release)
wrong_release["version"] = "1.2.4"
with self.assertRaisesRegex(DistributionError, "release field"):
verify_backup_evidence(
self._evidence(),
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=wrong_release,
now=self.now,
)
wrong_keyring = dict(self.keyring)
wrong_keyring["purpose"] = "govoplan-runtime-distribution"
with self.assertRaisesRegex(DistributionError, "wrong purpose"):
verify_backup_evidence(
self._evidence(),
wrong_keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
now=self.now,
)
skewed = self._evidence()
skewed["components"]["database"]["captured_at"] = (
self.now - timedelta(hours=1)
).isoformat()
skewed["signatures"] = [self._signature(skewed)]
with self.assertRaisesRegex(DistributionError, "one recovery point"):
verify_backup_evidence(
skewed,
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
now=self.now,
)
false_rto = self._evidence()
false_rto["restore_drill"]["measured_rto_seconds"] = 1
false_rto["signatures"] = [self._signature(false_rto)]
with self.assertRaisesRegex(DistributionError, "RTO"):
verify_backup_evidence(
false_rto,
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
now=self.now,
)
def test_provider_signing_tool_emits_canonical_verified_evidence(self) -> None:
self.now = datetime.now(UTC)
self.keyring["keys"][0]["not_before"] = (
self.now - timedelta(days=1)
).isoformat()
self.keyring["keys"][0]["expires_at"] = (
self.now + timedelta(days=365)
).isoformat()
evidence = self._evidence()
evidence["signatures"] = []
with tempfile.TemporaryDirectory(prefix="govoplan-backup-signer-") as value:
root = Path(value)
source = root / "unsigned.json"
output = root / "signed.json"
keyring = root / "keyring.json"
private_key = root / "private.pem"
atomic_write(source, canonical_json(evidence), mode=0o600)
atomic_write(keyring, canonical_json(self.keyring), mode=0o600)
atomic_write(
private_key,
self.private.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
),
mode=0o600,
)
result = subprocess.run(
[
sys.executable,
str(META_ROOT / "tools/deployment/sign-backup-evidence.py"),
"--input",
str(source),
"--output",
str(output),
"--trusted-keyring",
str(keyring),
"--signing-key",
f"backup-controller-1={private_key}",
],
cwd=META_ROOT,
check=False,
capture_output=True,
text=True,
)
self.assertEqual(0, result.returncode, result.stderr)
encoded = output.read_bytes()
signed = json.loads(encoded)
self.assertEqual(canonical_distribution_json(signed), encoded)
summary = verify_backup_evidence(
signed,
self.keyring,
installation_id="govoplan-test",
profile="self-hosted",
release=self.release,
)
self.assertEqual("backup-controller-1", summary["signature_key_id"])
def test_cli_adoption_gates_the_next_release_against_previous_receipt(self) -> None:
self.now = datetime.now(UTC)
self.keyring["keys"][0]["not_before"] = (
self.now - timedelta(days=1)
).isoformat()
self.keyring["keys"][0]["expires_at"] = (
self.now + timedelta(days=365)
).isoformat()
evidence = self._evidence()
encoded_evidence = canonical_distribution_json(evidence)
encoded_keyring = canonical_distribution_json(self.keyring)
with tempfile.TemporaryDirectory(prefix="govoplan-backup-evidence-") as value:
paths = bundle_paths(Path(value))
paths.root.chmod(0o700)
raw = default_spec(
installation_id="govoplan-test",
profile="self-hosted",
public_url="https://govoplan.example.test",
ingress_mode="existing-proxy",
trusted_proxy_cidrs=("127.0.0.1/32",),
).to_dict()
raw["release"] = {
**raw["release"],
**self.release,
}
current = parse_spec(raw)
atomic_write(paths.spec, canonical_json(current.to_dict()), mode=0o600)
source_evidence = paths.root / "source-backup.json"
source_keyring = paths.root / "source-keyring.json"
atomic_write(source_evidence, encoded_evidence, mode=0o600)
atomic_write(source_keyring, encoded_keyring, mode=0o600)
output = io.StringIO()
with redirect_stdout(output), redirect_stderr(output):
result = deploy_main(
[
"verify-backup",
"--directory",
str(paths.root),
"--evidence",
str(source_evidence),
"--evidence-sha256",
hashlib.sha256(encoded_evidence).hexdigest(),
"--trusted-keyring",
str(source_keyring),
"--adopt",
]
)
self.assertEqual(0, result, output.getvalue())
runtime_environment = read_env(paths.env)
self.assertEqual(
"verified",
runtime_environment["GOVOPLAN_BACKUP_EVIDENCE_STATE"],
)
self.assertEqual(
"recovery-1",
runtime_environment["GOVOPLAN_BACKUP_RECOVERY_POINT_ID"],
)
self.assertNotIn("snapshot:postgres", str(runtime_environment))
self.assertNotIn("urn:kms", str(runtime_environment))
receipt = {
"installation_id": current.installation_id,
"profile": current.profile,
"release": dict(self.release),
}
atomic_write(paths.receipt, canonical_json(receipt), mode=0o600)
target_raw = current.to_dict()
target_raw["release"]["version"] = "1.2.4"
target_raw["release"]["manifest_sha256"] = "9" * 64
target = parse_spec(target_raw)
self.assertTrue(release_change_requires_backup(target, receipt))
summary = verify_stored_backup_evidence(
target,
paths,
receipt=receipt,
)
self.assertEqual("recovery-1", summary["recovery_point_id"])
def _evidence(self, *, captured: datetime | None = None) -> dict[str, object]:
captured = captured or self.now - timedelta(hours=2)
started = captured + timedelta(minutes=15)
completed = captured + timedelta(minutes=30)
issued = completed + timedelta(minutes=10)
artifact_time = captured.isoformat()
payload: dict[str, object] = {
"schema_version": "1",
"evidence_id": "backup-1",
"installation_id": "govoplan-test",
"deployment_subject": {
"profile": "self-hosted",
"topology": "compose",
"subject_ref": "urn:govoplan:installation:govoplan-test",
},
"release": dict(self.release),
"recovery_point": {
"id": "recovery-1",
"captured_at": captured.isoformat(),
"consistency": "application-quiesced",
"rpo_seconds": 300,
"write_fence": {
"mode": "application-quiesce",
"token_sha256": "e" * 64,
"established_at": captured.isoformat(),
},
},
"components": {
"database": {
"provider": "postgres",
"artifact_ref": "snapshot:postgres:backup-1",
"artifact_sha256": "1" * 64,
"snapshot_id": "pg-snapshot-1",
"lsn": "0/16B6C50",
"protected": True,
"encryption_key_ref": "urn:kms:key:database-backup",
"captured_at": artifact_time,
},
"objects": {
"provider": "s3",
"artifact_ref": "s3://backup/govoplan-test/recovery-1",
"manifest_sha256": "2" * 64,
"version_id": "object-snapshot-1",
"object_count": 4,
"total_bytes": 1024,
"protected": True,
"encryption_key_ref": "urn:kms:key:object-backup",
"captured_at": artifact_time,
},
"configuration": {
"artifact_ref": "backup:configuration:recovery-1",
"sha256": "3" * 64,
"protected": True,
"encryption_key_ref": "urn:kms:key:configuration-backup",
"captured_at": artifact_time,
},
"key_custody": {
"provider": "kms",
"keyset_ref": "urn:kms:keyset:govoplan-test",
"keyset_version": "version-4",
"recoverable": True,
"captured_at": artifact_time,
},
},
"restore_drill": {
"drill_id": "restore-1",
"recovery_point_id": "recovery-1",
"started_at": started.isoformat(),
"completed_at": completed.isoformat(),
"isolated_target_ref": "urn:govoplan:restore-target:restore-1",
"release_manifest_sha256": self.release["manifest_sha256"],
"migration_heads_sha256": "4" * 64,
"representative_object_manifest_sha256": "2" * 64,
"database_verified": True,
"objects_verified": True,
"configuration_verified": True,
"key_custody_verified": True,
"semantic_checks": [
{
"id": "institutional-journey",
"status": "passed",
"evidence_ref": "evidence:journey:institutional-1",
}
],
"measured_rpo_seconds": 120,
"measured_rto_seconds": 900,
"evidence_ref": "evidence:restore:restore-1",
},
"issued_at": issued.isoformat(),
"expires_at": (self.now + timedelta(days=7)).isoformat(),
"revoked": False,
"signatures": [],
}
payload["signatures"] = [self._signature(payload)]
return payload
def _signature(self, payload: dict[str, object]) -> dict[str, str]:
return {
"key_id": "backup-controller-1",
"algorithm": "ed25519",
"value": base64.b64encode(
self.private.sign(canonical_signed_payload(payload))
).decode("ascii"),
}
if __name__ == "__main__":
unittest.main()
+198
View File
@@ -0,0 +1,198 @@
from __future__ import annotations
import base64
from datetime import UTC, datetime
import json
from pathlib import Path
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
META_ROOT = Path(__file__).resolve().parents[1]
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
if str(tools_root) not in sys.path:
sys.path.insert(0, str(tools_root))
from govoplan_assessment.boundary_evidence import ( # noqa: E402
issue_boundary_evidence,
)
from govoplan_assessment.evidence import ( # noqa: E402
InstalledEvidenceReview,
canonical_sha256,
review_boundary_evidence,
validate_payload,
)
class BoundaryEvidenceIssuerTests(unittest.TestCase):
def setUp(self) -> None:
self.assessment = {
"assessment_id": "assessment:test",
"release": {"ref": "stable-catalog-202608020001"},
"deployment_profile": {"id": "deployment:target"},
}
self.installed = {
"schema_version": "0.1.0",
"assessment_id": "assessment:test",
"artifacts": [],
}
self.private_key = Ed25519PrivateKey.generate()
public_key = base64.b64encode(
self.private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("ascii")
self.authority = {
"$schema": "./capability-fit-proof-authority-keyring.schema.json",
"schema_version": "0.1.0",
"purpose": "govoplan.capability-fit-proof-authorities",
"keys": [
{
"key_id": "authority:target",
"status": "active",
"public_key": public_key,
"allowed_scopes": ["target_environment", "recovery"],
"not_before": "2026-08-01T00:00:00Z",
"not_after": "2026-09-01T00:00:00Z",
}
],
}
self.boundary_schema = json.loads(
(
META_ROOT / "docs" / "capability-fit-boundary-evidence.schema.json"
).read_text(encoding="utf-8")
)
self.authority_schema = json.loads(
(
META_ROOT
/ "docs"
/ "capability-fit-proof-authority-keyring.schema.json"
).read_text(encoding="utf-8")
)
def test_issues_sanitized_hash_bound_proof_and_verifies_it(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
result_path = Path(temp_dir) / "recovery-result.json"
result_path.write_text(
'{"rto_seconds":42,"rpo_seconds":0,"reconstructed":true}\n',
encoding="utf-8",
)
proof = self.issue(
claims=[
{
"scope": "target_environment",
"result": "passed",
"control_ids": ["topology:shared-state-v1"],
"artifacts": [
{"artifact_id": "target:run-1", "path": result_path}
],
},
{
"scope": "recovery",
"result": "passed",
"control_ids": ["recovery:restore-v1"],
"artifacts": [
{"artifact_id": "recovery:run-1", "path": result_path}
],
},
]
)
self.assertEqual(
(), validate_payload(payload=proof, schema=self.boundary_schema)
)
self.assertNotIn(str(result_path), json.dumps(proof))
self.assertEqual(
canonical_sha256(self.installed), proof["installed_evidence_sha256"]
)
installed_review = InstalledEvidenceReview(
findings=(),
changes=(),
changed_repositories=frozenset(),
affected_module_ids=frozenset(),
proof_scope={
"installed_artifacts": {"valid": True},
"installed_release_origin": {"valid": True},
},
evidence_sha256=canonical_sha256(self.installed),
)
review = review_boundary_evidence(
assessment=self.assessment,
installed_review=installed_review,
evidence=proof,
evidence_schema=self.boundary_schema,
authority_keyring=self.authority,
authority_keyring_schema=self.authority_schema,
verification_time=datetime(2026, 8, 2, 12, 30, tzinfo=UTC),
)
self.assertEqual((), review.findings)
self.assertTrue(review.proof_scope["target_environment"]["valid"])
self.assertTrue(review.proof_scope["recovery"]["valid"])
def test_refuses_missing_release_origin_or_scope_authority(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
artifact = Path(temp_dir) / "result.txt"
artifact.write_text("passed\n", encoding="utf-8")
claim = {
"scope": "security",
"result": "passed",
"control_ids": ["security:sast-v1"],
"artifacts": [{"artifact_id": "security:run-1", "path": artifact}],
}
with self.assertRaisesRegex(ValueError, "release origin"):
self.issue(claims=[claim], release_origin_verified=False)
with self.assertRaisesRegex(ValueError, "authorized for scopes: security"):
self.issue(claims=[claim])
def test_refuses_authority_that_expires_before_the_proof(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
artifact = Path(temp_dir) / "result.txt"
artifact.write_text("passed\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "full proof interval"):
self.issue(
expires_at=datetime(2026, 10, 1, tzinfo=UTC),
claims=[
{
"scope": "recovery",
"result": "passed",
"control_ids": ["recovery:restore-v1"],
"artifacts": [
{
"artifact_id": "recovery:run-1",
"path": artifact,
}
],
}
],
)
def issue(
self,
*,
claims: list[dict[str, object]],
expires_at: datetime = datetime(2026, 8, 3, tzinfo=UTC),
release_origin_verified: bool = True,
) -> dict[str, object]:
return issue_boundary_evidence(
assessment=self.assessment,
installed_evidence=self.installed,
proof_id="proof:target:1",
claims=claims,
authority_keyring=self.authority,
signing_keys={"authority:target": self.private_key},
issued_at=datetime(2026, 8, 2, 12, tzinfo=UTC),
expires_at=expires_at,
release_origin_verified=release_origin_verified,
)
if __name__ == "__main__":
unittest.main()
+190
View File
@@ -0,0 +1,190 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import stat
import sys
import tempfile
import unittest
from unittest import mock
META_ROOT = Path(__file__).resolve().parents[1]
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
if str(tools_root) not in sys.path:
sys.path.insert(0, str(tools_root))
from govoplan_assessment.atomic_io import ( # noqa: E402
AtomicJsonWriteError,
atomic_write_json,
)
class CapabilityFitAtomicIOTests(unittest.TestCase):
def test_writes_private_json_with_expected_content(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
target = Path(temporary_directory) / "evidence.json"
payload = {"z": [1, 2], "message": "Grüße"}
created_in: list[Path] = []
original_mkstemp = tempfile.mkstemp
def observed_mkstemp(*args, **kwargs):
created_in.append(Path(kwargs["dir"]))
return original_mkstemp(*args, **kwargs)
with (
mock.patch(
"govoplan_assessment.atomic_io.tempfile.mkstemp",
side_effect=observed_mkstemp,
),
mock.patch(
"govoplan_assessment.atomic_io.os.fsync",
wraps=os.fsync,
) as fsync,
):
atomic_write_json(target, payload, max_bytes=1024)
self.assertEqual(payload, json.loads(target.read_text(encoding="utf-8")))
self.assertTrue(target.read_bytes().endswith(b"\n"))
self.assertEqual(0o600, stat.S_IMODE(target.stat().st_mode))
self.assertEqual([target.parent], created_in)
self.assertEqual(2, fsync.call_count)
def test_replaces_existing_regular_file_and_secures_mode(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
target = Path(temporary_directory) / "report.json"
target.write_text('{"old": true}\n', encoding="utf-8")
target.chmod(0o644)
old_handle = target.open("rb")
self.addCleanup(old_handle.close)
atomic_write_json(target, {"new": True}, max_bytes=1024)
self.assertEqual(b'{"old": true}\n', old_handle.read())
self.assertEqual({"new": True}, json.loads(target.read_text("utf-8")))
self.assertEqual(0o600, stat.S_IMODE(target.stat().st_mode))
def test_size_bound_leaves_existing_target_unchanged(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
directory = Path(temporary_directory)
target = directory / "evidence.json"
original = b'{"original": true}\n'
target.write_bytes(original)
with self.assertRaisesRegex(AtomicJsonWriteError, "size limit"):
atomic_write_json(target, {"large": "x" * 100}, max_bytes=32)
self.assertEqual(original, target.read_bytes())
self.assertEqual([], list(directory.glob(".govoplan-json-*.tmp")))
@unittest.skipUnless(hasattr(os, "symlink"), "symbolic links are unavailable")
def test_rejects_symlink_without_modifying_link_or_referent(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
directory = Path(temporary_directory)
referent = directory / "outside.json"
referent.write_bytes(b'{"keep": true}\n')
target = directory / "evidence.json"
target.symlink_to(referent.name)
with self.assertRaisesRegex(AtomicJsonWriteError, "symbolic link"):
atomic_write_json(target, {"replace": True}, max_bytes=1024)
self.assertTrue(target.is_symlink())
self.assertEqual(b'{"keep": true}\n', referent.read_bytes())
self.assertEqual([], list(directory.glob(".govoplan-json-*.tmp")))
@unittest.skipUnless(hasattr(os, "symlink"), "symbolic links are unavailable")
def test_rejects_symlinked_parent_component(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
directory = Path(temporary_directory)
real_parent = directory / "real-parent"
real_parent.mkdir()
linked_parent = directory / "linked-parent"
linked_parent.symlink_to(real_parent, target_is_directory=True)
target = linked_parent / "new" / "evidence.json"
with self.assertRaisesRegex(AtomicJsonWriteError, "parent path"):
atomic_write_json(target, {"unsafe": True}, max_bytes=1024)
self.assertFalse((real_parent / "new").exists())
def test_requires_existing_parent_without_creating_directories(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
directory = Path(temporary_directory)
missing_parent = directory / "missing" / "nested"
target = missing_parent / "evidence.json"
with self.assertRaisesRegex(AtomicJsonWriteError, "already exist"):
atomic_write_json(target, {"safe": True}, max_bytes=1024)
self.assertFalse(missing_parent.exists())
def test_rejects_post_replace_mode_or_identity_change(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
directory = Path(temporary_directory)
target = directory / "evidence.json"
original_replace = os.replace
def insecure_replace(source, destination):
original_replace(source, destination)
Path(destination).chmod(0o644)
with mock.patch(
"govoplan_assessment.atomic_io.os.replace",
side_effect=insecure_replace,
):
with self.assertRaisesRegex(
AtomicJsonWriteError,
"permissions changed and were restored",
):
atomic_write_json(target, {"safe": True}, max_bytes=1024)
self.assertEqual(0o600, stat.S_IMODE(target.stat().st_mode))
self.assertEqual({"safe": True}, json.loads(target.read_text("utf-8")))
def test_does_not_unlink_unknown_post_replace_inode(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
directory = Path(temporary_directory)
target = directory / "evidence.json"
original_replace = os.replace
def replaced_again(source, destination):
original_replace(source, destination)
Path(destination).unlink()
Path(destination).write_bytes(b"unknown replacement\n")
with mock.patch(
"govoplan_assessment.atomic_io.os.replace",
side_effect=replaced_again,
):
with self.assertRaisesRegex(
AtomicJsonWriteError,
"did not preserve",
):
atomic_write_json(target, {"secret": True}, max_bytes=1024)
self.assertEqual(b"unknown replacement\n", target.read_bytes())
def test_replace_failure_removes_private_temporary_file(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
directory = Path(temporary_directory)
target = directory / "evidence.json"
with mock.patch(
"govoplan_assessment.atomic_io.os.replace",
side_effect=OSError("simulated replacement failure"),
):
with self.assertRaisesRegex(
AtomicJsonWriteError, "could not be written atomically"
):
atomic_write_json(target, {"safe": True}, max_bytes=1024)
self.assertFalse(target.exists())
self.assertEqual([], list(directory.glob(".govoplan-json-*.tmp")))
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large Load Diff
+729
View File
@@ -0,0 +1,729 @@
from __future__ import annotations
import base64
from copy import deepcopy
from datetime import UTC, datetime, timedelta
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from unittest import mock
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
META_ROOT = Path(__file__).resolve().parents[1]
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
if str(tools_root) not in sys.path:
sys.path.insert(0, str(tools_root))
from govoplan_assessment.capability_fit import ( # noqa: E402
enforce_required_boundary_scopes,
local_tag_provenance,
render_review,
review_capability_fit,
trusted_keys_from_keyring,
)
class CapabilityFitReviewTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.assessment = json.loads(
(META_ROOT / "docs" / "capability-fit-current.json").read_text(
encoding="utf-8"
)
)
cls.schema = json.loads(
(META_ROOT / "docs" / "capability-fit.schema.json").read_text(
encoding="utf-8"
)
)
def test_matching_signed_catalog_is_current(self) -> None:
catalog, keyring = signed_catalog(self.assessment)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual("current", report["status"])
self.assertEqual([], report["changes"])
self.assertFalse(report["proof_scope"]["installed_artifacts"]["checked"])
self.assertFalse(report["proof_scope"]["target_environment"]["checked"])
self.assertFalse(report["proof_scope"]["local_tag_provenance"]["checked"])
repeated = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual(
json.dumps(report, sort_keys=True), json.dumps(repeated, sort_keys=True)
)
def test_required_boundary_scope_blocks_admission_until_positive(self) -> None:
catalog, keyring = signed_catalog(self.assessment)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
enforce_required_boundary_scopes(
report, required_scopes=("security", "recovery")
)
self.assertEqual("blocked", report["status"])
self.assertFalse(report["proof_scope"]["admission"]["valid"])
self.assertEqual(
["recovery", "security"],
report["proof_scope"]["admission"]["failed_scopes"],
)
self.assertEqual(
2,
sum(
item["code"] == "required_boundary_scope_unsatisfied"
for item in report["findings"]
),
)
def test_release_drift_identifies_affected_conclusions(self) -> None:
catalog, keyring = signed_catalog(
self.assessment, versions={"campaigns": "0.1.11"}, sequence=202607230001
)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual("review_required", report["status"])
target_ids = {item["id"] for item in report["review_targets"]}
self.assertIn("campaign.journey", target_ids)
self.assertIn("composition.campaigns", target_ids)
self.assertIn("assessment.release", target_ids)
self.assertIn(
"composition_version_changed", {item["code"] for item in report["findings"]}
)
def test_contradictory_signed_selected_unit_metadata_blocks_rerun(self) -> None:
core = next(
item
for item in self.assessment["composition"]
if item["module_id"] == "core"
)
catalog, keyring = signed_catalog(
self.assessment,
selected_units=[
{
"repo": core["repository"],
"version": core["manifest_version"],
"tag": "v999.0.0",
"commit_sha": "0" * 40,
"tag_object_sha": "1" * 40,
}
],
)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual("blocked", report["status"])
codes = {item["code"] for item in report["findings"]}
self.assertIn("catalog_release_metadata", codes)
self.assertIn("composition_commit_changed", codes)
self.assertTrue(report["proof_scope"]["catalog_signature_and_keyring"]["valid"])
self.assertFalse(report["proof_scope"]["release_metadata"]["valid"])
def test_signed_selected_unit_commit_drift_requires_review(self) -> None:
core = next(
item
for item in self.assessment["composition"]
if item["module_id"] == "core"
)
catalog, keyring = signed_catalog(
self.assessment,
selected_units=[
{
"repo": core["repository"],
"version": core["manifest_version"],
"tag": f"v{core['manifest_version']}",
"commit_sha": "0" * 40,
"tag_object_sha": "1" * 40,
}
],
)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual("review_required", report["status"])
self.assertIn(
"composition_commit_changed", {item["code"] for item in report["findings"]}
)
self.assertIn(
"composition.core", {item["id"] for item in report["review_targets"]}
)
self.assertFalse(report["proof_scope"]["release_metadata"]["valid"])
def test_missing_duplicate_and_malformed_selected_unit_provenance_blocks(
self,
) -> None:
core = next(
item
for item in self.assessment["composition"]
if item["module_id"] == "core"
)
valid_unit = {
"repo": core["repository"],
"version": core["manifest_version"],
"tag": f"v{core['manifest_version']}",
"commit_sha": (str(core["commit"]) + "0" * 40)[:40],
"tag_object_sha": "1" * 40,
}
cases = (
("missing", None, False, "catalog_source_provenance"),
(
"duplicate",
[valid_unit, deepcopy(valid_unit)],
True,
"catalog_release_metadata",
),
(
"malformed",
[{**valid_unit, "commit_sha": "not-a-git-object"}],
True,
"catalog_source_provenance",
),
)
for label, selected_units, include_selected_units, expected_code in cases:
with self.subTest(label=label):
catalog, keyring = signed_catalog(
self.assessment,
selected_units=selected_units,
include_selected_units=include_selected_units,
)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual("blocked", report["status"])
self.assertIn(
expected_code, {item["code"] for item in report["findings"]}
)
self.assertFalse(report["proof_scope"]["release_metadata"]["valid"])
def test_catalog_validation_ignores_and_preserves_installer_replay_state(
self,
) -> None:
catalog, keyring = signed_catalog(self.assessment)
with tempfile.TemporaryDirectory() as temp_dir:
state_path = Path(temp_dir) / "catalog-sequence.json"
state_path.write_text(
json.dumps(
{"channels": {"stable": {"last_sequence": catalog["sequence"]}}}
),
encoding="utf-8",
)
replay_environment = {
"GOVOPLAN_MODULE_PACKAGE_CATALOG_SEQUENCE_STATE": str(state_path),
"GOVOPLAN_MODULE_PACKAGE_CATALOG_ENFORCE_SEQUENCE": "true",
}
with mock.patch.dict(os.environ, replay_environment, clear=False):
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual(
replay_environment,
{key: os.environ[key] for key in replay_environment},
)
self.assertEqual("current", report["status"])
self.assertTrue(report["proof_scope"]["catalog_signature_and_keyring"]["valid"])
def test_self_consistent_substitution_fails_independent_trust_root(self) -> None:
_, trusted_keyring = signed_catalog(self.assessment)
catalog, published_keyring = signed_catalog(self.assessment)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=published_keyring,
trusted_keyring=trusted_keyring,
)
self.assertEqual("blocked", report["status"])
codes = {item["code"] for item in report["findings"]}
self.assertIn("catalog_trust", codes)
self.assertNotIn("catalog_keyring_hash_mismatch", codes)
self.assertTrue(report["proof_scope"]["assessment_schema"]["valid"])
self.assertFalse(
report["proof_scope"]["catalog_signature_and_trusted_keyring"]["valid"]
)
self.assertTrue(report["proof_scope"]["published_keyring_hash"]["valid"])
def test_locally_pinned_rotation_keys_fail_closed(self) -> None:
keys = trusted_keys_from_keyring(
{
"keys": [
{"key_id": "active", "status": "active", "public_key": "a"},
{"key_id": "next", "status": "next", "public_key": "b"},
{"key_id": "retired", "status": "retired", "public_key": "c"},
]
}
)
self.assertEqual({"active": "a", "next": "b"}, keys)
self.assertEqual(
{"legacy": "base64-key"},
trusted_keys_from_keyring({"legacy": "base64-key"}),
)
self.assertEqual(
{},
trusted_keys_from_keyring(
{
"keys": [
{
"key_id": "typo",
"status": "retierd",
"public_key": "unsafe",
}
]
}
),
)
self.assertEqual(
{},
trusted_keys_from_keyring(
{
"keys": [
{"key_id": "duplicate", "public_key": "a"},
{"key_id": "duplicate", "public_key": "b"},
]
}
),
)
self.assertEqual({}, trusted_keys_from_keyring({"keys": "invalid"}))
self.assertEqual(
{},
trusted_keys_from_keyring(
{"keys": [{"key_id": " ", "public_key": "unsafe"}]}
),
)
def test_schema_error_blocks_before_comparison(self) -> None:
assessment = deepcopy(self.assessment)
assessment.pop("scope")
catalog, keyring = signed_catalog(self.assessment)
report = review_capability_fit(
assessment=assessment,
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
self.assertEqual("blocked", report["status"])
self.assertEqual(
{"assessment_schema"}, {item["code"] for item in report["findings"]}
)
self.assertFalse(report["proof_scope"]["assessment_schema"]["valid"])
self.assertFalse(
report["proof_scope"]["catalog_signature_and_keyring"]["checked"]
)
self.assertIsNone(
report["proof_scope"]["catalog_signature_and_keyring"]["valid"]
)
def test_human_report_states_proof_limit(self) -> None:
catalog, keyring = signed_catalog(self.assessment)
report = review_capability_fit(
assessment=deepcopy(self.assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
)
rendered = render_review(report)
self.assertIn("Capability fit rerun: current", rendered)
self.assertIn(
"No installed-composition, installed-release-origin, target-environment, external-providers, accessibility, privacy, security, operations, recovery, production-approval proof",
rendered,
)
def test_public_fetch_failure_is_generic_and_blocking(self) -> None:
script = META_ROOT / "tools" / "assessments" / "capability-fit.py"
spec = importlib.util.spec_from_file_location("capability_fit_cli", script)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
with tempfile.TemporaryDirectory() as temp_dir:
trusted_path = Path(temp_dir) / "trusted-keyring.json"
trusted_path.write_text('{"keys": []}\n', encoding="utf-8")
with mock.patch.object(
module,
"fetch_json",
return_value={
"ok": False,
"error": "Authorization: secret at https://internal.invalid",
},
):
with self.assertRaisesRegex(
SystemExit, r"^Could not fetch fixed public catalog endpoint[.]$"
):
module.main(
[
"--public",
"--trusted-keyring",
str(trusted_path),
"--skip-tag-provenance",
]
)
def test_cli_requires_an_independent_local_trust_root(self) -> None:
script = META_ROOT / "tools" / "assessments" / "capability-fit.py"
spec = importlib.util.spec_from_file_location(
"capability_fit_cli_trust", script
)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
with mock.patch.dict(
os.environ,
{module.TRUSTED_KEYRING_FILE_ENV: ""},
clear=False,
):
with self.assertRaisesRegex(SystemExit, r"--trusted-keyring .* required"):
module.main(["--public", "--skip-tag-provenance"])
def test_local_selected_tag_commit_and_object_must_match_exactly(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
workspace = Path(temp_dir)
commit_sha, tag_object_sha = create_tagged_repository(
workspace=workspace,
repository="govoplan-core",
version="0.1.0",
)
assessment = deepcopy(self.assessment)
core = next(
deepcopy(item)
for item in assessment["composition"]
if item["module_id"] == "core"
)
core["manifest_version"] = "0.1.0"
core["commit"] = commit_sha[:12]
assessment["composition"] = [core]
selected_unit = {
"repo": "govoplan-core",
"version": "0.1.0",
"tag": "v0.1.0",
"commit_sha": commit_sha,
"tag_object_sha": tag_object_sha,
}
catalog, keyring = signed_catalog(
assessment,
selected_units=[selected_unit],
)
baseline = review_capability_fit(
assessment=deepcopy(assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
workspace_root=workspace,
)
self.assertEqual("current", baseline["status"])
self.assertTrue(baseline["proof_scope"]["local_tag_provenance"]["valid"])
forged_commit = commit_sha[:12] + ("0" * 28)
if forged_commit == commit_sha:
forged_commit = commit_sha[:12] + ("f" * 28)
forged_catalog, forged_keyring = signed_catalog(
assessment,
selected_units=[{**selected_unit, "commit_sha": forged_commit}],
)
commit_mismatch = review_capability_fit(
assessment=deepcopy(assessment),
schema=self.schema,
catalog=forged_catalog,
published_keyring=forged_keyring,
trusted_keyring=forged_keyring,
workspace_root=workspace,
)
self.assertEqual("review_required", commit_mismatch["status"])
self.assertIn(
"tag_signed_commit_changed",
{item["kind"] for item in commit_mismatch["changes"]},
)
repo = workspace / "govoplan-core"
git_text(repo, "tag", "-d", "v0.1.0")
git_text(repo, "tag", "-a", "v0.1.0", "-m", "Re-annotated release")
reannotated = review_capability_fit(
assessment=deepcopy(assessment),
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
workspace_root=workspace,
)
self.assertEqual("review_required", reannotated["status"])
self.assertIn(
"tag_object_changed",
{item["kind"] for item in reannotated["changes"]},
)
self.assertTrue(reannotated["proof_scope"]["local_tag_provenance"]["checked"])
self.assertFalse(reannotated["proof_scope"]["local_tag_provenance"]["valid"])
def test_missing_composition_entry_does_not_claim_complete_tag_proof(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
workspace = Path(temp_dir)
core_commit, core_tag_object = create_tagged_repository(
workspace=workspace,
repository="govoplan-core",
version="0.1.0",
)
tenancy_commit, _ = create_tagged_repository(
workspace=workspace,
repository="govoplan-tenancy",
version="0.1.0",
)
reviewed_assessment = deepcopy(self.assessment)
core = next(
deepcopy(item)
for item in reviewed_assessment["composition"]
if item["module_id"] == "core"
)
tenancy = next(
deepcopy(item)
for item in reviewed_assessment["composition"]
if item["module_id"] == "tenancy"
)
core.update(manifest_version="0.1.0", commit=core_commit[:12])
tenancy.update(manifest_version="0.1.0", commit=tenancy_commit[:12])
catalog_assessment = deepcopy(reviewed_assessment)
catalog_assessment["composition"] = [core]
reviewed_assessment["composition"] = [core, tenancy]
catalog, keyring = signed_catalog(
catalog_assessment,
selected_units=[
{
"repo": "govoplan-core",
"version": "0.1.0",
"tag": "v0.1.0",
"commit_sha": core_commit,
"tag_object_sha": core_tag_object,
}
],
)
report = review_capability_fit(
assessment=reviewed_assessment,
schema=self.schema,
catalog=catalog,
published_keyring=keyring,
trusted_keyring=keyring,
workspace_root=workspace,
)
proof = report["proof_scope"]["local_tag_provenance"]
self.assertEqual("review_required", report["status"])
self.assertFalse(proof["checked"])
self.assertIsNone(proof["valid"])
self.assertEqual(2, proof["attempted_count"])
self.assertEqual(2, proof["expected_count"])
self.assertNotIn(
"tag_provenance_changed", {item["code"] for item in report["findings"]}
)
def test_repository_path_traversal_is_not_resolved(self) -> None:
with mock.patch("govoplan_assessment.capability_fit.subprocess.run") as run:
result = local_tag_provenance(
workspace_root=META_ROOT.parent,
repository="../outside",
version="0.1.0",
assessed_commit="0123456",
)
self.assertEqual("tag_provenance_unavailable", result["kind"])
self.assertIn("invalid repository identifier", result["message"])
run.assert_not_called()
def create_tagged_repository(
*, workspace: Path, repository: str, version: str
) -> tuple[str, str]:
repo = workspace / repository
git_text(workspace, "init", "-b", "main", str(repo))
git_text(repo, "config", "user.name", "Capability Fit Test")
git_text(repo, "config", "user.email", "capability-fit@example.invalid")
(repo / "README.md").write_text(f"{repository}\n", encoding="utf-8")
git_text(repo, "add", "README.md")
git_text(repo, "commit", "-m", "Initial release fixture")
git_text(repo, "tag", "-a", f"v{version}", "-m", f"Release v{version}")
return (
git_text(repo, "rev-parse", "HEAD"),
git_text(repo, "rev-parse", f"refs/tags/v{version}"),
)
def git_text(cwd: Path, *args: str) -> str:
result = subprocess.run(
("git", *args),
cwd=cwd,
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise AssertionError(result.stderr or result.stdout)
return result.stdout.strip()
def signed_catalog(
assessment: dict[str, object],
*,
versions: dict[str, str] | None = None,
sequence: int = 202607220843,
selected_units: list[dict[str, object]] | None = None,
include_selected_units: bool = True,
release_artifacts: list[dict[str, object]] | None = None,
) -> tuple[dict[str, object], dict[str, object]]:
versions = versions or {}
private_key = Ed25519PrivateKey.generate()
public_key = base64.b64encode(
private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("ascii")
keyring: dict[str, object] = {
"keyring_version": "1",
"purpose": "test",
"generated_at": "2026-01-01T00:00:00Z",
"keys": [{"key_id": "test-key", "status": "active", "public_key": public_key}],
}
now = datetime.now(tz=UTC)
modules: list[dict[str, object]] = []
core_release: dict[str, object] | None = None
for component in assessment["composition"]:
module_id = str(component["module_id"])
repository = str(component["repository"])
version = versions.get(module_id, str(component["manifest_version"]))
entry: dict[str, object] = {
"module_id": module_id,
"name": module_id,
"version": version,
"python_package": repository,
"python_ref": f"{repository} @ git+ssh://git@example.invalid/example/{repository}.git@v{version}",
}
if module_id == "core":
core_release = entry
else:
modules.append(entry)
assert core_release is not None
if selected_units is None:
selected_units = [
{
"repo": component["repository"],
"version": versions.get(
str(component["module_id"]), str(component["manifest_version"])
),
"tag": "v"
+ versions.get(
str(component["module_id"]), str(component["manifest_version"])
),
"commit_sha": (str(component["commit"]) + "0" * 40)[:40],
"tag_object_sha": hashlib.sha256(
f"{component['repository']}:{component['manifest_version']}:tag".encode()
).hexdigest()[:40],
}
for component in assessment["composition"]
]
release: dict[str, object] = {"keyring_sha256": canonical_hash(keyring)}
if include_selected_units:
release["selected_units"] = selected_units
if release_artifacts is not None:
release["artifacts"] = release_artifacts
catalog: dict[str, object] = {
"catalog_version": "1",
"channel": "stable",
"sequence": sequence,
"generated_at": now.isoformat().replace("+00:00", "Z"),
"expires_at": (now + timedelta(days=30)).isoformat().replace("+00:00", "Z"),
"core_release": core_release,
"modules": modules,
"release": release,
}
signature_payload = json.dumps(
catalog, sort_keys=True, separators=(",", ":"), ensure_ascii=False
).encode("utf-8")
catalog["signatures"] = [
{
"algorithm": "ed25519",
"key_id": "test-key",
"value": base64.b64encode(private_key.sign(signature_payload)).decode(
"ascii"
),
}
]
return catalog, keyring
def canonical_hash(payload: object) -> str:
encoded = json.dumps(
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,79 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import unittest
from govoplan_core.core.configuration_packages import (
ConfigurationPackageManifest,
ConfigurationPreflightContext,
configuration_package_claim_issues,
dry_run_configuration_package,
)
ROOT = Path(__file__).resolve().parents[1]
PACKAGE_ROOT = ROOT / "packages"
class ConfigurationPackageArtifactTests(unittest.TestCase):
def test_product_package_manifests_are_portable_and_evidence_backed(self) -> None:
paths = tuple(sorted(PACKAGE_ROOT.glob("*/*/package.json")))
self.assertGreaterEqual(len(paths), 2)
package_ids: set[str] = set()
for path in paths:
manifest = ConfigurationPackageManifest.from_mapping(
json.loads(path.read_text(encoding="utf-8"))
)
self.assertNotIn(manifest.package_id, package_ids)
package_ids.add(manifest.package_id)
self.assertEqual((), configuration_package_claim_issues(manifest))
for evidence in manifest.evidence:
evidence_path = ROOT / evidence.reference
self.assertTrue(
evidence_path.is_file(),
f"Missing evidence {evidence.reference} for {manifest.package_id}",
)
if evidence.checksum is not None:
self.assertEqual(
evidence.checksum,
"sha256:" + hashlib.sha256(evidence_path.read_bytes()).hexdigest(),
f"Stale evidence checksum for {manifest.package_id}: {evidence.reference}",
)
for requirement in (
*manifest.required_modules,
*manifest.optional_modules,
):
repository = ROOT.parent / (
"govoplan-" + requirement.module_id.replace("_", "-")
)
self.assertTrue(
repository.is_dir(),
f"Missing repository for {requirement.module_id}",
)
result = dry_run_configuration_package(
manifest,
(),
ConfigurationPreflightContext(
installed_modules={
item.module_id: item.version or "workspace"
for item in manifest.required_modules
},
capabilities=frozenset(manifest.required_capabilities),
),
)
self.assertFalse(
any(item.severity == "blocker" for item in result.diagnostics),
tuple(item.to_dict() for item in result.diagnostics),
)
self.assertIn("product.governed-communication", package_ids)
self.assertIn("product.governed-data-assurance", package_ids)
self.assertIn("product.service-to-decision", package_ids)
if __name__ == "__main__":
unittest.main()
+152
View File
@@ -0,0 +1,152 @@
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = META_ROOT / "tools" / "checks" / "check_dependency_boundaries.py"
def load_boundary_module():
spec = importlib.util.spec_from_file_location("check_dependency_boundaries", SCRIPT)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load {SCRIPT}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class DependencyBoundaryDiscoveryTests(unittest.TestCase):
def test_editable_install_metadata_is_not_treated_as_a_source_package(self) -> None:
boundary = load_boundary_module()
with tempfile.TemporaryDirectory(prefix="govoplan-boundary-discovery-") as directory:
root = Path(directory)
package = root / "govoplan-example" / "src" / "govoplan_example"
package.mkdir(parents=True)
(package / "__init__.py").write_text("", encoding="utf-8")
(package.parent / "govoplan_example.egg-info").mkdir()
repos, prefixes, errors = boundary._discover_python_repos(root)
self.assertEqual(errors, ())
self.assertEqual(repos, {"govoplan-example": package})
self.assertEqual(prefixes, {"govoplan-example": "govoplan_example"})
def test_ambiguous_or_missing_source_packages_fail_discovery(self) -> None:
boundary = load_boundary_module()
with tempfile.TemporaryDirectory(prefix="govoplan-boundary-discovery-") as directory:
root = Path(directory)
missing_source = root / "govoplan-missing" / "src"
missing_source.mkdir(parents=True)
(missing_source / "govoplan_missing.egg-info").mkdir()
ambiguous_source = root / "govoplan-ambiguous" / "src"
for name in ("govoplan_alpha", "govoplan_beta"):
package = ambiguous_source / name
package.mkdir(parents=True)
(package / "__init__.py").write_text("", encoding="utf-8")
repos, prefixes, errors = boundary._discover_python_repos(root)
self.assertEqual(repos, {})
self.assertEqual(prefixes, {})
self.assertEqual(len(errors), 2)
self.assertTrue(any("govoplan-missing" in error and "found 0" in error for error in errors))
self.assertTrue(any("govoplan-ambiguous" in error and "found 2" in error for error in errors))
def test_duplicate_python_package_ownership_fails_discovery(self) -> None:
boundary = load_boundary_module()
with tempfile.TemporaryDirectory(prefix="govoplan-boundary-discovery-") as directory:
root = Path(directory)
for owner in ("govoplan-first", "govoplan-second"):
package = root / owner / "src" / "govoplan_shared"
package.mkdir(parents=True)
(package / "__init__.py").write_text("", encoding="utf-8")
repos, prefixes, errors = boundary._discover_python_repos(root)
self.assertEqual(set(repos), {"govoplan-first"})
self.assertEqual(prefixes, {"govoplan-first": "govoplan_shared"})
self.assertEqual(len(errors), 1)
self.assertIn("already owned", errors[0])
def test_workspace_discovery_covers_changed_capability_repositories(self) -> None:
boundary = load_boundary_module()
self.assertEqual(boundary.PYTHON_DISCOVERY_ERRORS, ())
self.assertGreaterEqual(len(boundary.REPOS), 40)
for owner in (
"govoplan-core",
"govoplan-identity",
"govoplan-idm",
"govoplan-calendar",
"govoplan-poll",
"govoplan-scheduling",
"govoplan-workflow-engine",
):
self.assertIn(owner, boundary.REPOS)
def test_transitional_allowlist_entries_have_a_removal_target(self) -> None:
boundary = load_boundary_module()
for item in boundary.ALLOWLIST:
self.assertIn("remove", item.reason.casefold())
self.assertRegex(item.reason, r"#[0-9]+")
def test_webui_discovery_is_fail_closed(self) -> None:
boundary = load_boundary_module()
with tempfile.TemporaryDirectory(prefix="govoplan-boundary-webui-") as directory:
root = Path(directory)
good = root / "govoplan-good" / "webui"
good.mkdir(parents=True)
(good / "package.json").write_text(
json.dumps({"name": "@govoplan/good-webui"}),
encoding="utf-8",
)
missing = root / "govoplan-missing" / "webui"
missing.mkdir(parents=True)
invalid = root / "govoplan-invalid" / "webui"
invalid.mkdir(parents=True)
(invalid / "package.json").write_text("{", encoding="utf-8")
unnamed = root / "govoplan-unnamed" / "webui"
unnamed.mkdir(parents=True)
(unnamed / "package.json").write_text("{}", encoding="utf-8")
duplicate = root / "govoplan-zduplicate" / "webui"
duplicate.mkdir(parents=True)
(duplicate / "package.json").write_text(
json.dumps({"name": "@govoplan/good-webui"}),
encoding="utf-8",
)
repos, packages, errors = boundary._discover_webui_repos(root)
self.assertEqual(repos, {"govoplan-good": good})
self.assertEqual(packages, {"govoplan-good": "@govoplan/good-webui"})
self.assertEqual(len(errors), 4)
self.assertTrue(any("govoplan-missing" in error and "no package.json" in error for error in errors))
self.assertTrue(any("govoplan-invalid" in error and "invalid" in error for error in errors))
self.assertTrue(any("govoplan-unnamed" in error and "package name" in error for error in errors))
self.assertTrue(any("govoplan-zduplicate" in error and "already owned" in error for error in errors))
def test_workspace_webui_discovery_covers_composition_repositories(self) -> None:
boundary = load_boundary_module()
self.assertEqual(boundary.WEBUI_DISCOVERY_ERRORS, ())
self.assertGreaterEqual(len(boundary.WEBUI_REPOS), 17)
for owner in (
"govoplan-core",
"govoplan-calendar",
"govoplan-scheduling",
"govoplan-notifications",
):
self.assertIn(owner, boundary.WEBUI_REPOS)
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
from __future__ import annotations
import base64
from datetime import UTC, datetime, timedelta
import hashlib
from pathlib import Path
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
META_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(META_ROOT / "tools" / "deployment"))
from govoplan_deploy.bundle import bundle_paths # noqa: E402
from govoplan_deploy.cli import main # noqa: E402
from govoplan_deploy.distribution import ( # noqa: E402
canonical_json,
canonical_signed_payload,
)
from govoplan_deploy.model import load_spec # noqa: E402
from govoplan_deploy.planning import static_checks # noqa: E402
class DeploymentReleaseAdoptionTests(unittest.TestCase):
def test_adopts_verified_manifest_and_makes_release_checks_pass(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-release-adopt-") as value:
root = Path(value)
self.assertEqual(
0,
main(
[
"init",
"--directory",
str(root),
"--non-interactive",
"--module-set",
"core",
]
),
)
manifest, keyring = self._signed_distribution()
manifest_path = root / "source-manifest.json"
keyring_path = root / "source-keyring.json"
encoded_manifest = canonical_json(manifest)
manifest_path.write_bytes(encoded_manifest)
keyring_path.write_bytes(canonical_json(keyring))
result = main(
[
"verify-release",
"--directory",
str(root),
"--manifest",
str(manifest_path),
"--manifest-sha256",
hashlib.sha256(encoded_manifest).hexdigest(),
"--trusted-keyring",
str(keyring_path),
"--adopt",
]
)
self.assertEqual(0, result)
paths = bundle_paths(root)
spec = load_spec(paths.spec)
self.assertEqual("1.2.3", spec.release.version)
self.assertEqual("release-1", spec.release.manifest_signature_key_id)
self.assertTrue(spec.release.api_image.endswith("a" * 64))
release_checks = {
item.id: item for item in static_checks(spec, paths)
if item.id.startswith("release.") or item.id == "modules.image_composition"
}
self.assertEqual("ok", release_checks["release.manifest"].level)
self.assertEqual(
"ok", release_checks["release.signature_verification"].level
)
self.assertEqual("ok", release_checks["modules.image_composition"].level)
def test_rejects_manifest_whose_independent_digest_does_not_match(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-release-adopt-") as value:
root = Path(value)
main(
[
"init",
"--directory",
str(root),
"--non-interactive",
"--module-set",
"core",
]
)
manifest, keyring = self._signed_distribution()
manifest_path = root / "source-manifest.json"
keyring_path = root / "source-keyring.json"
manifest_path.write_bytes(canonical_json(manifest))
keyring_path.write_bytes(canonical_json(keyring))
self.assertEqual(
1,
main(
[
"verify-release",
"--directory",
str(root),
"--manifest",
str(manifest_path),
"--manifest-sha256",
"0" * 64,
"--trusted-keyring",
str(keyring_path),
]
),
)
@staticmethod
def _signed_distribution() -> tuple[dict[str, object], dict[str, object]]:
now = datetime.now(UTC)
private = Ed25519PrivateKey.generate()
public = private.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
).decode("ascii")
artifact = {
"url": "https://downloads.example.test/artifact.json",
"sha256": "f" * 64,
}
payload: dict[str, object] = {
"schema_version": "1",
"channel": "stable",
"sequence": 1,
"version": "1.2.3",
"issued_at": (now - timedelta(minutes=1)).isoformat(),
"expires_at": (now + timedelta(days=30)).isoformat(),
"revoked": False,
"deployer": {
"url": "https://downloads.example.test/govoplan-deploy.pyz",
"sha256": "e" * 64,
},
"images": {
"api": {
"index": "registry.example/govoplan/api@sha256:" + "a" * 64,
"platforms": {
"linux/amd64": "registry.example/govoplan/api@sha256:" + "1" * 64,
"linux/arm64": "registry.example/govoplan/api@sha256:" + "2" * 64,
},
"sbom": dict(artifact),
"provenance": dict(artifact),
},
"web": {
"index": "registry.example/govoplan/web@sha256:" + "b" * 64,
"platforms": {
"linux/amd64": "registry.example/govoplan/web@sha256:" + "3" * 64,
"linux/arm64": "registry.example/govoplan/web@sha256:" + "4" * 64,
},
"sbom": dict(artifact),
"provenance": dict(artifact),
},
},
"dependencies": {
"postgres": "docker.io/library/postgres@sha256:" + "5" * 64,
"redis": "docker.io/library/redis@sha256:" + "6" * 64,
"load_balancer": "docker.io/library/haproxy@sha256:" + "7" * 64,
},
"composition": {
"sha256": "c" * 64,
"module_ids": [],
"packages": [
{
"name": "govoplan-core",
"version": "1.2.3",
"wheel_sha256": "8" * 64,
}
],
},
}
payload["signatures"] = [
{
"key_id": "release-1",
"algorithm": "ed25519",
"value": base64.b64encode(
private.sign(canonical_signed_payload(payload))
).decode("ascii"),
}
]
keyring = {
"schema_version": "1",
"purpose": "govoplan-runtime-distribution",
"keys": [
{
"key_id": "release-1",
"algorithm": "ed25519",
"status": "active",
"public_key_pem": public,
"not_before": (now - timedelta(days=1)).isoformat(),
"expires_at": (now + timedelta(days=365)).isoformat(),
}
],
}
return payload, keyring
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
import importlib.util
import pathlib
import sys
import tempfile
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "tools" / "gitea" / "gitea-sync-wiki.py"
TOOLS = SCRIPT.parent
if str(TOOLS) not in sys.path:
sys.path.insert(0, str(TOOLS))
SPEC = importlib.util.spec_from_file_location("gitea_sync_wiki", SCRIPT)
assert SPEC and SPEC.loader
wiki_sync = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = wiki_sync
SPEC.loader.exec_module(wiki_sync)
class WikiSourceDiscoveryTests(unittest.TestCase):
def test_generated_and_incidental_govoplan_files_are_not_docs(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
paths = {
"readme": root / "README.md",
"architecture": root / "docs" / "DATASOURCE_ARCHITECTURE.md",
"plan": root / "workflow-plan.md",
"audit": root / "audit-reports" / "full" / "manifest.json",
"runtime": root / "runtime" / "release" / "manifest.json",
"incidental": root / "tools" / "semgrep" / "govoplan.yml",
"manifest": root / "manifest.json",
}
for path in paths.values():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("content\n", encoding="utf-8")
self.assertTrue(wiki_sync.is_repo_doc(root, paths["readme"]))
self.assertTrue(wiki_sync.is_repo_doc(root, paths["architecture"]))
self.assertTrue(wiki_sync.is_repo_doc(root, paths["plan"]))
self.assertFalse(wiki_sync.is_repo_doc(root, paths["audit"]))
self.assertFalse(wiki_sync.is_repo_doc(root, paths["runtime"]))
self.assertFalse(wiki_sync.is_repo_doc(root, paths["incidental"]))
self.assertFalse(wiki_sync.is_repo_doc(root, paths["manifest"]))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,591 @@
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import UTC, datetime, timedelta
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.institutional import (
ActorRepresentationReference,
DecisionEffectReference,
EvidenceReference,
InformationGovernanceReference,
InstitutionalReference,
LegalBasisReference,
MandateDefinition,
PartyRepresentation,
PartySubjectReference,
ProcedureParty,
ServiceBinding,
ServiceDefinition,
TemporalRevision,
service_launch_capability,
)
from govoplan_cases.backend.party_context import CasePartyContext
from govoplan_cases.backend.db.models import (
CaseAccessGrant,
CaseIdentity,
CaseRecordRevision,
CaseStatusDefinition,
CaseTimelineEntry,
CaseTypeDefinition,
)
from govoplan_cases.backend.service import (
create_case_from_intake,
get_case,
upsert_case_status,
upsert_case_type,
)
from govoplan_cases.backend.service_intake import CaseServiceIntake
from govoplan_committee.backend.db.models import (
CommitteeDecisionProjection,
CommitteeWorkspaceEvent,
CommitteeWorkspaceRevision,
)
from govoplan_committee.backend.decision_path import (
CommitteeDecisionPath,
CommitteeDecisionProposal,
)
from govoplan_committee.backend.workspace import (
CommitteeWorkspaceRecord,
SqlCommitteeWorkspace,
get_workspace_object,
record_workspace_object,
)
from govoplan_portal.backend.service_directory import PortalServiceDirectory
from govoplan_decisions.backend.db.models import FormalDecisionRevision
from govoplan_decisions.backend.service import SqlDecisionRegistry
from govoplan_mandates.backend.db.models import MandateRevision
from govoplan_mandates.backend.service import SqlMandateResolver, record_mandate
from govoplan_parties.backend.db.models import ProcedurePartyRevision
from govoplan_parties.backend.service import SqlPartyResolver, record_procedure_party
from govoplan_services.backend.db.models import ServiceDefinitionRevision
from govoplan_services.backend.service import SqlServiceDefinitionProvider, record_service_definition
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
def _reference(
kind: str,
object_id: str,
*,
owner: str,
version: str = "1",
) -> InstitutionalReference:
return InstitutionalReference(
kind=kind, # type: ignore[arg-type]
owner_module=owner,
object_id=object_id,
tenant_id="tenant-1",
version=version,
valid_at=NOW,
)
def _evidence(kind: str, evidence_id: str, owner: str) -> EvidenceReference:
return EvidenceReference(
kind=kind, # type: ignore[arg-type]
owner_module=owner,
evidence_id=evidence_id,
tenant_id="tenant-1",
version="1",
checksum=f"sha256:{evidence_id}",
captured_at=NOW,
)
@dataclass
class _Principal:
tenant_id: str = "tenant-1"
account_id: str = "account-1"
class _Registry:
def __init__(self, capabilities: dict[str, object]) -> None:
self.capabilities = capabilities
def has(self, module_id: str) -> bool:
return module_id in {
"portal",
"cases",
"committee",
"postbox",
"services",
"parties",
"mandates",
"decisions",
}
def has_capability(self, name: str) -> bool:
return name in self.capabilities
def capability(self, name: str) -> object:
return self.capabilities[name]
class InstitutionalGovernanceJourneyTests(unittest.TestCase):
def test_service_to_formal_outcome_retains_governed_context(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
for table in (
ServiceDefinitionRevision.__table__,
CaseStatusDefinition.__table__,
CaseTypeDefinition.__table__,
CaseIdentity.__table__,
CaseRecordRevision.__table__,
CaseAccessGrant.__table__,
CaseTimelineEntry.__table__,
ProcedurePartyRevision.__table__,
MandateRevision.__table__,
FormalDecisionRevision.__table__,
CommitteeWorkspaceRevision.__table__,
CommitteeWorkspaceEvent.__table__,
CommitteeDecisionProjection.__table__,
):
table.create(engine)
session = Session(engine)
principal = _Principal()
self.addCleanup(engine.dispose)
self.addCleanup(session.close)
organization = _reference(
"organization_unit",
"permit-office",
owner="organizations",
)
function = _reference("function", "permit-officer", owner="organizations")
jurisdiction = _reference(
"jurisdiction",
"city-1",
owner="organizations",
)
mandate_ref = _reference(
"mandate",
"permit-mandate",
owner="mandates",
version="4",
)
legal_basis = LegalBasisReference(
kind="law",
authority="Example legislature",
reference="permit-law:3",
version="2026-01",
effective_from=NOW - timedelta(days=100),
)
service = ServiceDefinition(
reference=_reference(
"service",
"permit-service",
owner="services",
version="5",
),
key="permit.apply",
temporal=TemporalRevision(
revision="5",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=30),
recorded_at=NOW - timedelta(days=2),
change_reason="Service published.",
),
title="Apply for a permit",
audience=("resident",),
legal_bases=(legal_basis,),
required_evidence_types=("application", "identity"),
channels=("portal", "postbox"),
responsible_organization_ref=organization,
responsible_function_ref=function,
mandate_ref=mandate_ref,
jurisdiction_refs=(jurisdiction,),
bindings=(
ServiceBinding("case", "permit-application"),
ServiceBinding("workflow", "workflow:permit-review"),
ServiceBinding("result", "decision:permit"),
),
remedy_refs=("review:administrative-court",),
publication_state="published",
)
record_service_definition(session, principal, definition=service)
service_registry = _Registry(
{
"services.definitions": SqlServiceDefinitionProvider(),
service_launch_capability("case"): object(),
}
)
entry = PortalServiceDirectory(service_registry).list_entries(
session,
principal,
tenant_id="tenant-1",
effective_at=NOW,
audiences=("resident",),
)[0]
self.assertTrue(entry.available)
intake = CaseServiceIntake().plan(
entry.definition,
case_id="case-1",
effective_at=NOW,
)
application_evidence = _evidence("document", "application-1", "files")
upsert_case_status(
session,
principal,
status_key="intake",
label="Intake",
)
upsert_case_status(
session,
principal,
status_key="decided",
label="Decided",
category="decided",
)
upsert_case_type(
session,
principal,
type_key="permit-application",
label="Permit application",
initial_status_key="intake",
allowed_status_keys=("intake", "decided"),
)
case_record = create_case_from_intake(
session,
principal,
plan=intake,
case_number="PERMIT-2026-0001",
title="Permit application",
status_key=None,
opened_at=NOW,
recorded_at=NOW,
change_reason="Portal application received.",
idempotency_key="journey-case-create",
evidence_refs=(application_evidence,),
deadline_at=NOW + timedelta(days=30),
)
applicant = _reference("party", "applicant", owner="parties")
representative = _reference("party", "representative", owner="parties")
address_evidence = _evidence(
"snapshot",
"address-snapshot-1",
"addresses",
)
representative_party = ProcedureParty(
reference=representative,
procedure_ref=case_record.reference,
role="representative",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-2",
tenant_id="tenant-1",
version="2",
),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
recorded_at=NOW - timedelta(days=1),
change_reason="Representative added to the procedure.",
),
permitted_channels=("postbox",),
preferred_channels=("postbox",),
delivery_recipient=True,
representations=(
PartyRepresentation(
representative_party_ref=representative,
represented_party_ref=applicant,
power_ref="power-1",
permitted_actions=("submit", "receive"),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
recorded_at=NOW - timedelta(days=1),
change_reason="Representation power recorded.",
),
evidence=(address_evidence,),
),
),
contact_snapshot_refs=("addresses:snapshot-1",),
evidence=(address_evidence,),
)
record_procedure_party(session, principal, party=representative_party)
party_context = CasePartyContext(
_Registry({"parties.resolver": SqlPartyResolver()})
)
parties = party_context.resolve(
session,
principal,
case_ref=case_record.reference,
effective_at=NOW,
)
delivery_target = party_context.delivery_targets(
parties,
channel="postbox",
)[0]
mandate = MandateDefinition(
reference=mandate_ref,
temporal=TemporalRevision(
revision="4",
valid_from=NOW - timedelta(days=30),
valid_to=NOW + timedelta(days=30),
recorded_at=NOW - timedelta(days=31),
change_reason="Permit authority delegated.",
),
task_types=("committee.formal_decision",),
authority_types=("permit",),
organization_unit_refs=(organization,),
function_refs=(function,),
jurisdiction_refs=(jurisdiction,),
legal_bases=(legal_basis,),
evidence=(_evidence("record", "mandate-record-4", "mandates"),),
authority_ceiling="permit:standard",
)
record_mandate(session, principal, definition=mandate)
workspace = SqlCommitteeWorkspace()
body = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="body",
object_id="permit-board",
revision=1,
state="active",
title="Permit board",
recorded_at=NOW,
change_reason="Permit board configured.",
attributes={
"organization_unit_ref": organization.to_dict(),
"function_refs": [function.to_dict()],
"quorum": {"minimum_count": 1},
},
)
meeting = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="meeting",
object_id="meeting-1",
revision=1,
state="open",
title="Permit board meeting",
parent_id=body.object_id,
recorded_at=NOW,
change_reason="Meeting opened.",
attributes={
"starts_at": NOW.isoformat(),
"ends_at": (NOW + timedelta(hours=1)).isoformat(),
},
context=case_record.context,
)
agenda = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="agenda_item",
object_id="item-1",
revision=1,
state="deliberating",
title="Permit application",
parent_id=meeting.object_id,
recorded_at=NOW,
change_reason="Agenda item entered deliberation.",
attributes={
"position": 1,
"subject_refs": [case_record.reference.to_dict()],
},
context=case_record.context,
evidence=(application_evidence,),
)
approval_ref = _reference("approval", "approval-1", owner="approvals")
vote = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="vote",
object_id="vote-1",
revision=1,
state="closed",
title="Vote on permit application",
parent_id=agenda.object_id,
recorded_at=NOW,
change_reason="Vote result accepted.",
attributes={
"method": "recorded",
"choices": ["yes", "no"],
"eligible_count": 3,
"cast_count": 3,
"counts": {"yes": 3, "no": 0},
"quorum_met": True,
"approval_ref": approval_ref.to_dict(),
},
context=case_record.context,
evidence=(_evidence("record", "vote-result-1", "committee"),),
)
for record, key in (
(body, "journey-body"),
(meeting, "journey-meeting"),
(agenda, "journey-agenda"),
(vote, "journey-vote"),
):
record_workspace_object(
session,
principal,
record=record,
idempotency_key=key,
)
decision_registry = SqlDecisionRegistry()
decision_result = CommitteeDecisionPath(
_Registry(
{
"mandates.resolver": SqlMandateResolver(),
"decisions.registry": decision_registry,
"committee.workspace": workspace,
}
)
).decide(
session,
principal,
proposal=CommitteeDecisionProposal(
tenant_id="tenant-1",
decision_id="decision-1",
revision="1",
effective_at=NOW,
meeting_ref="meeting-1",
agenda_item_ref="item-1",
decision_type="permit",
subject_refs=(case_record.reference,),
organization_unit_ref=organization,
function_ref=function,
actor=ActorRepresentationReference(
tenant_id="tenant-1",
account_id="account-1",
identity_id="identity-1",
represented_function_ref=function,
mandate_ref=mandate_ref,
),
approval_refs=(
approval_ref,
),
fact_evidence=(application_evidence, address_evidence),
legal_bases=(legal_basis,),
operative_result="Permit granted.",
reasoning="The application satisfies the effective rule.",
case_ref=case_record.reference,
jurisdiction_refs=(jurisdiction,),
party_refs=(applicant, representative),
record_refs=(
_reference("record", "record-1", owner="audit"),
),
remedy_refs=service.remedy_refs,
review_refs=("review:administrative-court",),
information_governance=InformationGovernanceReference(
classification="restricted",
purposes=("permit-decision", "party-delivery"),
legal_basis_refs=("permit-law:3@2026-01",),
retention_policy_ref="records:permit",
disclosure_state="partly_disclosable",
),
),
observed_effects=(
DecisionEffectReference(
effect_key="postbox.deliver_decision",
state="confirmed",
resource_refs=(
f"postbox:{delivery_target.party_ref.object_id}",
),
audit_event_refs=("audit:delivery-1",),
evidence_refs=(address_evidence.evidence_id,),
),
),
)
decided_agenda = replace(
agenda,
revision=2,
state="decided",
recorded_at=NOW + timedelta(minutes=1),
change_reason="Formal Decision recorded.",
attributes={
**dict(agenda.attributes),
"decision_ref": decision_result.decision.reference.to_dict(),
},
)
record_workspace_object(
session,
principal,
record=decided_agenda,
expected_revision=1,
idempotency_key="journey-agenda-decided",
)
closed_meeting = replace(
meeting,
revision=2,
state="closed",
recorded_at=NOW + timedelta(hours=1),
change_reason="All agenda items completed.",
)
record_workspace_object(
session,
principal,
record=closed_meeting,
expected_revision=1,
idempotency_key="journey-meeting-closed",
)
minute = CommitteeWorkspaceRecord(
tenant_id="tenant-1",
object_kind="minute",
object_id="minute-1",
revision=1,
state="accepted",
title="Accepted permit board minutes",
parent_id=meeting.object_id,
recorded_at=NOW + timedelta(hours=2),
change_reason="Minutes approved.",
attributes={
"content_ref": _reference(
"record",
"meeting-minutes-1",
owner="records",
).to_dict(),
"approval_ref": _reference(
"approval",
"minutes-approval-1",
owner="approvals",
).to_dict(),
},
context=case_record.context,
evidence=(_evidence("record", "minutes-proof-1", "records"),),
)
record_workspace_object(
session,
principal,
record=minute,
idempotency_key="journey-minute",
)
reconstruction = decision_result.reconstruction_payload()["decision"]
persisted = decision_registry.get_decision(
session,
principal,
reference=decision_result.decision.reference,
)
self.assertTrue(decision_result.persisted_by_decision_registry)
self.assertEqual(decision_result.decision, persisted)
self.assertEqual(
case_record,
get_case(session, principal, case_id="case-1"),
)
self.assertEqual(
"decided",
get_workspace_object(
session,
principal,
object_kind="agenda_item",
object_id="item-1",
).state,
)
self.assertEqual(service.reference, intake.context.service_ref)
self.assertEqual("applicant", delivery_target.represented_party_refs[0].object_id)
self.assertEqual("permit-mandate", reconstruction["authority_context"]["mandate_ref"]["object_id"])
self.assertEqual("city-1", reconstruction["authority_context"]["jurisdiction_refs"][0]["object_id"])
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
self.assertEqual("The application satisfies the effective rule.", reconstruction["reasoning"])
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
if __name__ == "__main__":
unittest.main()
+271
View File
@@ -0,0 +1,271 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.institutional import (
CAPABILITY_FORM_DEFINITIONS,
CAPABILITY_SERVICE_DEFINITIONS,
FormDefinition,
FormFieldDefinition,
InstitutionalReference,
ServiceBinding,
ServiceDefinition,
TemporalRevision,
service_launch_capability,
)
from govoplan_cases.backend.service_intake import (
CAPABILITY_CASES_SERVICE_INTAKE,
CaseServiceIntake,
)
from govoplan_forms.backend.db.models import FormDefinitionRevision
from govoplan_forms.backend.service import (
SqlFormDefinitionProvider,
record_form_definition,
)
from govoplan_forms_runtime.backend.db.models import (
FormInstanceEvent,
FormInstanceIdentity,
FormInstanceRevision,
)
from govoplan_forms_runtime.backend.service import (
FormRuntimeService,
FormsServiceLauncher,
)
from govoplan_portal.backend.service_directory import PortalServiceDirectory
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
def _service() -> ServiceDefinition:
return ServiceDefinition(
reference=InstitutionalReference(
kind="service",
owner_module="portal",
object_id="permit",
tenant_id="tenant-1",
version="5",
),
key="permit.apply",
temporal=TemporalRevision(
revision="5",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
title="Apply for a permit",
audience=("resident",),
required_evidence_types=("application",),
bindings=(
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
ServiceBinding("case", "permit-application"),
ServiceBinding("workflow", "workflow:permit-review"),
),
publication_state="published",
)
class _Provider:
def __init__(self, definition: ServiceDefinition) -> None:
self.definition = definition
def get_service_definition(self, session, principal, *, reference, effective_at=None):
return self.definition
def list_service_definitions(self, session, principal, *, tenant_id, query="", limit=100):
return (self.definition,)
class _Registry:
def __init__(self, definition: ServiceDefinition) -> None:
self.capabilities = {
CAPABILITY_SERVICE_DEFINITIONS: _Provider(definition),
CAPABILITY_CASES_SERVICE_INTAKE: CaseServiceIntake(),
service_launch_capability("case"): object(),
}
def has(self, module_id: str) -> bool:
return module_id in {"portal", "cases"}
def has_capability(self, name: str) -> bool:
return name in self.capabilities
def capability(self, name: str) -> object:
return self.capabilities[name]
def require_capability(self, name: str) -> object:
return self.capabilities[name]
@dataclass
class _Principal:
tenant_id: str = "tenant-1"
account_id: str = "account-1"
class _FormRegistry(_Registry):
def __init__(self, definition: ServiceDefinition) -> None:
super().__init__(definition)
self.capabilities[CAPABILITY_FORM_DEFINITIONS] = (
SqlFormDefinitionProvider()
)
self.capabilities[service_launch_capability("form")] = (
FormsServiceLauncher(self)
)
def has(self, module_id: str) -> bool:
return module_id in {"portal", "forms", "forms_runtime"}
class InstitutionalServiceJourneyTests(unittest.TestCase):
def test_one_service_version_drives_portal_and_case_intake(self) -> None:
definition = _service()
registry = _Registry(definition)
entries = PortalServiceDirectory(registry).list_entries(
None,
None,
tenant_id="tenant-1",
effective_at=NOW,
audiences=("resident",),
)
plan = registry.capability(CAPABILITY_CASES_SERVICE_INTAKE).plan(
entries[0].definition,
case_id="case-1",
effective_at=NOW,
)
self.assertTrue(entries[0].available)
self.assertIs(definition, entries[0].definition)
self.assertEqual(definition.reference, plan.service_ref)
self.assertEqual("5", plan.context.service_ref.version)
self.assertEqual("workflow:permit-review", plan.workflow_refs[0])
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
for table in (
FormDefinitionRevision.__table__,
FormInstanceIdentity.__table__,
FormInstanceRevision.__table__,
FormInstanceEvent.__table__,
):
table.create(engine)
session = Session(engine)
principal = _Principal()
try:
form = record_form_definition(
session,
principal,
definition=FormDefinition(
reference=InstitutionalReference(
kind="form",
owner_module="forms",
object_id="permit-application",
tenant_id="tenant-1",
version="3",
),
key="permit-application",
temporal=TemporalRevision(
revision="3",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
change_reason="Publish the permit application.",
),
title="Permit application",
fields=(
FormFieldDefinition(
key="applicant_name",
label="Applicant name",
required=True,
constraints={"min_length": 2},
),
),
publication_state="published",
allow_drafts=True,
handoff_kinds=("case",),
),
)
binding = ServiceBinding(
"form",
f"{form.reference.object_id}/{form.reference.version}",
)
service = ServiceDefinition(
reference=InstitutionalReference(
kind="service",
owner_module="services",
object_id="permit",
tenant_id="tenant-1",
version="6",
),
key="permit.apply",
temporal=TemporalRevision(
revision="6",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
title="Apply for a permit",
audience=("public",),
bindings=(binding,),
publication_state="published",
)
registry = _FormRegistry(service)
directory = PortalServiceDirectory(registry)
launched = directory.launch_service(
session,
principal,
reference=service.reference,
requested_at=NOW,
idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"},
)
replay = directory.launch_service(
session,
principal,
reference=service.reference,
requested_at=NOW,
idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"},
)
instance = FormRuntimeService(registry).get_instance(
session,
principal,
instance_id=str(launched.metadata["form_instance_id"]),
)
self.assertEqual("form_submission", launched.target_ref.kind)
self.assertEqual(service.reference, launched.service_ref)
self.assertEqual("3", launched.metadata["form_definition_revision"])
self.assertTrue(replay.replayed)
self.assertEqual(launched.target_ref, replay.target_ref)
self.assertEqual(
(
form.reference.owner_module,
form.reference.object_id,
form.reference.tenant_id,
form.reference.version,
),
(
instance.definition_ref.owner_module,
instance.definition_ref.object_id,
instance.definition_ref.tenant_id,
instance.definition_ref.version,
),
)
self.assertEqual(NOW, instance.definition_ref.valid_at)
self.assertEqual(service.reference, instance.service_ref)
self.assertEqual("Ada Lovelace", instance.values["applicant_name"])
finally:
session.close()
engine.dispose()
if __name__ == "__main__":
unittest.main()
+158
View File
@@ -0,0 +1,158 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import json
import subprocess
import sys
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
def _load_module():
path = ROOT / "tools/checks/managed-ingress-drill.py"
spec = importlib.util.spec_from_file_location("managed_ingress_drill", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
INGRESS = _load_module()
class ManagedIngressDrillTests(unittest.TestCase):
def test_config_is_streamed_into_a_daemon_visible_volume(self) -> None:
completed = subprocess.CompletedProcess([], 0, "", "")
with patch.object(INGRESS, "_run", return_value=completed) as run:
INGRESS._write_volume_file(
image="registry.example/caddy@sha256:" + "1" * 64,
volume="config-volume",
filename="Caddyfile",
content=":8080 { respond /health 200 }\n",
)
argv = run.call_args.args[0]
self.assertIn("type=volume,src=config-volume,dst=/govoplan-config", argv)
self.assertIn("0:0", argv)
self.assertNotIn("type=bind", " ".join(argv))
self.assertEqual(
":8080 { respond /health 200 }\n",
run.call_args.kwargs["input_text"],
)
def test_config_filename_cannot_escape_the_volume(self) -> None:
with self.assertRaisesRegex(ValueError, "invalid config filename"):
INGRESS._write_volume_file(
image="registry.example/caddy@sha256:" + "1" * 64,
volume="config-volume",
filename="../Caddyfile",
content="",
)
def test_drill_has_no_runner_local_bind_mounts(self) -> None:
source = (ROOT / "tools/checks/managed-ingress-drill.py").read_text(
encoding="utf-8"
)
self.assertNotIn("type=bind", source)
self.assertIn('"--network-alias",\n "load-balancer"', source)
self.assertNotIn('"127.0.0.1::8080"', source)
self.assertIn("requested_http_port", source)
self.assertIn("requested_https_port", source)
self.assertIn('"--cap-add",\n "NET_BIND_SERVICE"', source)
def test_published_port_reads_the_docker_mapping(self) -> None:
completed = subprocess.CompletedProcess(
[],
0,
json.dumps(
{
"8443/tcp": [
{"HostIp": "127.0.0.1", "HostPort": "49152"}
]
}
),
"",
)
with patch.object(INGRESS, "_run", return_value=completed) as run:
port = INGRESS._published_port("ingress", 8443)
self.assertEqual(49152, port)
self.assertEqual(
[
"docker",
"inspect",
"--format",
"{{json .HostConfig.PortBindings}}",
"ingress",
],
run.call_args.args[0],
)
def test_published_port_rejects_non_loopback_binding(self) -> None:
completed = subprocess.CompletedProcess(
[],
0,
'{"8443/tcp":[{"HostIp":"0.0.0.0","HostPort":"49152"}]}',
"",
)
with patch.object(INGRESS, "_run", return_value=completed):
with self.assertRaisesRegex(RuntimeError, "loopback binding"):
INGRESS._published_port("ingress", 8443)
def test_probe_runs_as_a_network_sibling_from_a_digest_image(self) -> None:
completed = subprocess.CompletedProcess([], 0, "", "")
image = "registry.example/runtime-api@sha256:" + "1" * 64
with patch.object(INGRESS, "_run", return_value=completed) as run:
INGRESS._probe_ingress(
image=image,
network="deployment-network",
container="ingress",
)
argv = run.call_args.args[0]
self.assertEqual("docker", argv[0])
self.assertIn("deployment-network", argv)
self.assertIn(image, argv)
self.assertIn('(\"ingress\", port)', argv[-1])
self.assertIn("server_hostname=\"localhost\"", argv[-1])
self.assertNotIn("localhost:49152", argv[-1])
def test_probe_diagnostics_include_container_stderr(self) -> None:
probe_failure = subprocess.CalledProcessError(1, ["docker", "run"])
state = subprocess.CompletedProcess([], 0, '{"Running":false}', "")
logs = subprocess.CompletedProcess([], 0, "", "caddy startup failed")
with patch.object(
INGRESS,
"_run",
side_effect=[probe_failure, state, logs],
), patch.object(INGRESS.sys, "stderr") as stderr:
with self.assertRaises(subprocess.CalledProcessError):
INGRESS._probe_ingress(
image="registry.example/runtime-api@sha256:" + "1" * 64,
network="deployment-network",
container="ingress",
)
rendered = "".join(call.args[0] for call in stderr.write.call_args_list)
self.assertIn('"Running":false', rendered)
self.assertIn("caddy startup failed", rendered)
def test_standalone_workflow_is_dispatch_only_and_digest_bounded(self) -> None:
workflow = (
ROOT / ".gitea/workflows/runtime-ingress-drill.yml"
).read_text(encoding="utf-8")
self.assertIn("workflow_dispatch:", workflow)
self.assertNotIn("\n push:", workflow)
self.assertIn("--probe-image \"$PROBE_IMAGE\"", workflow)
self.assertIn("GOVOPLAN_REGISTRY_TOKEN", workflow)
if __name__ == "__main__":
unittest.main()
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
import json
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
class ModulePackageWorkflowTests(unittest.TestCase):
def test_template_enforces_tag_version_hash_and_registry_contract(self) -> None:
workflow = (
META_ROOT / "tools/repo/templates/module-package-release.yml"
).read_text(encoding="utf-8")
self.assertIn("GITEA_REPOSITORY: ${{ gitea.repository }}", workflow)
self.assertNotIn("tag_protections", workflow)
self.assertNotIn("secrets.GITEA_TOKEN", workflow)
self.assertIn("git merge-base --is-ancestor", workflow)
self.assertIn("does not match", workflow)
self.assertIn("package-artifacts.json", workflow)
self.assertIn("api/packages/GovOPlaN/pypi", workflow)
self.assertIn("api/packages/GovOPlaN/npm", workflow)
self.assertIn('npm publish "./${webui_packages[0]}"', workflow)
self.assertIn("Check immutable registry state", workflow)
self.assertIn('files[0].get("sha256") != expected_sha256', workflow)
self.assertIn('if [[ "$PUBLISH_PYPI" == 1 ]]', workflow)
self.assertIn('[[ "$PUBLISH_NPM" == 1 ]]', workflow)
self.assertIn("GOVOPLAN_PACKAGE_TOKEN", workflow)
self.assertIn("must resolve to an exact registry version", workflow)
self.assertIn("git\\\\.add-ideas\\\\.de/(?:GovOPlaN|add-ideas)", workflow)
self.assertIn("release package identity does not match", workflow)
self.assertNotIn("Generic", workflow)
@unittest.skipUnless(shutil.which("node"), "Node.js is required")
def test_webui_publication_normalizes_internal_git_dependencies(self) -> None:
workflow = (
META_ROOT / "tools/repo/templates/module-package-release.yml"
).read_text(encoding="utf-8")
marker = " node <<'NODE'\n"
script = workflow.split(marker, 1)[1].split("\n NODE", 1)[0]
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
package_dir = root / ".package-webui"
package_dir.mkdir()
package_path = package_dir / "package.json"
package_path.write_text(
json.dumps(
{
"name": "@govoplan/core-webui",
"version": "0.1.14",
"private": True,
"dependencies": {
"@govoplan/access-webui": (
"git+ssh://git@git.add-ideas.de/GovOPlaN/"
"govoplan-access.git#v0.1.11"
),
"@govoplan/admin-webui": (
"git+ssh://git@git.add-ideas.de/add-ideas/"
"govoplan-admin.git#v0.1.8"
)
},
}
),
encoding="utf-8",
)
subprocess.run(
["node"],
input=script,
cwd=root,
check=True,
text=True,
capture_output=True,
)
package = json.loads(package_path.read_text(encoding="utf-8"))
self.assertNotIn("private", package)
self.assertEqual(
"0.1.11", package["dependencies"]["@govoplan/access-webui"]
)
self.assertEqual(
"0.1.8", package["dependencies"]["@govoplan/admin-webui"]
)
def test_sync_script_only_targets_packageable_govoplan_repositories(self) -> None:
namespace: dict[str, object] = {
"__file__": str(META_ROOT / "tools/repo/sync-module-package-workflows.py"),
"__name__": "test_sync_module_package_workflows",
}
script = (META_ROOT / "tools/repo/sync-module-package-workflows.py").read_text(
encoding="utf-8"
)
exec(compile(script, str(namespace["__file__"]), "exec"), namespace)
with tempfile.TemporaryDirectory() as temporary:
parent = Path(temporary)
package_repositories = namespace["package_repositories"]
# The production inventory is authoritative, so a temporary parent
# only exposes matching paths that are present in that inventory.
known = parent / "govoplan-core"
known.mkdir()
(known / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
self.assertEqual(package_repositories(parent), (known,))
if __name__ == "__main__":
unittest.main()
+167
View File
@@ -0,0 +1,167 @@
from __future__ import annotations
from io import BytesIO
import importlib.util
import json
from pathlib import Path
import sys
import tarfile
import tempfile
import tomllib
import unittest
import zipfile
ROOT = Path(__file__).resolve().parents[1]
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
PACKAGE_SET = _load(
"generate_release_package_set",
ROOT / "tools/release/generate-release-package-set.py",
)
ARTIFACTS = _load(
"resolve_package_artifacts",
ROOT / "tools/release/resolve-package-artifacts.py",
)
class PackageRegistryReleaseTests(unittest.TestCase):
def test_current_release_sources_form_a_hash_bound_package_set(self) -> None:
core_version = tomllib.loads(
(ROOT.parent / "govoplan-core/pyproject.toml").read_text(encoding="utf-8")
)["project"]["version"]
payload = PACKAGE_SET.generate_package_set(
core_version=core_version,
requirements=ROOT / "requirements-release.txt",
workspace=ROOT.parent,
)
self.assertEqual("1", payload["schema_version"])
self.assertEqual("govoplan-core", payload["python"][0]["name"])
self.assertIn(
"@govoplan/core-webui",
{item["name"] for item in payload["webui"]},
)
unsigned = dict(payload)
digest = unsigned.pop("package_set_sha256")
self.assertEqual(ARTIFACTS._canonical_sha256(unsigned), digest)
def test_wheel_and_webui_artifacts_are_verified_by_embedded_identity(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-package-artifacts-") as value:
root = Path(value)
wheels = root / "wheels"
webui = root / "webui"
wheels.mkdir()
webui.mkdir()
wheel = wheels / "govoplan_demo-1.2.3-py3-none-any.whl"
with zipfile.ZipFile(wheel, "w") as archive:
archive.writestr(
"govoplan_demo-1.2.3.dist-info/METADATA",
"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: 1.2.3\n",
)
package_json = json.dumps(
{"name": "@govoplan/demo-webui", "version": "1.2.3"}
).encode("utf-8")
npm = webui / "govoplan-demo-webui-1.2.3.tgz"
with tarfile.open(npm, "w:gz") as archive:
member = tarfile.TarInfo("package/package.json")
member.size = len(package_json)
archive.addfile(member, BytesIO(package_json))
source = {
"version": "1.2.3",
"repository": "govoplan-demo",
"tag": "v1.2.3",
"commit": "1" * 40,
}
python_rows = ARTIFACTS._verify_wheels(
({"name": "govoplan-demo", "extras": ["server"], **source},), wheels
)
webui_rows = ARTIFACTS._verify_webui(
({"name": "@govoplan/demo-webui", **source},), webui
)
self.assertEqual("govoplan-demo", python_rows[0]["name"])
self.assertEqual(["server"], python_rows[0]["extras"])
self.assertEqual("@govoplan/demo-webui", webui_rows[0]["name"])
self.assertTrue(str(webui_rows[0]["integrity"]).startswith("sha512-"))
def test_package_set_rejects_argument_shaped_package_names(self) -> None:
payload = {
"schema_version": "1",
"release_version": "1.2.3",
"registries": {
"python": "https://packages.example.test/pypi/simple",
"npm": "https://packages.example.test/npm/",
},
"python": [
{
"name": "--index-url",
"version": "1.2.3",
"repository": "govoplan-demo",
"extras": [],
"tag": "v1.2.3",
"commit": "1" * 40,
}
],
"webui": [
{
"name": "@govoplan/demo-webui",
"version": "1.2.3",
"repository": "govoplan-demo",
"tag": "v1.2.3",
"commit": "1" * 40,
}
],
}
payload["package_set_sha256"] = ARTIFACTS._canonical_sha256(payload)
with tempfile.TemporaryDirectory() as value:
path = Path(value) / "packages.json"
path.write_text(json.dumps(payload), encoding="utf-8")
with self.assertRaisesRegex(
ARTIFACTS.PackageArtifactError, "invalid identity"
):
ARTIFACTS._load_package_set(path)
def test_runtime_workflow_consumes_registry_artifacts_and_publishes_lock(self) -> None:
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
encoding="utf-8"
)
self.assertIn("resolve-package-artifacts.py", workflow)
self.assertIn("package-artifacts.lock.json", workflow)
self.assertIn(
"--package-lock runtime-output/package-artifacts.lock.json",
workflow,
)
self.assertNotIn(
"pip wheel --no-deps --wheel-dir runtime-output/local-wheels",
workflow,
)
def test_developer_meta_package_matches_workspace_versions(self) -> None:
script = _load(
"generate_developer_meta_package",
ROOT / "tools/release/generate-developer-meta-package.py",
)
expected = script.render(
workspace=ROOT.parent,
requirements=ROOT / "requirements-release.txt",
)
actual = (ROOT / "packages/govoplan-meta/pyproject.toml").read_text(
encoding="utf-8"
)
self.assertEqual(expected, actual)
if __name__ == "__main__":
unittest.main()
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import sys
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
TOOLS_ROOT = META_ROOT / "tools" / "gitea"
if str(TOOLS_ROOT) not in sys.path:
sys.path.insert(0, str(TOOLS_ROOT))
SCRIPT = TOOLS_ROOT / "gitea-dispatch-package-set.py"
SPEC = importlib.util.spec_from_file_location("gitea_dispatch_package_set", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class PackageSetDispatchTests(unittest.TestCase):
def test_meta_package_resolves_to_exact_tagged_repository_targets(self) -> None:
targets = MODULE.package_targets()
self.assertEqual(66, len(targets))
self.assertEqual(66, len({target.distribution for target in targets}))
by_name = {target.distribution: target for target in targets}
self.assertEqual("v0.1.14", by_name["govoplan-core"].tag)
self.assertEqual("v0.1.8", by_name["govoplan-access"].tag)
self.assertTrue(by_name["govoplan-core"].tag_exists)
self.assertTrue(by_name["govoplan-access"].has_webui)
self.assertEqual(
"@govoplan/access-webui",
by_name["govoplan-access"].webui_package,
)
if __name__ == "__main__":
unittest.main()
+312
View File
@@ -0,0 +1,312 @@
from __future__ import annotations
import ast
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
SCRIPT = (
Path(__file__).resolve().parents[1]
/ "tools"
/ "inventory"
/ "platform-interface-inventory.py"
)
SPEC = importlib.util.spec_from_file_location("platform_interface_inventory", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
inventory = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(inventory)
class PlatformInterfaceInventoryTests(unittest.TestCase):
def test_workspace_resolution_prefers_populated_checkout_siblings(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
sibling = root / "checkout"
meta = sibling / "govoplan"
configured = root / "configured"
(sibling / "govoplan-core" / "src").mkdir(parents=True)
(configured / "govoplan-core").mkdir(parents=True)
previous = inventory.META_ROOT
inventory.META_ROOT = meta
try:
resolved = inventory._resolve_workspace_root(
{
"default_parent": str(configured),
"repositories": [{"path": "govoplan-core"}],
}
)
finally:
inventory.META_ROOT = previous
self.assertEqual(sibling.resolve(), resolved)
def test_canonical_api_path_normalizes_versions_and_parameters(self) -> None:
self.assertEqual(
inventory.canonical_api_path(
"http://localhost/api/v1/campaigns/${campaignId}?limit=10"
),
"/campaigns/{}",
)
self.assertEqual(
inventory.canonical_api_path("/api/v2/campaigns/{campaign_id}"),
"/campaigns/{}",
)
self.assertEqual(
inventory.canonical_api_path("/api/v1/calendar/events/delta${querySuffix}"),
"/calendar/events/delta",
)
self.assertEqual(
inventory.canonical_api_path("/api/v1/calendar/events/${eventId}"),
"/calendar/events/{}",
)
def test_endpoint_declarations_are_exact_and_require_missing_ui_issue(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "endpoints.json"
path.write_text(
json.dumps(
{
"schema_version": 1,
"endpoints": [
{
"repository": "govoplan-example",
"method": "GET",
"path": "/example/items/{}",
"category": "public_integration",
"rationale": "Published integration API.",
}
],
}
),
encoding="utf-8",
)
declarations = inventory._load_endpoint_declarations(path)
self.assertIn(
("govoplan-example", "GET", "/example/items/{}"),
declarations,
)
payload = json.loads(path.read_text(encoding="utf-8"))
payload["endpoints"][0]["category"] = "missing_ui"
path.write_text(json.dumps(payload), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "tracking_issue"):
inventory._load_endpoint_declarations(path)
def test_inventory_reports_unclassified_and_stale_endpoint_declarations(
self,
) -> None:
webui = {
"frontendApiReferences": [],
"translationUsages": [],
"translationCatalog": {"en": {}, "de": {}},
"fields": [],
"labels": [],
"visibleText": [],
"routes": [],
"navigation": [],
"uiCapabilities": [],
"dynamicTranslationUsages": [],
}
endpoint = {
"repository": "govoplan-example",
"method": "GET",
"path": "/api/v1/example/items",
"file": "src/example.py",
"line": 1,
"handler": "items",
"router": "router",
}
stale = {
"repository": "govoplan-example",
"method": "GET",
"path": "/example/removed",
"category": "removable",
"rationale": "Removal is pending.",
}
result = inventory._assemble_inventory(
webui=webui,
backend_endpoints=[endpoint],
manifests=[],
endpoint_declarations={
("govoplan-example", "GET", "/example/removed"): stale,
},
)
self.assertEqual(1, result["summary"]["unclassified_backend_endpoints"])
self.assertEqual(1, result["summary"]["stale_endpoint_declarations"])
self.assertIsNone(result["api"]["backend_endpoints"][0]["surface"])
def test_endpoint_only_strict_mode_does_not_fail_on_translation_debt(
self,
) -> None:
result = {
"translation_health": {"missing_catalog_entries": ["missing.key"]},
"api": {
"unclassified_endpoints": [],
"stale_endpoint_declarations": [],
},
}
self.assertEqual(
[],
inventory._strict_failures(
result,
check_translations=False,
check_endpoints=True,
),
)
self.assertEqual(
["used translation keys are missing from generated catalogs"],
inventory._strict_failures(
result,
check_translations=True,
check_endpoints=True,
),
)
def test_fastapi_route_scanner_includes_router_prefix(self) -> None:
tree = ast.parse(
"""
from fastapi import APIRouter
router = APIRouter(prefix="/api/v1/items")
@router.get("/{item_id}")
def read_item(item_id: str):
return item_id
"""
)
prefixes = inventory._router_prefixes(tree)
function = next(
node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)
)
route = inventory._endpoint_from_decorator(
function.decorator_list[0],
prefixes=prefixes,
)
self.assertEqual(
route,
{
"method": "GET",
"path": "/api/v1/items/{item_id}",
"router": "router",
},
)
def test_source_declarations_normalize_stable_control_and_contribution_ids(
self,
) -> None:
webui = {
"fields": [
{
"repository": "govoplan-example",
"file": "webui/src/Example.tsx",
"line": 12,
"column": 3,
"id": "govoplan-example.field.example.name.abc123",
"idSource": "source_anchor",
"explicitId": None,
"context": "Example",
"helpId": "govoplan-example.field.example.name.abc123.help",
"helpDynamic": False,
}
],
"actions": [],
"contributions": [
{
"repository": "govoplan-example",
"file": "webui/src/module.ts",
"line": 20,
"column": 5,
"kind": "frontend_route",
"id": "/examples/:exampleId",
"path": "/examples/:exampleId",
}
],
"translationCatalog": {"en": {}, "de": {}},
}
manifests = [{"repository": "govoplan-example", "id": "examples"}]
declarations = inventory._source_interface_declarations(webui, manifests)
keys = {item["key"] for item in declarations}
self.assertIn("field:examples.field.example.name.abc123", keys)
self.assertIn(
"help:examples.field.example.name.abc123.help",
keys,
)
self.assertIn(
"frontend_route:examples.route.examples.exampleid",
keys,
)
def test_declaration_health_rejects_duplicate_and_undeclared_source_ids(
self,
) -> None:
declaration = {
"key": "frontend_route:example.route.unlisted",
"id": "example.route.unlisted",
"module_id": "example",
"kind": "frontend_route",
"origin": "webui_contribution",
}
manifests = [
{
"id": "example",
"repository": "govoplan-example",
"interface_catalog": {"declarations": []},
}
]
health = inventory._declaration_health(
[declaration, dict(declaration)],
manifests,
)
self.assertEqual(1, len(health["duplicate_ids"]))
self.assertEqual(1, len(health["undeclared_source_surfaces"]))
def test_runtime_snapshot_comparison_accepts_an_installed_subset(self) -> None:
manifests = [
{
"id": "one",
"interface_catalog": {
"contract_version": "1",
"module_id": "one",
"module_version": "1.0.0",
"digest": "sha256:one",
},
},
{
"id": "two",
"interface_catalog": {
"contract_version": "1",
"module_id": "two",
"module_version": "1.0.0",
"digest": "sha256:two",
},
},
]
snapshot = {
"contract_version": "1",
"modules": [dict(manifests[1]["interface_catalog"])],
}
comparison = inventory._compare_runtime_snapshot(snapshot, manifests)
self.assertEqual(["two"], comparison["matched_modules"])
self.assertEqual([], comparison["mismatches"])
snapshot["modules"][0]["digest"] = "sha256:changed"
comparison = inventory._compare_runtime_snapshot(snapshot, manifests)
self.assertEqual("digest_mismatch", comparison["mismatches"][0]["reason"])
if __name__ == "__main__":
unittest.main()
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
import importlib.util
import os
from pathlib import Path
import unittest
from unittest.mock import patch
META_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = META_ROOT / "tools" / "checks" / "postgres-integration-check.py"
def _load_script():
spec = importlib.util.spec_from_file_location(
"postgres_integration_check",
SCRIPT,
)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load {SCRIPT}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class PostgresIntegrationCheckTests(unittest.TestCase):
def test_staging_check_has_explicit_runtime_safety_settings(self) -> None:
module = _load_script()
with patch.dict(os.environ, {}, clear=True):
env = module._check_env(
database_url="postgresql+psycopg://user:password@postgres/db",
modules="tenancy,access,search",
app_env="staging",
)
self.assertEqual(
env["CORS_ORIGINS"],
"http://127.0.0.1:5173,http://localhost:5173",
)
self.assertEqual(
env["GOVOPLAN_TRUSTED_HOSTS"],
"127.0.0.1,localhost,testserver",
)
self.assertEqual(
env["GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS"],
"false",
)
self.assertEqual(
env["GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE"],
"true",
)
def test_existing_runtime_safety_settings_are_preserved(self) -> None:
module = _load_script()
configured = {
"CORS_ORIGINS": "https://govoplan.example.test",
"GOVOPLAN_TRUSTED_HOSTS": "govoplan.example.test",
"GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "true",
"GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE": "false",
}
with patch.dict(os.environ, configured, clear=True):
env = module._check_env(
database_url="postgresql+psycopg://user:password@postgres/db",
modules="tenancy,access,search",
app_env="staging",
)
for key, value in configured.items():
self.assertEqual(env[key], value)
if __name__ == "__main__":
unittest.main()
+206
View File
@@ -0,0 +1,206 @@
from __future__ import annotations
import hashlib
import importlib.util
from pathlib import Path
import sys
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = META_ROOT / "tools" / "repo" / "sync-python-environment.py"
def load_sync_module():
spec = importlib.util.spec_from_file_location("sync_python_environment", SCRIPT)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load {SCRIPT}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class PythonEnvironmentSyncTests(unittest.TestCase):
def test_stale_local_projects_share_one_resolver_transaction(self) -> None:
sync = load_sync_module()
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
root = Path(directory)
requirements = root / "requirements-dev.txt"
requirements.write_text("-e ./govoplan-core\n-e ./govoplan-access\n", encoding="utf-8")
for project in ("govoplan-core", "govoplan-access"):
project_root = root / project
project_root.mkdir()
(project_root / "pyproject.toml").write_text(
f'[project]\nname = "{project}"\nversion = "0.1.10"\n',
encoding="utf-8",
)
local_requirements = sync.local_requirement_entries(requirements)
fingerprint = sync.build_fingerprint(
requirements=requirements,
python="/test/venv/bin/python",
local_requirements=local_requirements,
)
requirements_digest = hashlib.sha256(requirements.read_bytes()).hexdigest()
previous = {
"version": sync.STAMP_VERSION,
"python": "/test/venv/bin/python",
"inputs": [
{"path": str(requirements), "sha256": requirements_digest},
*(
{"path": entry.pyproject, "sha256": "stale"}
for entry in local_requirements
),
],
"requirements_entries": [
entry.as_dict() for entry in sync.parse_requirement_entries(requirements)
],
}
plan = sync.build_install_plan(
previous=previous,
fingerprint=fingerprint,
requirements=requirements,
python="/test/venv/bin/python",
local_requirements=local_requirements,
force=False,
)
self.assertEqual(plan.mode, "Selective Python environment sync")
self.assertIn("one resolver transaction", plan.reason)
self.assertEqual(len(plan.commands), 1)
command = plan.commands[0]
self.assertEqual(command[:5], ("/test/venv/bin/python", "-m", "pip", "install", "-e"))
self.assertEqual(command.count("-e"), 2)
self.assertIn(str(root / "govoplan-core"), command)
self.assertIn(str(root / "govoplan-access"), command)
def test_missing_editable_distribution_is_not_hidden_by_current_stamp(self) -> None:
sync = load_sync_module()
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
root = Path(directory)
project_root = root / "govoplan-probe-missing"
project_root.mkdir()
(project_root / "pyproject.toml").write_text(
"\n".join(
(
"[project]",
'name = "govoplan-probe-definitely-not-installed"',
'version = "0.1.0"',
"",
'[project.entry-points."govoplan.modules"]',
'probe_missing = "govoplan_probe.backend.manifest:get_manifest"',
"",
)
),
encoding="utf-8",
)
requirements = root / "requirements-dev.txt"
requirements.write_text("-e ./govoplan-probe-missing\n", encoding="utf-8")
entries = sync.local_requirement_entries(requirements)
validation = sync.validate_local_installations(sys.executable, entries)
plan = sync.build_environment_repair_plan(
python=sys.executable,
stale_requirements=validation.stale_requirements,
)
self.assertFalse(validation.current)
self.assertEqual(validation.stale_requirements, entries)
self.assertIn("distribution is not installed", validation.issues[0])
self.assertEqual(plan.mode, "Selective Python environment repair")
self.assertEqual(plan.commands[0][-2:], ("-e", str(project_root)))
def test_metadata_sync_also_repairs_unrelated_missing_distribution(self) -> None:
sync = load_sync_module()
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
root = Path(directory)
requirements = root / "requirements-dev.txt"
requirements.write_text("-e ./govoplan-changed\n-e ./govoplan-missing\n", encoding="utf-8")
for project in ("govoplan-changed", "govoplan-missing"):
project_root = root / project
project_root.mkdir()
(project_root / "pyproject.toml").write_text(
f'[project]\nname = "{project}"\nversion = "0.1.10"\n',
encoding="utf-8",
)
entries = sync.local_requirement_entries(requirements)
fingerprint = sync.build_fingerprint(
requirements=requirements,
python="/test/venv/bin/python",
local_requirements=entries,
)
requirements_digest = hashlib.sha256(requirements.read_bytes()).hexdigest()
previous = {
"version": sync.STAMP_VERSION,
"python": "/test/venv/bin/python",
"inputs": [
{"path": str(requirements), "sha256": requirements_digest},
{"path": entries[0].pyproject, "sha256": "stale"},
{
"path": entries[1].pyproject,
"sha256": hashlib.sha256(Path(entries[1].pyproject).read_bytes()).hexdigest(),
},
],
"requirements_entries": [
entry.as_dict() for entry in sync.parse_requirement_entries(requirements)
],
}
plan = sync.build_install_plan(
previous=previous,
fingerprint=fingerprint,
requirements=requirements,
python="/test/venv/bin/python",
local_requirements=entries,
repair_requirements=(entries[1],),
force=False,
)
self.assertEqual(plan.mode, "Selective Python environment sync")
self.assertEqual(len(plan.commands), 1)
command = plan.commands[0]
self.assertEqual(command.count("-e"), 2)
self.assertIn(str(root / "govoplan-changed"), command)
self.assertIn(str(root / "govoplan-missing"), command)
def test_declared_module_entry_points_are_part_of_environment_validation(self) -> None:
sync = load_sync_module()
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
root = Path(directory)
project_root = root / "govoplan-probe"
project_root.mkdir()
(project_root / "pyproject.toml").write_text(
"\n".join(
(
"[project]",
'name = "govoplan-probe"',
'version = "0.1.0"',
"",
'[project.entry-points."govoplan.modules"]',
'probe = "govoplan_probe.backend.manifest:get_manifest"',
"",
)
),
encoding="utf-8",
)
requirements = root / "requirements-dev.txt"
requirements.write_text("-e ./govoplan-probe\n", encoding="utf-8")
expectations = sync.local_installation_expectations(
sync.local_requirement_entries(requirements)
)
self.assertEqual(len(expectations), 1)
self.assertEqual(expectations[0].distribution, "govoplan-probe")
self.assertEqual(
expectations[0].entry_points,
(("govoplan.modules", "probe", "govoplan_probe.backend.manifest:get_manifest"),),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,61 @@
from __future__ import annotations
import json
from pathlib import Path
from urllib.parse import urlparse
ROOT = Path(__file__).resolve().parents[1]
INVENTORY = ROOT / "docs" / "recovery-operation-inventory.json"
MODES = {
"atomic",
"compensation",
"snapshot_restore",
"forward_recovery",
"irreversible",
}
ADOPTION_STATES = {"planned", "reference-implementation", "adopted"}
REQUIRED_PREFIXES = {
"campaign.",
"files.",
"mail.",
"connectors.",
"dataflow.",
"workflow-engine.",
"core.module-lifecycle.",
"core.module-runtime.",
}
ATOMIC_EXTERNAL_READS = {
"connectors.sync.read-snapshot",
"mail.mailbox.sync-cursor",
}
def test_recovery_operation_inventory_is_complete_and_actionable() -> None:
payload = json.loads(INVENTORY.read_text(encoding="utf-8"))
assert payload["schema_version"] == 1
operations = payload["operations"]
ids = [item["id"] for item in operations]
assert len(ids) == len(set(ids))
assert all(any(item.startswith(prefix) for item in ids) for prefix in REQUIRED_PREFIXES)
for item in operations:
assert item["mode"] in MODES
assert item["adoption"] in ADOPTION_STATES
assert item["repository"].startswith("govoplan-")
assert item["resources"]
assert item["fenced"] is True
issue = urlparse(item["issue"])
assert issue.scheme == "https"
assert issue.netloc == "git.add-ideas.de"
assert issue.path.startswith(f"/GovOPlaN/{item['repository']}/issues/")
def test_non_atomic_operations_do_not_claim_plain_database_rollback() -> None:
operations = json.loads(INVENTORY.read_text(encoding="utf-8"))["operations"]
for item in operations:
if item["mode"] == "atomic":
assert (
item["resources"] == ["postgresql"]
or item["id"] in ATOMIC_EXTERNAL_READS
)
+277
View File
@@ -0,0 +1,277 @@
from __future__ import annotations
import csv
from datetime import UTC, datetime
import io
from pathlib import Path
import sys
import tempfile
import unittest
from unittest import mock
import zipfile
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_TOOLS_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
from govoplan_release.artifact_identity import ( # noqa: E402
ArtifactIdentityError,
inspect_python_wheel,
selected_artifact_identity_issues,
)
from govoplan_release.selective_catalog import ( # noqa: E402
apply_python_artifact_identities,
)
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
if str(ASSESSMENT_TOOLS_ROOT) not in sys.path:
sys.path.insert(0, str(ASSESSMENT_TOOLS_ROOT))
from govoplan_assessment.installer_receipt import issue_installer_receipt # noqa: E402
class ReleaseArtifactIdentityTests(unittest.TestCase):
def test_built_wheel_bytes_become_catalog_identity(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
wheel = self._wheel(Path(temp_dir), console_script=True)
payload = self._catalog_payload()
changes = apply_python_artifact_identities(
payload,
repo_versions={"govoplan-demo": "1.2.3"},
python_artifacts={"govoplan-demo": wheel},
)
identity = payload["release"]["artifacts"][0]
self.assertEqual("govoplan-demo", identity["package_name"])
self.assertEqual("1.2.3", identity["package_version"])
self.assertRegex(identity["archive_sha256"], r"^[0-9a-f]{64}$")
self.assertTrue(identity["requires_installer_receipt"])
self.assertEqual("release.artifact", changes[0].field)
self.assertEqual((), selected_artifact_identity_issues(payload))
def test_version_update_without_wheel_removes_stale_identity_and_blocks_publish(self) -> None:
payload = self._catalog_payload()
payload["release"]["artifacts"] = [
{
"artifact_kind": "python-wheel",
"package_name": "govoplan-demo",
"package_version": "1.2.2",
"archive_sha256": "a" * 64,
"archive_size": 10,
"installed_payload": {
"algorithm": "govoplan-wheel-declared-payload-v1",
"sha256": "b" * 64,
"file_count": 1,
},
"requires_installer_receipt": False,
}
]
apply_python_artifact_identities(
payload,
repo_versions={"govoplan-demo": "1.2.3"},
python_artifacts={},
)
self.assertNotIn("artifacts", payload["release"])
self.assertIn(
"no built artifact identity",
" ".join(selected_artifact_identity_issues(payload)),
)
def test_tag_only_selection_needs_no_python_artifact_mapping(self) -> None:
payload = self._catalog_payload()
payload["release"]["selected_units"].append(
{"repo": "govoplan", "version": "1.2.3"}
)
apply_python_artifact_identities(
payload,
repo_versions={"govoplan": "1.2.3"},
python_artifacts={},
)
self.assertIn("selected_units", payload["release"])
def test_unsafe_or_mismatched_wheel_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
unsafe = root / "govoplan_demo-1.2.3-py3-none-any.whl"
with zipfile.ZipFile(unsafe, "w") as archive:
archive.writestr("../escape", b"bad")
archive.writestr(
"govoplan_demo-1.2.3.dist-info/METADATA",
"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: 1.2.3\n",
)
wrong = self._wheel(root, version="9.9.9")
with self.assertRaisesRegex(ArtifactIdentityError, "unsafe"):
inspect_python_wheel(unsafe)
with self.assertRaisesRegex(ValueError, "expected govoplan-demo 1.2.3"):
apply_python_artifact_identities(
self._catalog_payload(),
repo_versions={"govoplan-demo": "1.2.3"},
python_artifacts={"govoplan-demo": wrong},
)
def test_receipt_issuer_hashes_exact_consumed_wheel(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
original = self._wheel(root / "original", value=b"VALUE = 1\n")
different = self._wheel(root / "different", value=b"VALUE = 2\n")
identity = inspect_python_wheel(original)
catalog = self._catalog_payload()
catalog["channel"] = "stable"
catalog["sequence"] = 1
catalog["release"]["artifacts"] = [identity.catalog_payload()]
assessment = {
"assessment_id": "assessment:test",
"release": {"ref": "stable-catalog-1"},
"composition": [{"module_id": "demo", "enabled": True}],
}
installed_payload = {
"algorithm": "govoplan-installed-record-payload-v1",
"sha256": "c" * 64,
"file_count": identity.payload_file_count,
}
evidence = {
"assessment_id": "assessment:test",
"assessment_release": "stable-catalog-1",
"collected_at": "2026-07-22T12:00:00Z",
"artifacts": [
{
"package_name": "govoplan-demo",
"package_version": "1.2.3",
"record_integrity": {
"status": "verified",
"hashed_file_count": identity.payload_file_count,
"permitted_unhashed_file_count": 1,
"generated_unhashed_file_count": 0,
"unverifiable_file_count": 0,
"missing_file_count": 0,
"mismatched_file_count": 0,
"artifact_payload_identity": catalog["release"]["artifacts"][0]["installed_payload"],
"installed_payload_identity": installed_payload,
},
}
],
}
key = Ed25519PrivateKey.generate()
receipt = issue_installer_receipt(
assessment=assessment,
catalog=catalog,
installed_evidence=evidence,
receipt_id="install:test",
key_id="installer:test",
private_key=key,
consumed_artifacts={"govoplan-demo": original},
issued_at=datetime(2026, 7, 22, 12, 1, tzinfo=UTC),
same_process_observation=True,
)
with self.assertRaisesRegex(ValueError, "differs from the signed"):
issue_installer_receipt(
assessment=assessment,
catalog=catalog,
installed_evidence=evidence,
receipt_id="install:test",
key_id="installer:test",
private_key=key,
consumed_artifacts={"govoplan-demo": different},
issued_at=datetime(2026, 7, 22, 12, 1, tzinfo=UTC),
same_process_observation=True,
)
with self.assertRaisesRegex(ValueError, "within five minutes"):
issue_installer_receipt(
assessment=assessment,
catalog=catalog,
installed_evidence=evidence,
receipt_id="install:test",
key_id="installer:test",
private_key=key,
consumed_artifacts={"govoplan-demo": original},
issued_at=datetime(2026, 7, 22, 12, 6, tzinfo=UTC),
same_process_observation=True,
)
self.assertEqual(identity.archive_sha256, receipt["artifacts"][0]["catalog_archive_sha256"])
def test_path_replacement_during_one_descriptor_inspection_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
target = self._wheel(root / "target", value=b"VALUE = 1\n")
replacement = self._wheel(root / "replacement", value=b"VALUE = 2\n")
real_zip = zipfile.ZipFile
def swap_path(file_or_path, *args, **kwargs):
target.unlink()
replacement.rename(target)
return real_zip(file_or_path, *args, **kwargs)
with (
mock.patch(
"govoplan_release.artifact_identity.zipfile.ZipFile",
side_effect=swap_path,
),
self.assertRaisesRegex(ArtifactIdentityError, "changed"),
):
inspect_python_wheel(target)
@staticmethod
def _catalog_payload() -> dict[str, object]:
return {
"core_release": {},
"modules": [
{
"module_id": "demo",
"version": "1.2.3",
"python_package": "govoplan-demo",
"python_ref": "govoplan-demo @ git+ssh://git@example.test/acme/govoplan-demo.git@v1.2.3",
}
],
"release": {
"selected_units": [
{"repo": "govoplan-demo", "version": "1.2.3"}
]
},
}
@staticmethod
def _wheel(
root: Path,
*,
version: str = "1.2.3",
console_script: bool = False,
value: bytes = b"VALUE = 1\n",
) -> Path:
root.mkdir(parents=True, exist_ok=True)
wheel = root / f"govoplan_demo-{version}-py3-none-any.whl"
dist_info = f"govoplan_demo-{version}.dist-info"
files: dict[str, bytes] = {
"govoplan_demo.py": value,
f"{dist_info}/METADATA": (
f"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: {version}\n"
).encode(),
f"{dist_info}/WHEEL": b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
}
if console_script:
files[f"{dist_info}/entry_points.txt"] = (
b"[console_scripts]\ngovoplan-demo = govoplan_demo:main\n"
)
record_buffer = io.StringIO()
writer = csv.writer(record_buffer, lineterminator="\n")
for name, encoded in files.items():
writer.writerow((name, "", len(encoded)))
writer.writerow((f"{dist_info}/RECORD", "", ""))
files[f"{dist_info}/RECORD"] = record_buffer.getvalue().encode()
with zipfile.ZipFile(wheel, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for name, encoded in files.items():
archive.writestr(name, encoded)
return wheel
if __name__ == "__main__":
unittest.main()
+184
View File
@@ -0,0 +1,184 @@
from __future__ import annotations
import json
from datetime import UTC, datetime
from pathlib import Path
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_TOOLS_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
from govoplan_release.catalog import canonical_hash # noqa: E402
from govoplan_release.selective_catalog import ( # noqa: E402
candidate_keyring_from_authenticated_base,
load_authenticated_catalog_base,
public_key_base64,
signature,
)
class ReleaseBaseCatalogTrustTests(unittest.TestCase):
def test_exact_signed_base_pair_is_loaded_once_and_carried_in_memory(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
private_key = Ed25519PrivateKey.generate()
catalog, keyring = self._write_pair(root, private_key=private_key)
authenticated = load_authenticated_catalog_base(
base_catalog=catalog,
base_keyring=keyring,
web_root=root,
channel="stable",
public_base_url="https://invalid.example",
signer_public_keys={"release-key": public_key_base64(private_key)},
)
keyring.unlink()
self.assertEqual("release-key", authenticated.keyring["keys"][0]["key_id"])
self.assertEqual(
canonical_hash(authenticated.keyring),
authenticated.catalog["release"]["keyring_sha256"],
)
def test_deliberate_old_to_new_signer_rotation_is_carried_forward(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
private_key = Ed25519PrivateKey.generate()
new_key = Ed25519PrivateKey.generate()
catalog, keyring = self._write_pair(root, private_key=private_key)
configured = {
"release-key": public_key_base64(private_key),
"release-key-next": public_key_base64(new_key),
}
authenticated = load_authenticated_catalog_base(
base_catalog=catalog,
base_keyring=keyring,
web_root=root,
channel="stable",
public_base_url="https://invalid.example",
signer_public_keys=configured,
)
candidate = candidate_keyring_from_authenticated_base(
authenticated.keyring,
signer_public_keys=configured,
generated_at=datetime(2026, 7, 22, 12, tzinfo=UTC),
)
self.assertEqual(
{"release-key", "release-key-next"},
{item["key_id"] for item in candidate["keys"]},
)
def test_injected_extra_key_without_catalog_authorization_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
private_key = Ed25519PrivateKey.generate()
extra_key = Ed25519PrivateKey.generate()
catalog, keyring = self._write_pair(root, private_key=private_key)
payload = json.loads(keyring.read_text(encoding="utf-8"))
payload["keys"].append(
{
"key_id": "injected-key",
"status": "active",
"public_key": public_key_base64(extra_key),
}
)
keyring.write_text(json.dumps(payload), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "does not pin"):
load_authenticated_catalog_base(
base_catalog=catalog,
base_keyring=keyring,
web_root=root,
channel="stable",
public_base_url="https://invalid.example",
signer_public_keys={
"release-key": public_key_base64(private_key)
},
)
def test_unpinned_or_symlinked_base_keyring_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
private_key = Ed25519PrivateKey.generate()
catalog, keyring = self._write_pair(root, private_key=private_key)
payload = json.loads(keyring.read_text(encoding="utf-8"))
payload["generated_at"] = "changed"
keyring.write_text(json.dumps(payload), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "does not pin"):
load_authenticated_catalog_base(
base_catalog=catalog,
base_keyring=keyring,
web_root=root,
channel="stable",
public_base_url="https://invalid.example",
signer_public_keys={
"release-key": public_key_base64(private_key)
},
)
keyring.unlink()
outside = root / "outside.json"
outside.write_text("{}", encoding="utf-8")
keyring.symlink_to(outside)
with self.assertRaisesRegex(ValueError, "opened safely"):
load_authenticated_catalog_base(
base_catalog=catalog,
base_keyring=keyring,
web_root=root,
channel="stable",
public_base_url="https://invalid.example",
signer_public_keys={
"release-key": public_key_base64(private_key)
},
)
@staticmethod
def _write_pair(
root: Path, *, private_key: Ed25519PrivateKey
) -> tuple[Path, Path]:
keys = [
{
"key_id": "release-key",
"status": "active",
"public_key": public_key_base64(private_key),
}
]
keyring_payload = {
"keyring_version": "1",
"purpose": "govoplan module package catalog signatures",
"keys": keys,
}
catalog_payload = {
"channel": "stable",
"sequence": 1,
"core_release": {"version": "1.0.0"},
"modules": [],
"release": {"keyring_sha256": canonical_hash(keyring_payload)},
}
catalog_payload["signatures"] = [
signature(
catalog_payload,
key_id="release-key",
private_key=private_key,
)
]
catalog = root / "stable.json"
keyring = root / "keyring.json"
catalog.write_text(json.dumps(catalog_payload), encoding="utf-8")
keyring.write_text(json.dumps(keyring_payload), encoding="utf-8")
return catalog, keyring
if __name__ == "__main__":
unittest.main()
+153
View File
@@ -0,0 +1,153 @@
from __future__ import annotations
import json
from pathlib import Path
import sys
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_TOOLS_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
from govoplan_release.candidate_artifact import ( # noqa: E402
CandidateArtifactError,
candidate_output_path,
issue_candidate_id,
issue_candidate_receipt,
verify_candidate_receipt,
validate_release_channel,
)
class CandidateArtifactTests(unittest.TestCase):
def test_channel_is_one_canonical_basename(self) -> None:
self.assertEqual("stable_1", validate_release_channel("stable_1"))
for value in ("../stable", "/stable", ".", "..", "Stable", "stable/name"):
with self.subTest(value=value), self.assertRaises(CandidateArtifactError):
validate_release_channel(value)
def test_handle_is_deterministic_and_can_precede_output(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) / "not-created" / "release-candidates"
candidate_id = issue_candidate_id("rr-123", "attempt-456")
first = candidate_output_path(root, candidate_id)
second = candidate_output_path(root, candidate_id)
self.assertEqual(first, second)
self.assertEqual(candidate_id, first.name)
self.assertRegex(candidate_id, r"^candidate-[0-9a-f]{32}$")
def test_receipt_rejects_catalog_drift(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) / "release-candidates"
candidate_id = issue_candidate_id("run", "attempt")
catalog = self._write_candidate(root / candidate_id)
receipt = issue_candidate_receipt(
root=root, candidate_id=candidate_id, channel="stable"
)
resolved = verify_candidate_receipt(
root=root,
candidate_id=receipt.candidate_id,
catalog_sha256=receipt.catalog_sha256,
channel="stable",
)
catalog.write_text(
json.dumps({"channel": "stable", "sequence": 2, "signatures": [{}]}),
encoding="utf-8",
)
self.assertEqual(root / candidate_id, resolved)
with self.assertRaisesRegex(CandidateArtifactError, "no longer matches"):
verify_candidate_receipt(
root=root,
candidate_id=receipt.candidate_id,
catalog_sha256=receipt.catalog_sha256,
channel="stable",
)
def test_unissued_ids_traversal_and_symlinks_fail_closed(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) / "release-candidates"
root.mkdir(mode=0o700)
candidate_id = issue_candidate_id("run", "attempt")
outside = Path(temp_dir) / "outside"
outside.mkdir()
(root / candidate_id).symlink_to(outside, target_is_directory=True)
with self.assertRaises(CandidateArtifactError):
candidate_output_path(root, "../candidate-" + "0" * 32)
with self.assertRaisesRegex(CandidateArtifactError, "real directory"):
candidate_output_path(root, candidate_id)
def test_catalog_and_channel_components_must_not_be_symlinks(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) / "release-candidates"
candidate_id = issue_candidate_id("run", "attempt")
candidate = root / candidate_id
target = Path(temp_dir) / "target"
target.mkdir()
(target / "stable.json").write_text(
json.dumps({"signatures": [{}]}), encoding="utf-8"
)
root.mkdir(mode=0o700)
candidate.mkdir(mode=0o700)
(candidate / "channels").symlink_to(target, target_is_directory=True)
with self.assertRaisesRegex(CandidateArtifactError, "real directory"):
issue_candidate_receipt(
root=root, candidate_id=candidate_id, channel="stable"
)
def test_shared_candidate_roots_and_catalog_files_fail_closed(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) / "release-candidates"
root.mkdir(mode=0o755)
candidate_id = issue_candidate_id("run", "attempt")
with self.assertRaisesRegex(CandidateArtifactError, "another user"):
candidate_output_path(root, candidate_id)
root.chmod(0o700)
catalog = self._write_candidate(root / candidate_id)
catalog.chmod(0o640)
with self.assertRaisesRegex(CandidateArtifactError, "another user"):
issue_candidate_receipt(
root=root, candidate_id=candidate_id, channel="stable"
)
def test_receipt_rejects_catalog_with_inconsistent_channel(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir) / "release-candidates"
candidate_id = issue_candidate_id("run", "attempt")
catalog = self._write_candidate(root / candidate_id)
catalog.write_text(
json.dumps({"channel": "next", "signatures": [{}]}),
encoding="utf-8",
)
with self.assertRaisesRegex(CandidateArtifactError, "does not match"):
issue_candidate_receipt(
root=root, candidate_id=candidate_id, channel="stable"
)
@staticmethod
def _write_candidate(candidate: Path) -> Path:
candidate.parent.mkdir(mode=0o700, exist_ok=True)
candidate.mkdir(mode=0o700)
channels = candidate / "channels"
channels.mkdir(mode=0o700)
catalog = channels / "stable.json"
catalog.write_text(
json.dumps({"channel": "stable", "sequence": 1, "signatures": [{}]}),
encoding="utf-8",
)
catalog.chmod(0o600)
return catalog
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,168 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.provider_governance import (
ExternalProviderDeclaration,
ModuleArchitectureDeclaration,
ModuleMaturityEvidence,
ProviderBehaviorDeclaration,
ProviderObjectDeclaration,
)
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_ROOT))
from govoplan_release.catalog_entry_synthesis import ( # noqa: E402
manifest_catalog_entry,
validate_initial_entry_closure,
)
from govoplan_release.selective_catalog import apply_repo_updates # noqa: E402
class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
def test_catalog_entry_preserves_architecture_and_provider_declarations(
self,
) -> None:
provider = ExternalProviderDeclaration(
id="example.records",
module_id="example",
label="Example records",
maturity="read",
operations=("read",),
objects=(
ProviderObjectDeclaration(
object_type="record",
field_groups=("identity", "content"),
authority_modes=("external_mirror",),
default_authority_mode="external_mirror",
),
),
behavior=ProviderBehaviorDeclaration(
freshness="Reports the acquisition timestamp.",
health="Reports source and parser health.",
max_read_items=100,
classifications=("internal",),
purposes=("release contract test",),
retention="The owning package retention policy applies.",
),
)
architecture = ModuleArchitectureDeclaration(
layer="data_reporting_integration",
kind="integration",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_release_catalog_entry_synthesis.py",
summary="Proves catalog metadata preservation.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
summary="Defines the provider declaration contract.",
),
),
known_limits=("Test-only provider.",),
supported_authority_modes=("external_mirror",),
owned_concepts=("example transport",),
target_tested_providers=(provider.id,),
)
entry = manifest_catalog_entry(
manifest=ModuleManifest(
id="example",
name="Example",
version="1.2.3",
architecture=architecture,
external_providers=(provider,),
),
repo="govoplan-example",
package="govoplan-example",
version="1.2.3",
description=None,
root=META_ROOT,
repository_base="git+ssh://git@git.add-ideas.de/GovOPlaN",
)
self.assertEqual("vertical_slice", entry["architecture"]["maturity"])
self.assertEqual(
"external_mirror",
entry["external_providers"][0]["objects"][0][
"default_authority_mode"
],
)
def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None:
payload: dict[str, object] = {
"core_release": {},
"release": {},
"modules": [
{
"module_id": "access",
"version": "0.1.11",
"python_package": "govoplan-access",
"python_ref": "govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.11",
}
],
}
changes = apply_repo_updates(
payload,
repo_versions={
"govoplan-addresses": "0.1.9",
"govoplan-poll": "0.1.11",
"govoplan-scheduling": "0.1.11",
},
repo_contracts={},
repository_base="git+ssh://git@git.add-ideas.de/GovOPlaN",
workspace=META_ROOT.parent,
)
modules = {
item["module_id"]: item
for item in payload["modules"] # type: ignore[index]
}
self.assertEqual({"access", "addresses", "poll", "scheduling"}, set(modules))
self.assertEqual("@govoplan/addresses-webui", modules["addresses"]["webui_package"])
self.assertIn(
{"name": "addresses.people_search", "version": "0.1.0"},
modules["addresses"]["provides_interfaces"],
)
self.assertEqual("govoplan-poll", modules["poll"]["python_package"])
self.assertEqual(["poll"], modules["scheduling"]["dependencies"])
self.assertEqual("@govoplan/scheduling-webui", modules["scheduling"]["webui_package"])
self.assertEqual("requires_review", modules["scheduling"]["migration_safety"])
self.assertTrue(
any(
item["name"] == "poll.governed_participation" and item["optional"] is False
for item in modules["scheduling"]["requires_interfaces"]
)
)
self.assertEqual(
{"govoplan-addresses", "govoplan-poll", "govoplan-scheduling"},
{change.repo for change in changes},
)
self.assertEqual({"catalog_entry"}, {change.field for change in changes})
def test_initial_required_dependency_must_be_represented(self) -> None:
with self.assertRaisesRegex(ValueError, "requires catalog module 'poll'"):
validate_initial_entry_closure(
catalog_modules=[
{
"module_id": "scheduling",
"dependencies": ["poll"],
}
],
initial_module_ids={"scheduling"},
)
if __name__ == "__main__":
unittest.main()
+796
View File
@@ -0,0 +1,796 @@
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_TOOLS_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
from govoplan_release.publisher import ( # noqa: E402
FrozenPublicationRemote,
bind_publication_remote,
commit_publication_tree,
publication_mutation_trust_issues,
publish_catalog_candidate,
remote_publication_identity,
run_checked,
verify_committed_publication,
verify_remote_branch_head,
verify_remote_publication,
_git_bytes,
_sanitized_git_environment,
_seal_git_metadata_file,
_trusted_npm_command,
)
class ReleaseCatalogPublicationTests(unittest.TestCase):
def test_publication_git_writes_ignore_permissive_operator_umask(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
repository = Path(temp_dir) / "website"
repository.mkdir()
self._git(repository, "init", "--quiet")
payload_number = 0
while True:
payload = f"private publication object {payload_number}\n".encode()
header = f"blob {len(payload)}\0".encode()
expected_object = hashlib.sha1(
header + payload,
usedforsecurity=False,
).hexdigest()
object_parent = repository / ".git" / "objects" / expected_object[:2]
if not object_parent.exists():
break
payload_number += 1
previous_umask = os.umask(0o002)
try:
object_id = (
_git_bytes(
repository,
"hash-object",
"-w",
"--stdin",
input_bytes=payload,
)
.decode("ascii")
.strip()
)
finally:
os.umask(previous_umask)
object_path = object_parent / expected_object[2:]
self.assertEqual(expected_object, object_id)
self.assertEqual(os.geteuid(), object_parent.stat().st_uid)
self.assertEqual(0o700, object_parent.stat().st_mode & 0o777)
self.assertEqual(os.geteuid(), object_path.stat().st_uid)
self.assertEqual(0o400, object_path.stat().st_mode & 0o777)
def test_publication_git_metadata_is_sealed_after_git_writes(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
metadata = Path(temp_dir) / "index"
metadata.write_bytes(b"index")
metadata.chmod(0o664)
_seal_git_metadata_file(metadata, label="test index")
self.assertEqual(0o600, metadata.stat().st_mode & 0o777)
def test_publication_git_identity_is_fixed_and_non_personal(self) -> None:
with patch.dict(
os.environ,
{
"GIT_AUTHOR_NAME": "Attacker",
"GIT_AUTHOR_EMAIL": "attacker@example.test",
"GIT_COMMITTER_NAME": "Attacker",
"GIT_COMMITTER_EMAIL": "attacker@example.test",
},
clear=False,
):
environment = _sanitized_git_environment()
self.assertEqual(
"GovOPlaN Release Automation",
environment["GIT_AUTHOR_NAME"],
)
self.assertEqual(
"release@govoplan.invalid",
environment["GIT_AUTHOR_EMAIL"],
)
self.assertEqual(
environment["GIT_AUTHOR_NAME"],
environment["GIT_COMMITTER_NAME"],
)
self.assertEqual(
environment["GIT_AUTHOR_EMAIL"],
environment["GIT_COMMITTER_EMAIL"],
)
def test_build_command_uses_its_pinned_node_directory(self) -> None:
completed = subprocess.CompletedProcess(
["/trusted/node/bin/npm"],
0,
stdout=b"",
stderr=b"",
)
with patch(
"govoplan_release.publisher.subprocess.run",
return_value=completed,
) as runner:
run_checked(
["/trusted/node/bin/npm", "run", "build"],
cwd=Path("/trusted/website"),
)
environment = runner.call_args.kwargs["env"]
self.assertEqual(
"/trusted/node/bin:/usr/bin:/bin",
environment["PATH"],
)
def test_npm_command_requires_trusted_npm_and_sibling_node(self) -> None:
with (
patch(
"govoplan_release.publisher.shutil.which",
return_value="/trusted/node/bin/npm",
),
patch(
"govoplan_release.publisher._trusted_runtime_executable_issue",
side_effect=(None, None),
) as trust_check,
):
self.assertEqual(
"/trusted/node/bin/npm",
_trusted_npm_command("npm"),
)
self.assertEqual(Path("/trusted/node/bin/npm"), trust_check.call_args_list[0].args[0])
self.assertEqual(Path("/trusted/node/bin/node"), trust_check.call_args_list[1].args[0])
def test_npm_command_rejects_caller_relative_path(self) -> None:
with (
patch("govoplan_release.publisher.shutil.which") as which,
self.assertRaisesRegex(RuntimeError, "absolute path or the bare name"),
):
_trusted_npm_command("./npm")
which.assert_not_called()
def test_committed_publication_must_match_validated_blobs_exactly(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
web_root = Path(tmp) / "website"
module_root = web_root / "public" / "catalogs" / "v1" / "modules"
module_root.mkdir(parents=True)
catalog = (
web_root / "public" / "catalogs" / "v1" / "channels" / "stable.json"
)
keyring = web_root / "public" / "catalogs" / "v1" / "keyring.json"
catalog.parent.mkdir(parents=True)
expected = {
catalog.relative_to(web_root).as_posix(): b'{"catalog":true}\n',
keyring.relative_to(web_root).as_posix(): b'{"keys":[]}\n',
(module_root / "index.json")
.relative_to(web_root)
.as_posix(): b'{"modules":[]}\n',
}
for relative, encoded in expected.items():
target = web_root / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(encoded)
self._git(web_root, "init", "--quiet")
self._git(web_root, "config", "user.email", "test@example.test")
self._git(web_root, "config", "user.name", "Test")
self._git(web_root, "add", ".")
self._git(web_root, "commit", "--quiet", "-m", "publication")
commit_sha = self._git(web_root, "rev-parse", "HEAD").strip()
verify_committed_publication(
web_root=web_root,
commit_sha=commit_sha,
expected_blobs=expected,
module_root=module_root,
)
changed = dict(expected)
changed[catalog.relative_to(web_root).as_posix()] = b'{"catalog":false}\n'
with self.assertRaisesRegex(RuntimeError, "differs"):
verify_committed_publication(
web_root=web_root,
commit_sha=commit_sha,
expected_blobs=changed,
module_root=module_root,
)
catalog_path = catalog.relative_to(web_root).as_posix()
self._git(web_root, "update-index", "--chmod=+x", catalog_path)
self._git(web_root, "commit", "--quiet", "-m", "unsafe mode")
executable_commit = self._git(web_root, "rev-parse", "HEAD").strip()
with self.assertRaisesRegex(RuntimeError, "regular blob"):
verify_committed_publication(
web_root=web_root,
commit_sha=executable_commit,
expected_blobs=expected,
module_root=module_root,
)
def test_private_index_commit_has_one_parent_and_only_computed_delta(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
web_root = root / "website"
module_root = web_root / "public" / "catalogs" / "v1" / "modules"
channel = (
web_root / "public" / "catalogs" / "v1" / "channels" / "stable.json"
)
keyring = web_root / "public" / "catalogs" / "v1" / "keyring.json"
old_module = module_root / "obsolete.json"
unrelated = web_root / "unrelated.txt"
for path, encoded in (
(channel, b'{"old":true}\n'),
(keyring, b'{"keys":[]}\n'),
(old_module, b'{"obsolete":true}\n'),
(unrelated, b"keep\n"),
):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(encoded)
self._git(web_root, "init", "--quiet")
self._git(web_root, "config", "user.email", "test@example.test")
self._git(web_root, "config", "user.name", "Test")
self._git(web_root, "add", ".")
self._git(web_root, "commit", "--quiet", "-m", "base")
frozen_head = self._git(web_root, "rev-parse", "HEAD").strip()
branch = self._git(web_root, "branch", "--show-current").strip()
malicious = web_root / "not-in-publication.txt"
malicious.write_text("staged but excluded\n", encoding="utf-8")
self._git(web_root, "add", malicious.name)
marker = root / "reference-hook-ran"
hook = web_root / ".git" / "hooks" / "reference-transaction"
hook.write_text(f"#!/bin/sh\ntouch {marker}\n", encoding="utf-8")
hook.chmod(0o755)
expected = {
channel.relative_to(web_root).as_posix(): b'{"new":true}\n',
keyring.relative_to(web_root).as_posix(): b'{"keys":["release"]}\n',
(module_root / "index.json")
.relative_to(web_root)
.as_posix(): b'{"modules":[]}\n',
}
redirected = root / "attacker-index"
with patch.dict(
os.environ,
{
"GIT_DIR": str(root / "attacker-git-dir"),
"GIT_INDEX_FILE": str(redirected),
"GIT_OBJECT_DIRECTORY": str(root / "attacker-objects"),
"GIT_CONFIG_GLOBAL": str(root / "attacker-config"),
},
):
commit_sha = commit_publication_tree(
web_root=web_root,
frozen_head=frozen_head,
branch=branch,
expected_blobs=expected,
module_root=module_root,
message="Exact publication",
)
parents = self._git(
web_root, "rev-list", "--parents", "-n", "1", commit_sha
).split()
changed = set(
filter(
None,
self._git(
web_root,
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
frozen_head,
commit_sha,
).splitlines(),
)
)
hook_ran = marker.exists()
inherited_index_used = redirected.exists()
commit_paths = self._git(
web_root, "ls-tree", "-r", "--name-only", commit_sha
)
self.assertEqual([commit_sha, frozen_head], parents)
self.assertEqual(
{
"public/catalogs/v1/channels/stable.json",
"public/catalogs/v1/keyring.json",
"public/catalogs/v1/modules/index.json",
"public/catalogs/v1/modules/obsolete.json",
},
changed,
)
self.assertFalse(hook_ran)
self.assertFalse(inherited_index_used)
self.assertNotIn("not-in-publication.txt", commit_paths)
def test_remote_binding_and_annotated_publication_identity_are_exact(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
remote_root = root / "website.git"
remote_root.mkdir()
self._git(remote_root, "init", "--bare", "--quiet")
web_root = root / "website"
web_root.mkdir()
self._git(web_root, "init", "--quiet")
self._git(web_root, "config", "user.email", "test@example.test")
self._git(web_root, "config", "user.name", "Test")
self._git(web_root, "branch", "-M", "main")
web_root.joinpath("base.txt").write_text("base\n", encoding="utf-8")
self._git(web_root, "add", ".")
self._git(web_root, "commit", "--quiet", "-m", "base")
self._git(web_root, "remote", "add", "origin", str(remote_root))
self._git(web_root, "push", "--quiet", "-u", "origin", "main")
base_commit = self._git(web_root, "rev-parse", "HEAD").strip()
frozen = bind_publication_remote(
web_root=web_root,
remote="origin",
registered_remote=str(remote_root),
)
self.assertIsInstance(frozen, FrozenPublicationRemote)
verify_remote_branch_head(
web_root=web_root,
remote_url=frozen.url,
branch="main",
expected_commit=base_commit,
)
web_root.joinpath("catalog.json").write_text("{}\n", encoding="utf-8")
self._git(web_root, "add", "catalog.json")
self._git(web_root, "commit", "--quiet", "-m", "publication")
commit_sha = self._git(web_root, "rev-parse", "HEAD").strip()
tag_name = "catalog-test"
self._git(web_root, "tag", "-a", tag_name, "-m", "publication")
tag_object = self._git(
web_root, "rev-parse", f"refs/tags/{tag_name}"
).strip()
self._git(
web_root,
"push",
"--quiet",
"--atomic",
"origin",
f"{commit_sha}:refs/heads/main",
f"{tag_object}:refs/tags/{tag_name}",
)
identity = remote_publication_identity(
web_root=web_root,
remote_url=frozen.url,
branch="main",
tag_name=tag_name,
)
verified = verify_remote_publication(
web_root=web_root,
remote_url=frozen.url,
branch="main",
tag_name=tag_name,
expected_commit=commit_sha,
expected_tag_object=tag_object,
)
self._git(
web_root,
"remote",
"set-url",
"--add",
"--push",
"origin",
str(root / "other.git"),
)
with self.assertRaisesRegex(RuntimeError, "do not match"):
bind_publication_remote(
web_root=web_root,
remote="origin",
registered_remote=str(remote_root),
)
self.assertEqual(identity, verified)
self.assertEqual(commit_sha, identity["publication_commit_sha"])
self.assertEqual(tag_object, identity["publication_tag_object_sha"])
self.assertEqual(commit_sha, identity["publication_tag_commit_sha"])
def test_mutation_rejects_writable_website_and_git_configuration(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
web_root = root / "website"
web_root.mkdir()
self._git(web_root, "init", "--quiet")
candidate = self._candidate(root, key="trusted-key")
target_root = web_root / "public" / "catalogs" / "v1"
target_catalog = target_root / "channels" / "stable.json"
target_keyring = target_root / "keyring.json"
target_keyring.parent.mkdir(parents=True)
target_keyring.write_text("{}\n", encoding="utf-8")
web_root.chmod(0o777)
root_issues = publication_mutation_trust_issues(
web_root=web_root,
candidate_catalog=candidate / "channels" / "stable.json",
candidate_keyring=candidate / "keyring.json",
target_catalog=target_catalog,
target_keyring=target_keyring,
target_modules=target_root / "modules",
)
web_root.chmod(0o755)
config = web_root / ".git" / "config"
config.chmod(0o666)
config_issues = publication_mutation_trust_issues(
web_root=web_root,
candidate_catalog=candidate / "channels" / "stable.json",
candidate_keyring=candidate / "keyring.json",
target_catalog=target_catalog,
target_keyring=target_keyring,
target_modules=target_root / "modules",
)
self.assertIn("website root is writable by another user", root_issues)
self.assertIn("website Git config is writable by another user", config_issues)
def test_push_returns_only_independently_verified_publication_receipt(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
candidate = self._candidate(root, key="trusted-key")
catalog_path = candidate / "channels" / "stable.json"
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
catalog["sequence"] = 7
catalog["core_release"]["python_package"] = "govoplan-core"
catalog["release"] = {
"selected_units": [
{
"repo": "govoplan-core",
"version": "1.2.3",
"tag": "v1.2.3",
"commit_sha": "a" * 40,
"tag_object_sha": "b" * 40,
}
],
"artifacts": [
{
"artifact_kind": "python-wheel",
"package_name": "govoplan-core",
"package_version": "1.2.3",
"archive_sha256": "c" * 64,
"archive_size": 100,
"installed_payload": {
"algorithm": "govoplan-wheel-declared-payload-v1",
"sha256": "d" * 64,
"file_count": 1,
},
"requires_installer_receipt": True,
}
],
}
catalog["signatures"] = [{}]
catalog_path.write_text(json.dumps(catalog), encoding="utf-8")
remote_root = root / "website.git"
remote_root.mkdir()
self._git(remote_root, "init", "--bare", "--quiet")
web_root = root / "website"
target_keyring = web_root / "public" / "catalogs" / "v1" / "keyring.json"
target_keyring.parent.mkdir(parents=True)
target_keyring.write_text(json.dumps(self._keyring("trusted-key")))
self._git(web_root, "init", "--quiet")
self._git(web_root, "config", "user.email", "test@example.test")
self._git(web_root, "config", "user.name", "Test")
self._git(web_root, "branch", "-M", "main")
self._git(web_root, "remote", "add", "origin", str(remote_root))
self._git(web_root, "add", ".")
self._git(web_root, "commit", "--quiet", "-m", "base")
self._git(web_root, "push", "--quiet", "-u", "origin", "main")
frozen_head = self._git(web_root, "rev-parse", "HEAD").strip()
frozen_remote = bind_publication_remote(
web_root=web_root,
remote="origin",
registered_remote=str(remote_root),
)
with (
patch(
"govoplan_release.publisher.validate_module_package_catalog",
return_value={"valid": True, "warnings": [], "error": None},
),
patch(
"govoplan_release.publisher.source_tag_provenance_issues",
return_value=(),
),
patch(
"govoplan_release.publisher.publication_runtime_trust_issues",
return_value=(),
),
patch(
"govoplan_release.publisher.registered_website_remote",
return_value=str(remote_root),
),
):
result = publish_catalog_candidate(
candidate_dir=candidate,
web_root=web_root,
workspace_root=root,
apply=True,
push=True,
branch="main",
tag_name="catalog-test",
expected_website_head=frozen_head,
expected_website_branch="main",
expected_remote_sha256=frozen_remote.sha256,
)
remote_identity = remote_publication_identity(
web_root=web_root,
remote_url=str(remote_root),
branch="main",
tag_name="catalog-test",
)
self.assertEqual("published", result.status)
self.assertEqual("origin", result.remote)
self.assertEqual(
remote_identity["publication_commit_sha"], result.publication_commit_sha
)
self.assertEqual(
remote_identity["publication_tag_object_sha"],
result.publication_tag_object_sha,
)
self.assertEqual(
remote_identity["publication_tag_commit_sha"],
result.publication_tag_commit_sha,
)
def test_apply_writes_validated_objects_even_if_candidate_path_changes(
self,
) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
candidate = self._candidate(root, key="trusted-key")
catalog_path = candidate / "channels" / "stable.json"
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
catalog.update({"channel": "stable", "sequence": 1})
catalog["core_release"]["python_package"] = "govoplan-core"
catalog["release"] = {
"selected_units": [
{
"repo": "govoplan-core",
"version": "1.2.3",
"tag": "v1.2.3",
"commit_sha": "a" * 40,
"tag_object_sha": "b" * 40,
}
],
"artifacts": [
{
"artifact_kind": "python-wheel",
"package_name": "govoplan-core",
"package_version": "1.2.3",
"archive_sha256": "c" * 64,
"archive_size": 100,
"installed_payload": {
"algorithm": "govoplan-wheel-declared-payload-v1",
"sha256": "d" * 64,
"file_count": 1,
},
"requires_installer_receipt": True,
}
],
}
catalog["signatures"] = [{}]
catalog_path.write_text(json.dumps(catalog), encoding="utf-8")
web_root = root / "website"
target_keyring = web_root / "public" / "catalogs" / "v1" / "keyring.json"
target_keyring.parent.mkdir(parents=True)
target_keyring.write_text(json.dumps(self._keyring("trusted-key")))
registered_remote = "ssh://example.test/release/website.git"
self._git(web_root, "init", "--quiet")
self._git(web_root, "config", "user.email", "test@example.test")
self._git(web_root, "config", "user.name", "Test")
self._git(web_root, "remote", "add", "origin", registered_remote)
self._git(web_root, "add", ".")
self._git(web_root, "commit", "--quiet", "-m", "base")
def validate_and_swap(*args, **kwargs):
del args, kwargs
catalog_path.write_text(
json.dumps(
{
"channel": "stable",
"sequence": 999,
"malicious": True,
"signatures": [{}],
}
),
encoding="utf-8",
)
return {"valid": True, "warnings": [], "error": None}
with (
patch(
"govoplan_release.publisher.validate_module_package_catalog",
side_effect=validate_and_swap,
),
patch(
"govoplan_release.publisher.source_tag_provenance_issues",
return_value=(),
),
patch("govoplan_release.publisher.website_dirty", return_value=False),
patch(
"govoplan_release.publisher.publication_runtime_trust_issues",
return_value=(),
),
patch(
"govoplan_release.publisher.registered_website_remote",
return_value=registered_remote,
),
):
result = publish_catalog_candidate(
candidate_dir=candidate,
web_root=web_root,
workspace_root=root,
apply=True,
allow_dirty_website=True,
)
published = json.loads(
(
web_root / "public" / "catalogs" / "v1" / "channels" / "stable.json"
).read_text(encoding="utf-8")
)
self.assertEqual("applied", result.status)
self.assertEqual(1, published["sequence"])
self.assertNotIn("malicious", published)
def test_apply_blocks_selected_python_release_without_built_identity(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
candidate = self._candidate(root, key="trusted-key")
catalog_path = candidate / "channels" / "stable.json"
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
catalog["core_release"]["python_package"] = "govoplan-core"
catalog["release"] = {
"selected_units": [
{
"repo": "govoplan-core",
"version": "1.2.3",
"tag": "v1.2.3",
"commit_sha": "a" * 40,
"tag_object_sha": "b" * 40,
}
]
}
catalog_path.write_text(json.dumps(catalog), encoding="utf-8")
web_root = root / "website"
target_keyring = web_root / "public" / "catalogs" / "v1" / "keyring.json"
target_keyring.parent.mkdir(parents=True)
target_keyring.write_text(json.dumps(self._keyring("trusted-key")))
(web_root / ".git").mkdir()
with (
patch(
"govoplan_release.publisher.validate_module_package_catalog",
return_value={"valid": True, "warnings": [], "error": None},
),
patch(
"govoplan_release.publisher.source_tag_provenance_issues",
return_value=(),
),
patch("govoplan_release.publisher.website_dirty", return_value=False),
):
result = publish_catalog_candidate(
candidate_dir=candidate,
web_root=web_root,
workspace_root=root,
apply=True,
)
self.assertEqual("blocked", result.status)
self.assertIn(
"selected Python repository govoplan-core has no built artifact identity",
" ".join(result.notes),
)
def test_publication_requires_an_existing_trust_anchor(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
candidate = self._candidate(root, key="candidate-key")
web_root = root / "website"
web_root.mkdir()
with patch(
"govoplan_release.publisher.validate_module_package_catalog",
return_value={"valid": True, "warnings": [], "error": None},
):
result = publish_catalog_candidate(
candidate_dir=candidate,
web_root=web_root,
workspace_root=root,
)
self.assertEqual("blocked", result.status)
self.assertIn("publication trust anchor is missing", " ".join(result.notes))
def test_publication_rejects_rebinding_an_existing_key_id(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
candidate = self._candidate(root, key="replacement-key")
web_root = root / "website"
target_keyring = web_root / "public" / "catalogs" / "v1" / "keyring.json"
target_keyring.parent.mkdir(parents=True)
target_keyring.write_text(json.dumps(self._keyring("trusted-key")))
with patch(
"govoplan_release.publisher.validate_module_package_catalog",
return_value={"valid": True, "warnings": [], "error": None},
):
result = publish_catalog_candidate(
candidate_dir=candidate,
web_root=web_root,
workspace_root=root,
)
self.assertEqual("blocked", result.status)
self.assertIn("changes the public key", " ".join(result.notes))
@staticmethod
def _candidate(root: Path, *, key: str) -> Path:
candidate = root / "candidate"
channel = candidate / "channels"
channel.mkdir(parents=True)
channel.joinpath("stable.json").write_text(
json.dumps(
{
"channel": "stable",
"core_release": {
"version": "1.2.3",
"python_ref": (
"govoplan-core @ git+ssh://git@example.test/acme/"
"govoplan-core.git@v1.2.3"
),
},
"modules": [],
}
)
)
candidate.joinpath("keyring.json").write_text(
json.dumps(ReleaseCatalogPublicationTests._keyring(key))
)
return candidate
@staticmethod
def _keyring(key: str) -> dict[str, object]:
return {
"keys": [
{
"key_id": "release-key",
"status": "active",
"public_key": key,
}
]
}
@staticmethod
def _git(root: Path, *args: str) -> str:
return subprocess.run(
["git", *args],
cwd=root,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
).stdout
if __name__ == "__main__":
unittest.main()
+66
View File
@@ -0,0 +1,66 @@
from __future__ import annotations
import subprocess
import sys
import unittest
from pathlib import Path
from fastapi.testclient import TestClient
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_ROOT))
from server.app import create_app # noqa: E402
class ReleaseConsoleSecurityTests(unittest.TestCase):
def test_api_token_is_accepted_only_from_header(self) -> None:
with TestClient(create_app(workspace_root=META_ROOT, token="test-console-token")) as client:
query_response = client.get("/api/health?token=test-console-token")
header_response = client.get(
"/api/health",
headers={"X-Release-Console-Token": "test-console-token"},
)
self.assertEqual(query_response.status_code, 401)
self.assertEqual(header_response.status_code, 200)
def test_bootstrap_token_uses_and_clears_url_fragment(self) -> None:
launcher = (RELEASE_ROOT / "release-console.py").read_text(encoding="utf-8")
webui = (RELEASE_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
self.assertIn("#token={token}", launcher)
self.assertNotIn("?token={token}", launcher)
self.assertIn("window.location.hash.slice(1)", webui)
self.assertIn("history.replaceState", webui)
def test_tokenless_embedding_is_read_only(self) -> None:
with TestClient(create_app(workspace_root=META_ROOT)) as client:
read_response = client.get("/api/health")
write_response = client.post("/api/selective-plan", json={})
self.assertEqual(200, read_response.status_code)
self.assertEqual(403, write_response.status_code)
self.assertIn("read-only", write_response.json()["detail"])
def test_launcher_rejects_non_loopback_and_tokenless_modes(self) -> None:
launcher = RELEASE_ROOT / "release-console.py"
for arguments in (("--host", "0.0.0.0"), ("--no-token",)):
with self.subTest(arguments=arguments):
result = subprocess.run(
(sys.executable, str(launcher), *arguments),
cwd=META_ROOT,
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=10,
)
self.assertEqual(2, result.returncode)
if __name__ == "__main__":
unittest.main()
+177
View File
@@ -0,0 +1,177 @@
from __future__ import annotations
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from fastapi.testclient import TestClient
META_ROOT = Path(__file__).resolve().parents[1]
RELEASE_ROOT = META_ROOT / "tools" / "release"
if str(RELEASE_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_ROOT))
from server.app import create_app # noqa: E402
class ReleaseDashboardDiagnosticsTests(unittest.TestCase):
def test_malformed_core_version_is_bounded_and_blocks_api_plans(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
workspace = Path(temp_dir)
core = workspace / "govoplan-core"
core.mkdir()
(core / "pyproject.toml").write_text("[project\n", encoding="utf-8")
with TestClient(create_app(workspace_root=workspace)) as client:
dashboard_response = client.get(
"/api/dashboard", params={"public_catalog": "false"}
)
selective_response = client.get(
"/api/selective-plan",
params={
"repos": "govoplan-core",
"repo_versions": "govoplan-core:1.2.4",
"public_catalog": "false",
},
)
full_plan_response = client.get(
"/api/plan", params={"public_catalog": "false"}
)
self.assertEqual(200, dashboard_response.status_code)
dashboard = dashboard_response.json()
self.assertEqual("blocked", dashboard["summary"]["status"])
error = next(
item
for item in dashboard["collection_errors"]
if item["code"] == "core_version_metadata_unreadable"
)
self.assertEqual("govoplan-core/pyproject.toml", error["source"])
self.assertIn("TOMLDecodeError", error["message"])
self.assertIn("Repair govoplan-core/pyproject.toml", error["remediation"])
self.assertLess(len(error["message"]), 200)
self.assertNotIn(str(workspace), error["message"])
self.assertEqual(200, selective_response.status_code)
selective_plan = selective_response.json()
self.assertEqual("blocked", selective_plan["status"])
self.assertFalse(selective_plan["source_preflight_ready"])
finding = next(
item
for item in selective_plan["gate_findings"]
if item["code"] == "core_version_metadata_unreadable"
)
self.assertEqual(error["remediation"], finding["remediation"])
self.assertEqual(
"resolve_release_gate", selective_plan["recommended_action"]["id"]
)
self.assertEqual(200, full_plan_response.status_code)
full_plan = full_plan_response.json()
self.assertEqual("blocked", full_plan["status"])
self.assertTrue(
any(
action["id"].startswith("dashboard:core_version_metadata_unreadable:")
for action in full_plan["actions"]
)
)
def test_malformed_manifest_is_not_silently_omitted_from_selected_plan(
self,
) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
workspace = Path(temp_dir)
core = workspace / "govoplan-core"
core.mkdir()
(core / "pyproject.toml").write_text(
'[project]\nname = "govoplan-core"\nversion = "0.1.13"\n',
encoding="utf-8",
)
files = workspace / "govoplan-files"
manifest = files / "src" / "govoplan_files" / "backend" / "manifest.py"
manifest.parent.mkdir(parents=True)
(files / "pyproject.toml").write_text(
'[project]\nname = "govoplan-files"\nversion = "1.2.4"\n',
encoding="utf-8",
)
manifest.write_text("def broken(:\n", encoding="utf-8")
initialize_repository(files)
with TestClient(create_app(workspace_root=workspace)) as client:
dashboard_response = client.get(
"/api/dashboard", params={"public_catalog": "false"}
)
selective_response = client.get(
"/api/selective-plan",
params={
"repos": "govoplan-files",
"repo_versions": "govoplan-files:1.2.4",
"public_catalog": "false",
},
)
self.assertEqual(200, dashboard_response.status_code)
dashboard = dashboard_response.json()
error = next(
item
for item in dashboard["collection_errors"]
if item["code"] == "module_contract_unreadable"
)
self.assertEqual("govoplan-files", error["repo"])
self.assertEqual(
"govoplan-files/src/govoplan_files/backend/manifest.py",
error["source"],
)
self.assertIn("SyntaxError", error["message"])
self.assertIn("Repair the manifest", error["remediation"])
self.assertEqual(200, selective_response.status_code)
plan = selective_response.json()
self.assertEqual("blocked", plan["status"])
self.assertFalse(plan["source_preflight_ready"])
self.assertIn(
"module_contract_unreadable",
{finding["code"] for finding in plan["gate_findings"]},
)
self.assertEqual("resolve_release_gate", plan["recommended_action"]["id"])
def initialize_repository(path: Path) -> None:
subprocess.run(
["git", "init", "--initial-branch=main", str(path)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
["git", "-C", str(path), "add", "."],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
[
"git",
"-C",
str(path),
"-c",
"user.name=Release Test",
"-c",
"user.email=release@example.invalid",
"-c",
"commit.gpgsign=false",
"commit",
"-m",
"Initial fixture",
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if __name__ == "__main__":
unittest.main()

Some files were not shown because too many files have changed in this diff Show More