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.
Files changed (114) hide show
  1. {agcoord-0.7.2 → agcoord-0.7.4}/CHANGELOG.md +39 -1
  2. {agcoord-0.7.2 → agcoord-0.7.4}/Cargo.lock +1 -1
  3. {agcoord-0.7.2 → agcoord-0.7.4}/Cargo.toml +1 -1
  4. {agcoord-0.7.2 → agcoord-0.7.4}/MANIFEST.in +1 -0
  5. {agcoord-0.7.2 → agcoord-0.7.4}/PKG-INFO +1 -1
  6. {agcoord-0.7.2 → agcoord-0.7.4}/conformance/manifest-v3.json +4 -1
  7. {agcoord-0.7.2 → agcoord-0.7.4}/docs/coordinator.md +14 -0
  8. {agcoord-0.7.2 → agcoord-0.7.4}/docs/native_broker.md +35 -9
  9. {agcoord-0.7.2 → agcoord-0.7.4}/docs/native_host.md +76 -15
  10. {agcoord-0.7.2 → agcoord-0.7.4}/docs/releasing.md +28 -9
  11. {agcoord-0.7.2 → agcoord-0.7.4}/docs/troubleshooting.md +6 -3
  12. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/recovery.rs +143 -53
  13. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/store.rs +2 -2
  14. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/host_recovery.rs +288 -1
  15. agcoord-0.7.4/native/build-toolchain.json +5 -0
  16. agcoord-0.7.4/packaging/apparmor/usr.libexec.agcoord.agcoord-broker +161 -0
  17. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/build-native-broker +32 -3
  18. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-native-host-package +8 -4
  19. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-native-reproducible +19 -0
  20. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/install-native-host +12 -0
  21. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/native-source-id +2 -1
  22. agcoord-0.7.4/scripts/native-toolchain +75 -0
  23. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/test-native-host-enforcement +29 -1
  24. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/test-native-host-recovery +52 -2
  25. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/verify-release-candidate +13 -0
  26. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/__init__.py +1 -1
  27. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/native_host.py +40 -5
  28. agcoord-0.7.4/src/agcoord/native_host_pin.json +5 -0
  29. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/PKG-INFO +1 -1
  30. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/SOURCES.txt +3 -0
  31. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_conformance_gate.py +1 -1
  32. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_assets.py +69 -7
  33. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_recovery.py +125 -5
  34. agcoord-0.7.4/tests/test_native_toolchain.py +196 -0
  35. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_release_candidate.py +19 -1
  36. agcoord-0.7.2/packaging/apparmor/usr.libexec.agcoord.agcoord-broker +0 -74
  37. agcoord-0.7.2/src/agcoord/native_host_pin.json +0 -5
  38. {agcoord-0.7.2 → agcoord-0.7.4}/AGENTS.md +0 -0
  39. {agcoord-0.7.2 → agcoord-0.7.4}/LICENSE +0 -0
  40. {agcoord-0.7.2 → agcoord-0.7.4}/README.md +0 -0
  41. {agcoord-0.7.2 → agcoord-0.7.4}/docs/agents.md +0 -0
  42. {agcoord-0.7.2 → agcoord-0.7.4}/docs/assets/README.md +0 -0
  43. {agcoord-0.7.2 → agcoord-0.7.4}/docs/assets/agcoord-gourd-mascot.png +0 -0
  44. {agcoord-0.7.2 → agcoord-0.7.4}/docs/changelog.md +0 -0
  45. {agcoord-0.7.2 → agcoord-0.7.4}/docs/comparison.md +0 -0
  46. {agcoord-0.7.2 → agcoord-0.7.4}/docs/concepts.md +0 -0
  47. {agcoord-0.7.2 → agcoord-0.7.4}/docs/conformance.md +0 -0
  48. {agcoord-0.7.2 → agcoord-0.7.4}/docs/contributing.md +0 -0
  49. {agcoord-0.7.2 → agcoord-0.7.4}/docs/guardrails.md +0 -0
  50. {agcoord-0.7.2 → agcoord-0.7.4}/docs/index.md +0 -0
  51. {agcoord-0.7.2 → agcoord-0.7.4}/docs/native_migration.md +0 -0
  52. {agcoord-0.7.2 → agcoord-0.7.4}/docs/overview.md +0 -0
  53. {agcoord-0.7.2 → agcoord-0.7.4}/docs/quickstart.md +0 -0
  54. {agcoord-0.7.2 → agcoord-0.7.4}/docs/session_handoff_format.md +0 -0
  55. {agcoord-0.7.2 → agcoord-0.7.4}/docs/why.md +0 -0
  56. {agcoord-0.7.2 → agcoord-0.7.4}/native/THIRD_PARTY_LICENSES.tsv +0 -0
  57. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/Cargo.toml +0 -0
  58. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/build.rs +0 -0
  59. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/broker.rs +0 -0
  60. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/cgroup.rs +0 -0
  61. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/error.rs +0 -0
  62. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/host.rs +0 -0
  63. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/main.rs +0 -0
  64. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/platform.rs +0 -0
  65. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/project_quota.rs +0 -0
  66. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/resources.rs +0 -0
  67. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/src/worker.rs +0 -0
  68. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/client_compatibility.rs +0 -0
  69. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/host_deployment.rs +0 -0
  70. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/identity.rs +0 -0
  71. {agcoord-0.7.2 → agcoord-0.7.4}/native/agcoord-broker/tests/scheduler.rs +0 -0
  72. {agcoord-0.7.2 → agcoord-0.7.4}/packaging/systemd/agcoord-broker.service +0 -0
  73. {agcoord-0.7.2 → agcoord-0.7.4}/pyproject.toml +0 -0
  74. {agcoord-0.7.2 → agcoord-0.7.4}/rust-toolchain.toml +0 -0
  75. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/audit-native-broker +0 -0
  76. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/build-native-host-package +0 -0
  77. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-conformance +0 -0
  78. {agcoord-0.7.2 → agcoord-0.7.4}/scripts/check-native-licenses +0 -0
  79. {agcoord-0.7.2 → agcoord-0.7.4}/setup.cfg +0 -0
  80. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/__main__.py +0 -0
  81. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/avoid.py +0 -0
  82. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/cli.py +0 -0
  83. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/config.py +0 -0
  84. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/frame.py +0 -0
  85. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/github.py +0 -0
  86. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/github_release.py +0 -0
  87. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/land.py +0 -0
  88. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/merge.py +0 -0
  89. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/native_client.py +0 -0
  90. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/py.typed +0 -0
  91. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/pytest_xdist.py +0 -0
  92. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/queue.py +0 -0
  93. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/resources.py +0 -0
  94. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord/tui.py +0 -0
  95. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/dependency_links.txt +0 -0
  96. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/entry_points.txt +0 -0
  97. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/requires.txt +0 -0
  98. {agcoord-0.7.2 → agcoord-0.7.4}/src/agcoord.egg-info/top_level.txt +0 -0
  99. {agcoord-0.7.2 → agcoord-0.7.4}/tests/conftest.py +0 -0
  100. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_avoid.py +0 -0
  101. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_cli.py +0 -0
  102. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_distribution.py +0 -0
  103. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_github_adapter.py +0 -0
  104. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_host_config.py +0 -0
  105. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_leases.py +0 -0
  106. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_merge.py +0 -0
  107. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_client.py +0 -0
  108. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_download.py +0 -0
  109. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_native_host_upgrade.py +0 -0
  110. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_pytest_xdist.py +0 -0
  111. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_queue.py +0 -0
  112. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_resource_bindings.py +0 -0
  113. {agcoord-0.7.2 → agcoord-0.7.4}/tests/test_tui.py +0 -0
  114. {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
- ## Unreleased
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
 
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "agcoord-broker"
7
- version = "0.7.2"
7
+ version = "0.7.4"
8
8
  dependencies = [
9
9
  "libc",
10
10
  "rusqlite",
@@ -3,7 +3,7 @@ members = ["native/agcoord-broker"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.7.2"
6
+ version = "0.7.4"
7
7
  edition = "2024"
8
8
  rust-version = "1.94.1"
9
9
  license = "MIT"
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agcoord
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: Local CI and merge queue for coding agents that share one machine
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/xu-hao/agcoord
@@ -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`. It closes every inherited descriptor except standard I/O and the
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` before setting `no_new_privs` and before reporting successful setup. Arbitrary
438
- interpreters inherit that domain. When admitted work invokes the broker, AppArmor stacks
439
- `agcoord-broker-client` onto the admitted domain; adding confinement is compatible with
440
- `no_new_privs` and cannot regain setup permission. Both restricted profiles deny user-namespace
441
- creation and changing back to the setup domain. Every domain is compiled in explicit enforce
442
- mode rather than Ubuntu 24.04's unconfined `default_allow` mode. A failure to verify the loaded
443
- setup profile, service cgroup, global restriction, or backend namespace probe makes required work
444
- unavailable.
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. It also calls the installed Python
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, and only then clears capabilities and sets
345
- `no_new_privs` before release. Arbitrary interpreter execution inherits that profile. When
346
- admitted work invokes the fixed broker, AppArmor stacks `agcoord-broker-client` onto the existing
347
- admitted confinement; this adds restrictions without requesting a replacement domain after
348
- `no_new_privs`. Both restricted profiles deny user-namespace creation and changing back to setup.
349
- All three profiles use explicit enforce mode and broad enumerated host permissions;
350
- `default_allow` is not accepted because Ubuntu 24.04 implements it as an unconfined profile that
351
- does not apply these denials.
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 clears the guard.
426
- A process exit, service restart, normal `agc drain`, or `agc resume` cannot clear it.
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, and incident.
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 open receipt without another proof or recovery operation.
451
- If completion still cannot be confirmed, the error says to inspect the durable receipt; it
452
- does not claim the guard remains when completion may already have cleared it.
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 `scripts/native-source-id`. The artifact embeds
27
- that digest as its build identity and reports protocol, implementation, target, and bundled
28
- SQLite version through `identity --json`. Install Rust 1.94.1 with the declared musl target and
29
- Ubuntu's `musl-tools`, then run:
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 source epoch. The
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. This proves reproducibility for the declared
52
- runner and compiler inputs. It does not claim byte identity across different C compilers or
53
- host toolchain builds. `AGCOORD_MUSL_CC=cc` is an explicit local compatibility escape hatch for
54
- an audited x86_64 compiler when `musl-gcc` cannot be installed; release CI never uses it.
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. | Wait for the rows to end, or cancel them. |
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 open receipt after a committed completion. Otherwise repair and retry the same package/helper; drain/resume cannot clear an active recovery guard. |
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. |