agcoord 0.7.2__tar.gz → 0.7.4__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.
- {agcoord-0.7.2 → agcoord-0.7.4}/CHANGELOG.md +39 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/Cargo.lock +1 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/Cargo.toml +1 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/MANIFEST.in +1 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/PKG-INFO +1 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/conformance/manifest-v3.json +4 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/coordinator.md +14 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/native_broker.md +35 -9
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/native_host.md +76 -15
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/releasing.md +28 -9
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/troubleshooting.md +6 -3
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/recovery.rs +143 -53
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/store.rs +2 -2
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/host_recovery.rs +288 -1
- agcoord-0.7.4/native/build-toolchain.json +5 -0
- agcoord-0.7.4/packaging/apparmor/usr.libexec.agcoord.agcoord-broker +161 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/build-native-broker +32 -3
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-native-host-package +8 -4
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-native-reproducible +19 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/install-native-host +12 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/native-source-id +2 -1
- agcoord-0.7.4/scripts/native-toolchain +75 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/test-native-host-enforcement +29 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/test-native-host-recovery +52 -2
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/verify-release-candidate +13 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/__init__.py +1 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/native_host.py +40 -5
- agcoord-0.7.4/src/agcoord/native_host_pin.json +5 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/PKG-INFO +1 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/SOURCES.txt +3 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_conformance_gate.py +1 -1
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_assets.py +69 -7
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_recovery.py +125 -5
- agcoord-0.7.4/tests/test_native_toolchain.py +196 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_release_candidate.py +19 -1
- agcoord-0.7.2/packaging/apparmor/usr.libexec.agcoord.agcoord-broker +0 -74
- agcoord-0.7.2/src/agcoord/native_host_pin.json +0 -5
- {agcoord-0.7.2 → agcoord-0.7.4}/AGENTS.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/LICENSE +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/README.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/agents.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/assets/README.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/assets/agcoord-gourd-mascot.png +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/changelog.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/comparison.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/concepts.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/conformance.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/contributing.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/guardrails.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/index.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/native_migration.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/overview.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/quickstart.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/session_handoff_format.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/docs/why.md +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/THIRD_PARTY_LICENSES.tsv +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/Cargo.toml +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/build.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/broker.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/cgroup.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/error.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/host.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/main.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/platform.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/project_quota.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/resources.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/worker.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/client_compatibility.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/host_deployment.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/identity.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/scheduler.rs +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/packaging/systemd/agcoord-broker.service +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/pyproject.toml +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/rust-toolchain.toml +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/audit-native-broker +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/build-native-host-package +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-conformance +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-native-licenses +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/setup.cfg +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/__main__.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/avoid.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/cli.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/config.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/frame.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/github.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/github_release.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/land.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/merge.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/native_client.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/py.typed +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/pytest_xdist.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/queue.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/resources.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/tui.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/dependency_links.txt +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/entry_points.txt +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/requires.txt +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/top_level.txt +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/conftest.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_avoid.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_cli.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_distribution.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_github_adapter.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_host_config.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_leases.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_merge.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_client.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_download.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_upgrade.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_pytest_xdist.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_queue.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_resource_bindings.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_tui.py +0 -0
- {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_user_broker_install.py +0 -0
|
@@ -3,7 +3,45 @@
|
|
|
3
3
|
All notable user-facing changes to AGCoord are recorded here. Versions follow semantic
|
|
4
4
|
versioning; dates use ISO 8601.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## 0.7.4 — 2026-10-21
|
|
7
|
+
|
|
8
|
+
- Make the pinned native broker digest a property of the source and a declared toolchain. The
|
|
9
|
+
C compiler and musl versions that build the bundled SQLite are now declared in
|
|
10
|
+
`native/build-toolchain.json`, included in the native source identity, and recorded in broker
|
|
11
|
+
provenance. `build-native-broker` refuses an undeclared C toolchain, and both
|
|
12
|
+
`check-native-reproducible` and the release-candidate verifier refuse a broker built with one.
|
|
13
|
+
Native CI and tag builds run on a fixed `ubuntu-24.04` runner with exact musl packages instead
|
|
14
|
+
of `ubuntu-latest`, whose image change had produced different broker bytes from identical
|
|
15
|
+
source (#250).
|
|
16
|
+
|
|
17
|
+
- Let admitted jobs build unprivileged Bubblewrap sandboxes to isolate their own test
|
|
18
|
+
processes, while every other submitted program keeps the user-namespace denial. The packaged
|
|
19
|
+
AppArmor policy enters a permissive `agcoord-admitted` anchor before `no_new_privs`; the
|
|
20
|
+
anchor's exec rule then stacks a restricting `agcoord-job` profile at `execve` (denying
|
|
21
|
+
capabilities and user-namespace creation), so submitted code runs as
|
|
22
|
+
`agcoord-admitted//&agcoord-job`. Stacking onto the retained anchor is always permitted under
|
|
23
|
+
`no_new_privs`, and the transition fails closed — the command cannot start — if `agcoord-job`
|
|
24
|
+
is not loaded. A static rule routes the fixed system executable `/usr/bin/bwrap` to
|
|
25
|
+
`agcoord-admitted//&agcoord-bwrap`, which may create a user namespace and use in-namespace
|
|
26
|
+
capabilities to assemble a sandbox, and routes everything it launches to the sticky, capability-
|
|
27
|
+
and namespace-denied `agcoord-bwrap-child`. The widening needs no per-job flag, operator
|
|
28
|
+
allowlist, or executable digest, so an ordinary Bubblewrap package upgrade keeps working; a
|
|
29
|
+
program of that name reached through another mount namespace is named under a disconnected
|
|
30
|
+
prefix and never matches. Activation refuses unless all six domains are loaded in enforce mode.
|
|
31
|
+
The observable admitted label an admitted job reports changes from `agcoord-admitted (enforce)`
|
|
32
|
+
to `agcoord-admitted//&agcoord-job (enforce)`. Bubblewrap stays an optional host package: the
|
|
33
|
+
enforced-host probe exercises the sandbox widening when `/usr/bin/bwrap` is installed and
|
|
34
|
+
reports that it was not exercised otherwise.
|
|
35
|
+
|
|
36
|
+
## 0.7.3 — 2026-10-02
|
|
37
|
+
|
|
38
|
+
- Recover a normal drain stranded behind a dead or unstartable managed broker without losing
|
|
39
|
+
accepted work or maintenance intent. Host repair preserves the original drain ID and
|
|
40
|
+
metadata, keeps submissions continuously guarded, and restores the drain after the bound
|
|
41
|
+
replacement-host proof. Accepted queued jobs finish normally; new submissions remain blocked
|
|
42
|
+
until explicit resume with the original drain ID after maintenance. Completion retries retain
|
|
43
|
+
that restoration result and audit instead of always opening the spool; malformed metadata
|
|
44
|
+
and live or ambiguous workers refuse, and proof attempts are never reset (#244).
|
|
7
45
|
|
|
8
46
|
## 0.7.2 — 2026-10-01
|
|
9
47
|
|
|
@@ -7,6 +7,7 @@ include rust-toolchain.toml
|
|
|
7
7
|
recursive-include conformance *.json
|
|
8
8
|
recursive-include docs *.md *.png
|
|
9
9
|
recursive-include native *.rs *.toml *.tsv
|
|
10
|
+
include native/build-toolchain.json
|
|
10
11
|
recursive-include packaging *
|
|
11
12
|
include scripts/*native*
|
|
12
13
|
include scripts/check-conformance
|
|
@@ -385,7 +385,10 @@
|
|
|
385
385
|
"host_recovery::a_successful_unenforced_proof_cannot_complete_or_resume_recovery",
|
|
386
386
|
"host_recovery::mismatched_recovery_identity_and_changed_helper_preserve_the_guard",
|
|
387
387
|
"host_recovery::failed_proofs_retry_with_the_same_identity_and_park_at_the_attempt_bound",
|
|
388
|
-
"host_recovery::parking_cancels_only_the_active_proof_and_allows_a_bounded_retry"
|
|
388
|
+
"host_recovery::parking_cancels_only_the_active_proof_and_allows_a_bounded_retry",
|
|
389
|
+
"host_recovery::stranded_normal_drain_can_enter_recovery_without_releasing_accepted_work",
|
|
390
|
+
"host_recovery::verified_recovery_restores_original_drain_and_replays_without_an_owner",
|
|
391
|
+
"host_recovery::malformed_original_drain_cannot_be_read_or_reused_as_a_recovery_guard"
|
|
389
392
|
]
|
|
390
393
|
}
|
|
391
394
|
}
|
|
@@ -99,6 +99,20 @@ in one transaction and returns the spool to `open`; a wrong or stale identifier
|
|
|
99
99
|
submissions. Migration and rollback preserve the current marker across the protocol boundary
|
|
100
100
|
and never resurrect a marker already removed by a successful resume.
|
|
101
101
|
|
|
102
|
+
A normal drain stranded behind a dead or unstartable managed owner may be repaired through
|
|
103
|
+
[guarded managed host recovery](native_host.md#guarded-recovery-when-the-outgoing-broker-cannot-drain).
|
|
104
|
+
The original drain ID, state, reason, and start time remain durable maintenance
|
|
105
|
+
intent. Submission guards stay continuous while the temporary recovery guard admits only its
|
|
106
|
+
bound host proof. After verification, recovery atomically restores the original drain;
|
|
107
|
+
accepted queued work finishes normally, and the owner yields after the last accepted row
|
|
108
|
+
becomes terminal. New submissions remain refused until explicit `resume` with the exact
|
|
109
|
+
original drain ID after the intended maintenance succeeds. Repair and proof do not authorize that resume.
|
|
110
|
+
Recovery completion and its retry receipt retain the original drain audit rather than always
|
|
111
|
+
reporting `open`. Invalid maintenance metadata or live/ambiguous worker identities refuse;
|
|
112
|
+
retries never reset proof attempts. Repeating a stranded drain alone cannot make an
|
|
113
|
+
unstartable broker progress, and raw spool edits or cancellation of accepted work are not a
|
|
114
|
+
repair path.
|
|
115
|
+
|
|
102
116
|
## Repository lanes and resources
|
|
103
117
|
|
|
104
118
|
Each submission belongs to a stable repository lane and records its resolved worktree. A
|
|
@@ -251,6 +251,17 @@ start only while the marker is `draining` and live rows remain; it adopts or saf
|
|
|
251
251
|
those rows and completes the handoff. A `drained` marker prevents activity writes and ordinary
|
|
252
252
|
autostart until exact-ID `resume` holds the owner lock and removes every guard.
|
|
253
253
|
|
|
254
|
+
Guarded managed-host recovery may temporarily suspend a stranded normal drain's accepted-job
|
|
255
|
+
admission to verify a pinned replacement, without relaxing its submission guards. It retains
|
|
256
|
+
the original drain ID, state, reason, and start time in its durable recovery audit.
|
|
257
|
+
Successful host proof restores that drain atomically instead of opening submissions: accepted
|
|
258
|
+
queued rows finish normally and the owner yields when the drain completes. Only explicit
|
|
259
|
+
`resume` with the exact original drain ID after maintenance reopens submissions. Completion retries preserve
|
|
260
|
+
the same restoration result and never reset proof attempts or perform an implicit resume.
|
|
261
|
+
Malformed maintenance metadata and live or ambiguous worker identities refuse repair. The
|
|
262
|
+
[host recovery contract](native_host.md#guarded-recovery-when-the-outgoing-broker-cannot-drain)
|
|
263
|
+
defines the verification-only admission, bounded failure, and parking rules.
|
|
264
|
+
|
|
254
265
|
### Implemented worker boundary
|
|
255
266
|
|
|
256
267
|
The native owner now forks its launcher path directly without an internal command-line mode.
|
|
@@ -264,7 +275,11 @@ Between the releases, the child resets inherited broker signal handlers and comp
|
|
|
264
275
|
and scratch setup. When it inherited the managed `agcoord-broker` AppArmor domain, it makes the
|
|
265
276
|
one-way transition into `agcoord-admitted` and verifies that domain before clearing effective,
|
|
266
277
|
permitted, inheritable, and ambient capabilities, setting `no_new_privs`, and verifying those
|
|
267
|
-
values from `/proc/self/status`.
|
|
278
|
+
values from `/proc/self/status`. The `agcoord-admitted` anchor's exec rule then confines the
|
|
279
|
+
submitted command: its `execve` transitions, at exec, to the stack `agcoord-admitted//&agcoord-job`
|
|
280
|
+
(`agcoord-job` is the restriction that denies namespace creation and capabilities). Stacking onto
|
|
281
|
+
the retained anchor is always allowed under `no_new_privs`, and the exec fails closed if
|
|
282
|
+
`agcoord-job` is not loaded. It closes every inherited descriptor except standard I/O and the
|
|
268
283
|
two private channels, reports the token-bound setup result, waits for final release, closes those
|
|
269
284
|
channels, verifies the final descriptor set, and calls `execve`. Under an applied tmpfs policy it
|
|
270
285
|
instead forks once more, supervises the command from those namespaces while sampling the mount for
|
|
@@ -434,14 +449,25 @@ this resource boundary does not prevent the command from explicitly naming paths
|
|
|
434
449
|
The setup-only `agcoord-broker` AppArmor profile attaches to the immutable root-owned executable,
|
|
435
450
|
not to a user-editable service directive. Its public command parser exposes no worker mode or
|
|
436
451
|
arbitrary setup-domain exec path. The authenticated worker makes a one-way transition into
|
|
437
|
-
`agcoord-admitted`
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
452
|
+
`agcoord-admitted` and sets `no_new_privs`; `agcoord-admitted` is therefore the kernel's recorded
|
|
453
|
+
anchor, which every later transition retains. The anchor's `/** px -> agcoord-admitted//&agcoord-job`
|
|
454
|
+
exec rule confines submitted code at `execve`: the command runs as `agcoord-admitted//&agcoord-job`.
|
|
455
|
+
Stacking onto the retained anchor is always allowed under `no_new_privs` (it only removes
|
|
456
|
+
permission), and the exec fails closed if `agcoord-job` is not loaded; the anchor alone permits
|
|
457
|
+
user-namespace creation, but submitted code always runs with `agcoord-job` stacked, which denies
|
|
458
|
+
it. When admitted work invokes the broker, AppArmor substitutes `agcoord-broker-client` for
|
|
459
|
+
`agcoord-job` on the anchor; adding confinement is compatible with `no_new_privs` and cannot regain
|
|
460
|
+
setup permission. The one deliberate widening is the fixed system Bubblewrap
|
|
461
|
+
executable `/usr/bin/bwrap`, which transitions to `agcoord-admitted//&agcoord-bwrap` and may build
|
|
462
|
+
an unprivileged sandbox; everything it launches runs as
|
|
463
|
+
`agcoord-admitted//&agcoord-bwrap-child//&agcoord-job` (AppArmor's sorted rendering of the stack),
|
|
464
|
+
which is capability- and user-namespace-denied and sticky across further execs, so no other
|
|
465
|
+
submitted code gains namespace permission. Bubblewrap is an optional host package; without
|
|
466
|
+
`/usr/bin/bwrap` these domains are inert. Every one of the six domains is compiled in explicit enforce mode rather than
|
|
467
|
+
Ubuntu 24.04's unconfined `default_allow` mode. A failure to verify the loaded setup profile, the
|
|
468
|
+
service cgroup, the global restriction, or the backend namespace probe makes required work
|
|
469
|
+
unavailable; a missing `agcoord-job` profile makes the submitted command's exec transition fail
|
|
470
|
+
closed so the command cannot start.
|
|
445
471
|
The complete package and transition contract is in [the native host runbook](native_host.md).
|
|
446
472
|
|
|
447
473
|
## Compatibility and migration
|
|
@@ -318,7 +318,9 @@ machine to the managed host later needs an empty default spool: drain the user s
|
|
|
318
318
|
Run the shipped probe as an ordinary coordinated job with exactly one CPU unit; do not invoke
|
|
319
319
|
it from a nested coordinator. It proves the admitted AppArmor transition, user-namespace
|
|
320
320
|
denial, cgroup namespace root, exact `cpu.max`, release owner identity, and the restricted
|
|
321
|
-
profile reached when admitted work invokes the broker.
|
|
321
|
+
profile reached when admitted work invokes the broker. When `/usr/bin/bwrap` is installed it also
|
|
322
|
+
proves that Bubblewrap can build an unprivileged sandbox whose processes keep the
|
|
323
|
+
capability- and user-namespace-denied restriction. It also calls the installed Python
|
|
322
324
|
client's operation-specific callback by showing the exact admitted run while it is live; this
|
|
323
325
|
guards the complete `agc`-to-native path used by land reporting and pytest-xdist child leases.
|
|
324
326
|
The final assertion proves that the durable public receipt records the enforced allocation:
|
|
@@ -341,14 +343,45 @@ the quota when short parallel bursts are rounded upward.
|
|
|
341
343
|
The setup-only `agcoord-broker` profile attaches only to the fixed root-owned executable; it is
|
|
342
344
|
not selected by the user-editable systemd unit. The public binary has no internal-worker or
|
|
343
345
|
arbitrary setup-domain exec command. The broker's authenticated in-process worker makes a one-way
|
|
344
|
-
transition into `agcoord-admitted`, verifies it,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
admitted
|
|
348
|
-
`
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
346
|
+
transition into `agcoord-admitted`, verifies it, clears capabilities, and sets `no_new_privs`
|
|
347
|
+
before release. The `agcoord-admitted` anchor is the domain the kernel records at `no_new_privs`,
|
|
348
|
+
so every later transition must retain it. The anchor never runs submitted code as itself: its
|
|
349
|
+
`/** px -> agcoord-admitted//&agcoord-job` exec rule means every program it runs transitions, at
|
|
350
|
+
`execve`, to the stack `agcoord-admitted//&agcoord-job`. Stacking a profile onto the retained
|
|
351
|
+
anchor is always permitted under `no_new_privs` (it only removes permission), and the transition
|
|
352
|
+
fails closed — the command cannot start — if `agcoord-job` is not loaded. `agcoord-job` is the
|
|
353
|
+
step that confines submitted code: the anchor alone permits user-namespace creation and
|
|
354
|
+
capabilities, but submitted code always runs as `agcoord-admitted//&agcoord-job`, and `agcoord-job`
|
|
355
|
+
— like the inner profile of every other stack — denies both. When admitted work invokes the fixed
|
|
356
|
+
broker, AppArmor substitutes `agcoord-broker-client` for `agcoord-job` on the anchor; this keeps
|
|
357
|
+
the anchor without requesting a replacement domain after `no_new_privs`.
|
|
358
|
+
|
|
359
|
+
One exec is widened on purpose: when admitted work runs the fixed system Bubblewrap executable
|
|
360
|
+
`/usr/bin/bwrap`, AppArmor transitions it to `agcoord-admitted//&agcoord-bwrap`, which may create a
|
|
361
|
+
user namespace and use capabilities inside that namespace to assemble and pivot into an
|
|
362
|
+
unprivileged sandbox. This is a static rule in the packaged policy, not a per-job flag, operator
|
|
363
|
+
allowlist, or executable digest, so an ordinary distribution upgrade of Bubblewrap keeps working.
|
|
364
|
+
Everything `/usr/bin/bwrap` launches transitions to the stack of `agcoord-admitted`,
|
|
365
|
+
`agcoord-job`, and `agcoord-bwrap-child`, which denies capabilities and user-namespace creation and
|
|
366
|
+
stays confined across every further exec, so the sandbox builder cannot hand its namespace
|
|
367
|
+
permission to arbitrary code — the pattern of Ubuntu's `bwrap-userns-restrict` profile. AppArmor
|
|
368
|
+
renders a stacked label with its profiles in sorted order, so a sandboxed process reports
|
|
369
|
+
`agcoord-admitted//&agcoord-bwrap-child//&agcoord-job (enforce)`. Only the fixed `/usr/bin/bwrap`
|
|
370
|
+
path is widened: a program of that name reached through another mount namespace is named under a
|
|
371
|
+
distinct disconnected prefix and never matches the rule, and submitted code that is not
|
|
372
|
+
`/usr/bin/bwrap` still cannot create a user namespace at all. The `/usr/bin/bwrap` transition keeps
|
|
373
|
+
the environment (matching the upstream profile), so the sandbox sees the job's variables such as
|
|
374
|
+
`TMPDIR` unless Bubblewrap is told otherwise. All six profiles use explicit enforce mode and broad
|
|
375
|
+
enumerated host permissions; `default_allow` is not accepted because Ubuntu 24.04 implements it as
|
|
376
|
+
an unconfined profile that does not apply these denials. Activation fails closed unless every one of
|
|
377
|
+
the six domains is loaded in enforce mode.
|
|
378
|
+
|
|
379
|
+
Bubblewrap itself is an optional host package. AGCoord neither requires nor installs it; without
|
|
380
|
+
`/usr/bin/bwrap` the `agcoord-bwrap` domains are inert and every other boundary is unchanged.
|
|
381
|
+
Install it (`sudo apt-get install bubblewrap`) only on hosts whose admitted jobs build sandboxes.
|
|
382
|
+
The enforced-host probe exercises the widening when `/usr/bin/bwrap` is present and prints
|
|
383
|
+
`bubblewrap sandbox widening verified`; otherwise it prints that the widening was not exercised
|
|
384
|
+
and still proves the user-namespace denial.
|
|
352
385
|
|
|
353
386
|
Within the admitted user namespace, `stat(2)` reports the host-root-owned installed binary with
|
|
354
387
|
the overflow UID because host root is intentionally absent from the one-entry identity map. The
|
|
@@ -410,6 +443,12 @@ recovery refuses a live owner, live or ambiguous recorded workers, unavailable i
|
|
|
410
443
|
evidence, and an invalid package. It never substitutes a forced activation or deletion of the
|
|
411
444
|
spool. Normal `agc host upgrade` and its drain flow remain unchanged.
|
|
412
445
|
|
|
446
|
+
An existing normal drain is maintenance intent, not permission to reopen submissions. Recovery
|
|
447
|
+
preserves its original drain ID, state, reason, and start time as durable audit
|
|
448
|
+
metadata within the shared protocol-5 recovery contract. Its submission guards remain continuous
|
|
449
|
+
while the recovery guard temporarily blocks ordinary accepted-job admission for host verification. Malformed original maintenance metadata
|
|
450
|
+
refuses recovery; neither a retry nor repair may replace its identity or reset proof attempts.
|
|
451
|
+
|
|
413
452
|
The command stages the verified replacement and stops the failed service. The staged native
|
|
414
453
|
holder takes the exclusive owner/maintenance lock, proves every recorded worker identity is
|
|
415
454
|
gone, and establishes a durable `recovery-` ID followed by 12 hexadecimal digits. It holds the
|
|
@@ -422,8 +461,15 @@ The durable guard blocks new submissions and ordinary queued-job admission throu
|
|
|
422
461
|
and restart. Recovery admits only the exact bound enforcement helper, with `cpu=1` and
|
|
423
462
|
`jobs=1`, while ordinary work stays blocked. Completion requires that proof to pass with exit
|
|
424
463
|
status zero, a receipt with requested and applied CPU both 1 and observed peak CPU at least 1,
|
|
425
|
-
the unchanged helper digest, and no running rows. Only successful completion
|
|
426
|
-
|
|
464
|
+
the unchanged helper digest, and no running rows. Only successful completion ends the
|
|
465
|
+
verification-only guard. If no normal drain existed, completion returns the spool to `open`.
|
|
466
|
+
If recovery inherited a normal drain, completion atomically restores that original drain and
|
|
467
|
+
its submission guards. Accepted queued jobs then finish through their normal state machines;
|
|
468
|
+
proven-dead running rows remain interrupted. The owner reaches `drained` and yields when no
|
|
469
|
+
accepted work remains. New submissions stay blocked until explicit `agc resume` with the exact
|
|
470
|
+
original drain ID after the intended maintenance succeeds. Host proof never performs that
|
|
471
|
+
resume. A process exit, service restart, normal `agc drain`, or `agc resume` cannot clear an
|
|
472
|
+
active recovery guard.
|
|
427
473
|
|
|
428
474
|
If the verified replacement is already running under the same active recovery guard, rerunning
|
|
429
475
|
`agc host recover` continues that guard instead of stopping or reactivating the service. The
|
|
@@ -432,7 +478,8 @@ a mismatched guard still refuses. Continuation reuses a queued, running, or pass
|
|
|
432
478
|
that recovery is verifying; it does not submit a duplicate or spend another proof attempt.
|
|
433
479
|
|
|
434
480
|
The native hold, status, proof, completion, and park operations retain the exact recovery ID,
|
|
435
|
-
replacement identity, helper path and digest, proof job ID, attempt count, phase,
|
|
481
|
+
replacement identity, helper path and digest, proof job ID, attempt count, phase, incident,
|
|
482
|
+
and any original drain metadata. The completed recovery audit retains that original drain.
|
|
436
483
|
Each host phase and enforcement proof is bounded to 120 seconds; owner acquisition and dead-row
|
|
437
484
|
settlement waits are each bounded to 30 seconds. At most three proof attempts are allowed
|
|
438
485
|
across retries. Retrying must use the same exact verified replacement package and helper path;
|
|
@@ -447,9 +494,23 @@ recovery ID and address the reported incident before retrying the same package.
|
|
|
447
494
|
|
|
448
495
|
Completion retries up to three times with the exact recovery ID. If completion committed but
|
|
449
496
|
its reply was lost, the native operation validates the retained completed identity and passed
|
|
450
|
-
CPU proof, then returns the same
|
|
451
|
-
|
|
452
|
-
|
|
497
|
+
CPU proof, then returns the same completion receipt without another proof or recovery
|
|
498
|
+
operation. The completion receipt carries `state`, `recovery_id`, `proof_run_id`, and `drain_id`.
|
|
499
|
+
Without an inherited drain, its state is `open` and `drain_id` is null. With an inherited drain,
|
|
500
|
+
it retains that original ID and reports `draining` if accepted work remains, or `drained` if
|
|
501
|
+
none remains. Completion retries return that recorded result and never implicitly resume it.
|
|
502
|
+
The public host result reports service `verified` for a retained drain, since the broker may
|
|
503
|
+
finish the accepted work and yield before the command returns; `active` applies to recovery
|
|
504
|
+
that opens the spool.
|
|
505
|
+
If completion still cannot be confirmed, inspect the durable recovery and original drain
|
|
506
|
+
receipts. Do not assume submissions reopened or that the verification-only guard remains.
|
|
507
|
+
|
|
508
|
+
A stranded drain has an original marker plus accepted queued or running rows, while the
|
|
509
|
+
outgoing broker is dead or cannot start. Repeating the normal drain returns the same marker
|
|
510
|
+
but cannot make those rows progress. Earlier recovery refused this state with `normal
|
|
511
|
+
maintenance is active; recovery cannot replace its identity`. Use the verified recovery path
|
|
512
|
+
above; retain both IDs and the original maintenance metadata. Raw database edits, premature
|
|
513
|
+
resume, or cancelling accepted work are not repair workarounds.
|
|
453
514
|
|
|
454
515
|
### Normal restart and rollback
|
|
455
516
|
|
|
@@ -23,10 +23,19 @@ has one target: `x86_64-unknown-linux-musl`. Other architectures remain unsuppor
|
|
|
23
23
|
have a native CI runner and the same kernel conformance coverage.
|
|
24
24
|
|
|
25
25
|
Build inputs are pinned by `rust-toolchain.toml`, `Cargo.lock`, exact direct dependency
|
|
26
|
-
requirements, and the source digest produced by
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
requirements, `native/build-toolchain.json`, and the source digest produced by
|
|
27
|
+
`scripts/native-source-id`. The C compiler and musl headers compile the bundled SQLite, so they
|
|
28
|
+
are build inputs too: `native/build-toolchain.json` declares the exact C compiler version line and
|
|
29
|
+
musl version, and it is part of the source digest. `scripts/native-toolchain describe CC` reports
|
|
30
|
+
the toolchain a compiler provides. `build-native-broker` refuses an undeclared C toolchain, and both
|
|
31
|
+
`check-native-reproducible` and the release-candidate verifier refuse a broker whose provenance
|
|
32
|
+
records one, so a different compiler or musl can never silently change a pinned digest. Changing the
|
|
33
|
+
C toolchain is a deliberate source change: update the declaration, which gives a new build
|
|
34
|
+
identity, and re-pin. The artifact embeds the source digest as its build identity and reports
|
|
35
|
+
protocol, implementation, target, and bundled SQLite version through `identity --json`. The
|
|
36
|
+
declared toolchain is Ubuntu 24.04's: install Rust 1.94.1 with the declared musl target and
|
|
37
|
+
`musl=1.2.4-2 musl-dev=1.2.4-2 musl-tools=1.2.4-2` on Ubuntu 24.04, as the native CI and tag
|
|
38
|
+
jobs do on their fixed `ubuntu-24.04` runner, then run:
|
|
30
39
|
|
|
31
40
|
```bash
|
|
32
41
|
./scripts/build-native-broker
|
|
@@ -40,7 +49,8 @@ Ubuntu's `musl-tools`, then run:
|
|
|
40
49
|
```
|
|
41
50
|
|
|
42
51
|
The build emits the executable, a SHA-256 sidecar, and JSON provenance recording the artifact
|
|
43
|
-
and source digests, Rust and Cargo versions, C compiler, target, protocol, and
|
|
52
|
+
and source digests, Rust and Cargo versions, C compiler and musl versions, target, protocol, and
|
|
53
|
+
source epoch. The
|
|
44
54
|
audit requires a static or static-PIE ELF, no program interpreter, no `DT_NEEDED` entry, a valid
|
|
45
55
|
checksum, and an exact release identity from an empty environment. Bundled SQLite is part of
|
|
46
56
|
that ELF. The dependency graph must exactly match `native/THIRD_PARTY_LICENSES.tsv`, use only
|
|
@@ -48,10 +58,16 @@ the crates.io registry or workspace source, and contain only the reviewed licens
|
|
|
48
58
|
|
|
49
59
|
Reproducibility CI builds two fresh checkout copies with separate Cargo target directories,
|
|
50
60
|
path remapping, disabled incremental compilation, a fixed source epoch, and the same recorded
|
|
51
|
-
toolchains; their executable bytes must match.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
toolchains; their executable bytes must match. The build remaps both the checkout and the Cargo
|
|
62
|
+
home (where dependency sources live), so the artifact does not depend on where either sits on
|
|
63
|
+
the builder, and the check refuses an artifact that still embeds the builder's Cargo home,
|
|
64
|
+
checkout, or temporary build path. This proves reproducibility for the declared compiler inputs:
|
|
65
|
+
the same Rust toolchain, `musl` package, and C compiler produce the same bytes on a maintainer
|
|
66
|
+
host as on the release runner. It does not claim byte identity across different C compilers or
|
|
67
|
+
host toolchain builds; the declaration makes that limit explicit rather than leaving it to the
|
|
68
|
+
runner image. `AGCOORD_MUSL_CC=cc` is an explicit local compatibility escape hatch for an audited
|
|
69
|
+
x86_64 compiler when `musl-gcc` cannot be installed: such a build records its actual toolchain and
|
|
70
|
+
is never accepted by the reproducibility check or the release verifier; release CI never uses it.
|
|
55
71
|
|
|
56
72
|
The host-package build emits a deterministic root-owned tar archive, package checker, staged
|
|
57
73
|
installer, enforced-host probe, and their SHA-256 sidecars under `dist/host/`. CI retains these
|
|
@@ -160,6 +176,9 @@ interface and must be smoke-tested from the built wheel.
|
|
|
160
176
|
[guarded managed recovery path](native_host.md#guarded-recovery-when-the-outgoing-broker-cannot-drain)
|
|
161
177
|
with the exact pinned replacement. Retain its recovery ID and proof receipt; ordinary work
|
|
162
178
|
stays blocked until verified completion, and normal drain/resume cannot clear that guard.
|
|
179
|
+
If recovery inherited a normal drain, completion restores its original ID and metadata;
|
|
180
|
+
accepted work finishes under that drain while new submissions remain blocked. Retain both
|
|
181
|
+
receipts and resume the exact original drain only after its intended maintenance succeeds.
|
|
163
182
|
Keep `kernel.apparmor_restrict_unprivileged_userns=1`, start the ordinary unprivileged managed
|
|
164
183
|
service, and retain the shipped `cpu=1` receipt proving its AppArmor transition, cgroup
|
|
165
184
|
namespace root, exact CPU control, and durable applied/peak evidence. A spool left below
|
|
@@ -70,7 +70,9 @@ These arrive as a failed row. `failure_reason` is stable; the log has the detail
|
|
|
70
70
|
| --- | --- | --- |
|
|
71
71
|
| `agc drain --reason …` | Installs a durable guard, waits for admitted work, then the owner yields. The receipt's `drain_id` is the only key that reopens the spool. | Save the ID. `--no-wait` returns after installing the guard. Repeating `drain` returns the original receipt. |
|
|
72
72
|
| `agc resume <id>` refused with `broker-drain-id-mismatch` or `broker-drain-id-invalid` | Not the exact `drain-…` identifier the drain printed. | Use the receipt's ID; `agc list` prints it in its header while drained. |
|
|
73
|
-
| `broker-drain-live-work` | Resume was asked while rows are still queued or running. |
|
|
73
|
+
| `broker-drain-live-work` | Resume was asked while rows are still queued or running. | Let accepted work finish. If the outgoing broker cannot start, use guarded host recovery preserving the original drain; cancelling accepted work is not a repair workaround. |
|
|
74
|
+
| Normal drain marker with queued/running rows, outgoing broker dead or unable to start; earlier recovery says `normal maintenance is active; recovery cannot replace its identity` | The drain is stranded: repeating it preserves its marker but cannot progress accepted work. | Use [guarded host recovery](native_host.md#guarded-recovery-when-the-outgoing-broker-cannot-drain) with the matching client and pinned replacement. Preserve original drain metadata and both IDs; do not raw-edit the spool, prematurely resume, or cancel accepted work to bypass repair. |
|
|
75
|
+
| Host recovery completes with retained normal maintenance | Host proof succeeded and restored the original drain as `draining` or `drained`; submissions stay blocked. Public service status `verified` permits the owner to finish accepted work and yield. | Keep the original drain and recovery receipts. Complete intended maintenance, wait for the drain to yield, then explicitly resume its exact original ID. Host completion does not authorize an automatic resume. |
|
|
74
76
|
| `broker-resume-owner-live` | A broker still holds the ownership lock. | Let it exit; on a managed host, stop the service first. |
|
|
75
77
|
| `broker-not-draining` | Nothing to resume. | No action. |
|
|
76
78
|
| `agc avoid` refused with `avoid-sha-invalid` | The commit must be the full 40-character SHA. | Pass the full SHA; `--list` shows the stored set and `--remove SHA` deletes one. |
|
|
@@ -111,9 +113,9 @@ These arrive as a failed row. `failure_reason` is stable; the log has the detail
|
|
|
111
113
|
| `native-host-user-live-broker` from recovery | A broker owns the spool without a matching active recovery identity/helper; recovery refuses to replace it. | Use the normal drain/upgrade flow for a working owner. A verified replacement under the exact matching guard may continue without stop or reactivation; retry the same package/helper. |
|
|
112
114
|
| `host-recovery-worker-live`, `host-recovery-owner-invalid`, `host-recovery-invalid` | Worker absence, spool ownership, recovery metadata, or the bound replacement/helper cannot be proven. | Resolve the stated cause without deleting the spool or changing the recovery identity. Retry the same exact verified package and helper path. |
|
|
113
115
|
| `host-recovery-settling` | Dead running rows have not yet settled; the enforcement proof cannot be admitted. | The command waits up to 30 seconds. If it parks, inspect the reported phase and repair before retrying. Ordinary queued work remains blocked. |
|
|
114
|
-
| `host-recovery-proof-failed`, `host-recovery-proof-timeout` | The bound proof did not produce a passed enforced CPU receipt within its 120-second bound. | Repair the enforcement failure and retry the same package/helper. Only a successful applied/peak receipt permits completion. |
|
|
116
|
+
| `host-recovery-proof-failed`, `host-recovery-proof-timeout` | The bound proof did not produce a passed enforced CPU receipt within its 120-second bound. | Repair the enforcement failure and retry the same package/helper. Only a successful applied/peak receipt permits completion; an inherited normal drain is restored, not resumed. |
|
|
115
117
|
| `host-recovery-exhausted` | Three proof attempts have already been used across retries. | Operator repair is required. Do not delete or reset the guard, identity, or attempt budget to bypass exhaustion. |
|
|
116
|
-
| `native-host-recovery-incomplete` | A bounded recovery phase failed. Before completion, any established guard remains; a completion error instead reports whether its durable result could be confirmed. Service state is reported as unchanged, stopped, or stop unconfirmed. | Preserve the recovery ID and incident evidence. Inspect the durable completion receipt if the reply was lost; exact-ID completion retries return the same validated
|
|
118
|
+
| `native-host-recovery-incomplete` | A bounded recovery phase failed. Before completion, any established guard remains; a completion error instead reports whether its durable result could be confirmed. Service state is reported as unchanged, stopped, or stop unconfirmed. | Preserve the recovery ID and incident evidence. Inspect the durable completion receipt if the reply was lost; exact-ID completion retries return the same validated result after a committed completion, including retained original drain metadata rather than always `open`. Otherwise repair and retry the same package/helper; drain/resume cannot clear an active recovery guard. |
|
|
117
119
|
| `native-host-upgrade-drain-invalid` | The drain receipt the upgrade needs is missing or malformed. | Rerun `agc drain`, then the upgrade. |
|
|
118
120
|
| `systemctl --user stop` or `restart` does not return | A graceful stop waits for running jobs, up to `stop_grace` (600 seconds by default). | Wait, or interrupt them now with `systemctl --user kill --kill-whom=main --signal=SIGTERM agcoord-broker.service`; they end `interrupted` (`broker-stopped`). Never use a plain `systemctl kill`, which signals the jobs too. |
|
|
119
121
|
|
|
@@ -127,6 +129,7 @@ check; `journalctl -k` distinguishes a kernel refusal from an AppArmor denial.
|
|
|
127
129
|
| --- | --- | --- |
|
|
128
130
|
| `host-apparmor-profile-mismatch` | The service did not enter the enforced `agcoord-broker` profile. | `aa-status`; reload the profile from `/etc/apparmor.d`; `systemctl --user restart agcoord-broker.service`. |
|
|
129
131
|
| `host-apparmor-restriction-disabled` | `kernel.apparmor_restrict_unprivileged_userns` is not 1. | Restore the sysctl; the boundary depends on it. |
|
|
132
|
+
| A submitted command exits with an exec/permission error and never runs | The `agcoord-admitted` exec rule stacks `agcoord-job` at `execve`; if `agcoord-job` (or another of the six domains) is not loaded, that transition fails closed so the command cannot start. | Confirm all six domains (`agcoord-admitted`, `agcoord-job`, `agcoord-bwrap`, `agcoord-bwrap-child`, `agcoord-broker`, `agcoord-broker-client`) are loaded in enforce mode (`aa-status`); reload `/etc/apparmor.d/usr.libexec.agcoord.agcoord-broker` and restart the service. A partial or stale policy load is the usual cause. |
|
|
130
133
|
| `host-cgroup-controllers-unavailable`, `host-cgroup-mount-invalid`, `host-service-cgroup-mismatch` | CPU, memory, or PID controllers are not delegated, the mount is not read-write `nsdelegate` cgroup2, or the service's cgroup is not the configured root. | `cat /sys/fs/cgroup/user.slice/user-<uid>.slice/user@<uid>.service/cgroup.subtree_control`; compare `cgroup_root` in `config.json` with the unit's `Delegate=yes` and `DelegateSubgroup=supervisor`. |
|
|
131
134
|
| `host-executable-digest-mismatch`, `host-executable-identity-mismatch`, `host-executable-invalid`, `host-executable-path-mismatch` | The installed file changed, is not root-owned, or the configuration does not select it with release trust and `managed_service: true`. | Rerun `agc host install --download` or `agc host upgrade --download`; check `native_broker` in `config.json`. |
|
|
132
135
|
| `host-drain-required` and the other `host-drain-*` codes | Activation on an existing spool needs the exact drain receipt. | `agc drain`, keep the ID, and let the upgrade pass it. |
|