@miller-tech/uap 1.148.2 → 1.148.5

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 (39) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cli/deliver.d.ts +10 -0
  3. package/dist/cli/deliver.d.ts.map +1 -1
  4. package/dist/cli/deliver.js +89 -1
  5. package/dist/cli/deliver.js.map +1 -1
  6. package/dist/cli/hooks.d.ts +1 -0
  7. package/dist/cli/hooks.d.ts.map +1 -1
  8. package/dist/cli/hooks.js +74 -1
  9. package/dist/cli/hooks.js.map +1 -1
  10. package/dist/cli/verify.d.ts.map +1 -1
  11. package/dist/cli/verify.js +28 -7
  12. package/dist/cli/verify.js.map +1 -1
  13. package/dist/cli/worktree.js +2 -2
  14. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  15. package/dist/delivery/agentic-executor.js +25 -8
  16. package/dist/delivery/agentic-executor.js.map +1 -1
  17. package/dist/delivery/user-validation.d.ts.map +1 -1
  18. package/dist/delivery/user-validation.js +13 -1
  19. package/dist/delivery/user-validation.js.map +1 -1
  20. package/dist/delivery/vision-judge.d.ts +11 -0
  21. package/dist/delivery/vision-judge.d.ts.map +1 -1
  22. package/dist/delivery/vision-judge.js +60 -2
  23. package/dist/delivery/vision-judge.js.map +1 -1
  24. package/dist/delivery/visual-gate.d.ts +0 -7
  25. package/dist/delivery/visual-gate.d.ts.map +1 -1
  26. package/dist/delivery/visual-gate.js +51 -2
  27. package/dist/delivery/visual-gate.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  30. package/src/policies/enforcers/_common.py +6 -0
  31. package/src/policies/enforcers/delivery_enforcement.py +100 -4
  32. package/src/policies/enforcers/enforcement_self_protect.py +21 -4
  33. package/src/policies/enforcers/expert_review_required.py +13 -4
  34. package/templates/hooks/fastpath_gate.py +123 -0
  35. package/templates/hooks/uap-policy-gate.sh +98 -6
  36. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  37. package/tools/agents/scripts/anthropic_proxy.py +217 -0
  38. package/tools/agents/tests/test_empty_maxtokens_recovery.py +94 -0
  39. package/tools/agents/tests/test_error_loop_break.py +88 -0
@@ -50,8 +50,74 @@ EXEMPT_PREFIXES = (
50
50
  "src/policies/", "scripts/", "docs/", "policies/", "test/", "tests/",
51
51
  )
52
52
 
