@miller-tech/uap 1.222.0 → 2.1.4

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 (234) hide show
  1. package/README.md +19 -9
  2. package/config/exl3-profiles/qwen38-27b-exl3.env +68 -0
  3. package/config/llama-profiles/qwen38-27b-dflash2.env +6 -1
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/benchmarks/paired/adapter.d.ts +15 -0
  6. package/dist/benchmarks/paired/adapter.d.ts.map +1 -1
  7. package/dist/benchmarks/paired/adapter.js +22 -1
  8. package/dist/benchmarks/paired/adapter.js.map +1 -1
  9. package/dist/bin/cli.js +20 -24
  10. package/dist/bin/cli.js.map +1 -1
  11. package/dist/cli/deliver.d.ts +34 -0
  12. package/dist/cli/deliver.d.ts.map +1 -1
  13. package/dist/cli/deliver.js +147 -20
  14. package/dist/cli/deliver.js.map +1 -1
  15. package/dist/cli/guided-setup.d.ts.map +1 -1
  16. package/dist/cli/guided-setup.js +15 -0
  17. package/dist/cli/guided-setup.js.map +1 -1
  18. package/dist/cli/init.d.ts.map +1 -1
  19. package/dist/cli/init.js +3 -0
  20. package/dist/cli/init.js.map +1 -1
  21. package/dist/cli/plan.d.ts.map +1 -1
  22. package/dist/cli/plan.js +8 -2
  23. package/dist/cli/plan.js.map +1 -1
  24. package/dist/cli/policy.d.ts.map +1 -1
  25. package/dist/cli/policy.js +104 -2
  26. package/dist/cli/policy.js.map +1 -1
  27. package/dist/cli/quality.d.ts +11 -0
  28. package/dist/cli/quality.d.ts.map +1 -0
  29. package/dist/cli/quality.js +213 -0
  30. package/dist/cli/quality.js.map +1 -0
  31. package/dist/cli/wizard-config.d.ts.map +1 -1
  32. package/dist/cli/wizard-config.js +19 -9
  33. package/dist/cli/wizard-config.js.map +1 -1
  34. package/dist/cli/worktree.d.ts.map +1 -1
  35. package/dist/cli/worktree.js +8 -1
  36. package/dist/cli/worktree.js.map +1 -1
  37. package/dist/dashboard/savings.d.ts +1 -4
  38. package/dist/dashboard/savings.d.ts.map +1 -1
  39. package/dist/dashboard/savings.js +2 -52
  40. package/dist/dashboard/savings.js.map +1 -1
  41. package/dist/delivery/acceptance-judge.d.ts +10 -0
  42. package/dist/delivery/acceptance-judge.d.ts.map +1 -1
  43. package/dist/delivery/acceptance-judge.js +45 -3
  44. package/dist/delivery/acceptance-judge.js.map +1 -1
  45. package/dist/delivery/agentic-executor.d.ts +12 -1
  46. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  47. package/dist/delivery/agentic-executor.js +25 -5
  48. package/dist/delivery/agentic-executor.js.map +1 -1
  49. package/dist/delivery/bash-sweep.d.ts +12 -0
  50. package/dist/delivery/bash-sweep.d.ts.map +1 -1
  51. package/dist/delivery/bash-sweep.js +8 -1
  52. package/dist/delivery/bash-sweep.js.map +1 -1
  53. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  54. package/dist/delivery/convergence-loop.js +6 -4
  55. package/dist/delivery/convergence-loop.js.map +1 -1
  56. package/dist/delivery/execution-gate.d.ts +1 -1
  57. package/dist/delivery/execution-gate.d.ts.map +1 -1
  58. package/dist/delivery/execution-gate.js +248 -5
  59. package/dist/delivery/execution-gate.js.map +1 -1
  60. package/dist/delivery/snapshot.d.ts +16 -0
  61. package/dist/delivery/snapshot.d.ts.map +1 -1
  62. package/dist/delivery/snapshot.js +113 -2
  63. package/dist/delivery/snapshot.js.map +1 -1
  64. package/dist/delivery/user-validation.d.ts.map +1 -1
  65. package/dist/delivery/user-validation.js +55 -10
  66. package/dist/delivery/user-validation.js.map +1 -1
  67. package/dist/delivery/verifier-ladder.d.ts +24 -0
  68. package/dist/delivery/verifier-ladder.d.ts.map +1 -1
  69. package/dist/delivery/verifier-ladder.js +119 -2
  70. package/dist/delivery/verifier-ladder.js.map +1 -1
  71. package/dist/generators/claude-md.js +17 -4
  72. package/dist/generators/claude-md.js.map +1 -1
  73. package/dist/policies/liveness.d.ts +105 -0
  74. package/dist/policies/liveness.d.ts.map +1 -0
  75. package/dist/policies/liveness.js +251 -0
  76. package/dist/policies/liveness.js.map +1 -0
  77. package/dist/policies/policy-memory.d.ts.map +1 -1
  78. package/dist/policies/policy-memory.js +3 -4
  79. package/dist/policies/policy-memory.js.map +1 -1
  80. package/dist/quality/baseline.d.ts +29 -0
  81. package/dist/quality/baseline.d.ts.map +1 -0
  82. package/dist/quality/baseline.js +79 -0
  83. package/dist/quality/baseline.js.map +1 -0
  84. package/dist/quality/complexity.d.ts +38 -0
  85. package/dist/quality/complexity.d.ts.map +1 -0
  86. package/dist/quality/complexity.js +239 -0
  87. package/dist/quality/complexity.js.map +1 -0
  88. package/dist/quality/config.d.ts +40 -0
  89. package/dist/quality/config.d.ts.map +1 -0
  90. package/dist/quality/config.js +77 -0
  91. package/dist/quality/config.js.map +1 -0
  92. package/dist/quality/crap.d.ts +11 -0
  93. package/dist/quality/crap.d.ts.map +1 -0
  94. package/dist/quality/crap.js +54 -0
  95. package/dist/quality/crap.js.map +1 -0
  96. package/dist/quality/languages.d.ts +25 -0
  97. package/dist/quality/languages.d.ts.map +1 -0
  98. package/dist/quality/languages.js +150 -0
  99. package/dist/quality/languages.js.map +1 -0
  100. package/dist/quality/report.d.ts +38 -0
  101. package/dist/quality/report.d.ts.map +1 -0
  102. package/dist/quality/report.js +251 -0
  103. package/dist/quality/report.js.map +1 -0
  104. package/dist/quality/scanner.d.ts +32 -0
  105. package/dist/quality/scanner.d.ts.map +1 -0
  106. package/dist/quality/scanner.js +127 -0
  107. package/dist/quality/scanner.js.map +1 -0
  108. package/dist/quality/tools.d.ts +28 -0
  109. package/dist/quality/tools.d.ts.map +1 -0
  110. package/dist/quality/tools.js +189 -0
  111. package/dist/quality/tools.js.map +1 -0
  112. package/dist/types/config.d.ts +148 -0
  113. package/dist/types/config.d.ts.map +1 -1
  114. package/dist/types/config.js +35 -0
  115. package/dist/types/config.js.map +1 -1
  116. package/docs/ATTRIBUTION.md +0 -1
  117. package/docs/INDEX.md +38 -4
  118. package/docs/analysis/MARKET_POSITION.md +179 -0
  119. package/docs/architecture/OVERVIEW.md +82 -18
  120. package/docs/architecture/PROTOCOL.md +1 -1
  121. package/docs/benchmarks/PAIRED_FINDINGS.md +106 -0
  122. package/docs/benchmarks/PAIRED_HARNESS.md +16 -0
  123. package/docs/benchmarks/README.md +59 -0
  124. package/docs/design/UAP_REACTOR.md +0 -1
  125. package/docs/getting-started/INSTALLATION.md +1 -1
  126. package/docs/guides/AUTOMATIC.md +0 -14
  127. package/docs/guides/AUTOMATIC_FEATURES.md +1 -9
  128. package/docs/guides/COORDINATION.md +1 -1
  129. package/docs/guides/DELIVERY_PIPELINE.md +5 -0
  130. package/docs/guides/DEPLOY_BATCHING.md +1 -1
  131. package/docs/guides/DROIDS_AND_SKILLS.md +1 -1
  132. package/docs/guides/LOCAL_MODELS.md +6 -1
  133. package/docs/guides/MCP_ROUTER.md +1 -1
  134. package/docs/guides/MEMORY.md +1 -1
  135. package/docs/guides/MULTI_MODEL.md +1 -1
  136. package/docs/guides/ORCHESTRATOR.md +1 -1
  137. package/docs/guides/POLICIES.md +19 -3
  138. package/docs/guides/QWEN38_EXL3.md +114 -0
  139. package/docs/guides/WORKTREE_WORKFLOW.md +45 -1
  140. package/docs/integrations/MCP_ROUTER.md +1 -4
  141. package/docs/plans/deliver-hardening-plan-2026-07-13.md +173 -0
  142. package/docs/plans/deliver-hardening-review-response-2026-07-13.md +180 -0
  143. package/docs/plans/k3-uap-uplift-validation.md +216 -0
  144. package/docs/plans/qwen38-uap-retest-plan.md +196 -0
  145. package/docs/reference/API.md +1 -1
  146. package/docs/reference/CLI.md +49 -14
  147. package/docs/reference/COMPARISON.md +248 -0
  148. package/docs/reference/CONFIGURATION.md +1 -3
  149. package/docs/reference/DATABASE_SCHEMA.md +1 -1
  150. package/docs/reference/FEATURES.md +3 -3
  151. package/docs/reference/PATTERNS.md +1 -1
  152. package/docs/reference/PLATFORMS.md +1 -1
  153. package/package.json +2 -2
  154. package/scripts/maintenance/update-skills.py +0 -22
  155. package/scripts/maintenance/verify-compliance.sh +3 -3
  156. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  157. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  158. package/src/policies/enforcers/adr_guard.py +0 -0
  159. package/src/policies/enforcers/bearer_lockdown.py +0 -0
  160. package/src/policies/enforcers/branch_freshness.py +0 -0
  161. package/src/policies/enforcers/design_token_gate.py +0 -0
  162. package/src/policies/enforcers/enforcement_self_protect.py +70 -0
  163. package/src/policies/enforcers/expert_review_required.py +22 -0
  164. package/src/policies/enforcers/iac_plan_destruction_check.py +0 -0
  165. package/src/policies/enforcers/quality_metrics_gate.py +447 -0
  166. package/src/policies/enforcers/ship_loop_gate.py +0 -0
  167. package/src/policies/enforcers/validate_plan_on_change.py +0 -0
  168. package/src/policies/enforcers/visual_verification.py +0 -0
  169. package/src/policies/enforcers/workdir_scope.py +0 -0
  170. package/src/policies/schemas/policies/expert-review-required.md +9 -1
  171. package/src/policies/schemas/policies/quality-metrics-gate.md +74 -0
  172. package/templates/CLAUDE.template.md +1 -1
  173. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  174. package/templates/hooks/pre-tool-use-bash.sh +50 -3
  175. package/templates/hooks/pre-tool-use-edit-write.sh +31 -0
  176. package/templates/hooks/session-start.sh +15 -0
  177. package/templates/hooks/uap-policy-gate.sh +187 -5
  178. package/templates/stryker.config.json +14 -0
  179. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  180. package/tools/agents/scripts/anthropic_proxy.py +254 -28
  181. package/tools/agents/tests/run_acceptance.py +0 -0
  182. package/tools/agents/tests/test_client_disconnect.py +0 -0
  183. package/tools/agents/tests/test_cycle_break_exploration.py +0 -0
  184. package/tools/agents/tests/test_deferral_break.py +0 -0
  185. package/tools/agents/tests/test_delivery_enforcement_all_langs.py +0 -0
  186. package/tools/agents/tests/test_delivery_enforcement_exemptions.py +0 -0
  187. package/tools/agents/tests/test_delivery_enforcement_filepath.py +0 -0
  188. package/tools/agents/tests/test_delivery_enforcement_web_and_bash.py +0 -0
  189. package/tools/agents/tests/test_delivery_enforcement_worktree.py +0 -0
  190. package/tools/agents/tests/test_delivery_enforcement_write_complexity.py +0 -0
  191. package/tools/agents/tests/test_disconnect_watcher.py +0 -0
  192. package/tools/agents/tests/test_doubling_break.py +0 -0
  193. package/tools/agents/tests/test_empty_maxtokens_recovery.py +0 -0
  194. package/tools/agents/tests/test_empty_tool_loop_break.py +0 -0
  195. package/tools/agents/tests/test_enforcement_self_protect.py +90 -0
  196. package/tools/agents/tests/test_enforcer_escape_hatches.py +0 -0
  197. package/tools/agents/tests/test_error_loop_break.py +0 -0
  198. package/tools/agents/tests/test_expert_review_pr_scope.py +13 -5
  199. package/tools/agents/tests/test_fastpath_config.py +0 -0
  200. package/tools/agents/tests/test_finalize_suppression.py +0 -0
  201. package/tools/agents/tests/test_gate_evidence.py +0 -0
  202. package/tools/agents/tests/test_gate_failclosed_indirection.py +0 -0
  203. package/tools/agents/tests/test_gate_integrity.py +0 -0
  204. package/tools/agents/tests/test_live_deliver_kill.py +0 -0
  205. package/tools/agents/tests/test_malformed_unclosed_think.py +0 -0
  206. package/tools/agents/tests/test_mandate_beats_recon.py +0 -0
  207. package/tools/agents/tests/test_mandate_deliver.py +0 -0
  208. package/tools/agents/tests/test_non_stream_resp_log.py +0 -0
  209. package/tools/agents/tests/test_output_token_ceilings.py +0 -0
  210. package/tools/agents/tests/test_overflow_truncate_count_tokens.py +0 -0
  211. package/tools/agents/tests/test_path_containment.py +0 -0
  212. package/tools/agents/tests/test_path_normalizer_hardened.py +0 -0
  213. package/tools/agents/tests/test_repeat_call_guard.py +0 -0
  214. package/tools/agents/tests/test_repeat_guard.py +0 -0
  215. package/tools/agents/tests/test_session_admission.py +0 -0
  216. package/tools/agents/tests/test_stream_heartbeat.py +0 -0
  217. package/tools/agents/tests/test_stream_thinking_deltas.py +405 -0
  218. package/tools/agents/tests/test_stuck_break_hard.py +0 -0
  219. package/tools/agents/tests/test_stuck_break_reattach.py +0 -0
  220. package/tools/agents/tests/test_turn_count_breaker_periodic.py +0 -0
  221. package/tools/agents/tests/test_upstream_chokepoint.py +0 -0
  222. package/tools/agents/tests/test_upstream_model_aliases.py +0 -0
  223. package/tools/agents/tests/test_validate_plan_gate.py +0 -0
  224. package/tools/agents/tests/test_vision_passthrough.py +0 -0
  225. package/tools/agents/tests/test_workdir_scope_enforcer.py +0 -0
  226. package/dist/cli/rtk.d.ts +0 -34
  227. package/dist/cli/rtk.d.ts.map +0 -1
  228. package/dist/cli/rtk.js +0 -401
  229. package/dist/cli/rtk.js.map +0 -1
  230. package/docs/integrations/RTK.md +0 -113
  231. package/scripts/setup/install-rtk.sh +0 -184
  232. package/src/policies/enforcers/rtk_wrap.py +0 -205
  233. package/src/policies/schemas/policies/rtk-wrap.md +0 -50
  234. package/tools/agents/tests/test_rtk_wrap.py +0 -151
