@miller-tech/uap 2.21.3 → 2.36.1

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 (328) hide show
  1. package/README.md +22 -10
  2. package/config/model-profiles/qwen38-coder390.json +46 -0
  3. package/config/model-profiles/qwen38.json +3 -2
  4. package/config/model-registry.json +110 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/benchmarks/paired/types.d.ts +2 -2
  7. package/dist/bin/cli.js +315 -20
  8. package/dist/bin/cli.js.map +1 -1
  9. package/dist/cli/attention.d.ts +6 -0
  10. package/dist/cli/attention.d.ts.map +1 -0
  11. package/dist/cli/attention.js +115 -0
  12. package/dist/cli/attention.js.map +1 -0
  13. package/dist/cli/bench-report.d.ts +82 -0
  14. package/dist/cli/bench-report.d.ts.map +1 -0
  15. package/dist/cli/bench-report.js +237 -0
  16. package/dist/cli/bench-report.js.map +1 -0
  17. package/dist/cli/clients.d.ts +6 -0
  18. package/dist/cli/clients.d.ts.map +1 -0
  19. package/dist/cli/clients.js +142 -0
  20. package/dist/cli/clients.js.map +1 -0
  21. package/dist/cli/dashboard-grants.d.ts +26 -0
  22. package/dist/cli/dashboard-grants.d.ts.map +1 -0
  23. package/dist/cli/dashboard-grants.js +69 -0
  24. package/dist/cli/dashboard-grants.js.map +1 -0
  25. package/dist/cli/dashboard-pace.d.ts +31 -0
  26. package/dist/cli/dashboard-pace.d.ts.map +1 -0
  27. package/dist/cli/dashboard-pace.js +42 -0
  28. package/dist/cli/dashboard-pace.js.map +1 -0
  29. package/dist/cli/dashboard-session.d.ts +25 -0
  30. package/dist/cli/dashboard-session.d.ts.map +1 -0
  31. package/dist/cli/dashboard-session.js +814 -0
  32. package/dist/cli/dashboard-session.js.map +1 -0
  33. package/dist/cli/dashboard-types.d.ts +18 -0
  34. package/dist/cli/dashboard-types.d.ts.map +1 -0
  35. package/dist/cli/dashboard-types.js +9 -0
  36. package/dist/cli/dashboard-types.js.map +1 -0
  37. package/dist/cli/dashboard.d.ts +2 -18
  38. package/dist/cli/dashboard.d.ts.map +1 -1
  39. package/dist/cli/dashboard.js +43 -793
  40. package/dist/cli/dashboard.js.map +1 -1
  41. package/dist/cli/deliver.d.ts +35 -0
  42. package/dist/cli/deliver.d.ts.map +1 -1
  43. package/dist/cli/deliver.js +93 -9
  44. package/dist/cli/deliver.js.map +1 -1
  45. package/dist/cli/diff.d.ts +85 -0
  46. package/dist/cli/diff.d.ts.map +1 -0
  47. package/dist/cli/diff.js +293 -0
  48. package/dist/cli/diff.js.map +1 -0
  49. package/dist/cli/eta.d.ts +35 -0
  50. package/dist/cli/eta.d.ts.map +1 -0
  51. package/dist/cli/eta.js +101 -0
  52. package/dist/cli/eta.js.map +1 -0
  53. package/dist/cli/handsfree.d.ts +7 -0
  54. package/dist/cli/handsfree.d.ts.map +1 -1
  55. package/dist/cli/handsfree.js +27 -0
  56. package/dist/cli/handsfree.js.map +1 -1
  57. package/dist/cli/hooks.d.ts.map +1 -1
  58. package/dist/cli/hooks.js +126 -5
  59. package/dist/cli/hooks.js.map +1 -1
  60. package/dist/cli/loops.d.ts +24 -5
  61. package/dist/cli/loops.d.ts.map +1 -1
  62. package/dist/cli/loops.js +92 -12
  63. package/dist/cli/loops.js.map +1 -1
  64. package/dist/cli/models.d.ts +62 -0
  65. package/dist/cli/models.d.ts.map +1 -0
  66. package/dist/cli/models.js +567 -0
  67. package/dist/cli/models.js.map +1 -0
  68. package/dist/cli/plan.d.ts +9 -0
  69. package/dist/cli/plan.d.ts.map +1 -1
  70. package/dist/cli/plan.js +57 -4
  71. package/dist/cli/plan.js.map +1 -1
  72. package/dist/cli/proxy-relay.d.ts +55 -0
  73. package/dist/cli/proxy-relay.d.ts.map +1 -0
  74. package/dist/cli/proxy-relay.js +116 -0
  75. package/dist/cli/proxy-relay.js.map +1 -0
  76. package/dist/cli/proxy.d.ts +4 -0
  77. package/dist/cli/proxy.d.ts.map +1 -1
  78. package/dist/cli/proxy.js +26 -1
  79. package/dist/cli/proxy.js.map +1 -1
  80. package/dist/cli/sandbox.d.ts +101 -4
  81. package/dist/cli/sandbox.d.ts.map +1 -1
  82. package/dist/cli/sandbox.js +393 -44
  83. package/dist/cli/sandbox.js.map +1 -1
  84. package/dist/cli/self.d.ts +13 -0
  85. package/dist/cli/self.d.ts.map +1 -0
  86. package/dist/cli/self.js +94 -0
  87. package/dist/cli/self.js.map +1 -0
  88. package/dist/cli/systemd-services.d.ts +2 -8
  89. package/dist/cli/systemd-services.d.ts.map +1 -1
  90. package/dist/cli/systemd-services.js +5 -20
  91. package/dist/cli/systemd-services.js.map +1 -1
  92. package/dist/cli/task.js +1 -1
  93. package/dist/cli/task.js.map +1 -1
  94. package/dist/cli/wizard-config.d.ts.map +1 -1
  95. package/dist/cli/wizard-config.js +31 -2
  96. package/dist/cli/wizard-config.js.map +1 -1
  97. package/dist/config/settings-registry.d.ts +1 -1
  98. package/dist/config/settings-registry.d.ts.map +1 -1
  99. package/dist/config/settings-registry.js +108 -0
  100. package/dist/config/settings-registry.js.map +1 -1
  101. package/dist/coordination/capability-router.js +2 -2
  102. package/dist/coordination/capability-router.js.map +1 -1
  103. package/dist/coordination/database.d.ts +13 -0
  104. package/dist/coordination/database.d.ts.map +1 -1
  105. package/dist/coordination/database.js +63 -7
  106. package/dist/coordination/database.js.map +1 -1
  107. package/dist/coordination/eta-inject.d.ts +32 -0
  108. package/dist/coordination/eta-inject.d.ts.map +1 -0
  109. package/dist/coordination/eta-inject.js +64 -0
  110. package/dist/coordination/eta-inject.js.map +1 -0
  111. package/dist/coordination/reactor.d.ts.map +1 -1
  112. package/dist/coordination/reactor.js +14 -0
  113. package/dist/coordination/reactor.js.map +1 -1
  114. package/dist/coordination/service.d.ts +18 -8
  115. package/dist/coordination/service.d.ts.map +1 -1
  116. package/dist/coordination/service.js +38 -28
  117. package/dist/coordination/service.js.map +1 -1
  118. package/dist/dashboard/client-registry.d.ts +163 -0
  119. package/dist/dashboard/client-registry.d.ts.map +1 -0
  120. package/dist/dashboard/client-registry.js +549 -0
  121. package/dist/dashboard/client-registry.js.map +1 -0
  122. package/dist/dashboard/controls.d.ts.map +1 -1
  123. package/dist/dashboard/controls.js +33 -4
  124. package/dist/dashboard/controls.js.map +1 -1
  125. package/dist/dashboard/data-seeder.d.ts +10 -7
  126. package/dist/dashboard/data-seeder.d.ts.map +1 -1
  127. package/dist/dashboard/data-seeder.js +31 -75
  128. package/dist/dashboard/data-seeder.js.map +1 -1
  129. package/dist/dashboard/data-service.d.ts +9 -0
  130. package/dist/dashboard/data-service.d.ts.map +1 -1
  131. package/dist/dashboard/data-service.js +32 -3
  132. package/dist/dashboard/data-service.js.map +1 -1
  133. package/dist/dashboard/placement-routes.d.ts +119 -0
  134. package/dist/dashboard/placement-routes.d.ts.map +1 -0
  135. package/dist/dashboard/placement-routes.js +331 -0
  136. package/dist/dashboard/placement-routes.js.map +1 -0
  137. package/dist/dashboard/server.d.ts.map +1 -1
  138. package/dist/dashboard/server.js +408 -2
  139. package/dist/dashboard/server.js.map +1 -1
  140. package/dist/delivery/attention.d.ts +130 -0
  141. package/dist/delivery/attention.d.ts.map +1 -0
  142. package/dist/delivery/attention.js +341 -0
  143. package/dist/delivery/attention.js.map +1 -0
  144. package/dist/delivery/decompose.d.ts +18 -0
  145. package/dist/delivery/decompose.d.ts.map +1 -1
  146. package/dist/delivery/decompose.js +22 -1
  147. package/dist/delivery/decompose.js.map +1 -1
  148. package/dist/delivery/eta-estimate.d.ts +111 -0
  149. package/dist/delivery/eta-estimate.d.ts.map +1 -0
  150. package/dist/delivery/eta-estimate.js +171 -0
  151. package/dist/delivery/eta-estimate.js.map +1 -0
  152. package/dist/delivery/eta.d.ts +23 -0
  153. package/dist/delivery/eta.d.ts.map +1 -0
  154. package/dist/delivery/eta.js +59 -0
  155. package/dist/delivery/eta.js.map +1 -0
  156. package/dist/delivery/orchestrated-mission.d.ts +10 -0
  157. package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
  158. package/dist/delivery/orchestrated-mission.js +15 -1
  159. package/dist/delivery/orchestrated-mission.js.map +1 -1
  160. package/dist/delivery/plan-check.d.ts +7 -1
  161. package/dist/delivery/plan-check.d.ts.map +1 -1
  162. package/dist/delivery/plan-check.js +23 -2
  163. package/dist/delivery/plan-check.js.map +1 -1
  164. package/dist/delivery/run-state.d.ts +1 -1
  165. package/dist/delivery/run-state.d.ts.map +1 -1
  166. package/dist/delivery/run-state.js +8 -1
  167. package/dist/delivery/run-state.js.map +1 -1
  168. package/dist/delivery/task-orchestrator.d.ts +8 -3
  169. package/dist/delivery/task-orchestrator.d.ts.map +1 -1
  170. package/dist/delivery/task-orchestrator.js +75 -43
  171. package/dist/delivery/task-orchestrator.js.map +1 -1
  172. package/dist/delivery/task-sync.d.ts +21 -0
  173. package/dist/delivery/task-sync.d.ts.map +1 -1
  174. package/dist/delivery/task-sync.js +44 -8
  175. package/dist/delivery/task-sync.js.map +1 -1
  176. package/dist/delivery/task-workspace.d.ts +3 -3
  177. package/dist/delivery/task-workspace.js +3 -3
  178. package/dist/generators/self-card.d.ts +65 -0
  179. package/dist/generators/self-card.d.ts.map +1 -0
  180. package/dist/generators/self-card.js +256 -0
  181. package/dist/generators/self-card.js.map +1 -0
  182. package/dist/inference/probe.js +1 -1
  183. package/dist/inference/probe.js.map +1 -1
  184. package/dist/memory/prepopulate.d.ts.map +1 -1
  185. package/dist/memory/prepopulate.js +2 -0
  186. package/dist/memory/prepopulate.js.map +1 -1
  187. package/dist/models/analytics.d.ts +32 -0
  188. package/dist/models/analytics.d.ts.map +1 -1
  189. package/dist/models/analytics.js +68 -5
  190. package/dist/models/analytics.js.map +1 -1
  191. package/dist/models/plan-readability.d.ts +55 -0
  192. package/dist/models/plan-readability.d.ts.map +1 -0
  193. package/dist/models/plan-readability.js +339 -0
  194. package/dist/models/plan-readability.js.map +1 -0
  195. package/dist/placement/admission.d.ts +59 -0
  196. package/dist/placement/admission.d.ts.map +1 -0
  197. package/dist/placement/admission.js +197 -0
  198. package/dist/placement/admission.js.map +1 -0
  199. package/dist/placement/auto.d.ts +44 -0
  200. package/dist/placement/auto.d.ts.map +1 -0
  201. package/dist/placement/auto.js +212 -0
  202. package/dist/placement/auto.js.map +1 -0
  203. package/dist/placement/enforce.d.ts +128 -0
  204. package/dist/placement/enforce.d.ts.map +1 -0
  205. package/dist/placement/enforce.js +655 -0
  206. package/dist/placement/enforce.js.map +1 -0
  207. package/dist/placement/idle.d.ts +50 -0
  208. package/dist/placement/idle.d.ts.map +1 -0
  209. package/dist/placement/idle.js +213 -0
  210. package/dist/placement/idle.js.map +1 -0
  211. package/dist/placement/ledger.d.ts +323 -0
  212. package/dist/placement/ledger.d.ts.map +1 -0
  213. package/dist/placement/ledger.js +263 -0
  214. package/dist/placement/ledger.js.map +1 -0
  215. package/dist/placement/preview.d.ts +51 -0
  216. package/dist/placement/preview.d.ts.map +1 -0
  217. package/dist/placement/preview.js +61 -0
  218. package/dist/placement/preview.js.map +1 -0
  219. package/dist/placement/probes.d.ts +47 -0
  220. package/dist/placement/probes.d.ts.map +1 -0
  221. package/dist/placement/probes.js +207 -0
  222. package/dist/placement/probes.js.map +1 -0
  223. package/dist/placement/proxy-env.d.ts +21 -0
  224. package/dist/placement/proxy-env.d.ts.map +1 -0
  225. package/dist/placement/proxy-env.js +42 -0
  226. package/dist/placement/proxy-env.js.map +1 -0
  227. package/dist/placement/registry.d.ts +1038 -0
  228. package/dist/placement/registry.d.ts.map +1 -0
  229. package/dist/placement/registry.js +274 -0
  230. package/dist/placement/registry.js.map +1 -0
  231. package/dist/policies/liveness.d.ts +6 -8
  232. package/dist/policies/liveness.d.ts.map +1 -1
  233. package/dist/policies/liveness.js +6 -25
  234. package/dist/policies/liveness.js.map +1 -1
  235. package/dist/policies/sandbox-anchor.d.ts +91 -0
  236. package/dist/policies/sandbox-anchor.d.ts.map +1 -0
  237. package/dist/policies/sandbox-anchor.js +187 -0
  238. package/dist/policies/sandbox-anchor.js.map +1 -0
  239. package/dist/policies/sandbox-grants.d.ts +131 -0
  240. package/dist/policies/sandbox-grants.d.ts.map +1 -0
  241. package/dist/policies/sandbox-grants.js +562 -0
  242. package/dist/policies/sandbox-grants.js.map +1 -0
  243. package/dist/state/manifest.d.ts +4 -2
  244. package/dist/state/manifest.d.ts.map +1 -1
  245. package/dist/state/manifest.js +32 -16
  246. package/dist/state/manifest.js.map +1 -1
  247. package/dist/utils/checkout-root.d.ts +2 -0
  248. package/dist/utils/checkout-root.d.ts.map +1 -0
  249. package/dist/utils/checkout-root.js +43 -0
  250. package/dist/utils/checkout-root.js.map +1 -0
  251. package/dist/utils/resolve-uap-bin.d.ts +5 -0
  252. package/dist/utils/resolve-uap-bin.d.ts.map +1 -0
  253. package/dist/utils/resolve-uap-bin.js +33 -0
  254. package/dist/utils/resolve-uap-bin.js.map +1 -0
  255. package/dist/utils/terminal-safe.d.ts +12 -0
  256. package/dist/utils/terminal-safe.d.ts.map +1 -0
  257. package/dist/utils/terminal-safe.js +53 -0
  258. package/dist/utils/terminal-safe.js.map +1 -0
  259. package/docs/ATTRIBUTION.md +1 -0
  260. package/docs/DASHBOARD_UPLIFT_SPEC.md +8 -8
  261. package/docs/INDEX.md +6 -5
  262. package/docs/architecture/OVERVIEW.md +15 -3
  263. package/docs/architecture/PROTOCOL.md +7 -1
  264. package/docs/architecture/adr/0006-operator-directed-model-placement.md +86 -0
  265. package/docs/architecture/adr/0007-auto-displacement-allowlist-as-standing-consent.md +79 -0
  266. package/docs/architecture/adr/0008-fleet-clients-link-out.md +92 -0
  267. package/docs/architecture/adr/0009-completion-triggered-dispatch-and-per-leaf-model-dials.md +71 -0
  268. package/docs/architecture/adr/0010-attention-queue-and-afk-auto-decider.md +159 -0
  269. package/docs/architecture/adr/0011-sandbox-grants-and-network-egress-boundary.md +219 -0
  270. package/docs/architecture/adr/0012-operator-legibility-surfaces.md +175 -0
  271. package/docs/getting-started/INSTALLATION.md +1 -1
  272. package/docs/guides/AUTOMATIC_FEATURES.md +8 -0
  273. package/docs/guides/LOCAL_MODELS.md +1 -1
  274. package/docs/guides/PROXY.md +2 -1
  275. package/docs/guides/SANDBOX.md +91 -4
  276. package/docs/integrations/MCP_ROUTER.md +1 -1
  277. package/docs/plans/model-performance-proof-options.md +176 -0
  278. package/docs/reference/API.md +1 -1
  279. package/docs/reference/CLI.md +169 -4
  280. package/docs/reference/COMPARISON.md +1 -1
  281. package/docs/reference/CONFIGURATION.md +12 -3
  282. package/docs/reference/CONFIGURATION_REFERENCE.md +269 -14
  283. package/docs/reference/DATABASE_SCHEMA.md +1 -1
  284. package/docs/reference/FEATURES.md +15 -3
  285. package/docs/reference/PATTERNS.md +1 -1
  286. package/docs/reference/PLATFORMS.md +14 -1
  287. package/docs/specs/operator-model-placement.md +1314 -0
  288. package/docs/specs/operator-model-placement.review.md +339 -0
  289. package/package.json +2 -2
  290. package/scripts/maintenance/publish-npm.sh +23 -11
  291. package/scripts/version-bump.sh +6 -51
  292. package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
  293. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
  294. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
  295. package/src/policies/schemas/policies/mandatory-testing-deployment.md +2 -5
  296. package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
  297. package/templates/hooks/session-start.sh +44 -0
  298. package/tools/agents/UAP/cli.py +14 -3
  299. package/tools/agents/UAP/paths.py +50 -0
  300. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
  301. package/tools/agents/scripts/anthropic_proxy.py +1386 -99
  302. package/tools/agents/scripts/toolcall_path_normalizer.py +46 -1
  303. package/tools/agents/tests/test_anthropic_proxy_streaming.py +29 -6
  304. package/tools/agents/tests/test_doubling_break.py +64 -5
  305. package/tools/agents/tests/test_error_loop_break.py +10 -4
  306. package/tools/agents/tests/test_error_loop_ignores_correctives.py +13 -2
  307. package/tools/agents/tests/test_lockstep_escalation.py +280 -9
  308. package/tools/agents/tests/test_path_containment.py +94 -0
  309. package/tools/agents/tests/test_proxy_unix_socket.py +162 -0
  310. package/tools/agents/tests/test_signature_quality.py +349 -0
  311. package/tools/agents/tests/test_uap_compliance.py +11 -1
  312. package/web/dash/core.js +1 -0
  313. package/web/dash/tab-clients.js +196 -0
  314. package/web/dash/tab-models.js +387 -8
  315. package/web/dash/tab-policies.js +87 -4
  316. package/web/dash/tabs.js +77 -73
  317. package/web/dashboard.html +6 -0
  318. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  319. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  320. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  321. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  322. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  323. package/web/dash/tab-agents.js +0 -21
  324. package/web/dash/tab-deliver.js +0 -12
  325. package/web/dash/tab-memory.js +0 -12
  326. package/web/dash/tab-orchestration.js +0 -21
  327. package/web/dash/tab-overview.js +0 -21
  328. package/web/dash/tab-tasks.js +0 -21