53
- # Test files are protected by deliver itself; never gate them here.
54
- TEST_MARKERS = (".test.", ".spec.", "_test.", "/test/", "/tests/", "/__tests__/")
53
+ # Test files are protected by deliver itself; never gate them here. Covers the
54
+ # path markers AND a file whose basename IS a test (e.g. a root-level `test.js`,
55
+ # `tests.py`, `spec.ts`, `test_foo.py`, `foo.test.tsx`) — those are the fast
56
+ # feedback loop and must not each trigger a full deliver cycle.
57
+ TEST_MARKERS = (".test.", ".spec.", "_test.", "test_", "/test/", "/tests/", "/__tests__/", "/spec/", "/specs/")
58
+ _TEST_BASENAMES = ("test", "tests", "spec", "specs", "conftest")
59
+
60
+
61
+ def _is_test_file(rel_posix: str) -> bool:
62
+ low = rel_posix.lower()
63
+ if any(m in "/" + low for m in TEST_MARKERS):
64
+ return True
65
+ stem = low.rsplit("/", 1)[-1].rsplit(".", 1)[0]
66
+ return stem in _TEST_BASENAMES or stem.startswith("test") or stem.endswith("test") or stem.endswith("spec")
67
+
68
+
69
+ # Trivial-edit fast-path: a tiny change (a one-line tweak, a constant, a typo)
70
+ # does not warrant a full deliver decompose→epics→gates cycle — on a slow local
71
+ # executor that is ~10+ min per edit. Below this many changed characters the
72
+ # edit is allowed directly (advisory nudge only). UAP_DELIVER_FASTPATH=off
73
+ # disables; UAP_DELIVER_TRIVIAL_EDIT_CHARS tunes the threshold.
74
+ def _fastpath_on() -> bool:
75
+ # The policy gate's cumulative fast-path (fastpath_gate.py) sets
76
+ # UAP_FASTPATH_ROUTED=1 when a trivial edit has crossed the per-file
77
+ # cumulative budget and must route through deliver. Honor it — otherwise the
78
+ # enforcer's own trivial allowance would independently re-approve the edit
79
+ # and defeat the budget (death-by-a-thousand-small-edits escape).
80
+ if os.environ.get("UAP_FASTPATH_ROUTED") == "1":
81
+ return False
82
+ return os.environ.get("UAP_DELIVER_FASTPATH", "on").lower() not in {"0", "off", "false", "no"}
83
+
84
+
85
+ def _trivial_edit_chars() -> int:
86
+ try:
87
+ return max(0, int(os.environ.get("UAP_DELIVER_TRIVIAL_EDIT_CHARS", "240")))
88
+ except ValueError:
89
+ return 240
90
+
91
+
92
+ def _changed_chars(args: dict) -> int | None:
93
+ """Total changed characters for an Edit/MultiEdit, or None when it can't be
94
+ measured (e.g. a whole-file Write) — None means 'not trivially small'."""
95
+ def pair(o: object, n: object) -> int:
96
+ return len(str(o or "")) + len(str(n or ""))
97
+ if "edits" in args and isinstance(args["edits"], list):
98
+ return sum(
99
+ pair(e.get("old_string") or e.get("oldString"), e.get("new_string") or e.get("newString"))
100
+ for e in args["edits"] if isinstance(e, dict)
101
+ )
102
+ old = args.get("old_string") or args.get("oldString")
103
+ new = args.get("new_string") or args.get("newString")
104
+ if old is not None or new is not None:
105
+ return pair(old, new)
106
+ return None # Write (full content) — not a trivial edit
107
+
108
+
109
+ def _deliver_lock_holder(root: Path) -> str | None:
110
+ """PID string of a LIVE deliver run holding the project lock, else None."""
111
+ lock = root / ".uap" / "deliver.lock"
112
+ try:
113
+ pid = int((lock.read_text().split("|")[0] or "").strip())
114
+ except (OSError, ValueError):
115
+ return None
116
+ try:
117
+ os.kill(pid, 0)
118
+ return str(pid)
119
+ except OSError:
120
+ return None
55
121
 
56
122
 
57
123
  def _is_local_model_session() -> bool:
@@ -115,8 +181,8 @@ def main() -> None:
115
181
  if any(rel_posix.startswith(p) for p in EXEMPT_PREFIXES):
116
182
  emit(True, f"exempt path: {rel_posix}")
117
183
  return
118
- if any(m in "/" + low for m in TEST_MARKERS):
119
- emit(True, "test file (protected by deliver itself)")
184
+ if _is_test_file(rel_posix):
185
+ emit(True, "test file (fast feedback loop; deliver protects it via gates)")
120
186
  return
121
187
 
122
188
  # Escape hatches.
@@ -127,6 +193,36 @@ def main() -> None:
127
193
  emit(True, "UAP_DELIVER_BYPASS override set")
128
194
  return
129
195
 
196
+ # A — trivial-edit fast-path: allow a tiny change directly (advisory) rather
197
+ # than paying a full deliver cycle for a one-liner.
198
+ if _fastpath_on():
199
+ changed = _changed_chars(args)
200
+ if changed is not None and changed <= _trivial_edit_chars():
201
+ print(
202
+ f"[delivery-enforcement] trivial edit to '{rel_posix}' "
203
+ f"({changed} chars) — allowed directly; route substantive changes through deliver.",
204
+ file=sys.stderr,
205
+ )
206
+ emit(True, f"trivial edit fast-path ({changed} <= {_trivial_edit_chars()} chars)")
207
+ return
208
+
209
+ # B — a deliver run is ALREADY working this project. Do NOT launch another
210
+ # (the lock would reject it anyway) and do NOT let the model keep editing/
211
+ # testing the same code underneath it — tell it to WAIT. route:wait so the
212
+ # autoroute stands down instead of enqueuing a duplicate.
213
+ holder = _deliver_lock_holder(root)
214
+ if holder is not None and os.environ.get("UAP_ENFORCE_DELIVERY", "block").lower() == "block":
215
+ emit(
216
+ False,
217
+ f"WAIT: a `uap deliver` run (pid {holder}) is ALREADY in progress for this project and is "
218
+ f"changing the code — do NOT edit '{rel_posix}', and do NOT re-run tests on it yet. "
219
+ "Let that run finish (check: uap deliver --resume latest / its output), THEN re-verify. "
220
+ "Launching or editing now only conflicts with the in-flight run.",
221
+ route="wait",
222
+ deliverHint="",
223
+ )
224
+ return
225
+
130
226
  # #3-F: terse, imperative, model-parseable. Weak local models otherwise