@@ -69,6 +69,37 @@ for pattern in "${EXEMPT_PATTERNS[@]}"; do
69
69
  fi
70
70
  done
71
71
 
72
+ # Worktree enforcement is opt-out (setup wizard "Worktree isolation" off →
73
+ # .uap.json worktrees.enforce=false). When disabled this hook is a no-op;
74
+ # unreadable/missing config keeps the historical behavior (enforce). Mirrors
75
+ # the config-authoritative delivery-enforcement read in uap-policy-gate.sh.
76
+ # UAP_NO_WORKTREE=1 remains the per-run escape hatch and always wins.
77
+ # The config lives at the MAIN checkout root and is typically gitignored, so
78
+ # worktree checkouts don't carry it: resolve the main root via
79
+ # git-common-dir. Probed only for in-repo, non-exempt paths.
80
+ if [ "${UAP_NO_WORKTREE:-0}" = "1" ]; then
81
+ exit 0
82
+ fi
83
+ _WT_COMMON="$(git -C "$REPO_ROOT" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)"
84
+ _WT_MAIN=""
85
+ if [ -n "$_WT_COMMON" ]; then
86
+ _WT_MAIN="$(dirname "$_WT_COMMON" 2>/dev/null || true)"
87
+ fi
88
+ if [ -z "$_WT_MAIN" ] || [ ! -d "$_WT_MAIN" ]; then
89
+ _WT_MAIN="$REPO_ROOT"
90
+ fi
91
+ _WT_ENFORCE="$(python3 -c '
92
+ import json, sys
93
+ try:
94
+ cfg = json.load(open(sys.argv[1]))
95
+ print("off" if (cfg.get("worktrees") or {}).get("enforce") is False else "on")
96
+ except Exception:
97
+ print("on")
98
+ ' "$_WT_MAIN/.uap.json" 2>/dev/null || echo on)"
99
+ if [ "$_WT_ENFORCE" = "off" ]; then
100
+ exit 0
101
+ fi
102
+
72
103
  # Allow if path is inside a worktree (substring match, handles nested worktrees).
