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.
- fdai_lifecycle_hub-0.1.1/PKG-INFO +445 -0
- fdai_lifecycle_hub-0.1.1/README.md +428 -0
- fdai_lifecycle_hub-0.1.1/pyproject.toml +31 -0
- fdai_lifecycle_hub-0.1.1/samples/catalog/channels.json +7 -0
- fdai_lifecycle_hub-0.1.1/samples/catalog/releases/1.4.0.json +121 -0
- fdai_lifecycle_hub-0.1.1/samples/catalog/releases/1.5.0.json +121 -0
- fdai_lifecycle_hub-0.1.1/samples/catalog/releases/1.6.0.json +121 -0
- fdai_lifecycle_hub-0.1.1/samples/core-settings.json +10 -0
- fdai_lifecycle_hub-0.1.1/samples/enrollment.json +57 -0
- fdai_lifecycle_hub-0.1.1/samples/ownership-tag-only.json +3 -0
- fdai_lifecycle_hub-0.1.1/samples/ownership.json +5 -0
- fdai_lifecycle_hub-0.1.1/samples/state-1.6.0.json +14 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/__init__.py +7 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/adoption_policy.py +226 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/agent_artifacts.py +143 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/api.py +488 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/audit.py +72 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/bootstrap.py +253 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog.py +102 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_adoption.py +492 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_import.py +148 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_integrity.py +260 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_integrity_state.py +173 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_trust.py +125 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/catalog_tuf_assessment.py +101 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/cli.py +591 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration.py +70 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_consumer.py +108 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_api.py +152 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_contracts.py +144 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_models.py +96 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_store.py +511 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_import_trust.py +268 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_key_api.py +138 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_key_store.py +482 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/configuration_keys.py +221 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/database_inspection.py +297 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/domain.py +486 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/enrollment.py +69 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/entity.py +101 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/errors.py +95 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/execution_settings.py +75 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/execution_settings_api.py +74 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/inventory.py +53 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/local_channel_api.py +66 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/local_channels.py +239 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/mapping.py +209 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/migrations.py +210 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/models.py +313 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_ownership.py +112 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_ownership_api.py +269 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_proofs.py +90 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_transition.py +464 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/native_transition_reconciliation.py +582 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/operations.py +253 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/planning.py +421 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/policy/catalog_adoption.rego +92 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/remote.py +714 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/remote_auth.py +545 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/remote_catalog.py +969 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/reports.py +110 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/rows.py +67 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/schemas.py +370 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_api.py +199 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_start.py +193 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_ui/index.html +160 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_ui/setup.css +119 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/setup_ui/setup.js +370 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/signing.py +137 -0
- fdai_lifecycle_hub-0.1.1/src/fdai_lifecycle_hub/store.py +972 -0
- fdai_lifecycle_hub-0.1.1/tests/configuration_import_helpers.py +264 -0
- fdai_lifecycle_hub-0.1.1/tests/configuration_key_helpers.py +138 -0
- fdai_lifecycle_hub-0.1.1/tests/conftest.py +280 -0
- fdai_lifecycle_hub-0.1.1/tests/remote_auth_helpers.py +35 -0
- fdai_lifecycle_hub-0.1.1/tests/test_agent_artifacts.py +73 -0
- fdai_lifecycle_hub-0.1.1/tests/test_api.py +214 -0
- fdai_lifecycle_hub-0.1.1/tests/test_application_binding.py +172 -0
- fdai_lifecycle_hub-0.1.1/tests/test_application_release.py +301 -0
- fdai_lifecycle_hub-0.1.1/tests/test_apply_reports.py +517 -0
- fdai_lifecycle_hub-0.1.1/tests/test_bootstrap.py +279 -0
- fdai_lifecycle_hub-0.1.1/tests/test_catalog_adoption.py +702 -0
- fdai_lifecycle_hub-0.1.1/tests/test_catalog_rebinding.py +144 -0
- fdai_lifecycle_hub-0.1.1/tests/test_catalog_tuf_flow.py +846 -0
- fdai_lifecycle_hub-0.1.1/tests/test_catalog_tuf_remote.py +334 -0
- fdai_lifecycle_hub-0.1.1/tests/test_catalog_tuf_verification.py +465 -0
- fdai_lifecycle_hub-0.1.1/tests/test_cli.py +384 -0
- fdai_lifecycle_hub-0.1.1/tests/test_configuration_import.py +525 -0
- fdai_lifecycle_hub-0.1.1/tests/test_configuration_import_postgres.py +382 -0
- fdai_lifecycle_hub-0.1.1/tests/test_configuration_keys.py +614 -0
- fdai_lifecycle_hub-0.1.1/tests/test_configuration_keys_postgres.py +308 -0
- fdai_lifecycle_hub-0.1.1/tests/test_configuration_revision.py +371 -0
- fdai_lifecycle_hub-0.1.1/tests/test_cross_process_suppression.py +271 -0
- fdai_lifecycle_hub-0.1.1/tests/test_database_inspection.py +679 -0
- fdai_lifecycle_hub-0.1.1/tests/test_enrollment.py +431 -0
- fdai_lifecycle_hub-0.1.1/tests/test_enrollment_signing.py +20 -0
- fdai_lifecycle_hub-0.1.1/tests/test_entity.py +80 -0
- fdai_lifecycle_hub-0.1.1/tests/test_execution_settings.py +332 -0
- fdai_lifecycle_hub-0.1.1/tests/test_first_install.py +318 -0
- fdai_lifecycle_hub-0.1.1/tests/test_hub_images.py +314 -0
- fdai_lifecycle_hub-0.1.1/tests/test_hubs_postgres.py +324 -0
- fdai_lifecycle_hub-0.1.1/tests/test_inventory_api.py +94 -0
- fdai_lifecycle_hub-0.1.1/tests/test_local_channels.py +410 -0
- fdai_lifecycle_hub-0.1.1/tests/test_native_ownership.py +296 -0
- fdai_lifecycle_hub-0.1.1/tests/test_native_proofs.py +329 -0
- fdai_lifecycle_hub-0.1.1/tests/test_native_transition.py +1723 -0
- fdai_lifecycle_hub-0.1.1/tests/test_no_azure_credentials.py +67 -0
- fdai_lifecycle_hub-0.1.1/tests/test_operations.py +74 -0
- fdai_lifecycle_hub-0.1.1/tests/test_operator_api.py +370 -0
- fdai_lifecycle_hub-0.1.1/tests/test_planning.py +670 -0
- fdai_lifecycle_hub-0.1.1/tests/test_prepared_concurrency.py +118 -0
- fdai_lifecycle_hub-0.1.1/tests/test_prepared_migration.py +353 -0
- fdai_lifecycle_hub-0.1.1/tests/test_release_configuration_schema.py +246 -0
- fdai_lifecycle_hub-0.1.1/tests/test_release_hub_consumer.py +207 -0
- fdai_lifecycle_hub-0.1.1/tests/test_remote_auth.py +1904 -0
- fdai_lifecycle_hub-0.1.1/tests/test_remote_bootstrap.py +51 -0
- fdai_lifecycle_hub-0.1.1/tests/test_remote_catalog.py +441 -0
- fdai_lifecycle_hub-0.1.1/tests/test_samples.py +76 -0
- fdai_lifecycle_hub-0.1.1/tests/test_schema_range.py +51 -0
- fdai_lifecycle_hub-0.1.1/tests/test_setup_api.py +219 -0
- fdai_lifecycle_hub-0.1.1/tests/test_setup_start.py +160 -0
- fdai_lifecycle_hub-0.1.1/tests/test_standalone_remote.py +195 -0
- fdai_lifecycle_hub-0.1.1/tests/test_store.py +556 -0
- fdai_lifecycle_hub-0.1.1/tests/test_suppressions.py +122 -0
- fdai_lifecycle_hub-0.1.1/tests/test_ui_static.py +64 -0
- fdai_lifecycle_hub-0.1.1/tests/tuf_helpers.py +139 -0
- fdai_lifecycle_hub-0.1.1/ui/.gitignore +6 -0
- fdai_lifecycle_hub-0.1.1/ui/index.html +15 -0
- fdai_lifecycle_hub-0.1.1/ui/package-lock.json +2568 -0
- fdai_lifecycle_hub-0.1.1/ui/package.json +27 -0
- fdai_lifecycle_hub-0.1.1/ui/playwright.config.ts +11 -0
- fdai_lifecycle_hub-0.1.1/ui/public/brand/fdai-favicon.svg +17 -0
- fdai_lifecycle_hub-0.1.1/ui/public/brand/fdai-logo.png +0 -0
- fdai_lifecycle_hub-0.1.1/ui/src/api.ts +432 -0
- fdai_lifecycle_hub-0.1.1/ui/src/app.tsx +53 -0
- fdai_lifecycle_hub-0.1.1/ui/src/auth.css +28 -0
- fdai_lifecycle_hub-0.1.1/ui/src/auth.test.ts +87 -0
- fdai_lifecycle_hub-0.1.1/ui/src/auth.ts +140 -0
- fdai_lifecycle_hub-0.1.1/ui/src/bindable-installations.test.ts +36 -0
- fdai_lifecycle_hub-0.1.1/ui/src/bindable-installations.ts +24 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/application-binding.tsx +92 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/async-boundary.tsx +38 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/badge.tsx +55 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/constraint-results.tsx +42 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/execution-settings.tsx +134 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/link.tsx +22 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/nav-icons.tsx +40 -0
- fdai_lifecycle_hub-0.1.1/ui/src/components/nav.tsx +66 -0
- fdai_lifecycle_hub-0.1.1/ui/src/constraint-status.test.ts +47 -0
- fdai_lifecycle_hub-0.1.1/ui/src/constraint-status.ts +25 -0
- fdai_lifecycle_hub-0.1.1/ui/src/enrollment-request.test.ts +72 -0
- fdai_lifecycle_hub-0.1.1/ui/src/enrollment-requester.test.ts +32 -0
- fdai_lifecycle_hub-0.1.1/ui/src/enrollment-requester.ts +26 -0
- fdai_lifecycle_hub-0.1.1/ui/src/execution-settings.test.ts +143 -0
- fdai_lifecycle_hub-0.1.1/ui/src/format.test.ts +45 -0
- fdai_lifecycle_hub-0.1.1/ui/src/format.ts +51 -0
- fdai_lifecycle_hub-0.1.1/ui/src/hooks/use-async.ts +45 -0
- fdai_lifecycle_hub-0.1.1/ui/src/local-channels.test.ts +10 -0
- fdai_lifecycle_hub-0.1.1/ui/src/main.tsx +10 -0
- fdai_lifecycle_hub-0.1.1/ui/src/plan-status.test.ts +21 -0
- fdai_lifecycle_hub-0.1.1/ui/src/plan-status.ts +10 -0
- fdai_lifecycle_hub-0.1.1/ui/src/report-status.test.ts +34 -0
- fdai_lifecycle_hub-0.1.1/ui/src/report-status.ts +29 -0
- fdai_lifecycle_hub-0.1.1/ui/src/router.test.ts +80 -0
- fdai_lifecycle_hub-0.1.1/ui/src/router.ts +90 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/catalog.tsx +95 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/enrollments.test.tsx +12 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/enrollments.tsx +337 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/installation.tsx +227 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/installations.tsx +114 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/inventory.tsx +99 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/local-channels.tsx +119 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/plan.tsx +131 -0
- fdai_lifecycle_hub-0.1.1/ui/src/routes/updates.tsx +262 -0
- fdai_lifecycle_hub-0.1.1/ui/src/styles.css +837 -0
- fdai_lifecycle_hub-0.1.1/ui/src/update-status.test.ts +280 -0
- fdai_lifecycle_hub-0.1.1/ui/src/update-status.ts +270 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/application-binding.spec.ts +148 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/bootstrap-session.integration.spec.ts +47 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/bootstrap.config.ts +11 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/bootstrap.spec.ts +332 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/catalog.spec.ts +245 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/enrollment-agent-request.spec.ts +65 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/enrollment-request.spec.ts +167 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/execution-settings.spec.ts +240 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/inventory.spec.ts +96 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/standalone.spec.ts +75 -0
- fdai_lifecycle_hub-0.1.1/ui/tests/browser/updates.spec.ts +278 -0
- fdai_lifecycle_hub-0.1.1/ui/tsconfig.json +26 -0
- fdai_lifecycle_hub-0.1.1/ui/tsconfig.tests.json +8 -0
- fdai_lifecycle_hub-0.1.1/ui/vite.config.ts +31 -0
- 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.
|