131
227
  # retry the blocked edit or hallucinate completion ("the files exist") when
132
228
  # the message is a passive explanation. State the exact next action.
@@ -24,7 +24,9 @@ import sys
24
24
  from pathlib import Path
25
25
 
26
26
  sys.path.insert(0, str(Path(__file__).parent))
27
- from _common import emit, parse_cli, repo_root # noqa: E402
27
+ from _common import ( # noqa: E402
28
+ emit, parse_cli, repo_root, REVIEW_ARTIFACT_DIR, REVIEW_WAIVER_DIR,
29
+ )
28
30
 
29
31
  EDIT_OPS = {"Edit", "Write", "MultiEdit", "edit", "write", "multiedit"}
30
32
 
@@ -66,6 +68,17 @@ def _is_protected_path(rel_posix: str) -> bool:
66
68
  # "/src/policies/", …) match only a genuine path SEGMENT. Filename markers
67
69
  # ("uap-policy-gate.sh", "anthropic-proxy.env") still match as substrings.
68
70
  low = "/" + rel_posix.lower().lstrip("/")
71
+ # Designed-writable escape hatches that live UNDER a protected marker but are
72
+ # NOT part of the delivery-enforcement control surface: expert-review
73
+ # artifacts (.uap/reviews/) and the committable review waiver
74
+ # (policies/waivers/). Writing these satisfies the SEPARATE
75
+ # expert-review-required gate and cannot weaken delivery enforcement — the
76
+ # markers above still guard .uap.json, proxy env, enforcer code and hooks.
77
+ # Without this carve-out the two gates deadlock: expert-review demands an
78
+ # artifact that self-protect forbids writing.
79
+ allow = ("/" + REVIEW_ARTIFACT_DIR + "/", "/" + REVIEW_WAIVER_DIR + "/")
80
+ if any(a in low for a in allow):
81
+ return False
69
82
  return any(m in low for m in PROTECTED_MARKERS)
70
83
 
71
84
 
@@ -79,11 +92,15 @@ def main() -> None:
79
92
  target = args.get("file_path") or args.get("path") or args.get("target") or ""
80
93
  if not target:
81
94
  emit(True, "no file path in args")
95
+ rp = Path(target).resolve()
82
96
  try:
83
- rel = str(Path(target).resolve().relative_to(repo_root()))
97
+ rel = str(rp.relative_to(repo_root()))
84
98
  except ValueError:
85
- # Out-of-repo writes (e.g. the proxy env in ~/.config) — match by name.
86
- rel = target
99
+ # Out-of-repo writes (e.g. the proxy env in ~/.config) — match by name
100
+ # against the RESOLVED path, never the raw arg: a raw
101
+ # ".uap/reviews/../../anthropic-proxy.env" would otherwise smuggle the
102
+ # allow-list carve-out to reach an out-of-repo control file.
103
+ rel = str(rp)
87
104
  rel_posix = rel.replace(os.sep, "/")
88
105
  if _is_protected_path(rel_posix):