73
104
  # Before allowing a real source edit, announce it to the SHARED coordination DB
74
105
  # and check for a live overlap so independently-launched agents coordinate and
@@ -282,6 +282,21 @@ if command -v uap >/dev/null 2>&1; then
282
282
  ( cd "$PROJECT_DIR" 2>/dev/null && timeout 30 uap proxy ensure --if-enabled --quiet --client "$_uap_pc" --client-pid "$PPID" ) >/dev/null 2>&1 || true
283
283
  fi
284
284
 
285
+ # F1 (deliver-hardening 2026-07-13): refresh the policy-liveness cache once per
286
+ # session so the gate hook's degrade consult and `uap policy status` judge the
287
+ # CURRENT environment, not a stale one — and surface unhealthy policies loudly:
288
+ # a policy whose compliant path is dead is a catch-22 in waiting. Fail-open
289
+ # and time-boxed, like the rest of this hook.
290
+ if command -v uap >/dev/null 2>&1; then
291
+ # Run from the MAIN checkout (COORD_ROOT): the liveness writers anchor
292
+ # there internally too (mainCheckoutRoot), and policies.db lives only in
293
+ # the main tree — a worktree cwd would otherwise check nothing.
294
+ _uap_liveness="$( cd "$COORD_ROOT" 2>/dev/null && timeout 20 uap policy liveness --quiet 2>/dev/null || true )"
295
+ if [ -n "$_uap_liveness" ]; then
296
+ output+=$'\n'"## Policy Liveness (unhealthy — operator action needed)"$'\n'"$_uap_liveness"$'\n'
297
+ fi
298
+ fi
299
+
285
300
  if [ -n "$output" ]; then
