fdai-lifecycle-hub 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. fdai_lifecycle_hub-0.1.1/PKG-INFO +445 -0
  2. fdai_lifecycle_hub-0.1.1/README.md +428 -0
  3. fdai_lifecycle_hub-0.1.1/pyproject.toml +31 -0
  4. fdai_lifecycle_hub-0.1.1/samples/catalog/channels.json +7 -0
  5. fdai_lifecycle_hub-0.1.1/samples/catalog/releases/1.4.0.json +121 -0
  6. fdai_lifecycle_hub-0.1.1/samples/catalog/releases/1.5.0.json +121 -0
  7. fdai_lifecycle_hub-0.1.1/samples/catalog/releases/1.6.0.json +121 -0
  8. fdai_lifecycle_hub-0.1.1/samples/core-settings.json +10 -0
  9. fdai_lifecycle_hub-0.1.1/samples/enrollment.json +57 -0
  10. fdai_lifecycle_hub-0.1.1/samples/ownership-tag-only.json +3 -0
  11. fdai_lifecycle_hub-0.1.1/samples/ownership.json +5 -0
  12. fdai_lifecycle_hub-0.1.1/samples/state-1.6.0.json +14 -0
  13. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/__init__.py +7 -0
  14. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/adoption_policy.py +226 -0
  15. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/agent_artifacts.py +143 -0
  16. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/api.py +488 -0
  17. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/audit.py +72 -0
  18. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/bootstrap.py +253 -0
  19. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog.py +102 -0
  20. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_adoption.py +492 -0
  21. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_import.py +148 -0
  22. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_integrity.py +260 -0
  23. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_integrity_state.py +173 -0
  24. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_trust.py +125 -0
  25. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_tuf_assessment.py +101 -0
  26. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/cli.py +591 -0
  27. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration.py +70 -0
  28. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_consumer.py +108 -0
  29. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_api.py +152 -0
  30. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_contracts.py +144 -0
  31. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_models.py +96 -0
  32. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_store.py +511 -0
  33. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_trust.py +268 -0
  34. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_key_api.py +138 -0
  35. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_key_store.py +482 -0
  36. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_keys.py +221 -0
  37. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/database_inspection.py +297 -0
  38. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/domain.py +486 -0
  39. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/enrollment.py +69 -0
  40. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/entity.py +101 -0
  41. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/errors.py +95 -0
  42. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/execution_settings.py +75 -0
  43. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/execution_settings_api.py +74 -0
  44. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/inventory.py +53 -0
  45. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/local_channel_api.py +66 -0
  46. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/local_channels.py +239 -0
  47. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/mapping.py +209 -0
  48. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/migrations.py +210 -0
  49. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/models.py +313 -0
  50. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_ownership.py +112 -0
  51. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_ownership_api.py +269 -0
  52. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_proofs.py +90 -0
  53. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_transition.py +464 -0
  54. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_transition_reconciliation.py +582 -0
  55. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/operations.py +253 -0
  56. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/planning.py +421 -0
  57. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/policy/catalog_adoption.rego +92 -0
  58. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/remote.py +714 -0
  59. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/remote_auth.py +545 -0
  60. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/remote_catalog.py +969 -0
  61. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/reports.py +110 -0
  62. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/rows.py +67 -0
  63. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/schemas.py +370 -0
  64. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_api.py +199 -0
  65. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_start.py +193 -0
  66. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_ui/index.html +160 -0
  67. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_ui/setup.css +119 -0
  68. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_ui/setup.js +370 -0
  69. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/signing.py +137 -0
  70. fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/store.py +972 -0
  71. fdai_lifecycle_hub-0.1.1/tests/configuration_import_helpers.py +264 -0
  72. fdai_lifecycle_hub-0.1.1/tests/configuration_key_helpers.py +138 -0
  73. fdai_lifecycle_hub-0.1.1/tests/conftest.py +280 -0
  74. fdai_lifecycle_hub-0.1.1/tests/remote_auth_helpers.py +35 -0
  75. fdai_lifecycle_hub-0.1.1/tests/test_agent_artifacts.py +73 -0
  76. fdai_lifecycle_hub-0.1.1/tests/test_api.py +214 -0
  77. fdai_lifecycle_hub-0.1.1/tests/test_application_binding.py +172 -0
  78. fdai_lifecycle_hub-0.1.1/tests/test_application_release.py +301 -0
  79. fdai_lifecycle_hub-0.1.1/tests/test_apply_reports.py +517 -0
  80. fdai_lifecycle_hub-0.1.1/tests/test_bootstrap.py +279 -0
  81. fdai_lifecycle_hub-0.1.1/tests/test_catalog_adoption.py +702 -0
  82. fdai_lifecycle_hub-0.1.1/tests/test_catalog_rebinding.py +144 -0
  83. fdai_lifecycle_hub-0.1.1/tests/test_catalog_tuf_flow.py +846 -0
  84. fdai_lifecycle_hub-0.1.1/tests/test_catalog_tuf_remote.py +334 -0
  85. fdai_lifecycle_hub-0.1.1/tests/test_catalog_tuf_verification.py +465 -0
  86. fdai_lifecycle_hub-0.1.1/tests/test_cli.py +384 -0
  87. fdai_lifecycle_hub-0.1.1/tests/test_configuration_import.py +525 -0
  88. fdai_lifecycle_hub-0.1.1/tests/test_configuration_import_postgres.py +382 -0
  89. fdai_lifecycle_hub-0.1.1/tests/test_configuration_keys.py +614 -0
  90. fdai_lifecycle_hub-0.1.1/tests/test_configuration_keys_postgres.py +308 -0
  91. fdai_lifecycle_hub-0.1.1/tests/test_configuration_revision.py +371 -0
  92. fdai_lifecycle_hub-0.1.1/tests/test_cross_process_suppression.py +271 -0
  93. fdai_lifecycle_hub-0.1.1/tests/test_database_inspection.py +679 -0
  94. fdai_lifecycle_hub-0.1.1/tests/test_enrollment.py +431 -0
  95. fdai_lifecycle_hub-0.1.1/tests/test_enrollment_signing.py +20 -0
  96. fdai_lifecycle_hub-0.1.1/tests/test_entity.py +80 -0
  97. fdai_lifecycle_hub-0.1.1/tests/test_execution_settings.py +332 -0
  98. fdai_lifecycle_hub-0.1.1/tests/test_first_install.py +318 -0
  99. fdai_lifecycle_hub-0.1.1/tests/test_hub_images.py +314 -0
  100. fdai_lifecycle_hub-0.1.1/tests/test_hubs_postgres.py +324 -0
  101. fdai_lifecycle_hub-0.1.1/tests/test_inventory_api.py +94 -0
  102. fdai_lifecycle_hub-0.1.1/tests/test_local_channels.py +410 -0
  103. fdai_lifecycle_hub-0.1.1/tests/test_native_ownership.py +296 -0
  104. fdai_lifecycle_hub-0.1.1/tests/test_native_proofs.py +329 -0
  105. fdai_lifecycle_hub-0.1.1/tests/test_native_transition.py +1723 -0
  106. fdai_lifecycle_hub-0.1.1/tests/test_no_azure_credentials.py +67 -0
  107. fdai_lifecycle_hub-0.1.1/tests/test_operations.py +74 -0
  108. fdai_lifecycle_hub-0.1.1/tests/test_operator_api.py +370 -0
  109. fdai_lifecycle_hub-0.1.1/tests/test_planning.py +670 -0
  110. fdai_lifecycle_hub-0.1.1/tests/test_prepared_concurrency.py +118 -0
  111. fdai_lifecycle_hub-0.1.1/tests/test_prepared_migration.py +353 -0
  112. fdai_lifecycle_hub-0.1.1/tests/test_release_configuration_schema.py +246 -0
  113. fdai_lifecycle_hub-0.1.1/tests/test_release_hub_consumer.py +207 -0
  114. fdai_lifecycle_hub-0.1.1/tests/test_remote_auth.py +1904 -0
  115. fdai_lifecycle_hub-0.1.1/tests/test_remote_bootstrap.py +51 -0
  116. fdai_lifecycle_hub-0.1.1/tests/test_remote_catalog.py +441 -0
  117. fdai_lifecycle_hub-0.1.1/tests/test_samples.py +76 -0
  118. fdai_lifecycle_hub-0.1.1/tests/test_schema_range.py +51 -0
  119. fdai_lifecycle_hub-0.1.1/tests/test_setup_api.py +219 -0
  120. fdai_lifecycle_hub-0.1.1/tests/test_setup_start.py +160 -0
  121. fdai_lifecycle_hub-0.1.1/tests/test_standalone_remote.py +195 -0
  122. fdai_lifecycle_hub-0.1.1/tests/test_store.py +556 -0
  123. fdai_lifecycle_hub-0.1.1/tests/test_suppressions.py +122 -0
  124. fdai_lifecycle_hub-0.1.1/tests/test_ui_static.py +64 -0
  125. fdai_lifecycle_hub-0.1.1/tests/tuf_helpers.py +139 -0
  126. fdai_lifecycle_hub-0.1.1/ui/.gitignore +6 -0
  127. fdai_lifecycle_hub-0.1.1/ui/index.html +15 -0
  128. fdai_lifecycle_hub-0.1.1/ui/package-lock.json +2568 -0
  129. fdai_lifecycle_hub-0.1.1/ui/package.json +27 -0
  130. fdai_lifecycle_hub-0.1.1/ui/playwright.config.ts +11 -0
  131. fdai_lifecycle_hub-0.1.1/ui/public/brand/fdai-favicon.svg +17 -0
  132. fdai_lifecycle_hub-0.1.1/ui/public/brand/fdai-logo.png +0 -0
  133. fdai_lifecycle_hub-0.1.1/ui/src/api.ts +432 -0
  134. fdai_lifecycle_hub-0.1.1/ui/src/app.tsx +53 -0
  135. fdai_lifecycle_hub-0.1.1/ui/src/auth.css +28 -0
  136. fdai_lifecycle_hub-0.1.1/ui/src/auth.test.ts +87 -0
  137. fdai_lifecycle_hub-0.1.1/ui/src/auth.ts +140 -0
  138. fdai_lifecycle_hub-0.1.1/ui/src/bindable-installations.test.ts +36 -0
  139. fdai_lifecycle_hub-0.1.1/ui/src/bindable-installations.ts +24 -0
  140. fdai_lifecycle_hub-0.1.1/ui/src/components/application-binding.tsx +92 -0
  141. fdai_lifecycle_hub-0.1.1/ui/src/components/async-boundary.tsx +38 -0
  142. fdai_lifecycle_hub-0.1.1/ui/src/components/badge.tsx +55 -0
  143. fdai_lifecycle_hub-0.1.1/ui/src/components/constraint-results.tsx +42 -0
  144. fdai_lifecycle_hub-0.1.1/ui/src/components/execution-settings.tsx +134 -0
  145. fdai_lifecycle_hub-0.1.1/ui/src/components/link.tsx +22 -0
  146. fdai_lifecycle_hub-0.1.1/ui/src/components/nav-icons.tsx +40 -0
  147. fdai_lifecycle_hub-0.1.1/ui/src/components/nav.tsx +66 -0
  148. fdai_lifecycle_hub-0.1.1/ui/src/constraint-status.test.ts +47 -0
  149. fdai_lifecycle_hub-0.1.1/ui/src/constraint-status.ts +25 -0
  150. fdai_lifecycle_hub-0.1.1/ui/src/enrollment-request.test.ts +72 -0
  151. fdai_lifecycle_hub-0.1.1/ui/src/enrollment-requester.test.ts +32 -0
  152. fdai_lifecycle_hub-0.1.1/ui/src/enrollment-requester.ts +26 -0
  153. fdai_lifecycle_hub-0.1.1/ui/src/execution-settings.test.ts +143 -0
  154. fdai_lifecycle_hub-0.1.1/ui/src/format.test.ts +45 -0
  155. fdai_lifecycle_hub-0.1.1/ui/src/format.ts +51 -0
  156. fdai_lifecycle_hub-0.1.1/ui/src/hooks/use-async.ts +45 -0
  157. fdai_lifecycle_hub-0.1.1/ui/src/local-channels.test.ts +10 -0
  158. fdai_lifecycle_hub-0.1.1/ui/src/main.tsx +10 -0
  159. fdai_lifecycle_hub-0.1.1/ui/src/plan-status.test.ts +21 -0
  160. fdai_lifecycle_hub-0.1.1/ui/src/plan-status.ts +10 -0
  161. fdai_lifecycle_hub-0.1.1/ui/src/report-status.test.ts +34 -0
  162. fdai_lifecycle_hub-0.1.1/ui/src/report-status.ts +29 -0
  163. fdai_lifecycle_hub-0.1.1/ui/src/router.test.ts +80 -0
  164. fdai_lifecycle_hub-0.1.1/ui/src/router.ts +90 -0
  165. fdai_lifecycle_hub-0.1.1/ui/src/routes/catalog.tsx +95 -0
  166. fdai_lifecycle_hub-0.1.1/ui/src/routes/enrollments.test.tsx +12 -0
  167. fdai_lifecycle_hub-0.1.1/ui/src/routes/enrollments.tsx +337 -0
  168. fdai_lifecycle_hub-0.1.1/ui/src/routes/installation.tsx +227 -0
  169. fdai_lifecycle_hub-0.1.1/ui/src/routes/installations.tsx +114 -0
  170. fdai_lifecycle_hub-0.1.1/ui/src/routes/inventory.tsx +99 -0
  171. fdai_lifecycle_hub-0.1.1/ui/src/routes/local-channels.tsx +119 -0
  172. fdai_lifecycle_hub-0.1.1/ui/src/routes/plan.tsx +131 -0
  173. fdai_lifecycle_hub-0.1.1/ui/src/routes/updates.tsx +262 -0
  174. fdai_lifecycle_hub-0.1.1/ui/src/styles.css +837 -0
  175. fdai_lifecycle_hub-0.1.1/ui/src/update-status.test.ts +280 -0
  176. fdai_lifecycle_hub-0.1.1/ui/src/update-status.ts +270 -0
  177. fdai_lifecycle_hub-0.1.1/ui/tests/browser/application-binding.spec.ts +148 -0
  178. fdai_lifecycle_hub-0.1.1/ui/tests/browser/bootstrap-session.integration.spec.ts +47 -0
  179. fdai_lifecycle_hub-0.1.1/ui/tests/browser/bootstrap.config.ts +11 -0
  180. fdai_lifecycle_hub-0.1.1/ui/tests/browser/bootstrap.spec.ts +332 -0
  181. fdai_lifecycle_hub-0.1.1/ui/tests/browser/catalog.spec.ts +245 -0
  182. fdai_lifecycle_hub-0.1.1/ui/tests/browser/enrollment-agent-request.spec.ts +65 -0
  183. fdai_lifecycle_hub-0.1.1/ui/tests/browser/enrollment-request.spec.ts +167 -0
  184. fdai_lifecycle_hub-0.1.1/ui/tests/browser/execution-settings.spec.ts +240 -0
  185. fdai_lifecycle_hub-0.1.1/ui/tests/browser/inventory.spec.ts +96 -0
  186. fdai_lifecycle_hub-0.1.1/ui/tests/browser/standalone.spec.ts +75 -0
  187. fdai_lifecycle_hub-0.1.1/ui/tests/browser/updates.spec.ts +278 -0
  188. fdai_lifecycle_hub-0.1.1/ui/tsconfig.json +26 -0
  189. fdai_lifecycle_hub-0.1.1/ui/tsconfig.tests.json +8 -0
  190. fdai_lifecycle_hub-0.1.1/ui/vite.config.ts +31 -0
  191. fdai_lifecycle_hub-0.1.1/uv.lock +1033 -0