89
106
  emit(
@@ -24,7 +24,9 @@ import sys
24
24
  from pathlib import Path
25
25
 
26
26
  sys.path.insert(0, str(Path(__file__).parent))
27
- from _common import emit, parse_cli, worktree_root, run # noqa: E402
27
+ from _common import ( # noqa: E402
28
+ emit, parse_cli, worktree_root, run, REVIEW_ARTIFACT_DIR, REVIEW_WAIVER_DIR,
29
+ )
28
30
 
29
31
  # Ship verbs are anchored to their tool prefix so that the bare tokens "merge"
30
32
  # or "signoff" inside read-only commands (git diff --merge-base, rg merge,
@@ -81,12 +83,12 @@ def _is_low_risk(f: str) -> bool:
81
83
  def _active_waiver(root: Path) -> bool:
82
84
  """A committable, env-free bypass: an active waiver file. Works in harnesses
83
85
  that strip env vars (where UAP_NO_REVIEW=1 cannot be set)."""
84
- wdir = root / "policies" / "waivers"
86
+ wdir = root / REVIEW_WAIVER_DIR
85
87
  if wdir.exists():
86
88
  for w in wdir.glob("*expert-review*.md"):
87
89
  if w.is_file():
88
90
  return True
89
- return (root / ".uap" / "reviews" / "WAIVER").exists()
91
+ return (root / REVIEW_ARTIFACT_DIR / "WAIVER").exists()
90
92
 
91
93
 
92
94
  def current_branch(root: Path) -> str | None:
@@ -129,6 +131,13 @@ def main() -> None:
129
131
  emit(True, "not a ship operation")
130
132
 
131
133
  cmd = args.get("command") or args.get("cmd") or ""
134
+ # The policy-gate hook runs in the harness env, not the inline command env,
135
+ # so `UAP_NO_REVIEW=1 git commit ...` never reaches os.environ above. Honor
136
+ # an inline assignment parsed from the command string too.
137
+ # Anchored to a LEADING env-assignment run so an incidental mention in a
138
+ # quoted arg / commit message does NOT silently skip review.
139
+ if re.search(r"^\s*(?:[A-Za-z_]\w*=\S*\s+)*UAP_NO_REVIEW=['\"]?1\b", cmd):
140
+ emit(True, "UAP_NO_REVIEW inline override set")
132
141
  if not any(p.search(cmd) for p in SHIP_PATTERNS):
133
142
  emit(True, "not a ship action")
134
143
 
@@ -159,7 +168,7 @@ def main() -> None:
159
168
  "— parallel expert review not required",
160
169
  )
161
170
 
162
- review = root / ".uap" / "reviews" / f"{slug}.json"
171
+ review = root / REVIEW_ARTIFACT_DIR / f"{slug}.json"
163
172
  if not review.exists():