286
301
  echo "$output"
287
302
  fi
@@ -43,12 +43,57 @@ except Exception:
43
43
  print("")
44
44
  PYEOF
45
45
  )"
46
+ # F4 (deliver-hardening 2026-07-13): a path-carrying op (Edit/Write/Read/…)
47
+ # must be judged by the TARGET file's repo, not by the hook's cwd. Before this,
48
+ # MAIN_ROOT came only from cwd, so a cross-repo edit applied THIS repo's
49
+ # posture and recorded compliance state in the wrong .uap — the target repo's
50
+ # policies never ran at all. Prefer the file_path's git toplevel; the cwd
51
+ # chain below remains the fallback for path-less ops (Bash, Task, …).
52
+ _FP_TARGET="$(UAP_PAYLOAD="$PAYLOAD" python3 - <<'PYEOF'
53
+ import json, os
54
+ try:
55
+ d = json.loads(os.environ.get("UAP_PAYLOAD") or "{}")
56
+ ti = d.get("tool_input") or d.get("args") or {}
57
+ # Same key set as the self-protect extractor below: harnesses differ
58
+ # (file_path vs filePath vs target/…), and an op whose path key this list
59
+ # misses anchors on cwd — the exact cross-repo mis-attribution F4 fixes.
60
+ for k in ("file_path", "path", "filePath", "target", "filename", "file"):
61
+ v = ti.get(k)
62
+ if isinstance(v, str) and v:
63
+ print(v)
64
+ break
65
+ else:
66
+ print("")
67
+ except Exception:
68
+ print("")
69
+ PYEOF
70
+ )"
46
71
  CHECKOUT_ROOT=""