@@ -0,0 +1,92 @@
1
+ # ADR 0008: Dashboard fleet "Clients" tab ships as link-out, not multi-root
2
+
3
+ - **Status**: Accepted
4
+ - **Date**: 2026-10-10 (branch `feature/348-dashboard-fleet-clients`)
5
+
6
+ ## Context
7
+
8
+ The dashboard server (`uap dashboard serve`) is single-project: every route
9
+ reads `process.cwd()` captured at start. The operator runs many UAP-managed
10
+ client folders as siblings on disk and wants one surface that shows each
11
+ client's job queue (tasks) and delivery processes, with the ability to
12
+ switch to any of them.
13
+
14
+ Two shapes solve this:
15
+
16
+ - **Option A — multi-root host**: the host server accepts a root parameter
17
+ on every API call and re-scopes all panels in place. Largest payoff, but
18
+ it turns the host into a cross-project write surface: every existing
19
+ mutation route (`/api/deliver/launch`, task mutations, policy toggles)
20
+ would need a second authority dimension, and the mutation token would
21
+ suddenly grant control of every sibling project at once.
22
+
23
+ - **Option B — fleet overview + link-out**: the host stays single-project
24
+ and gains a read-only `GET /api/clients` that summarizes the fleet
25
+ (deliver-run states, queue depth, git, activity, health probe). Opening a
26
+ client spawns that client's OWN `uap dashboard serve` on a loopback port
27
+ (registry-only paths) and links out to it. Full control exists per
28
+ client, but only through each client's token-gated instance.
29
+
30
+ ## Decision
31
+
32
+ Ship Option B. The host server gains no cross-project write authority; the
33
+ only wire-reachable cross-client action is `POST /api/clients/serve`
34
+ (token-gated, resolved strictly against the client registry — never a
35
+ raw path from the wire). Discovery is auto-scan of the scan root (default:
36
+ the host's parent directory, ignore-list filtered) merged with manual
37
+ entries in `~/.uap/clients.json` (`uap clients add/remove/list/scan`).
38
+
39
+ Consequences and guards:
40
+
41
+ - **Sticky ports**: spawned dashboards record their port in the registry
42
+ (`ports` map), and computed ports skip claimed ones — a fleet change
43
+ (new folder, rename) can never silently move a live dashboard's port,
44
+ which would orphan the old process and wedge the new assignment.
45
+ - **Root-matched probes**: liveness is true only when the answering
46
+ dashboard reports the expected project root, so a foreign listener on a
47
+ candidate port can never be link-mistaken for the client.
48
+ - **Spawn is registry-only and argv-fixed** (`dashboard serve --port N
49
+ --host 127.0.0.1`): no shell, no path from the wire.
50
+ - **Host never writes into client projects** — the earlier `.uap/dash.pid`
51
+ write was dropped; nothing reads it and it breached the invariant.
52
+ - **Disclosure scope** (recorded for the security review): `GET
53
+ /api/clients` is unauthenticated and CORS-open exactly like
54
+ `/api/dashboard`, but it widens exposure from one project to every
55
+ sibling (absolute paths, branches, activity). On `--host 0.0.0.0` binds
56
+ this is LAN-visible; operators choosing that bind accept the wider
57
+ read. If the CORS-open-read posture is ever tightened, this route
58
+ belongs in that sweep. No secrets, tokens, or pids are in the payload.
59
+ - **Environment**: spawned children inherit the operator's environment
60
+ (including `UAP_DASHBOARD_TOKEN`, deliberately kept so scripted fleet
61
+ control works) except the host's `UAP_CLIENTS_*` test overrides, which
62
+ are scrubbed so the child scans its own siblings.
63
+
64
+ ## Alternatives rejected
65
+
66
+ - **Multi-root host (Option A)**: rejected for the write-surface blast
67
+ radius above. Note this change prepares rather than blocks it — the
68
+ per-project read surface (`getTaskData(cwd)`, `listRuns(root)`) was
69
+ already root-parameterized, and the registry's `ClientEntry` model is
70
+ reusable if in-place switching is ever wanted.
71
+ - **Federated static links (no spawn)**: rejected because the operator
72
+ wants one click from the fleet view to a live dashboard; requiring a
73
+ manual `uap dashboard serve` per client defeats the point.
74
+
75
+ ## Follow-ups
76
+
77
+ Completed on this branch:
78
+
79
+ - **`uap clients stop` + tab Stop action** (spawned-instance teardown):
80
+ spawn-on-demand records the child pid in the registry (`pids` map —
81
+ host-owned bookkeeping in `~/.uap/clients.json`, never a write into the
82
+ client project). Stop requires BOTH a root-matched live probe and pid
83
+ ownership verification (`/proc/<pid>/cwd` must be the client root) before
84
+ SIGTERM — a recycled pid is never killed; on mismatch or missing evidence
85
+ it refuses and names the port for a manual stop. Stale pid records are
86
+ cleared opportunistically whenever a probe shows the dashboard gone.
87
+ - **`resolveUapBin` extracted** to `src/utils/resolve-uap-bin.ts`
88
+ (`controls.ts` and `client-registry.ts` share it; the util lives one level
89
+ under `dist/` so the same-install `<root>/dist/bin/cli.js` resolution is
90
+ unchanged).
91
+
92
+ None outstanding.
@@ -0,0 +1,71 @@
1
+ # ADR 0009: Completion-triggered dispatch and per-leaf model dials
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-11
5
+ - Spec: completion-triggered scheduling (C1) and routing dials (C2), responding to the
6
+ "serial execution / no delegation" and "wrong worker on the wrong job"
7
+ gaps in mtlynch.io's *Why Are Coding Agents So Dumb?*
8
+ - Supersedes: none — replaces the wave-barrier scheduling note documented in
9
+ `deliver.parallelTasks` (CONFIGURATION.md) and the `OrchestratorConfig`
10
+ doc block; extends the `DeliveryPhase` plan schema additively.
11
+
12
+ ## Context
13
+
14
+ Two independent costs lived in the task orchestrator:
15
+
16
+ 1. **Wave-barrier idling.** The main loop dispatched a "wave" of up to
17
+ `concurrency` READY tasks and waited for ALL of them (`Promise.all`)
18
+ before recomputing the ready set. A 2-minute task in a wave with a
19
+ 30-minute sibling left its slot idle for 28 minutes, and a dependent
20
+ whose only blocker finished early still waited for the whole wave.
21
+ 2. **Mission-wide model binding.** One model per mission meant paying the
22
+ strong model's rate for mechanical fill/test leaves, or starving the
23
+ hard design leaf by running everything cheap. Phase plans are the natural
24
+ place to say which leaf needs which worker, but `DeliveryPhase` had no
25
+ dial and deliver bound the executor model once, at mission start.
26
+
27
+ ## Decision
28
+
29
+ 1. **C1 — completion-triggered dispatch.** The orchestrator keeps an
30
+ in-flight array, fills free slots from the READY set (graph order), races
31
+ on completion, and settles the winner sequentially before refilling.
32
+ Invariants:
33
+ - settlement is strictly one-at-a-time; graph mutations (blackboard,
34
+ skip cascade, repair chains, P5 re-planning) never interleave;
35
+ - WHICH flight settles first is timing-dependent: `outcomes` interleaving
36
+ varies run-to-run and the per-id contract is last-entry-wins. This is
37
+ a deliberate trade of run-to-run reproducibility for utilization;
38
+ - a throwing settle drains in-flight flights (`allSettled` in a
39
+ `finally`) so a seam-callback throw cannot abandon tasks mid-mutation
40
+ (reachable only under completion-triggered dispatch — the wave barrier
41
+ guaranteed an empty in-flight set on any throw).
42
+ 2. **C2 — per-leaf model dials.** `DeliveryPhase.model?: string` is an
43
+ optional dial, planner-relayed (copy a key the MISSION text names
44
+ verbatim, never invent one) or hand-authored. Semantics:
45
+ - normalized once at parse (`normalizeModelDial`: lowercased to match
46
+ the preset table, control-character-stripped against log injection,
47
+ 64-char cap); the run-state resume sanitizer applies the SAME rules;
48
+ - bound only where a per-task executor exists (isolated + agentic
49
+ tasks); the shared non-isolated executor stays mission-bound at start
50
+ because its write-allowlist refs are shared mutable state;
51
+ - fail-closed: unknown keys, non-isolated tasks, and the blind-executor
52
+ path all degrade to the mission model with a warning naming the actual
53
+ reason, plus a plan-time lint at mission start (not hours later at
54
+ bind time). The preset lookup uses `Object.hasOwn` — prototype-chain
55
+ keys (`constructor`, `__proto__`) are planner-reachable untrusted
56
+ text and must not bind an unpinned model.
57
+ 3. **Known debt, out of scope here:** the production projection drops
58
+ `criteria` (pre-existing; the dial threading fixed `model` in the same
59
+ map). Filed for a separate change.
60
+
61
+ ## Consequences
62
+
63
+ - Utilization: a slot refills the moment a task settles; dependents start
64
+ when their last dependency succeeds, not when the wave drains.
65
+ - The first C1 tests would literally deadlock under wave scheduling — they
66
+ pin the behavior by construction (a regression manifests as a timeout).
67
+ - Resume round-trips keep dials; the persisted `model` key is additive and
68
+ old readers strip it harmlessly (their allowlist predates the field).
69
+ - Blast radius: `task-orchestrator.ts` (loop), `decompose.ts` (schema +
70
+ prompt), `orchestrated-mission.ts` (threading + P5 feed), `deliver.ts`
71
+ (projection + binding), `run-state.ts` (round-trip), two reference docs.
@@ -0,0 +1,159 @@
1
+ # ADR 0010: Attention queue and the AFK auto-decider
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-11
5
+ - Spec: unattended autonomy (A1) and the operator attention queue (A2), responding to
6
+ the "blocks on the operator / no safe way to proceed unattended" gap in
7
+ mtlynch.io's *Why Are Coding Agents So Dumb?*
8
+ - Supersedes: none — extends the placement park path (ADR 0006's pending
9
+ approvals) and the session-start surface; adds a new module, no schema it
10
+ depends on changes.
11
+
12
+ ## Context
13
+
14
+ When a decision is genuinely the operator's — a placement parked until a
15
+ swap is approved, a grant that exceeds standing consent — the system had
16
+ two bad options: stall the run waiting for a human, or silently drop the
17
+ ask and pick a default nobody signed off on. mtlynch's complaint applies
18
+ verbatim: the agent is either dumb-but-safe (idle) or autonomous-but-
19
+ unaccountable (silent).
20
+
21
+ The missing piece is not more automation; it is a **visible decision
22
+ queue with recorded consequences**:
23
+
24
+ - every ask carries its own context (what was requested, why it parked)
25
+ so the operator can answer it cold, minutes or hours later;
26
+ - every ask carries a **safe default** the asking surface itself vouches
27
+ for — an action that is correct *without further consent* (for a parked
28
+ placement: let the pending request expire on its own TTL — nothing is
29
+ loaded, nothing is evicted);
30
+ - an absent operator is expected, not exceptional: long unattended runs
31
+ are a first-class mode.
32
+
33
+ ## Decision
34
+
35
+ 1. **A2 — the attention queue** (`src/delivery/attention.ts`):
36
+ `.uap/attention.json` holds bounded, pending-or-resolved items
37
+ (`kind`, `title`, `detail`, `source`, optional `safeDefault{decision,
38
+ rationale}`, `graceDeadline`, `status`). Surfaces:
39
+ - **one queue per CHECKOUT, not per cwd**: the file lives in the MAIN
40
+ checkout's `.uap/` (resolved via `mainCheckoutRoot`, the same anchor
41
+ the policy-liveness fix established on 2026-07-13), because three
42
+ surfaces with three different cwds (dashboard server process, CLI,
43
+ session-start hook inside a worktree) would otherwise write and read
44
+ three different files and the rail would silently no-op — flagged
45
+ BLOCKER by the architecture review. `UAP_ATTENTION_QUEUE` pins an
46
+ explicit file for tests and odd layouts.
47
+ - **enqueue sites** vouch for their own safe default and may PIN the
48
+ item's deadline to the ask's own expiry (`expiresAt`): the placement
49
+ site pins to the pending entry's TTL, so the decider adopts at the
50
+ moment the pending actually expires — never advertising an actionable
51
+ swap for a dead pending, never recording an adoption before the
52
+ expiry it describes. Generic sites use the grace window. Only one
53
+ enqueue site ships with this ADR: the placement admit path (a park
54
+ that found no auto plan) enqueues a review-request sourced
55
+ `placement:<id>` whose safe default is the TTL expiry — the recorded
56
+ rationale says plainly that nothing is loaded or evicted by waiting.
57
+ A resolved ask (forward / auto-load) never enters the queue, and the
58
+ only park reason with options to apply (`not_resident`) gets the
59
+ "apply" instruction; the no-option reasons say so plainly instead of
60
+ inviting a decision that cannot be made.
61
+ - **the asking surface also CLOSES its asks**: applying or dismissing a
62
+ pending placement resolves the matching attention item by origin
63
+ (`resolveAttentionBySource`), so the queue learns of decisions made
64
+ through the placement surface itself — otherwise a later sweep would
65
+ record a false AFK adoption over an operator decision that was
66
+ actually made.
67
+ - **read surfaces**: `uap attention list/ack/decide` (CLI, new) and the
68
+ session-start hook, which sweeps, summarizes what was auto-decided
69
+ while away, and shows the top OPEN items so every session opens with
70
+ what is waiting on the operator. The default view is the open set
71
+ (pending + acknowledged + escalated) — an escalated ask is the
72
+ loudly-unresolved class and must not drop out of the casual view.
73
+ - **enqueue dedupes on open asks**: by `source` (the ledger's rule) AND
74
+ by (kind, title) — an expired-pending client retrying every TTL mints
75
+ a new placement id per retry, and without content dedupe an
76
+ unattended weekend would mint one never-pruned ask per retry. Each
77
+ item still self-resolves at its own pinned deadline.
78
+ - **sanitized twice**: at ingest AND on load. The file lives in
79
+ shared-write `.uap/` (writable by every agent on the project, and a
80
+ cloned third-party repo need not gitignore it), so the load path
81
+ re-applies the control-char/length sanitizer and validates
82
+ `kind`/`status` against their enums — a planted row with ANSI escapes
83
+ or a planted `safeDefault` must not reach operator terminals or ride
84
+ a sweep wearing its `[auto]` label (security review, F1).
85
+ - **fail-open everywhere**: the queue is an observability rail — a
86
+ corrupt or unwritable store logs a warning and never blocks the
87
+ asking surface (all enqueue call sites are best-effort; a failed
88
+ persist returns null instead of a phantom item). `source` is
89
+ server-generated, never client text, and the placement admit path
90
+ rejects model ids outside `[A-Za-z0-9._-]` before any queue write —
91
+ client text never becomes a durable prompt-injection channel. The
92
+ hook renders item text with an untrusted-data disclaimer.
93
+ 2. **A1 — the AFK auto-decider** (`sweepExpired`, same module; invoked by
94
+ `uap attention sweep`, at session start, and on a 60s timer inside the
95
+ dashboard server — gated by the same fleet-ownership env as the idle
96
+ sweeper, because during a long unattended run, the exact case this rail
97
+ exists for, session starts are rare and nothing else would fire):
98
+ - an item whose grace window elapsed WITH a recorded safe default has
99
+ the decider's resolution recorded **on the item only** — `status:
100
+ "auto-decided"`, the adopted decision and rationale logged. The
101
+ decider never mutates the underlying system: it adopts what the
102
+ surface already promised would happen unattended (for placement,
103
+ the expiry is the system's own TTL doing the work; the sweep just
104
+ makes the record honest instead of leaving a stale pending row).
105
+ - an item with NO safe default escalates (`status: "escalated"`) — the
106
+ opposite failure mode of mtlynch's complaint is the one we refuse:
107
+ never silently expire an ask that mattered.
108
+ - **operator beats the decider**: any operator resolution (ack keeps
109
+ the item alive but the grace clock keeps running — seen ≠ answered;
110
+ decide closes it) wins over the sweep; sweeps are idempotent against
111
+ already-resolved items; and a decision made through the asking
112
+ surface itself (apply/dismiss) resolves the queue item by origin.
113
+ - **standing consent stays the boundary**: a safe default is only as
114
+ trustworthy as the surface that recorded it (cf. ADR 0007's
115
+ allowlist discipline). The decider adds no authority of its own — it
116
+ is a bookkeeper for promises surfaces already had the right to make.
117
+ 3. **Grace window:** env `UAP_ATTENTION_GRACE_SECS` > config
118
+ `attention.graceSecs` > 300s, capped at 86,400 (24h); `0` adopts
119
+ immediately; a cleared/empty env value is treated as unset (fails safe
120
+ to config). The grace window is the DEFAULT deadline — a site that knows
121
+ its ask's real expiry pins it (see Decision §1). `UAP_ATTENTION_OFF=1`
122
+ is the kill switch: no enqueue, no sweep, list returns nothing, and no
123
+ session-start hint — the rail goes quiet end-to-end while the file is
124
+ left alone.
125
+
126
+ ## Consequences
127
+
128
+ - A parked placement now has three fates instead of two: answered
129
+ (operator decides — through the queue OR through the placement surface,
130
+ which closes the ask by origin), adopted (default the surface vouched
131
+ for, logged, at the deadline the site pinned), or escalated (no safe
132
+ default — visible, not silent). Never a quiet stall.
133
+ - The session-start hint makes the queue self-advertising: the operator
134
+ does not need to know the command exists to encounter what is waiting —
135
+ and a mid-session sweep in the dashboard keeps the decider alive during
136
+ exactly the unattended runs it exists for.
137
+ - Bounded: open items are never pruned but content-dedupe collapses
138
+ retry-floods to one ask per question; resolved history prunes past 250.
139
+ Open items still only ever leave the queue via ack/decide/sweep — a
140
+ surface that enqueues an ask with NO lifecycle of its own must provide
141
+ its own resolution path or its asks linger by design (visible, not
142
+ silent).
143
+ - **Accepted v1 debt — lost-update race:** writers are unlocked
144
+ read-modify-write (last writer wins); a sweep racing an operator decide
145
+ in the same millisecond window can drop the operator's record. The
146
+ placement ledger solves this with `withLedger` advisory locking; the
147
+ queue inherits it only if the race is ever observed in practice (both
148
+ reviews rated it rideable for an advisory rail; the failure is a lost
149
+ audit row, never a wrong system action).
150
+ - The decider is deliberately weak. Extending it to actually DO things
151
+ (issue grants, trigger displacement) would move it from bookkeeping to
152
+ agency and must be its own ADR with its own consent analysis.
153
+ - Blast radius: new module + new CLI file + command registration; the
154
+ placement admit path gains one best-effort enqueue (with a charset gate
155
+ on model ids); apply/dismiss close their asks; the session-start
156
+ template gains a fail-open block; the dashboard server gains a 60s
157
+ fail-open sweep timer; the settings registry gains four keys. No
158
+ existing schema or contract changes; readers of `.uap/attention.json`
159
+ are only the new surface.
@@ -0,0 +1,219 @@
1
+ # ADR 0011: Per-session sandbox grants and the network egress boundary
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-11
5
+ - Spec: session authority (D2) + network egress boundary (D1), responding to
6
+ the "agents with too much authority" gap in mtlynch.io's *Why Are Coding
7
+ Agents So Dumb?*
8
+ - Supersedes: none — extends the bubblewrap sandbox (`uap sandbox`) and
9
+ the inference proxy; no existing schema changes.
10
+
11
+ ## Context
12
+
13
+ `uap sandbox` (the base floor) is all-or-nothing authority: the workdir and
14
+ scratch are writable, everything else is EROFS, and the only escape hatch
15
+ was `UAP_SANDBOX_ALLOW=path:path` — an **unrecorded** env var: no session
16
+ scope, no expiry, no reason, invisible to the operator, surviving forever
17
+ in shell histories and hook templates. Meanwhile the sandbox shared the
18
+ host network by design (the agent must reach the proxy), so "too much
19
+ authority" had two open faces: unrecorded write holes and unrestricted
20
+ network egress.
21
+
22
+ The bar: **every new authority must state who consented.**
23
+
24
+ ## Decision
25
+
26
+ ### D2 — the consent ledger (`src/policies/sandbox-grants.ts`)
27
+
28
+ Every extra writable hole is a **grant**, recorded in
29
+ `.uap/sandbox-grants.json` at the main checkout root (the
30
+ `mainCheckoutRoot` anchor the attention queue established in ADR 0010 —
31
+ one ledger per checkout, shared by every surface; `UAP_SANDBOX_GRANTS`
32
+ pins an explicit file):
33
+
34
+ - `uap sandbox grant <path...> --session <id> --reason "..." [--ttl N]`
35
+ creates a grant `{id, sessionId, paths[], reason, grantedBy, grantedAt,
36
+ expiresAt}`. A grant is refused without a session, a stated reason, or a
37
+ grantable target (existing absolute path; `/`, `/home`, `$HOME` itself
38
+ are refused — an over-broad grant IS a sandbox escape). Default TTL 8h,
39
+ hard cap 7d: authority is bounded by construction. An identical open
40
+ grant is RENEWED, not duplicated.
41
+ - `uap sandbox --session <id> -- <cmd>` resolves the session's ACTIVE
42
+ grants and opens exactly those `--bind` holes; `uap sandbox grants` /
43
+ `revoke <id>` list and remove them. (The `--` separator is REQUIRED
44
+ before the command: without it, the sandbox's own flags — `--session`,
45
+ `--net`, `--ttl`, `--reason`, `--json` — are parsed as sandbox options,
46
+ not passed through to the session command.)
47
+ - Every run appends a line to `.uap/sandbox-audit.jsonl` — the consent
48
+ record: which grants were used, which network mode engaged, and any
49
+ legacy `UAP_SANDBOX_ALLOW` entries labeled `unrecordedAllow` (still
50
+ honored for compatibility, now named in the audit and the banner).
51
+ - Store rules inherited from ADR 0010's hardened queue: sanitize at ingest
52
+ AND on load (the ledger is shared-write `.uap/` and a planted row must
53
+ not become a kernel-writable hole), atomic writes with unique tmp
54
+ names, malformed rows dropped, and an unreadable ledger treated as
55
+ EMPTY — the safe direction for an authority store is least authority.
56
+ "Sanitize" for this ledger means more than text fields: the load path
57
+ RE-VALIDATES every path with the full grantable-path rule (absolute,
58
+ existing, symlink-resolved, and not `/`, `/home`, or `$HOME` — the P4
59
+ review caught that a string-prefix check alone would let a planted
60
+ well-formed row bind the whole root) and refuses expiries beyond the
61
+ hard TTL bound (a planted `expiresAt: "9999"` must never load).
62
+ `UAP_SANDBOX_GRANTS_OFF=1` resolves zero grants; the sandbox's kernel
63
+ boundary itself is never disabled by any of this.
64
+ - **Consent must come from the operator, not from inside the sandbox**
65
+ (the security review's forgeability finding, three parts):
66
+ 1. `grant`/`revoke` are REFUSED when `UAP_SANDBOX_ACTIVE=1` — that env
67
+ is set by the sandbox itself on the untrusted side, so an agent
68
+ cannot write new authority or remove the operator's (listing stays
69
+ allowed: read-only).
70
+ 2. The planner `--ro-bind`s the ledger and audit files over their host
71
+ paths in EVERY sandbox namespace (a missing file gets `/dev/null`
72
+ bound at the path). A bind-mounted file cannot be unlinked, replaced,
73
+ or written inside the namespace, so the sandboxed agent can neither
74
+ plant grant rows nor forge audit lines — the load-path re-validation
75
+ checks SHAPE, but shape cannot establish WHO consented; the kernel
76
+ mount does.
77
+ 3. Granted paths are re-resolved (`realpath`) at PLAN time and the
78
+ resolved value is re-checked for breadth, closing the load→exec
79
+ TOCTOU window where a concurrent process could swap a granted path
80
+ for a symlink. (A microsecond window between this resolution and
81
+ bwrap's own remains — accepted, documented.)
82
+ - Fail-closed everywhere authority is parsed: a garbage or negative
83
+ `--ttl` is REFUSED (the old expression silently granted the 8h default
84
+ — the wrong direction); `--ttl 0` floors to 1s; an unknown
85
+ `UAP_SANDBOX_NET` value fails loud with exit 2 rather than silently
86
+ meaning FULL network; and every persist is followed by a reload
87
+ verification (a concurrent writer's lost update is retried once, then
88
+ reported as failure — never a silent success).
89
+
90
+ ### D1 — the network egress boundary
91
+
92
+ **`--net off`** — `uap sandbox --net off` adds `--unshare-net`
93
+ with nothing brought up: no TCP, no DNS, not even loopback, at the kernel.
94
+ Any `connect()` fails. This is the complete boundary, intended for
95
+ untrusted code, evals, and data-only work; a harness that needs the model
96
+ fails loudly with connection refused, which is the boundary working. The
97
+ net mode defaults from `UAP_SANDBOX_NET=off`; every run's actual mode is
98
+ audit-logged (`netMode: shared|off`).
99
+
100
+ **The proxy's unix-socket surface** — `PROXY_SOCKET=<path>`
101
+ makes `anthropic_proxy.py` serve the SAME app on an AF_UNIX socket
102
+ (0600, parent dir 0700) alongside its TCP port: a permission-gated model
103
+ choke point for shared boxes. Stale-socket policy (hardened by the
104
+ security review): a live listener owns its path — and "live" includes a
105
+ socket that cannot be dialed (`EACCES`/`EPERM` is not proof of death, so
106
+ another user's socket is never evicted and impersonated); only a refused
107
+ or missing socket is a corpse. A non-socket file at the path is NEVER
108
+ unlinked (`PROXY_SOCKET` can arrive from the operator's env or the
109
+ sandbox-writable `.uap/proxy.env`, so a typo or planted value must not
110
+ become an arbitrary-file-delete primitive), and a group/other-writable
111
+ parent dir draws a warning (another local user could unlink or squat the
112
+ path). `uap proxy connect --socket <path> [--port N] --allow-anyone`
113
+ re-serves that socket on local TCP for clients that cannot speak
114
+ AF_UNIX — behind an explicit consent flag, because while the relay runs
115
+ it inverts the socket's gate: every local user can reach the model path
116
+ through the loopback port.
117
+
118
+ ### D1's rejected design: the "only proxy" netns (and why)
119
+
120
+ The intended D1 was a netns cut with a SINGLE hole to the proxy: bwrap
121
+ `--unshare-net` plus a bind-mounted `PROXY_SOCKET`, with an in-namespace
122
+ relay re-serving the proxy on `127.0.0.1:4000`. **It cannot work on
123
+ rootless Linux**, and the failure is worth recording because the design
124
+ looks correct on paper:
125
+
126
+ 1. **AF_UNIX pathname sockets are network-namespace-scoped.** The socket
127
+ file bind-mounts fine, but `connect()` looks the listener up in the
128
+ CONNECTING process's netns unix-socket table and finds nothing:
129
+ empirically `ECONNREFUSED` from inside the namespace while the same
130
+ socket answers on the host (verified on Linux 7.2, bwrap 0.11). The
131
+ hole through the wall is a file that carries no reachability.
132
+ 2. **bwrap closes every inherited fd** (verified: an fd handed to the
133
+ child via `stdio` slot 3 arrives closed), so an SCM_RIGHTS broker that
134
+ passes pre-connected proxy sockets across the wall is impossible too.
135
+ 3. **The userspace network stacks cannot express the policy.**
136
+ pasta/slirp4netns bridge a rootless netns to the host, but neither
137
+ offers an outbound port allowlist — attaching one would give the
138
+ sandbox FULL egress, the exact opposite of the boundary.
139
+
140
+ A boundary that requires a helper that isn't installed, or a mechanism
141
+ the kernel doesn't provide, would be a suggestion rather than enforcement.
142
+ So `--net proxy` was **cut, not faked**: the two REAL modes are `shared`
143
+ (no claim) and `off` (kernel-complete), and the model-path choke point
144
+ (`PROXY_SOCKET`, 0600) ships on the proxy side where permission gating
145
+ actually holds.
146
+
147
+ ## Consequences
148
+
149
+ - Write authority beyond the workdir requires a recorded, bounded,
150
+ revocable consent. The legacy `UAP_SANDBOX_ALLOW` env still works for
151
+ compatibility — every use is audited and named as unrecorded — and its
152
+ entries get the same plan-time realpath re-check recorded grants get:
153
+ a symlink entry binds its resolved target, and an entry whose resolution
154
+ is `/`, `/home`, or `$HOME` is refused with an operator-visible note,
155
+ never bound.
156
+ - `--net off` gives untrusted-code runs a complete, kernel-enforced
157
+ egress boundary.
158
+ - Model-path gating becomes a socket permission problem (0600/0700) for
159
+ multi-user or sandboxed-adjacent setups, rather than a TCP port anyone
160
+ on the box can dial.
161
+ - The "agent session with ONLY model egress" use case remains open. The
162
+ honest paths to it: (a) upstream `pasta`-class outbound filtering, if it
163
+ ever grows port allowlists; (b) a setuid/privileged broker that can
164
+ join the sandbox netns; (c) rootful sandboxing (a future
165
+ `--net proxy,via:root` could revisit). None are rootless today, so none
166
+ are claimed.
167
+ - The audit JSONL is bounded: every line carries a monotonic `seq`, and
168
+ past 12,000 lines it compacts to the 10,000 newest event lines — plus
169
+ the triggering event and one `audit-compaction` marker recording the
170
+ drop, so the file lands at 10,002 — a pruned trail is an EXPECTED
171
+ state, never a silent one. Seq numbering is continuous across
172
+ compaction AND audit-file deletion: the trust anchor carries the
173
+ witnessed seq high-water (`seqHi`), and each line numbers from the max
174
+ of the file's last seq and the anchor's — a trail whose numbering can
175
+ restart is a forgeable trail. Honest limits: concurrent audit appends
176
+ can mint the same next-seq before either lands (a duplicate seq is
177
+ evidence to investigate, not proof of planting on its own), and an
178
+ older binary's next recorded mutation drops the optional `seqHi` field
179
+ — it is recomputed from the file at the next current-binary mutation,
180
+ which only matters if the audit file is deleted inside that
181
+ mixed-version window.
182
+ - The dashboard's session view lists the active authority per session —
183
+ id, session, remaining TTL, reason, paths, and the revoke command per
184
+ grant — plus the trust-anchor verdict (an INTEGRITY MISMATCH renders
185
+ red, rows listed-but-unverified, same contract as the CLI listing).
186
+ The per-checkout-root anchoring guarantees it reads the same ledger the
187
+ sandbox does.
188
+ - Tamper evidence (out-of-checkout anchor): every successful
189
+ grant/renew/revoke writes a 0600 anchor at
190
+ `$XDG_STATE_HOME/uap/sandbox-anchors/<hash-of-main-checkout-root>.json`
191
+ (`UAP_SANDBOX_ANCHOR_DIR` pins the dir for tests) recording the SHA-256
192
+ of the sanitized ledger as of that recorded operation. The key is the
193
+ MAIN checkout root (the same `git rev-parse --git-common-dir` root the
194
+ session-start hook and the python tools use), so a worktree session and
195
+ its main checkout share ONE anchor for the ONE ledger they share — a
196
+ worktree session is never silently unanchored, and a cross-cwd grant
197
+ never fabricates a mismatch. A granted path covering the anchor dir is
198
+ refused (a hole over the anchor would let the sandboxed side de-anchor
199
+ itself silently). Verification (`uap sandbox grants` states it; the
200
+ launch path enforces it) compares the on-disk ledger against the
201
+ anchor: a mismatch means something with the CHECKOUT's write reach
202
+ changed the ledger outside a recorded consent operation — new grants
203
+ are refused, and the session resolves ZERO grant-derived holes
204
+ (fail-closed; the kernel boundary is unaffected). A `revoke` on a
205
+ mismatched ledger still removes authority (the safe direction) but
206
+ re-anchors the remaining rows as the new baseline and names that at
207
+ the moment it stops being detectable.
208
+ What this remains honest about: it is tamper-EVIDENCE, not origin
209
+ proof. The anchor lives in the user's state dir, so a same-user process
210
+ OUTSIDE the checkout can still write it; a plant that predates the
211
+ first anchor is baselined into it (bootstrap cannot verify history);
212
+ and true origin proof (a different uid, a signed ledger, a hardware
213
+ key) stays open on purpose — the threat model closes the agent-side
214
+ vectors, and the anchor closes the checkout-write-reach side of the
215
+ operator residual.
216
+ - Residual, accepted: the microsecond TOCTOU between the planner's
217
+ `realpath` and bwrap's own bind (a concurrent process with write on a
218
+ granted path's parent could still race it), and the anchor's same-user
219
+ limit named above.