164
173
  emit(
165
174
  False,
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ """Edit fast-path decider with CUMULATIVE accounting.
3
+
4
+ The policy gate lets test-file edits and TRIVIAL source edits (<= TRIVIAL chars
5
+ changed) through directly, so iteration on a slow local executor stays fast
6
+ instead of paying a full deliver cycle per keystroke. But an unbounded per-edit
7
+ threshold is an escape hatch: a weak model can assemble a whole (broken) feature
8
+ out of dozens of sub-threshold edits, none of which ever routes to deliver or
9
+ gets validated ("death by a thousand small edits").
10
+
11
+ This decider keeps the fast-path for genuinely small changes but BOUNDS how much
12
+ un-routed change can accumulate per file. It tallies trivial-edit chars/count per
13
+ file in .uap/fastpath-accum.json; once a file crosses CUM_CHARS total chars OR
14
+ CUM_EDITS edits since it last routed, the NEXT edit is NOT fast-pathed — it falls
15
+ through to the deliver-routing gate (which writes + validates), and the file's
16
+ tally resets. So un-validated drift per file is bounded to ~CUM_CHARS.
17
+
18
+ stdin: the tool_input JSON. exit 0 = fast-path (allow directly); exit 1 = route
19
+ through deliver. On any parse/IO trouble it fails toward ROUTING (safe).
20
+
21
+ env: TRIVIAL (per-edit trivial threshold, default 240), CUM_CHARS (cumulative
22
+ budget, default 800), CUM_EDITS (cumulative edit-count budget, default 6),
23
+ UAP_MAIN_ROOT (project root holding .uap/, default cwd). Test files always
24
+ fast-path and never accumulate.
25
+ """
26
+ import json
27
+ import os
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ TEST_MARKERS = (
32
+ ".test.", ".spec.", "_test.", "test_", "/test/", "/tests/",
33
+ "/__tests__/", "/spec/", "/specs/",
34
+ )
35
+
36
+
37
+ def _is_test(target: str) -> bool:
38
+ base = target.rsplit("/", 1)[-1]
39
+ stem = base.rsplit(".", 1)[0]
40
+ return (
41
+ any(m in "/" + target for m in TEST_MARKERS)
42
+ or stem in ("test", "tests", "spec", "specs", "conftest")
43
+ or stem.startswith("test")
44
+ or stem.endswith("test")
45
+ or stem.endswith("spec")
46
+ )
47
+
48
+
49
+ def _changed_chars(a: dict):
50
+ """Total chars touched by this edit, or None when the shape carries no diff
51
+ (e.g. a whole-file Write) — None => never trivial => route."""
52
+ edits = a.get("edits")
53
+ if isinstance(edits, list):
54
+ return sum(
55
+ len(str(e.get("old_string") or e.get("oldString") or ""))
56
+ + len(str(e.get("new_string") or e.get("newString") or ""))
57
+ for e in edits
58
+ if isinstance(e, dict)
59
+ )
60
+ o = a.get("old_string") or a.get("oldString")
61
+ n = a.get("new_string") or a.get("newString")
62
+ if o is None and n is None:
63
+ return None
64
+ return len(str(o or "")) + len(str(n or ""))
65
+
66
+
67
+ def main() -> int:
68
+ try:
69
+ a = json.loads(sys.stdin.read() or "{}")
70
+ except Exception:
71
+ return 1 # unparseable → route (safe)
72
+
73
+ target = (
74
+ a.get("file_path") or a.get("filePath") or a.get("path")
75
+ or a.get("target") or a.get("filename") or a.get("file") or ""
76
+ ).lower()
77
+ if not target:
78
+ return 1
79
+
80
+ if _is_test(target):
81
+ return 0 # test files always fast-path, no accounting
82
+
83
+ c = _changed_chars(a)
84
+ trivial_threshold = int(os.environ.get("TRIVIAL", "240"))
85
+ if c is None or c > trivial_threshold:
86
+ return 1 # non-trivial (or no diff info) → route
87
+
88
+ # Trivial source edit: allow only while cumulative drift stays under budget.
89
+ cum_chars = int(os.environ.get("CUM_CHARS", "800"))
90
+ cum_edits = int(os.environ.get("CUM_EDITS", "6"))
91
+ root = os.environ.get("UAP_MAIN_ROOT") or "."
92
+ accum_file = Path(root) / ".uap" / "fastpath-accum.json"
93
+ try:
94
+ accum = json.loads(accum_file.read_text())
95
+ if not isinstance(accum, dict):
96
+ accum = {}
97
+ except Exception:
98
+ accum = {}
99
+
100
+ rec = accum.get(target) or {}
101
+ new_chars = int(rec.get("chars", 0)) + c
102
+ new_edits = int(rec.get("edits", 0)) + 1
103
+
104
+ if new_chars > cum_chars or new_edits > cum_edits:
105
+ # Budget crossed: this edit routes through deliver (which validates the
106
+ # accumulated change), and the file's tally resets for the next batch.
107
+ accum[target] = {"chars": 0, "edits": 0}
108
+ decision = 1
109
+ else:
110
+ accum[target] = {"chars": new_chars, "edits": new_edits}
111
+ decision = 0
112
+
113
+ try:
114
+ accum_file.parent.mkdir(parents=True, exist_ok=True)
115
+ accum_file.write_text(json.dumps(accum))
116
+ except Exception:
117
+ pass # best-effort; a write failure must not change the verdict
118
+
119
+ return decision
120
+
121
+
122
+ if __name__ == "__main__":
123
+ sys.exit(main())
@@ -63,11 +63,37 @@ export UAP_REPO_ROOT="$MAIN_ROOT"
63
63
  # actual WORKING TREE, not the (possibly bare) MAIN_ROOT. Expose the current checkout
64
64
  # so _common.worktree_root() targets the worktree when an op runs from inside one.
65
65
  export UAP_WORKTREE_ROOT="$CHECKOUT_ROOT"
66
- # Delivery enforcement defaults to BLOCK for UAP-managed projects: substantive
67
- # source edits must route through `uap deliver` (verified completion against the
68
- # gates). The `:-` preserves any explicit operator/CI override (advisory|block).
69
- # Escape hatches still apply: UAP_DELIVER_ACTIVE=1 (inside deliver) / UAP_DELIVER_BYPASS=1.
70
- export UAP_ENFORCE_DELIVERY="${UAP_ENFORCE_DELIVERY:-block}"
66
+ # Delivery enforcement posture. Precedence (highest first):
67
+ # 1. .uap.json `delivery.enforcement` the project's DECLARED posture is
68
+ # AUTHORITATIVE (deliberate, reproducible IaC), so a stale ambient
69
+ # UAP_ENFORCE_DELIVERY=advisory (leaked into a launching shell/session env,
70
+ # e.g. an opencode session started months ago when an env export was still
71
+ # in .bashrc) can NOT silently downgrade a project configured to `block`
72
+ # and let the agent bypass deliver. `delivery.localMode` is read the same
73
+ # authoritative way and selects how a local-model session resolves block
74
+ # (advisory|deliver|block — see delivery_enforcement.py).
75
+ # 2. ambient UAP_ENFORCE_DELIVERY (operator/CI per-run override) when the
76
+ # project declares nothing.
77
+ # 3. default: block.
78
+ # Escape hatches always apply and are honored by the enforcer BEFORE mode:
79
+ # UAP_DELIVER_ACTIVE=1 (inside a deliver run) / UAP_DELIVER_BYPASS=1 (sanctioned
80
+ # manual edit) — so config-authoritative block never re-routes deliver's own edits.
81
+ _DLV_CFG="$(python3 -c '
82
+ import json, sys
83
+ try:
84
+ d = (json.load(open(sys.argv[1])).get("delivery") or {})
85
+ e = str(d.get("enforcement", "")).lower()
86
+ print("%s|%s" % (e if e in ("block", "advisory", "off") else "", str(d.get("localMode", "")).lower()))
87
+ except Exception:
88
+ print("|")
89
+ ' "$MAIN_ROOT/.uap.json" 2>/dev/null || echo "|")"
90
+ _DLV_MODE="${_DLV_CFG%%|*}"; _DLV_LOCAL="${_DLV_CFG##*|}"
91
+ if [[ -n "$_DLV_MODE" ]]; then
92
+ export UAP_ENFORCE_DELIVERY="$_DLV_MODE" # config-authoritative: overrides stale ambient
93
+ else
94
+ export UAP_ENFORCE_DELIVERY="${UAP_ENFORCE_DELIVERY:-block}"
95
+ fi
96
+ [[ -n "$_DLV_LOCAL" ]] && export UAP_DELIVER_LOCAL_MODE="$_DLV_LOCAL"
71
97
  cd "$MAIN_ROOT"
72
98
 
73
99
  TOOL="$(printf '%s' "$PAYLOAD" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("tool_name") or d.get("tool") or "")' 2>/dev/null || true)"
@@ -103,6 +129,65 @@ print("1" if (hit or bypass) else "0")
103
129
  # Operator out-of-band override disables the fail-closed guard too.
104
130
  [[ "${UAP_SELF_PROTECT_OFF:-}" == "1" ]] && SEC_SENSITIVE=0
105
131
 
132
+ # ── Edit fast-path (A) ───────────────────────────────────────────────────────
133
+ # Test files and TRIVIAL edits (a one-liner, a constant, a typo) are the fast
134
+ # feedback loop; routing each through the full worktree/task/deliver workflow
135
+ # gates costs a ~10+ min deliver cycle on a slow local executor. Allow them
136
+ # directly — BEFORE the workflow-gate loop — so iteration stays fast. This never
137
+ # bypasses SAFETY: a sensitive control-surface path (SEC_SENSITIVE) is excluded,
138
+ # and self/infra-protect gate bash, not these edits. UAP_DELIVER_FASTPATH=off
139
+ # disables; UAP_DELIVER_TRIVIAL_EDIT_CHARS tunes the trivial threshold.
140
+ if [[ "${UAP_DELIVER_FASTPATH:-on}" != "off" && "$SEC_SENSITIVE" != "1" ]]; then
141
+ case " Edit Write MultiEdit edit write multiedit " in
142
+ *" $TOOL "*)
143
+ # Prefer the cumulative-aware decider (fastpath_gate.py): it bounds how
144
+ # much un-routed trivial change can accumulate per file (CUM_CHARS/
145
+ # CUM_EDITS) so a weak model can't assemble a whole broken feature out of
146
+ # sub-threshold edits that never route to deliver. Fall back to the inline
147
+ # per-edit trivial check when the helper isn't deployed.
148
+ _fp_ok=0
149
+ if [[ -f "$HOOK_DIR/fastpath_gate.py" ]]; then
150
+ # `if <pipeline>; then` (not `<pipeline> && x=1`): a route verdict (exit 1)
151
+ # is a CONDITION here, so `set -e` doesn't abort — it just falls through
152
+ # to the enforcer loop, which blocks+routes.
153
+ if printf '%s' "$ARGS" | TRIVIAL="${UAP_DELIVER_TRIVIAL_EDIT_CHARS:-240}" \
154
+ CUM_CHARS="${UAP_DELIVER_CUMULATIVE_CHARS:-800}" \
155
+ CUM_EDITS="${UAP_DELIVER_CUMULATIVE_EDITS:-6}" \
156
+ UAP_MAIN_ROOT="$MAIN_ROOT" \
157
+ python3 "$HOOK_DIR/fastpath_gate.py" 2>/dev/null; then
158
+ _fp_ok=1
159
+ fi
160
+ elif printf '%s' "$ARGS" | TRIVIAL="${UAP_DELIVER_TRIVIAL_EDIT_CHARS:-240}" python3 -c '
161
+ import json, os, sys
162
+ try: a = json.loads(sys.stdin.read() or "{}")
163
+ except Exception: sys.exit(1)
164
+ target = (a.get("file_path") or a.get("filePath") or a.get("path") or a.get("target") or a.get("filename") or a.get("file") or "").lower()
165
+ base = target.rsplit("/", 1)[-1]; stem = base.rsplit(".", 1)[0]
166
+ markers = (".test.", ".spec.", "_test.", "test_", "/test/", "/tests/", "/__tests__/", "/spec/", "/specs/")
167
+ is_test = any(m in "/" + target for m in markers) or stem in ("test","tests","spec","specs","conftest") or stem.startswith("test") or stem.endswith("test") or stem.endswith("spec")
168
+ def changed(a):
169
+ if isinstance(a.get("edits"), list):
170
+ return sum(len(str(e.get("old_string") or e.get("oldString") or "")) + len(str(e.get("new_string") or e.get("newString") or "")) for e in a["edits"] if isinstance(e, dict))
171
+ o=a.get("old_string") or a.get("oldString"); n=a.get("new_string") or a.get("newString")
172
+ return (len(str(o or ""))+len(str(n or ""))) if (o is not None or n is not None) else None
173
+ c = changed(a)
174
+ trivial = c is not None and c <= int(os.environ.get("TRIVIAL","240"))
175
+ sys.exit(0 if (is_test or trivial) else 1)
176
+ ' 2>/dev/null; then
177
+ _fp_ok=1
178
+ fi
179
+ if [[ "$_fp_ok" == "1" ]]; then
180
+ echo "[UAP policy gate] fast-path: test-file or trivial edit (within cumulative budget) — allowed directly (accumulated/substantive source changes route through deliver)." >&2
181
+ exit 0
182
+ fi
183
+ # The cumulative decider chose to ROUTE this edit (budget crossed, or
184
+ # non-trivial). Signal the delivery-enforcement enforcer to skip its OWN
185
+ # trivial allowance so it actually blocks+routes instead of re-approving.
186
+ export UAP_FASTPATH_ROUTED=1
187
+ ;;
188
+ esac
189
+ fi
190
+
106
191
  fail_closed() {
107
192
  echo "[UAP policy gate] FAIL-CLOSED: this operation touches the enforcement control surface but the self-protect enforcer could not run (${1:-machinery unavailable}). Blocked so a broken/absent gate can't become a bypass. (Operator override: UAP_SELF_PROTECT_OFF=1.)" >&2
108
193
  exit 2
@@ -182,7 +267,14 @@ except: print("")' 2>/dev/null || echo "")"
182
267
  echo "$msg" >&2
183
268
  exit 2
184
269
  fi
185
- done < <(sqlite3 "$DB" "SELECT p.id, p.name, t.toolName FROM policies p JOIN executable_tools t ON t.policyId=p.id WHERE p.isActive=1;")
270
+ # ORDER BY priority DESC: the policies table has a `priority` column but the
271
+ # gate previously ignored it, iterating in rowid order — so a high-priority
272
+ # routing/safety policy (e.g. delivery-enforcement, infra-protect) could be
273
+ # pre-empted by a lower-priority edit-gate that happened to be registered
274
+ # first, and the agent saw the wrong routing message. Honor priority now so
275
+ # the intended policy fires first. (Blocking short-circuits at exit 2, so the
276
+ # highest-priority applicable block wins.) Name is the stable tiebreak.
277
+ done < <(sqlite3 "$DB" "SELECT p.id, p.name, t.toolName FROM policies p JOIN executable_tools t ON t.policyId=p.id WHERE p.isActive=1 ORDER BY p.priority DESC, p.name ASC;")
186
278
 
187
279
  # A sensitive op that no self-protect enforcer ever evaluated = the control
188
280
  # surface is unguarded (self-protect not registered/active). Fail closed.