@@ -0,0 +1,445 @@
1
+ Metadata-Version: 2.4
2
+ Name: fdai-lifecycle-hub
3
+ Version: 0.1.1
4
+ Summary: FDAI Lifecycle Hub that computes signed Lifecycle Plans for Hub-managed installations
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: azure-identity<2,>=1.19
7
+ Requires-Dist: cryptography<51,>=45
8
+ Requires-Dist: fdai-deployment-cli<0.2,>=0.1.2
9
+ Requires-Dist: httpx<1,>=0.27
10
+ Requires-Dist: psycopg[binary]<4,>=3.2
11
+ Requires-Dist: pydantic<3,>=2.11
12
+ Requires-Dist: pyjwt[crypto]<3,>=2.10
13
+ Requires-Dist: sqlalchemy<3,>=2.0
14
+ Requires-Dist: starlette<2,>=1.3.1
15
+ Requires-Dist: uvicorn<1,>=0.35
16
+ Description-Content-Type: text/markdown
17
+
18
+ # FDAI Lifecycle Hub
19
+
20
+ `fdai-lifecycle-hub` enrolls installations, computes the next signed Lifecycle Plan for each
21
+ enrolled installation, and serves it to the installation's lifecycle agent. It reuses the pure
22
+ Release, configuration, and Plan checks in `fdai-deployment-cli`, so the Hub and the agent judge a
23
+ Plan with the same code.
24
+
25
+ Lifecycle I0 runs in observation mode (shadow mode): the Hub decides and records what the agent
26
+ reports, but never applies a change itself. It needs no Azure credential and grants no lifecycle
27
+ authority.
28
+
29
+ ## Local first-installation Bootstrap
30
+
31
+ `fdai-lifecycle-hub start` opens the packaged setup interface on an ephemeral IPv4 loopback port.
32
+ It starts without a Hub database or FDAI Runtime. A separate customer-side executor owns Azure
33
+ login and reuses the existing deployment CLI, Terraform, exact approvals and independent checks.
34
+ The web process never runs Terraform or holds Azure tokens.
35
+
36
+ Use Python 3.12 or later on Linux with a local browser. Install the reviewed Hub wheel and its
37
+ declared dependencies from your approved source; this change does not publish a PyPI release.
38
+ The executor checks tools and retains its own Azure CLI profile. Signed Release Hub images need
39
+ an independently approved public key; explicit source mode needs a clean checkout. The current
40
+ AKS/Console profile is not a Lite topology, and automatic lifecycle enrollment is not implied.
41
+ See the [Bootstrap guide](../../docs/roadmap/deployment/lifecycle-hub-bootstrap.md) for private
42
+ state, review, recovery and package/live-validation boundaries.
43
+
44
+ ## Local Release Hub catalog admission
45
+
46
+ The separate [Release Hub](../release-hub/README.md) provides a local-only vendor UI and export.
47
+ `recompute-pinned` accepts that export only with an out-of-band SHA-256 pin, a trusted development
48
+ public key, an expiry check, and an operator-retained revision floor. It reuses the existing
49
+ manifest parser, detached signature convention, recall records, and planner. The ordinary
50
+ `recompute --catalog` development directory loader remains unchanged. This opt-in transport is
51
+ not a production signed catalog protocol and does not prove registry availability.
52
+
53
+ ## Explicit remote-development profile
54
+
55
+ Local CLIs keep their loopback defaults. The independent
56
+ `python -m fdai_lifecycle_hub.remote` entrypoint requires direct Entra operator authentication,
57
+ its own passwordless PostgreSQL identity, a separately provisioned development signing key,
58
+ and explicit schema revision 1 migration. It never trusts ACA principal headers.
59
+ The existing enrollment approval API binds the approver to the verified operator OID.
60
+ Without explicit installation Agent bindings, enrollment/state/report/Plan transport remains
61
+ closed. The opt-in [Agent preparation](../agent/README.md#remote-development-preparation) opens
62
+ only an installation-bound Agent's Plan/state/report routes.
63
+
64
+ For a bound installation, an authenticated operator can submit a signed enrollment through
65
+ `POST /v1/enrollments/{installation_id}/requests`. The body uses the existing `signed_payload`
66
+ and `proof` base64 fields. The Hub verifies the installation signature, request freshness,
67
+ matching installation id and request-size limit, and audits the verified operator as
68
+ `requested_by`, including proof failures. Submission creates only a pending request visible in
69
+ Enrollment requests; approval remains separate and Entities remain unmanaged. Empty bindings
70
+ retain the existing closed route, and the original Agent enrollment route stays closed even to
71
+ an operator.
72
+
73
+ The Agent bound to exactly one installation can also propose that installation's pending
74
+ enrollment through `POST /v1/installations/{installation_id}/enrollment-request` (see
75
+ `fdai-lifecycle-agent enroll`). It requires the exact object/client binding, `Agent.Access` and
76
+ no delegated `scp`, reuses the proof, freshness, matching-id and size checks, and audits the
77
+ Agent object id as `requested_by` with `requested_via=agent`. The Agent cannot approve, reject,
78
+ manage Entities, record ownership or set `settings.product`, and an operator token cannot use
79
+ this route. Enrollment requests shows who proposed each pending request.
80
+
81
+ `GET /v1/enrollments/bindable` lists the bound installation IDs with their status.
82
+ `fdai-lifecycle-hub sign-enrollment <request.json> --key <installation.pem> --out <file>` signs
83
+ inside the installation boundary and writes only the upload file; it prints the key id and the
84
+ submission deadline and refuses to overwrite an existing file.
85
+
86
+ In the operator UI, open **Enrollment requests**, select the bound installation,
87
+ and choose the signed request JSON file. **Submit request** sends only its `signed_payload`
88
+ and `proof` fields to that operator route. Files are limited to 64 KiB; private keys must
89
+ never be uploaded. A successful submission remains pending and requires a separate approval
90
+ after independent installation-key verification. If submission times out, refresh the pending
91
+ list before retrying; the UI never retries automatically.
92
+
93
+ The **Updates** screen is the pull-only path. The Lifecycle Hub polls the bound Release Hub on
94
+ `catalog.check_interval_seconds`, verifies the Release key's signature over the exact export
95
+ bytes, and evaluates the packaged Rego policy (`policy/catalog_adoption.rego`, run with the
96
+ `opa` binary). In `auto` mode a change inside the standing authorization is adopted and Plans are
97
+ issued for enrolled installations. Otherwise the candidate waits for **Review and apply**. The
98
+ default is `review`; a missing signature, key, engine or authorization never adopts. Routes:
99
+ `GET /v1/catalog/updates`, `POST /v1/catalog/updates/check` and `POST /v1/catalog/updates/apply`.
100
+ Release Hub never calls this Hub. See [Automatic catalog adoption](../../docs/roadmap/deployment/lifecycle-releases-and-channels.md#automatic-catalog-adoption).
101
+
102
+ With `catalog.trust` set to `{ "mode": "tuf", "root_key_ids": [...], "root_threshold": n }` in place
103
+ of `release_public_key`, the poll verifies TUF-style signed roles instead of one key: the root
104
+ chain from the pinned keys (rotation needs the previous threshold), the snapshot and timestamp,
105
+ the export digest and revision they bind, and persisted anti-rollback floors
106
+ (`catalog_integrity_state`, created by `bind-catalog`). The Rego policy gains `integrity` facts
107
+ and the reasons `integrity_not_verified`, `timestamp_expired` and `root_rotation_requires_review`.
108
+ An expired timestamp adopts only recalls and plans only `recall-rolloff`; a root rotation always
109
+ waits for a person. **Updates** shows the root, snapshot and timestamp versions and expiry, and
110
+ `GET /v1/catalog/updates` carries an `integrity` object. A bound database refuses a changed
111
+ trust, so switching an installation from the single key needs a fresh binding. See
112
+ [Catalog integrity](../../docs/roadmap/deployment/lifecycle-releases-and-channels.md#signed-catalog-roles-in-the-hubs)
113
+ and the [operator guide](../../docs/runbooks/hub-lifecycle-operator-guide.md#c1-use-signed-catalog-roles-opt-in).
114
+
115
+ The Catalog import screen separates independent exact-pin approval from one bounded outbound
116
+ import using the customer's managed identity. Accepted bytes, digest, key binding, revision
117
+ floor, and approval/import audit survive restart in PostgreSQL. The existing planner revalidates
118
+ the persisted catalog, including expiry; publishing metadata alone never proves artifact
119
+ availability. No operation here applies a Plan or deploys FDAI.
120
+
121
+ The same screen supports reviewed customer-local membership from the fresh accepted `RELEASE`
122
+ set. It records exact catalog/key/Release bindings, operator attribution and a local revision;
123
+ the planner retains that provenance without changing subscriptions or execution approval.
124
+ Run the existing explicit migration before serving schema revision 4; older binaries hold on
125
+ that schema rather than ignore curation. See the [paired channel contract](../../docs/roadmap/deployment/lifecycle-releases-and-channels.md#customer-local-channel-curation-design-and-critique)
126
+ for bounds, expiry/recall holds, immutable history and the absence of a destructive downgrade.
127
+
128
+ **Available releases** reads the accepted catalog without fetching or importing it. Every read
129
+ revalidates its accepted pin, signatures and expiry; an unavailable catalog returns `503`, not
130
+ an empty success. The read-only **Configuration packages** screen lists stored revision metadata
131
+ and enrolled installation references, never configuration values or sealed identifiers. Enrollment can
132
+ create a revision without independent package signature provenance, so the screen explicitly
133
+ shows **Not recorded** rather than claiming signed-input readiness. Neither route is available
134
+ to an Agent identity.
135
+
136
+ See the [operator guide](../../docs/runbooks/hub-lifecycle-operator-guide.md) for diagrams,
137
+ responsibilities, the source of each identifier, human enrollment steps and production gaps.
138
+
139
+ See [Two-Hub development boundaries](../../infra/hubs-development/README.md) for images,
140
+ separate IaC boundaries, safe key/database provisioning, manual migration Jobs, validation
141
+ commands, and the remaining live Azure acceptance checks. Browser interaction tests cover
142
+ desktop and constrained layouts with built assets and synthetic API responses, not live Entra.
143
+ This development path does not change source or offline installation contracts.
144
+
145
+ Without operator login configured, either product's bundled UI displays only its own runtime
146
+ status and explicitly unavailable management APIs. Health and database-readiness probes are
147
+ independent and bounded to five seconds. A failed connection, invalid JSON or timeout marks only
148
+ that probe unavailable; the product heading, management notice and other probe's result remain.
149
+ This status page does not enable anonymous management access.
150
+
151
+ ## Enrollment and managed Entities
152
+
153
+ 1. The installation signs an enrollment request with its installation key and submits it. The
154
+ request carries the installation's settings, configuration, declared Entities, and reported
155
+ state. The Hub checks the proof with an injected verifier against the exact request bytes and
156
+ accepts a `requested_at` no more than five minutes from its own clock. A failed proof creates
157
+ nothing: the Hub audits `enrollment_proof_invalid` or `enrollment_proof_stale` and refuses the
158
+ request. The request reaches the Hub three ways: the loopback `enrollment` route, an operator
159
+ upload through `POST /v1/enrollments/{id}/requests`, or, in the explicit remote profile, the
160
+ installation Agent bound to exactly that installation through
161
+ `POST /v1/installations/{id}/enrollment-request` (`fdai-lifecycle-agent enroll`). Every way
162
+ creates only a pending request, and the audit records `requested_by` and `requested_via`.
163
+ 2. The installation is `pending`. It receives no Plan, the Hub doesn't recompute it, and it
164
+ accepts no reported state or suppression.
165
+ 3. A customer approver uses the operator UI or `approve` with the installation key id they
166
+ verified out of band, or rejects with a reason code. The audit records the approver.
167
+ Operator approval is separate from submission; an installation can't approve itself through
168
+ the agent API. Rejection is final.
169
+ 4. Every Entity starts unmanaged. `record-ownership` records an Entity's ownership evidence, and
170
+ the audit keeps the operator who supplied it and its digests. Ownership is proven only by a Foundation receipt digest together with a Terraform state
171
+ digest. The `fdai:managed=true` tag alone records `ownership_tag_only`, and anything less than
172
+ both digests records `ownership_unproven`.
173
+ 5. `manage` gives an Entity with proven ownership its settings: override blocks by Release version
174
+ range. The shared configuration resolver must accept them and find a block that covers the
175
+ Entity's running Release. An Entity with settings is managed, and only managed Entities appear
176
+ in Plans.
177
+
178
+ ## How a Plan is chosen
179
+
180
+ An enrolled installation without a managed Entity gets `no-managed-entity` and no Plan.
181
+ Otherwise:
182
+
183
+ 1. The Hub lists the Releases on the installation's channel that are newer than its oldest managed
184
+ entity, newest first.
185
+ 2. It skips a Release that a Release-specific check blocks, such as a recall, the version range, the
186
+ schema range, a missing artifact, or data residency.
187
+ 3. For the first Release that passes, a closed maintenance window or an active suppression makes the
188
+ installation wait. The Hub doesn't fall back to an older Release.
189
+ 4. Otherwise the Hub signs a Plan with the next sequence number. If the open Plan already targets the
190
+ same Release from the same reported state and configuration, the Hub keeps it.
191
+
192
+ Every recompute records its outcome and every constraint result. Every change appends to a
193
+ hash-chained audit.
194
+
195
+ Secret values never enter the Hub. Configuration values and Entity settings pass the shared
196
+ configuration-package secret scan, so a literal value under a secret-bearing key is refused and
197
+ only a Key Vault reference is accepted.
198
+
199
+ The Hub computes each configuration digest from the canonical JSON of its content, so a digest
200
+ always names exactly one configuration. Every report except `rejected` must name the SHA-256 of the
201
+ exact Plan bytes it checked, and the Hub rejects a report that names other bytes.
202
+
203
+ ## Apply reports and failure holds
204
+
205
+ An agent that applies a Plan reports `apply-started` with attempt *k* right before the change, then
206
+ `applied` (reason code `applied`) or `rolled-back` (reason code `health_check_failed` or
207
+ `apply_failed`) with attempt *k*+1. The Hub doesn't apply anything itself.
208
+
209
+ A new `rolled-back` report holds every Entity of its Plan in the same transaction: the Hub adds an
210
+ `entity:<id>` suppression with origin `failure`, the failed Plan id, and a 24-hour end, and audits
211
+ it. The agent API stops serving the Plan at once, and the next `recompute` waits with
212
+ `suppression_window_active` instead of planning the failed Release again. `unsuppress --scope
213
+ entity:<id>` lifts the hold early, and `show` lists every active suppression with its origin.
214
+
215
+ After an apply or a rollback, the agent reports its new snapshot with
216
+ `POST /v1/installations/{id}/state`. The Hub checks it as `record-state` does: the snapshot must be
217
+ newer than the current one, include every managed Entity, and name only known Entities.
218
+
219
+ ## Layout
220
+
221
+ | Path | Purpose |
222
+ |------|---------|
223
+ | `src/fdai_lifecycle_hub/domain.py` | Installation aggregate, reported state, and planning outcomes |
224
+ | `src/fdai_lifecycle_hub/entity.py` | Entities, ownership evidence, and settings |
225
+ | `src/fdai_lifecycle_hub/enrollment.py` | Enrollment requests, key proof, and enrollment status |
226
+ | `src/fdai_lifecycle_hub/catalog.py` | Release catalog and its development directory loader |
227
+ | `src/fdai_lifecycle_hub/planning.py` | `plan_next`: chooses the target Release and signs the Plan |
228
+ | `src/fdai_lifecycle_hub/signing.py` | Ed25519 development Hub keys |
229
+ | `src/fdai_lifecycle_hub/models.py` | SQLAlchemy ORM models for the Hub data model |
230
+ | `src/fdai_lifecycle_hub/store.py` | Repository: one transaction per public method |
231
+ | `src/fdai_lifecycle_hub/rows.py` | Row lookups, locks, and small row writes the transactions share |
232
+ | `src/fdai_lifecycle_hub/mapping.py` | Mapping between ORM rows and domain records |
233
+ | `src/fdai_lifecycle_hub/reports.py` | Report outcomes, their reason codes, and failure holds |
234
+ | `src/fdai_lifecycle_hub/errors.py` | Requests the Hub refuses |
235
+ | `src/fdai_lifecycle_hub/audit.py` | Hash-chained audit records and chain verification |
236
+ | `src/fdai_lifecycle_hub/schemas.py` | JSON boundary: request bodies, operator input files, and the UI's read-only view records |
237
+ | `src/fdai_lifecycle_hub/api.py` | The agent HTTP API, the operator UI's read-only and approval routes, and serving the built UI |
238
+ | `src/fdai_lifecycle_hub/cli.py` | `fdai-lifecycle-hub` command line |
239
+ | `tests/` | Hub-owned tests; the PostgreSQL variants run only when `FDAI_DATABASE_URL` names a loopback database |
240
+ | `ui/` | The operator UI: a read-only Preact and Vite single-page app. See [Operator UI](#operator-ui) |
241
+
242
+ ## Agent API
243
+
244
+ | Method and path | Result |
245
+ |-----------------|--------|
246
+ | `POST /v1/installations/{id}/enrollment` | `202` with the installation key id for a pending request, `403` with `enrollment_proof_invalid` or `enrollment_proof_stale`, `409` when the installation exists, `422` for an invalid body or another installation id |
247
+ | `POST /v1/installations/{id}/enrollment-request` | Remote profile only, for the Agent bound to exactly this installation (`Agent.Access`, no `scp`). Same body and checks as the enrollment route; `202` pending, `403` for any other caller, an invalid proof or a stale request, `409` `installation_exists`, `413` above 64 KiB, `422`. It creates a pending request only; the Agent cannot approve, reject, manage Entities or record ownership |
248
+ | `GET /v1/installations/{id}/plan` | `200` with `{"plan": {"signed_payload", "signature"}}` in base64, or `204` when the installation isn't enrolled or no Plan is open |
249
+ | `POST /v1/installations/{id}/plans/{plan_id}/reports` | `202` for a new or identical report, `409` for a conflicting attempt or other Plan bytes, `413` above 16 KiB, `422` for an invalid body, `503` with `Retry-After` when a concurrent write committed first |
250
+ | `POST /v1/installations/{id}/state` | `202` when the snapshot becomes current, `409` with `stale_state` when its `observed_at` isn't newer, `413` above 64 KiB, `422` with `state_invalid`, `404` for an unknown or unenrolled installation |
251
+ | `GET /healthz` | `200` |
252
+
253
+ ## Operator UI API
254
+
255
+ Read-only routes for the installations and Plans screens, and the two routes an approver's
256
+ browser calls to decide a pending enrollment. Like the agent API, these serve on loopback only
257
+ and have no caller authentication yet (see [Limits](#limits)).
258
+
259
+ Enrollment decisions require `Content-Type: application/json` and a loopback Host.
260
+ When the browser sends `Origin`, it must match the request's scheme, Host, and port;
261
+ cross-site requests are refused before their bodies are read. The Vite development
262
+ proxy preserves Host and Origin, so browser decisions stay same-origin in both modes.
263
+ This protects browser requests against cross-origin submission; it is not authentication.
264
+ Installation details include only suppressions active at the Hub's current clock.
265
+ Declared Entities missing from the latest report keep their rows with `null` release
266
+ and health values, displayed as "Not reported".
267
+
268
+ | Method and path | Result |
269
+ |-----------------|--------|
270
+ | `GET /v1/configurations` | `200` with stored revision digests, timestamps, counts and enrolled installation references; pending/rejected requests retain revision metadata without installation links; package signature provenance is explicitly `not-recorded`; values and sealed identifiers are omitted |
271
+ | `GET /v1/catalog/releases` (remote catalog profile) | `200` with accepted and reverified Release metadata; `503` when unimported, invalid or expired; never fetches/imports or asserts registry readiness |
272
+ | `GET /v1/installations` | `200` with every enrolled installation's channel, current and target Release, latest outcome, health, and the next maintenance window with whether it's already open |
273
+ | `GET /v1/installations/{id}` | `200` with settings, Entities, suppressions, the open Plan, the last evaluation, and every report; `404` for an unknown or not-yet-enrolled installation |
274
+ | `GET /v1/installations/{id}/plans/{plan_id}` | `200` with the Plan's status (`open` or `superseded`), its events, every report, and every recompute that targeted it; `404` for an unknown Plan |
275
+ | `GET /v1/enrollments/pending` | `200` with every pending enrollment request: the installation key id, requested time, `requested_by` and `requested_via` (`agent` or `operator`, `null` for a signed file or older record), settings, and declared Entities (unmanaged; ownership evidence doesn't exist until after approval) |
276
+ | `GET /v1/enrollments/pending/{id}` | `200` with one pending request's full detail; `404` when it isn't pending |
277
+ | `POST /v1/installations/{id}/enrollment/approve` | Body `{"approver", "installation_key_id"}`. `200` with `{"enrolled": id}`, `409` with `not_pending`, `422` with `installation_key_mismatch` or `approval_invalid` |
278
+ | `POST /v1/installations/{id}/enrollment/reject` | Body `{"approver", "reason"}`. `200` with `{"rejected": id}`, `409` with `not_pending`, `422` with `rejection_invalid` |
279
+
280
+ An enrollment body is `{"signed_payload", "proof"}` in base64. `signed_payload` is the JSON request
281
+ with `installation_key` (the raw Ed25519 public key in base64) and a timezone-aware
282
+ `requested_at`, and `proof` is the installation key's signature over exactly those bytes.
283
+
284
+ A Plan's `signed_payload` is exactly the canonical Plan bytes that the signature covers. A report's
285
+ `exact_plan_digest` is `sha256:` followed by the hex SHA-256 of those bytes. A report's `outcome` is
286
+ `dry-run-admitted`, `rejected`, `apply-started`, `applied`, or `rolled-back`; only `rejected` may
287
+ omit the digest. A state body has the shape of [samples/state-1.6.0.json](samples/state-1.6.0.json).
288
+
289
+ ## Try it locally
290
+
291
+ The Hub has its own lock file, like `packages/deployment-cli`, so `uv run` installs and runs the
292
+ `fdai-lifecycle-hub` command. [samples/](samples) holds three sample Releases, an enrollment
293
+ request for one installation on 1.4.0 whose maintenance window is open all day, ownership evidence,
294
+ and Entity settings. SQLite needs no setup.
295
+
296
+ Run these from `lifecycle/hub/`. `migrate` refuses a database that an earlier Hub created, because
297
+ Lifecycle I0 has no migrations, so start from a new database file:
298
+
299
+ ```bash
300
+ mkdir -p /tmp/fdai-hub && rm -f /tmp/fdai-hub/hub.db
301
+ export FDAI_LIFECYCLE_HUB_DATABASE_URL=sqlite+pysqlite:////tmp/fdai-hub/hub.db
302
+
303
+ uv run fdai-lifecycle-hub migrate
304
+ uv run fdai-lifecycle-hub dev-keygen /tmp/fdai-hub/hub.pem
305
+ uv run fdai-lifecycle-hub dev-keygen /tmp/fdai-hub/installation.pem
306
+ uv run fdai-lifecycle-hub dev-enroll samples/enrollment.json --key /tmp/fdai-hub/installation.pem
307
+ uv run fdai-lifecycle-hub show example
308
+ ```
309
+
310
+ `dev-enroll` stands in for the lifecycle agent: it adds the installation's public key and the
311
+ current time to the request, signs it, and submits it. It prints the installation key id, and
312
+ `show` prints `"enrollment": "pending"` with no Plan. Approve the enrollment with that key id:
313
+
314
+ ```bash
315
+ uv run fdai-lifecycle-hub approve example --approver alice --installation-key-id <key-id>
316
+ uv run fdai-lifecycle-hub recompute example --catalog samples/catalog --key /tmp/fdai-hub/hub.pem
317
+ ```
318
+
319
+ `recompute` prints `"no-managed-entity"`, because the `core` Entity starts unmanaged. Tag-only
320
+ evidence keeps it unmanaged, so `manage` fails with `ownership_tag_only`. Proven ownership lets
321
+ `manage` succeed:
322
+
323
+ ```bash
324
+ uv run fdai-lifecycle-hub record-ownership example core samples/ownership-tag-only.json --operator bob
325
+ uv run fdai-lifecycle-hub manage example core samples/core-settings.json --operator bob
326
+ uv run fdai-lifecycle-hub record-ownership example core samples/ownership.json --operator bob
327
+ uv run fdai-lifecycle-hub manage example core samples/core-settings.json --operator bob
328
+ uv run fdai-lifecycle-hub recompute example --catalog samples/catalog --key /tmp/fdai-hub/hub.pem
329
+ uv run fdai-lifecycle-hub show example
330
+ ```
331
+
332
+ `recompute` now prints `"outcome": "issued"` with target `1.6.0`. Run it again and it prints
333
+ `"unchanged"`, because nothing changed.
334
+
335
+ Start the API in a second terminal with the same environment variable, then read the Plan as an
336
+ agent would:
337
+
338
+ ```bash
339
+ uv run fdai-lifecycle-hub serve
340
+ curl -s localhost:8090/v1/installations/example/plan | jq -r .plan.signed_payload | base64 -d | jq
341
+ ```
342
+
343
+ Hold the Plan with a suppression. While a suppression that covers the open Plan is active, the API
344
+ stops serving it and answers `204`, even before anyone recomputes. A suppression set to start later
345
+ takes effect at its start time. `recompute` then prints `"waiting"` with
346
+ `suppression_window_active`, and after `unsuppress` the next `recompute` issues a new Plan:
347
+
348
+ ```bash
349
+ uv run fdai-lifecycle-hub suppress example --minutes 60
350
+ uv run fdai-lifecycle-hub recompute example --catalog samples/catalog --key /tmp/fdai-hub/hub.pem
351
+ uv run fdai-lifecycle-hub unsuppress example
352
+ uv run fdai-lifecycle-hub recompute example --catalog samples/catalog --key /tmp/fdai-hub/hub.pem
353
+ ```
354
+
355
+ `--scope` takes `installation` (the default), `entity:<entity-id>`, or `plan:<plan-type>`. A
356
+ suppression covers a Plan only when its scope names the installation, that Plan's type, or one of
357
+ its entities, the same rule the shared constraint check uses.
358
+
359
+ Record the upgraded state, and the Hub reports that nothing is left to do:
360
+
361
+ ```bash
362
+ uv run fdai-lifecycle-hub record-state example samples/state-1.6.0.json
363
+ uv run fdai-lifecycle-hub recompute example --catalog samples/catalog --key /tmp/fdai-hub/hub.pem
364
+ curl -s -o /dev/null -w "%{http_code}\n" localhost:8090/v1/installations/example/plan
365
+ ```
366
+
367
+ `recompute` prints `"up-to-date"`, and the API answers `204`. Execution reports come from the
368
+ lifecycle agent (#1951), and `show` lists every report on the installation's Plans with its reason
369
+ code. [`test_lifecycle_loop.py`](../tests/test_lifecycle_loop.py) runs the agent against this API
370
+ end to end.
371
+
372
+ The database URL comes only from `FDAI_LIFECYCLE_HUB_DATABASE_URL`, so credentials stay out of
373
+ process arguments. For PostgreSQL, use the loopback database from `infra/local/docker-compose.yml`.
374
+ A catalog directory contains `releases/<release-id>.json` runtime release manifests,
375
+ `channels.json` (`{"<channel>": ["<release-id>", ...]}`), and an optional `recalls.json`.
376
+
377
+ ## Operator UI
378
+
379
+ [`ui/`](ui) is a small read-only Preact, Vite, and TypeScript single-page app: the installations
380
+ list, one installation's detail, one Plan's detail, and the pending-enrollment list and approval
381
+ screen. It calls only the [Operator UI API](#operator-ui-api) above, and the Hub serves it as a
382
+ static build from `ui/dist/` - there's no separate frontend server in production. It shares the
383
+ Console's `ui/calm-slate-tokens.css` and `ui/calm-slate-primitives.css` foundation but keeps its
384
+ own accent (`--lc-accent`, teal) and sidebar instead of Console's steel-blue lockup, so the two
385
+ stay visually distinguishable: the Hub sits outside every installation and keeps working when one
386
+ installation's Console doesn't (see the design note in #1945).
387
+
388
+ The installations list separates update status from reported health and supports filtering by
389
+ installation ID. Use **Refresh** to fetch a new snapshot; the UI does not stream agent state.
390
+ Installation and Plan details show every recorded constraint block, including its reason code
391
+ and details. Failed reads offer **Try again** without reloading the application. Loading
392
+ placeholders and enrollment submissions honor the reduced-motion preference.
393
+
394
+ For local development, run the Hub API from this directory as in
395
+ [Try it locally](#try-it-locally), then in a second terminal:
396
+
397
+ ```bash
398
+ cd ui
399
+ npm install
400
+ npm run dev
401
+ ```
402
+
403
+ Vite serves the UI on `http://127.0.0.1:5290` and proxies `/v1` and `/healthz` to the Hub API on
404
+ its default port, 8090. If that port is already in use, start the Hub with `serve --port 8099`
405
+ and run `FDAI_HUB_API_URL=http://127.0.0.1:8099 npm run dev`.
406
+ For a production-shaped check, build it and let the Hub serve the result:
407
+
408
+ ```bash
409
+ npm run build
410
+ cd ..
411
+ uv run fdai-lifecycle-hub serve
412
+ ```
413
+
414
+ `serve` picks up `ui/dist/` automatically when it exists; pass `--ui <path>` to serve a different
415
+ build, or omit the build entirely to run the Hub API alone. `npm test` runs the UI's Vitest unit
416
+ tests, and `npm run typecheck` runs strict TypeScript across the app and its tests.
417
+
418
+ ## Limits
419
+
420
+ - The API binds to loopback and has no caller authentication. Agent identity is Lifecycle I1 work.
421
+ - `migrate` creates the schema directly and refuses tables from an earlier Hub schema. Versioned
422
+ migrations replace it in Lifecycle I1.
423
+ - Catalog files are trusted as-is. Vendor signatures on Releases and recalls are Lifecycle I2 work.
424
+ - Signing keys are development keys from `dev-keygen` (#1947).
425
+ - The enrollment proof binds the request bytes to the installation key and a five-minute window,
426
+ not to a Hub-issued challenge or a Hub identity. Without caller authentication, a request signed
427
+ with any key can claim a free installation id, and rejecting it keeps the id. Every failed proof
428
+ also appends an audit record, so the API needs a rate limit before it leaves loopback.
429
+ Re-enrollment and rate limiting arrive with agent authentication in Lifecycle I1. The enrollment
430
+ screen exists, but whoever can reach the loopback API can approve or reject through it; Microsoft
431
+ Entra ID sign-in and key rotation for approvers are later stories.
432
+ - The Hub records ownership evidence as digests that the installation reports. It doesn't verify
433
+ the Foundation receipt signature or match it to the Terraform state. By design the installation
434
+ agent checks both locally; that check arrives with its ownership-evidence inputs in Lifecycle I1.
435
+ - Entity settings are stored and checked but don't change Plans yet. Configuration package import
436
+ and Hub commands are later stories.
437
+ - Suppressions live on the installation record without an actor. A failure hold records its origin
438
+ and failed Plan id. The design's revocable `lifecycle_command` record, with actor and expiry,
439
+ arrives with Hub commands.
440
+ - A failure hold lasts a fixed 24 hours, and repeated failures don't suppress the whole
441
+ installation yet. The Hub doesn't check that `applied` or `rolled-back` follows `apply-started`,
442
+ and it treats the reported-state digest as an opaque value.
443
+ - Entities aren't revisioned yet. `manage` replaces an Entity's settings, and the hash-chained audit
444
+ keeps who changed them, when, and the settings digest, but not the earlier content. Entity
445
+ revisions with `effective_from` arrive with versioned migrations.