47
- for _cand in "$_CD_TARGET" "$_PAYLOAD_CWD"; do
48
- [[ -z "$_cand" || ! -d "$_cand" ]] && continue
49
- _top="$(git -C "$_cand" rev-parse --show-toplevel 2>/dev/null || true)"
50
- [[ -n "$_top" ]] && { CHECKOUT_ROOT="$_top"; break; }
51
- done
72
+ if [[ -n "$_FP_TARGET" ]]; then
73
+ _fp="$_FP_TARGET"
74
+ # A relative file_path resolves against the agent's cwd, not the hook's.
75
+ [[ "$_fp" != /* ]] && _fp="${_PAYLOAD_CWD:-$PWD}/$_fp"
76
+ # Walk to the nearest EXISTING ancestor: a Write into a not-yet-created
77
+ # directory (other-repo/newdir/file.ts) fails `[[ -d ]]` at dirname and
78
+ # would otherwise fall through to the cwd chain — re-introducing the
79
+ # cross-repo mis-attribution for exactly the "new file in another repo"
80
+ # case (review fix).
81
+ _fp_dir="$(dirname -- "$_fp")"
82
+ while [[ ! -d "$_fp_dir" && "$_fp_dir" != "/" ]]; do
83
+ _fp_dir="$(dirname -- "$_fp_dir")"
84
+ done
85
+ if [[ -d "$_fp_dir" ]]; then
86
+ _top="$(git -C "$_fp_dir" rev-parse --show-toplevel 2>/dev/null || true)"
87
+ [[ -n "$_top" ]] && CHECKOUT_ROOT="$_top"
88
+ fi
89
+ fi
90
+ if [[ -z "$CHECKOUT_ROOT" ]]; then
91
+ for _cand in "$_CD_TARGET" "$_PAYLOAD_CWD"; do
92
+ [[ -z "$_cand" || ! -d "$_cand" ]] && continue
93
+ _top="$(git -C "$_cand" rev-parse --show-toplevel 2>/dev/null || true)"
94
+ [[ -n "$_top" ]] && { CHECKOUT_ROOT="$_top"; break; }
95
+ done
96
+ fi
52
97
  [[ -z "$CHECKOUT_ROOT" ]] && CHECKOUT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
53
98
  [[ -z "$CHECKOUT_ROOT" ]] && CHECKOUT_ROOT="$(cd "$HOOK_DIR/../.." 2>/dev/null && pwd || pwd)"
54
99
  MAIN_ROOT="${CHECKOUT_ROOT%%/.worktrees/*}"
@@ -63,6 +108,98 @@ export UAP_REPO_ROOT="$MAIN_ROOT"
63
108
  # actual WORKING TREE, not the (possibly bare) MAIN_ROOT. Expose the current checkout
64
109
  # so _common.worktree_root() targets the worktree when an op runs from inside one.
65
110
  export UAP_WORKTREE_ROOT="$CHECKOUT_ROOT"
111
+ # Enforcer state (.uap) belongs to the TARGET repo this operation anchored to —
112
+ # never a bare relative path that drifts with the caller's cwd (F4: enforcers
113
+ # honor UAP_STATE_DIR; the hook now DERIVES it instead of leaving it ambient).
114
+ export UAP_STATE_DIR="$MAIN_ROOT/.uap"
115
+
116
+ # F3 (deliver-hardening 2026-07-13): operator overrides via a SIGNED FILE, not
117
+ # env only. Env does not reliably reach hook subprocesses in every harness, so
118
+ # the documented escape hatches were effectively unreachable mid-session. The
119
+ # file is `$MAIN_ROOT/.uap/operator-overrides.json` carrying
120
+ # {deliverBypass, workdirAllow[], selfProtectOff, expiresAt}. Verified before
121
+ # ANY field is honored: regular file (no symlink), owned by ROOT, not
122
+ # group/world-writable, and expiresAt in the future — so an override cannot
123
+ # rot open and a planted one is inert. Root ownership is the load-bearing
124
+ # check (deliver-hardening security review, 2026-07-13): the agent runs as the
125
+ # USER's uid, so any user-owned file is agent-mintable (a uid==euid check is
126
+ # vacuous against a same-uid agent, and an interpreter-mediated write like
127
+ # `python3 -c 'open(...)'` walks past the self-protect text scan). Requiring
128
+ # uid 0 makes the override genuinely out-of-band: the operator plants it with
129
+ # sudo, the agent cannot. (When the agent itself runs as root no local check
130
+ # can help — enforcement is already theater at that point.)
131
+ # Verified fields are exported as the SAME env vars enforcers already honor
132
+ # (and only when unset — an explicit env still wins), so every downstream
133
+ # consumer picks the file up unchanged.
134
+ # Early-out on the common case: no file, no python spawn on the hot path.
135
+ _OVERRIDES=""
136
+ if [[ -e "$MAIN_ROOT/.uap/operator-overrides.json" || -L "$MAIN_ROOT/.uap/operator-overrides.json" ]]; then
137
+ _OVERRIDES="$(python3 - "$MAIN_ROOT" <<'PYEOF'
138
+ import json, os, stat, sys
139
+ from datetime import datetime, timezone
140
+ import errno as errnomod
141
+ path = os.path.join(sys.argv[1], ".uap", "operator-overrides.json")
142
+ try:
143
+ # O_NOFOLLOW + fstat on the OPENED fd: a lstat-then-open sequence leaves a
144
+ # swap race (replace the file between check and read); the fd pins the
145
+ # inode that was verified. O_NOFOLLOW absent on some platforms → 0.
146
+ fd = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
147
+ except OSError as e:
148
+ if e.errno == errnomod.ELOOP:
149
+ print("[UAP policy gate] operator-overrides.json IGNORED: symlink", file=sys.stderr)
150
+ sys.exit(0) # no override file (or a refused symlink) — the common case
151
+ try:
152
+ st = os.fstat(fd)
153
+ if not stat.S_ISREG(st.st_mode):
154
+ raise ValueError("not a regular file")
155
+ # Root-owned ONLY — see the block comment above. A same-uid check cannot
156
+ # constrain a same-uid agent.
157
+ if st.st_uid != 0:
158
+ raise ValueError("not root-owned (agent-mintable — refusing)")
159
+ if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
160
+ raise ValueError("group/world-writable")
161
+ d = json.load(os.fdopen(fd))
162
+ exp = d.get("expiresAt")
163
+ if not exp:
164
+ raise ValueError("missing expiresAt")
165
+ exp_dt = datetime.fromisoformat(str(exp).replace("Z", "+00:00"))
166
+ if exp_dt.tzinfo is None:
167
+ exp_dt = exp_dt.replace(tzinfo=timezone.utc)
168
+ if exp_dt <= datetime.now(timezone.utc):
169
+ raise ValueError("expired")
170
+ if d.get("deliverBypass") is True:
171
+ print("UAP_DELIVER_BYPASS=1")
172
+ wa = d.get("workdirAllow")
173
+ if isinstance(wa, list) and wa and all(isinstance(x, str) and x for x in wa):
174
+ # The values are joined with ':' and re-parsed line-wise below, so a
175
+ # value carrying a newline would INJECT extra KEY=VALUE lines into the
176
+ # export loop (a "\nUAP_SELF_PROTECT_OFF=1" payload would escalate a
177
+ # workdirAllow grant into a self-protect kill), and a ':' would corrupt
178
+ # the list. Reject both outright.
179
+ if any(("\n" in x or "\r" in x or ":" in x) for x in wa):
180
+ raise ValueError("workdirAllow contains a newline/colon — refusing")
181
+ print("UAP_WORKDIR_ALLOW=" + ":".join(wa))
182
+ if d.get("selfProtectOff") is True:
183
+ print("UAP_SELF_PROTECT_OFF=1")
184
+ except Exception as e:
185
+ # An override that fails verification is INERT, never an error that wedges
186
+ # the gate — but say so loudly; a silently ignored override reads as a
187
+ # broken gate.
188
+ print("[UAP policy gate] operator-overrides.json IGNORED: %s" % e, file=sys.stderr)
189
+ PYEOF
190
+ )"
191
+ fi
192
+ if [[ -n "$_OVERRIDES" ]]; then
193
+ while IFS='=' read -r _okey _oval; do
194
+ [[ -z "$_okey" ]] && continue
195
+ case "$_okey" in
196
+ UAP_DELIVER_BYPASS|UAP_WORKDIR_ALLOW|UAP_SELF_PROTECT_OFF)
197
+ # Only fill what the environment did not already set explicitly.
198
+ if [[ -z "${!_okey:-}" ]]; then export "$_okey=$_oval"; fi
199
+ ;;
200
+ esac
201
+ done <<< "$_OVERRIDES"
202
+ fi
66
203
  # Delivery enforcement posture. Precedence (highest first):
67
204
  # 1. .uap.json `delivery.enforcement` — the project's DECLARED posture is
68
205
  # AUTHORITATIVE (deliberate, reproducible IaC), so a stale ambient
@@ -439,6 +576,51 @@ except: print("2")' 2>/dev/null || echo 2)"
439
576
  # For every other enforcer, an error still fails open (unchanged behavior).
440
577
  [[ "$allowed" == "2" ]] && allowed=1
441
578
  if [[ "$allowed" == "0" ]]; then
579
+ # F1 (deliver-hardening 2026-07-13): degrade-on-dead-path — OPT-IN and
580
+ # sabotage-guarded. The liveness cache (uap policy liveness / status /
581
+ # session-start) marks a policy degradable ONLY when the policy declared
582
+ # degradeOnDeadPath AND every failed requirement is outside agent-writable
583
+ # surfaces — so an agent that breaks its own compliant path (deletes a
584
+ # skill, wedges deliver) earns NOTHING: the policy keeps blocking. A
585
+ # genuinely dead environmental path downgrades block → loud advisory
586
+ # instead of wedging the session in a catch-22.
587
+ # The cache can DOWNGRADE a blocking policy to advisory, so it is trusted
588
+ # only under the same terms as operator-overrides.json (security review,
589
+ # 2026-07-13): root-owned — a user-owned cache is agent-mintable and one
590
+ # printf would otherwise turn any blocking policy advisory — not
591
+ # group/world-writable, no symlink, and FRESH (checkedAt < 24h, so a fixed
592
+ # path silently re-tightens). `uap policy liveness` run as the user still
593
+ # writes the cache for STATUS display; the gate simply never degrades on
594
+ # it. An operator enabling the degrade path refreshes it with sudo.
595
+ if [[ -f "$MAIN_ROOT/.uap/policy-liveness.json" ]]; then
596
+ _degradable="$(python3 -c '
597
+ import json, os, re, stat, sys
598
+ from datetime import datetime, timezone, timedelta
599
+ try:
600
+ fd = os.open(sys.argv[1], os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
601
+ st = os.fstat(fd)
602
+ if not stat.S_ISREG(st.st_mode) or st.st_uid != 0 or (st.st_mode & (stat.S_IWGRP | stat.S_IWOTH)):
603
+ print("0"); sys.exit(0)
604
+ d = json.load(os.fdopen(fd))
605
+ checked = datetime.fromisoformat(str(d.get("checkedAt", "")).replace("Z", "+00:00"))
606
+ if checked.tzinfo is None:
607
+ checked = checked.replace(tzinfo=timezone.utc)
608
+ if datetime.now(timezone.utc) - checked > timedelta(hours=24):
609
+ print("0"); sys.exit(0)
610
+ # DB names may be Title Case ("Delivery Enforcement"); the cache is keyed
611
+ # by slug (delivery-enforcement). Same slug rule as policyNameSlug.
612
+ name = re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", sys.argv[2].lower())).strip("-")
613
+ p = (d.get("policies") or {}).get(name) or {}
614
+ print("1" if (p.get("healthy") is False and p.get("degradable") is True) else "0")
615
+ except Exception:
616
+ print("0")
617
+ ' "$MAIN_ROOT/.uap/policy-liveness.json" "$pname" 2>/dev/null || echo 0)"
618
+ if [[ "$_degradable" == "1" ]]; then
619
+ echo "[UAP policy gate] ADVISORY (degraded): '$pname' would block, but its compliant path is dead OUTSIDE agent-writable surfaces (.uap/policy-liveness.json). Allowing this op — fix the path or the policy (uap policy status)." >&2
620
+ record_execution 1 "$pid" "degraded: dead compliant path ($pname)"
621
+ continue
622
+ fi
623
+ fi
442
624
  # R1: consume the enforcer's route:deliver signal (log intent, opt-in
443
625
  # background auto-route to `uap deliver`). Falls back to the plain reason if
444
626
  # the helper is missing/fails.
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker-js/master/packages/api/schema/stryker-core.json",
3
+ "_comment": "Template for `uap quality mutate` — incremental mutation testing scoped to changed files. Copy to the target project root after `npm i -D @stryker-mutator/core @stryker-mutator/vitest-runner`. Thresholds enforce the 0-surviving-mutants policy on the mutated (changed) scope.",
4
+ "testRunner": "vitest",
5
+ "incremental": true,
6
+ "incrementalFile": ".stryker-tmp/incremental.json",
7
+ "mutate": [],
8
+ "thresholds": { "high": 100, "low": 100, "break": 100 },
9
+ "coverageAnalysis": "perTest",
10
+ "reporters": ["progress", "clear-text", "json"],
11
+ "jsonReporter": { "fileName": ".stryker-tmp/mutation-report.json" },
12
+ "timeoutMS": 60000,
13
+ "concurrency": 4
14
+ }