@miller-tech/uap 1.211.0 → 1.211.2

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.
@@ -83,6 +83,8 @@ Top level: `version`, `project`, `memory`, `worktree`, `costOptimization`,
83
83
  | `UAP_DELIVER_SANDBOX` | Deliver sandbox root path | — |
84
84
  | `UAP_ACTIVE_RUNS_DIR` | Registry of live deliver runs, used for cross-root (nested-project) single-flight | `~/.uap/active-runs` |
85
85
  | `UAP_DELIVER_NO_LOCK` | `1` disables the per-project lock **and** the overlapping-root check; the run also becomes invisible to other runs' checks | enabled |
86
+ | `UAP_SCHEMA_DIFF_INLINE` | Set by the schema-diff gate on its own checker subprocess so a nested gate skips the inline check instead of recursing. Setting it yourself only drops the gate to its marker fallback — it does not disable it — and the **inline** form is refused, like every switch in this group. | set by the gate |
87
+ | `UAP_ENFORCER_TIMEOUT` | Seconds the policy hook allows each enforcer. Must exceed the schema enforcer's own worst case (`INLINE_TIMEOUT` × 2 sources) or a slow healthy check is killed, which on a commit is a refusal. | `30` |
86
88
  | `UAP_HALO_TRACE` | `1` enables HALO tracing | off |
87
89
  | `UAP_HALO_TRACE_PATH` | HALO trace output file | `.uap/halo/traces.jsonl` |
88
90
  | `UAP_HALO_PROJECT_ID` | HALO project id | `uap` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.211.0",
3
+ "version": "1.211.2",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "start": "node dist/bin/cli.js",
22
22
  "test": "vitest",
23
23
  "test:ci": "vitest run",
24
- "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_repeat_call_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_write_complexity tools.agents.tests.test_fastpath_config tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_non_stream_resp_log tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window tools.agents.tests.test_thinking_template_kwargs tools.agents.tests.test_cycle_break_wait_poll",
24
+ "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_repeat_call_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_gate_failclosed_schema_diff tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_write_complexity tools.agents.tests.test_fastpath_config tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_non_stream_resp_log tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window tools.agents.tests.test_thinking_template_kwargs tools.agents.tests.test_cycle_break_wait_poll",
25
25
  "test:coverage": "vitest --coverage",
26
26
  "bench": "vitest --config vitest.bench.config.ts",
27
27
  "lint": "eslint src --ext .ts",
@@ -31,7 +31,7 @@
31
31
  "install:web": "bash scripts/setup/install-web.sh",
32
32
  "install:desktop": "bash scripts/setup/install-desktop.sh",
33
33
  "install:cloakbrowser": "tsx scripts/setup/install-cloakbrowser.ts",
34
- "postinstall": "echo '\n\u2728 Run: npx @miller-tech/uap init --interactive'",
34
+ "postinstall": "echo '\n Run: npx @miller-tech/uap init --interactive'",
35
35
  "version:patch": "bash scripts/version-bump.sh patch",
36
36
  "version:minor": "bash scripts/version-bump.sh minor",
37
37
  "version:major": "bash scripts/version-bump.sh major",
@@ -93,6 +93,13 @@ BYPASS_PATTERNS = (
93
93
  # acceptance criteria. An inline assignment would let the agent waive its
94
94
  # own consistency requirement, so refuse it here.
95
95
  re.compile(r"UAP_ORACLE_CONSISTENCY\s*=\s*['\"]?0", re.I),
96
+ # schema-diff's re-entry guard. It exists so a nested gate skips its own
97
+ # inline checker run rather than recursing, and it no longer allows
98
+ # anything on its own -- but it still WEAKENS the gate to its fallback
99
+ # layer, and every other switch of that shape is listed here. An inline
100
+ # assignment is written by the constrained party into its own command
101
+ # line, which is an off switch rather than an override.
102
+ re.compile(r"UAP_SCHEMA_DIFF_INLINE\s*=\s*['\"]?1", re.I),
96
103
  )
97
104
 
98
105
  # The gateless-root override in its CLI-flag spelling. Anchored on both sides
@@ -205,8 +205,9 @@ def schema_diff_ok(root: Path) -> list | None:
205
205
  freshness window is applied here, per candidate.
206
206
 
207
207
  Every miss must return None, not False: main() consumes this value, and a
208
- stray bool crashed the enforcer -- which the policy hook turns into ALLOW,
209
- so a wrong type here opens the gate rather than closing it.
208
+ stray bool crashed the enforcer. On a commit or push the hook now turns
209
+ that crash into a refusal rather than an ALLOW, so the cost is a blocked
210
+ commit instead of a silent bypass -- still a bug, and still worth the type.
210
211
  """
211
212
  db = root / "agents" / "data" / "memory" / "short_term.db"
212
213
  if not db.exists():
@@ -268,10 +269,15 @@ def schema_diff_ok(root: Path) -> list | None:
268
269
 
269
270
  INLINE_GUARD = "UAP_SCHEMA_DIFF_INLINE"
270
271
  # Comfortably inside policy-tools' 30s enforcer budget. At 60s a slow checker
271
- # meant the ENFORCER was killed first, and a killed enforcer is an ALLOW -- so
272
- # the timeout meant to produce a safe fallback produced a bypass instead. 400
273
- # watched files measured at 6.3s, so this leaves ample headroom.
274
- INLINE_TIMEOUT = 15.0
272
+ # meant the ENFORCER was killed first, which on the TS path is still an ALLOW
273
+ # and on the shell-hook path is now a hard block on the commit -- a bypass or a
274
+ # deadlock depending which caller you are under, and neither is the intended
275
+ # fallback. 400 watched files measured at 6.3s for one source. The layers nest
276
+ # innermost-shortest: this x 2 sources (20s) < the hook's per-enforcer bound
277
+ # (30s, UAP_ENFORCER_TIMEOUT) < the harness hook budget. Raising this without
278
+ # raising those turns a slow check into a killed enforcer, which on a commit is
279
+ # now a refusal.
280
+ INLINE_TIMEOUT = 10.0
275
281
  # Verdict shapes this gate knows how to read (SCHEMA_DIFF_CONTRACT in the CLI).
276
282
  KNOWN_CONTRACTS = (1,)
277
283
  # Passed to the checker. Everything else -- API keys, tokens, proxy secrets --
@@ -690,12 +696,14 @@ def main() -> None:
690
696
 
691
697
 
692
698
  if __name__ == "__main__":
693
- # A crash must not read as consent. The policy hook turns a non-zero exit
694
- # or unparseable output into ALLOW for every enforcer except self-protect
695
- # (.claude/hooks/uap-policy-gate.sh), so an unhandled exception anywhere
696
- # above is a silent bypass -- and the review found two reachable ones, both
697
- # TypeErrors on a malformed verdict. The guards are still in place; this is
698
- # the backstop for the ones nobody thought of.
699
+ # A crash must not read as consent. The hook maps a non-zero exit or
700
+ # unparseable output to ALLOW for every enforcer except self-protect and,
701
+ # since the fail-closed net was widened, this one on a commit or push
702
+ # (.claude/hooks/uap-policy-gate.sh). Two reachable crashes were found by
703
+ # review, both TypeErrors on a malformed verdict. Emitting a refusal here
704
+ # is still the right backstop: it keeps the outcome correct on the paths
705
+ # the hook does NOT cover, and produces a reasoned message rather than a
706
+ # bare block on the ones it does.
699
707
  try:
700
708
  main()
701
709
  except SystemExit:
@@ -172,7 +172,8 @@ bypass = re.search(
172
172
  r"|UAP_SELF_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_NO_WORKTREE\s*=\s*[\x27\"]?1"
173
173
  r"|UAP_WORKDIR_SCOPE_OFF\s*=\s*[\x27\"]?1|UAP_USER_VALIDATION\s*=\s*[\x27\"]?0"
174
174
  r"|UAP_DELIVER_NO_LOCK\s*=\s*[\x27\"]?1|UAP_NO_REVIEW\s*=\s*[\x27\"]?1"
175
- r"|UAP_INFRA_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_ALLOW_GATELESS_ROOT\s*=\s*[\x27\"]?1",
175
+ r"|UAP_INFRA_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_ALLOW_GATELESS_ROOT\s*=\s*[\x27\"]?1"
176
+ r"|UAP_SCHEMA_DIFF_INLINE\s*=\s*[\x27\"]?1|UAP_ORACLE_CONSISTENCY\s*=\s*[\x27\"]?0",
176
177
  cmd, re.I)
177
178
  print("1" if (hit or bypass) else "0")
178
179
  ' 2>/dev/null || echo 1)"
@@ -239,8 +240,32 @@ sys.exit(0 if (is_test or trivial) else 1)
239
240
  esac
240
241
  fi
241
242
 
243
+ # $1 = short reason, $2 = the enforcer that could not run.
244
+ # The message has to name the RIGHT enforcer and an override that actually
245
+ # works. Routing schema_diff_gate through the self-protect wording told the
246
+ # operator the wrong thing had failed and pointed at UAP_SELF_PROTECT_OFF=1,
247
+ # which cleared SEC_SENSITIVE and not COMMIT_OP. A refusal that describes the
248
+ # wrong thing and offers no alternative is how a loop survives a guard.
242
249
  fail_closed() {
243
- 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
250
+ local why="${1:-machinery unavailable}"
251
+ local who="${2:-enforcement_self_protect}"
252
+ # Record before exiting. This is the most serious verdict the gate can
253
+ # reach and it was the one that left no trace: measured, policy_executions
254
+ # was unchanged across a fail-closed block while ordinary blocks recorded,
255
+ # so the compliance view showed zero blocks for exactly this failure mode.
256
+ # Guarded with declare -F because the earliest call sites (policies.db
257
+ # missing, no sqlite3) fire before record_execution is defined -- and in
258
+ # those states there is nothing to write to anyway.
259
+ declare -F record_execution >/dev/null \
260
+ && record_execution 0 "$who" "FAIL-CLOSED: $why"
261
+ case "$who" in
262
+ schema_diff_gate)
263
+ echo "[UAP policy gate] FAIL-CLOSED: this commit touches watched schema paths but the schema-diff enforcer could not run (${why}). Blocked so a broken/absent gate can't become a bypass. Restore it with: uap policy verify --repair (or uap policy install schema-diff-gate if the manifest is gone; npm run build if dist/ is missing). Operator override: UAP_SELF_PROTECT_OFF=1 in the environment." >&2
264
+ ;;
265
+ *)
266
+ echo "[UAP policy gate] FAIL-CLOSED: this operation touches the enforcement control surface but the self-protect enforcer could not run (${why}). Blocked so a broken/absent gate can't become a bypass. (Operator override: UAP_SELF_PROTECT_OFF=1.)" >&2
267
+ ;;
268
+ esac
244
269
  exit 2
245
270
  }
246
271
 
@@ -330,26 +355,88 @@ fi
330
355
  # Did the self-protect enforcer actually run and make a decision this call?
331
356
  sec_enforcer_ran=0
332
357
 
358
+ # Is this a commit or a push? schema_diff_gate is the only control standing
359
+ # between a breaking schema change and history, and like self-protect its
360
+ # failure must not be read as consent -- the loop below otherwise maps an
361
+ # errored or missing enforcer to ALLOW for everything except self-protect.
362
+ # Scoped to commit/push so a broken schema enforcer cannot block every shell
363
+ # command in the session; it mirrors the enforcer's own activation test.
364
+ COMMIT_OP="$(printf '%s' "$ARGS" | python3 -c '
365
+ import json, re, sys
366
+ try: a = json.loads(sys.stdin.read() or "{}")
367
+ except Exception: a = {}
368
+ cmd = str(a.get("command") or "")
369
+ # Quoted text is prose, not an invocation. While the enforcer was broken,
370
+ # echo "next step: git commit" and grep -rn "git push" docs/ were hard blocks.
371
+ # The enforcer over-matches the same way, but when IT over-matches it runs,
372
+ # finds no watched paths and allows -- so the fail-closed net has to be
373
+ # STRICTER than the thing it protects, not looser.
374
+ bare = re.sub(r"\x27[^\x27]*\x27|\"[^\"]*\"", " ", cmd).lower()
375
+ inv = re.search(r"(?:^|[;&|]|\bthen\b|\bdo\b)\s*git\b[^;&|]*\b(?:commit|push)\b", bare)
376
+ # --help and --dry-run store nothing, so refusing them is pure friction. The
377
+ # match itself ends at the verb, so the flags are searched in the rest of the
378
+ # command SEGMENT -- searching inv.group(0) found nothing and blocked them.
379
+ seg = re.split(r"[;&|]", bare[inv.start():])[0] if inv else ""
380
+ inert = inv and re.search(r"--help|--dry-run|(?:^|\s)-h(?:\s|$)", seg)
381
+ print("1" if (inv and not inert) else "0")
382
+ ' 2>/dev/null || echo 1)"
383
+
384
+ # The operator hatch clears the whole net. It already zeroed SEC_SENSITIVE
385
+ # further up; leaving COMMIT_OP armed made the override named in the refusal a
386
+ # no-op for exactly the case that prints it -- so a stale .policy-tools copy
387
+ # refused every commit and push in the session with no working remedy.
388
+ [[ "${UAP_SELF_PROTECT_OFF:-}" == "1" ]] && COMMIT_OP=0
389
+
390
+ # Which enforcers must fail CLOSED, and when. Called from `if`, never as the
391
+ # left side of `&&`: this script runs under `set -e`, and `if` suspends it for
392
+ # the whole condition.
393
+ must_fail_closed() {
394
+ case "$1" in
395
+ enforcement_self_protect) [[ "$SEC_SENSITIVE" == "1" ]] ;;
396
+ schema_diff_gate) [[ "$COMMIT_OP" == "1" ]] ;;
397
+ *) false ;;
398
+ esac
399
+ }
400
+
401
+ # Bound every enforcer. Without this a hung one stalled the hook until the
402
+ # harness killed the whole process -- and a killed HOOK is not a fail-closed,
403
+ # it is an unbounded stall with the outcome decided elsewhere. On timeout the
404
+ # output is empty, which the parser below already reads as allowed=2, so the
405
+ # existing net decides what that means per enforcer.
406
+ #
407
+ # The layers must nest, innermost shortest: the schema enforcer's own inline
408
+ # checker (10s x up to 2 sources) < this (30s) < the harness hook budget.
409
+ # gtimeout is the macOS spelling; with neither present the call is unbounded,
410
+ # which is the behaviour that shipped.
411
+ TIMEOUT_BIN="$(command -v timeout || command -v gtimeout || true)"
412
+ ENFORCER_TIMEOUT="${UAP_ENFORCER_TIMEOUT:-30}"
413
+
333
414
  # Iterate active policies with attached executable tools
334
415
  while IFS='|' read -r pid pname tool; do
335
416
  [[ -z "$pid" ]] && continue
336
417
  enforcer="$MAIN_ROOT/.policy-tools/${pid}_${tool}.py"
337
418
  if [[ ! -f "$enforcer" ]]; then
338
- # A missing self-protect enforcer on a sensitive op = fail closed.
339
- [[ "$SEC_SENSITIVE" == "1" && "$tool" == "enforcement_self_protect" ]] && fail_closed "enforcer file missing"
419
+ # A missing fail-closed enforcer on an operation it guards = fail closed.
420
+ if must_fail_closed "$tool"; then fail_closed "enforcer file missing" "$tool"; fi
340
421
  continue
341
422
  fi
342
- out="$(python3 "$enforcer" --operation "$TOOL" --args "$ARGS" 2>/dev/null || true)"
423
+ if [[ -n "$TIMEOUT_BIN" ]]; then
424
+ out="$("$TIMEOUT_BIN" "${ENFORCER_TIMEOUT}s" python3 "$enforcer" --operation "$TOOL" --args "$ARGS" 2>/dev/null || true)"
425
+ else
426
+ out="$(python3 "$enforcer" --operation "$TOOL" --args "$ARGS" 2>/dev/null || true)"
427
+ fi
343
428
  allowed="$(printf '%s' "$out" | python3 -c 'import json,sys;
344
429
  try: d=json.loads(sys.stdin.read()); print("1" if d.get("allowed",True) else "0")
345
430
  except: print("2")' 2>/dev/null || echo 2)"
346
431
  # allowed=2 => enforcer errored / emitted unparseable output. For a sensitive
347
432
  # op via the self-protect enforcer, that error must NOT default to allow.
433
+ if [[ "$allowed" == "2" ]] && must_fail_closed "$tool"; then
434
+ fail_closed "enforcer errored" "$tool"
435
+ fi
348
436
  if [[ "$tool" == "enforcement_self_protect" ]]; then
349
- [[ "$SEC_SENSITIVE" == "1" && "$allowed" == "2" ]] && fail_closed "enforcer errored"
350
437
  sec_enforcer_ran=1
351
438
  fi
352
- # For all other enforcers, an error still fails open (unchanged behavior).
439
+ # For every other enforcer, an error still fails open (unchanged behavior).
353
440
  [[ "$allowed" == "2" ]] && allowed=1
354
441
  if [[ "$allowed" == "0" ]]; then
355
442
  # R1: consume the enforcer's route:deliver signal (log intent, opt-in
@@ -0,0 +1,410 @@
1
+ """The schema-diff gate's failure must not read as consent.
2
+
3
+ Two residuals left over from the inline-check pivot (PR #733), both in files
4
+ the agent that wrote that change could not edit:
5
+
6
+ 1. `UAP_SCHEMA_DIFF_INLINE=1` was the only gate switch of its shape absent
7
+ from self-protect's BYPASS_PATTERNS. It no longer allows anything on its
8
+ own -- it skips the inline checker and lands in the fallback -- but it
9
+ still WEAKENS the control, and an inline assignment is written by the
10
+ constrained party into its own command line. Every comparable switch
11
+ (UAP_SELF_PROTECT_OFF, UAP_NO_REVIEW, UAP_DELIVER_NO_LOCK, ...) is
12
+ refused inline for exactly that reason.
13
+
14
+ 2. uap-policy-gate.sh maps an errored or missing enforcer to ALLOW for
15
+ everything except enforcement_self_protect. schema_diff_gate is the only
16
+ thing standing between a breaking schema change and history, so a crash
17
+ in it was a silent bypass. It now fails closed -- but only on a commit or
18
+ push, so a broken schema enforcer cannot block every shell command in a
19
+ session.
20
+
21
+ The drift test at the bottom is the one that will actually catch a regression:
22
+ the hook has NINE copies in this repo, and `uap worktree create` seeds new
23
+ worktrees from templates/hooks/, so patching only .claude/hooks/ silently
24
+ reverts the fix on the next worktree.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import os
31
+ import re
32
+ import sqlite3
33
+ import subprocess
34
+ import sys
35
+ import tempfile
36
+ import unittest
37
+ from pathlib import Path
38
+
39
+ REPO = Path(__file__).resolve().parents[3]
40
+ ENFORCERS = REPO / "src" / "policies" / "enforcers"
41
+ SELF_PROTECT = ENFORCERS / "enforcement_self_protect.py"
42
+ PRIMARY_HOOK = REPO / ".claude" / "hooks" / "uap-policy-gate.sh"
43
+ HOOK_NAME = "uap-policy-gate.sh"
44
+
45
+
46
+ def hook_copies() -> list[Path]:
47
+ """Every copy of the hook in this checkout.
48
+
49
+ Exclusions are matched on the path RELATIVE to REPO. Matching the absolute
50
+ path excluded the entire tree whenever REPO was itself a worktree, since
51
+ every path under .worktrees/240-x/ contains ".worktrees" -- so this
52
+ returned [] exactly where it runs, and the two sweeps below passed by
53
+ iterating nothing. Caught by running it for real; a scratch copy in /tmp
54
+ could not reproduce it.
55
+ """
56
+ tracked = subprocess.run(
57
+ ["git", "ls-files", "--full-name", "*" + HOOK_NAME],
58
+ cwd=REPO, capture_output=True, text=True,
59
+ ).stdout.split()
60
+ # TRACKED copies only. An rglob also swept `uap hooks install` output --
61
+ # .uap/omp/ and .codex|.cursor|.forge|.opencode/hooks/ are all gitignored --
62
+ # so on any machine whose installed hooks predate a change, the drift
63
+ # assertions failed on files no PR can commit. What matters here is that
64
+ # every copy the repo SHIPS stays in step, templates/hooks/ above all.
65
+ return sorted(REPO / t for t in tracked if (REPO / t).is_file())
66
+
67
+
68
+ def run_self_protect(command: str, root: Path) -> tuple[bool, str]:
69
+ env = {
70
+ "UAP_REPO_ROOT": str(root),
71
+ "UAP_WORKTREE_ROOT": str(root),
72
+ "PYTHONPATH": str(ENFORCERS),
73
+ "PATH": "/usr/bin:/bin",
74
+ }
75
+ r = subprocess.run(
76
+ [sys.executable, str(SELF_PROTECT), "--operation", "Bash",
77
+ "--args", json.dumps({"command": command})],
78
+ cwd=root, env=env, capture_output=True, text=True, timeout=60,
79
+ )
80
+ try:
81
+ payload = json.loads(r.stdout or "{}")
82
+ except json.JSONDecodeError:
83
+ return False, f"unparseable: {r.stdout[:120]}"
84
+ return payload.get("allowed", True), payload.get("reason", "")
85
+
86
+
87
+ class InlineGuardIsABypassPattern(unittest.TestCase):
88
+ def test_setting_it_inline_is_refused(self):
89
+ allowed, reason = run_self_protect(
90
+ "UAP_SCHEMA_DIFF_INLINE=1 git commit -m x", REPO
91
+ )
92
+ self.assertFalse(allowed, f"an inline off-switch must be refused: {reason}")
93
+
94
+ def test_a_comparable_switch_behaves_the_same(self):
95
+ """Anchors the assertion above to existing, agreed behaviour."""
96
+ allowed, _ = run_self_protect("UAP_NO_REVIEW=1 git commit -m x", REPO)
97
+ self.assertFalse(allowed)
98
+
99
+ def test_merely_naming_the_variable_is_not_refused(self):
100
+ """Docs, commit messages and greps must keep working.
101
+
102
+ The pattern requires an assignment to 1, not the bare name -- the same
103
+ distinction that made GATELESS_FLAG_RE scan `scannable_command`
104
+ instead of raw text after bare flag names started refusing honest work.
105
+ """
106
+ for cmd in (
107
+ "echo the UAP_SCHEMA_DIFF_INLINE guard is documented",
108
+ "grep -rn UAP_SCHEMA_DIFF_INLINE src/",
109
+ ):
110
+ with self.subTest(cmd=cmd):
111
+ allowed, reason = run_self_protect(cmd, REPO)
112
+ self.assertTrue(allowed, f"{cmd} must stay allowed: {reason}")
113
+
114
+
115
+ class HookFailsClosedOnCommits(unittest.TestCase):
116
+ """The hook's own logic, extracted and executed rather than eyeballed."""
117
+
118
+ def commit_op(self, command: str) -> str:
119
+ gate = PRIMARY_HOOK.read_text()
120
+ start = 'COMMIT_OP="$(printf \'%s\' "$ARGS" | python3 -c \''
121
+ end = "' 2>/dev/null || echo 1)\""
122
+ i = gate.index(start) + len(start)
123
+ code = gate[i:gate.index(end, i)]
124
+ p = subprocess.run(
125
+ [sys.executable, "-c", code],
126
+ input=json.dumps({"command": command}),
127
+ capture_output=True, text=True, timeout=30,
128
+ env={"PATH": "/usr/bin:/bin"},
129
+ )
130
+ return p.stdout.strip()
131
+
132
+ def must_fail_closed(self, tool: str, sec: str, commit: str) -> bool:
133
+ gate = PRIMARY_HOOK.read_text()
134
+ start = gate.index("must_fail_closed() {")
135
+ fn = gate[start:gate.index("\n}\n", start) + 3]
136
+ script = (
137
+ "set -euo pipefail\n"
138
+ f'SEC_SENSITIVE="{sec}"\nCOMMIT_OP="{commit}"\n'
139
+ + fn
140
+ + f'\nif must_fail_closed "{tool}"; then echo CLOSED; else echo open; fi\n'
141
+ )
142
+ p = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=30)
143
+ self.assertEqual(p.returncode, 0, f"set -e tripped: {p.stderr[:200]}")
144
+ return p.stdout.strip() == "CLOSED"
145
+
146
+ def test_commit_and_push_are_recognised(self):
147
+ for cmd in ("git commit -m x", "git push origin master", "git commit -am x && echo ok"):
148
+ with self.subTest(cmd=cmd):
149
+ self.assertEqual(self.commit_op(cmd), "1")
150
+
151
+ def test_ordinary_commands_are_not(self):
152
+ # Scoped deliberately: a broken schema enforcer must not turn every
153
+ # shell command in the session into a hard block.
154
+ for cmd in ("ls -la", "npm test", "cat README.md"):
155
+ with self.subTest(cmd=cmd):
156
+ self.assertEqual(self.commit_op(cmd), "0")
157
+
158
+ def test_the_schema_gate_fails_closed_on_a_commit(self):
159
+ self.assertTrue(self.must_fail_closed("schema_diff_gate", "0", "1"))
160
+
161
+ def test_the_schema_gate_does_not_fail_closed_otherwise(self):
162
+ self.assertFalse(self.must_fail_closed("schema_diff_gate", "0", "0"))
163
+
164
+ def test_self_protect_keeps_its_existing_condition(self):
165
+ self.assertTrue(self.must_fail_closed("enforcement_self_protect", "1", "0"))
166
+ self.assertFalse(self.must_fail_closed("enforcement_self_protect", "0", "0"))
167
+
168
+ def test_an_unrelated_enforcer_still_fails_open(self):
169
+ """Widening this to every enforcer would be a session-wide deadlock."""
170
+ self.assertFalse(self.must_fail_closed("worktree_required", "1", "1"))
171
+
172
+ def test_the_hook_parses(self):
173
+ for copy in hook_copies():
174
+ with self.subTest(copy=str(copy.relative_to(REPO))):
175
+ p = subprocess.run(["bash", "-n", str(copy)], capture_output=True, text=True)
176
+ self.assertEqual(p.returncode, 0, p.stderr[:200])
177
+
178
+
179
+ class HookCopiesDoNotDrift(unittest.TestCase):
180
+ """Nine copies, and the one that matters most is the one nobody edits.
181
+
182
+ `uap worktree create` seeds a new worktree from templates/hooks/, so a fix
183
+ applied only to .claude/hooks/ is reverted the next time anyone starts a
184
+ branch -- a documented failure in this repo. Any change to the gate has to
185
+ land in all of them.
186
+ """
187
+
188
+ def test_the_sweep_actually_finds_the_copies(self):
189
+ """Guards the two sweeps below against passing on an empty list.
190
+
191
+ hook_copies() returned [] in a worktree because its exclusions matched
192
+ the absolute path, and both drift tests went green while checking
193
+ nothing. An assertion about a collection is worthless without an
194
+ assertion that the collection is non-empty.
195
+ """
196
+ found = hook_copies()
197
+ self.assertGreaterEqual(
198
+ len(found), 2,
199
+ f"expected several hook copies under {REPO}, found {[str(p) for p in found]}",
200
+ )
201
+ self.assertIn(PRIMARY_HOOK, found)
202
+
203
+ def test_every_copy_is_identical_to_the_primary(self):
204
+ primary = PRIMARY_HOOK.read_text()
205
+ for copy in hook_copies():
206
+ with self.subTest(copy=str(copy.relative_to(REPO))):
207
+ self.assertEqual(
208
+ copy.read_text(), primary,
209
+ f"{copy.relative_to(REPO)} has drifted from .claude/hooks/",
210
+ )
211
+
212
+ def test_the_template_copy_carries_the_fail_closed_logic(self):
213
+ template = REPO / "templates" / "hooks" / HOOK_NAME
214
+ self.assertTrue(template.is_file(), "templates/hooks copy is missing")
215
+ self.assertIn(
216
+ "must_fail_closed", template.read_text(),
217
+ "new worktrees would be seeded with a gate that fails open",
218
+ )
219
+
220
+ class FailClosedIsAuditable(unittest.TestCase):
221
+ """The most serious verdict the gate can reach was the one leaving no trace.
222
+
223
+ fail_closed() exits, and it exited before record_execution ever ran --
224
+ measured, policy_executions was unchanged across a fail-closed block while
225
+ ordinary blocks recorded. The compliance view therefore showed zero blocks
226
+ for exactly the failure mode most worth seeing.
227
+ """
228
+
229
+ def fail_closed_fn(self) -> str:
230
+ gate = PRIMARY_HOOK.read_text()
231
+ start = gate.index("fail_closed() {")
232
+ return gate[start:gate.index("\n}\n", start) + 3]
233
+
234
+ def test_it_records_before_exiting(self):
235
+ script = (
236
+ "set -euo pipefail\n"
237
+ 'record_execution() { echo "RECORDED allowed=$1 policy=$2 reason=$3"; }\n'
238
+ + self.fail_closed_fn()
239
+ + '\nfail_closed "enforcer errored" "schema_diff_gate" || true\n'
240
+ )
241
+ p = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=30)
242
+ self.assertIn("RECORDED allowed=0", p.stdout)
243
+ self.assertIn("policy=schema_diff_gate", p.stdout, "the row must name the enforcer")
244
+ self.assertIn("FAIL-CLOSED", p.stdout)
245
+
246
+ def test_it_still_works_before_record_execution_exists(self):
247
+ """The earliest call sites fire before that function is defined.
248
+
249
+ `policies.db not found` and `sqlite3 not on PATH` both call fail_closed
250
+ from above record_execution's definition -- and in those states there is
251
+ nothing to write to anyway. An unguarded call would turn the refusal
252
+ into a bash error.
253
+ """
254
+ script = "set -euo pipefail\n" + self.fail_closed_fn() + '\nfail_closed "policies.db not found" || true\n'
255
+ p = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=30)
256
+ self.assertIn("FAIL-CLOSED", p.stderr)
257
+ self.assertNotIn("command not found", p.stderr)
258
+
259
+
260
+ class EnforcersAreBounded(unittest.TestCase):
261
+ """A hung enforcer stalled the hook until the harness killed the process.
262
+
263
+ A killed HOOK is not a fail-closed -- it is an unbounded stall whose outcome
264
+ is decided somewhere else entirely. Measured at 20.2s with a sleeping
265
+ enforcer before this.
266
+ """
267
+
268
+ def test_the_invocation_is_wrapped(self):
269
+ gate = PRIMARY_HOOK.read_text()
270
+ self.assertIn("TIMEOUT_BIN", gate)
271
+ self.assertIn('"${ENFORCER_TIMEOUT}s" python3 "$enforcer"', gate)
272
+
273
+ def test_it_degrades_to_the_shipped_behaviour_without_a_timeout_binary(self):
274
+ """macOS spells it gtimeout; with neither, unbounded is what shipped."""
275
+ gate = PRIMARY_HOOK.read_text()
276
+ self.assertIn("command -v timeout || command -v gtimeout || true", gate)
277
+ self.assertIn('if [[ -n "$TIMEOUT_BIN" ]]; then', gate)
278
+
279
+ def test_the_timeout_layers_nest_innermost_shortest(self):
280
+ """The arithmetic that makes the bound meaningful rather than decorative.
281
+
282
+ The schema enforcer may run its checker twice (index and worktree). If
283
+ that worst case can exceed the hook's per-enforcer bound, a slow but
284
+ healthy check is killed and -- on a commit -- refused. Previously
285
+ 2 x 15s met the 30s budget exactly, with nothing left over.
286
+ """
287
+ gate = PRIMARY_HOOK.read_text()
288
+ enforcer = (REPO / "src" / "policies" / "enforcers" / "schema_diff_gate.py").read_text()
289
+ hook_bound = int(
290
+ re.search(r'ENFORCER_TIMEOUT="\$\{UAP_ENFORCER_TIMEOUT:-(\d+)\}"', gate).group(1)
291
+ )
292
+ inline = float(re.search(r"INLINE_TIMEOUT = ([\d.]+)", enforcer).group(1))
293
+ self.assertLess(
294
+ inline * 2, hook_bound,
295
+ "the enforcer's worst case must fit inside the hook's bound, or a "
296
+ "slow healthy check becomes a refusal",
297
+ )
298
+
299
+
300
+ class GateCallSitesAreWired(unittest.TestCase):
301
+ """Drives the REAL hook script.
302
+
303
+ Everything above extracts a bash fragment and runs it, which cannot notice
304
+ if the fragment is never CALLED. Delete `must_fail_closed` from either call
305
+ site, or move the COMMIT_OP assignment below the loop, and every other test
306
+ here still passes. This one fails.
307
+ """
308
+
309
+ PID = "22222222-2222-2222-2222-222222222222"
310
+
311
+ def setUp(self):
312
+ self._tmp = tempfile.TemporaryDirectory(prefix="gate-e2e-")
313
+ self.sb = Path(self._tmp.name)
314
+ for d in (".policy-tools", "agents/data/memory", ".claude/hooks"):
315
+ (self.sb / d).mkdir(parents=True, exist_ok=True)
316
+ subprocess.run(["git", "init", "-q"], cwd=self.sb, capture_output=True)
317
+
318
+ helper = (
319
+ "import json, sys\n"
320
+ "def parse_cli():\n"
321
+ " a = sys.argv\n"
322
+ " op = a[a.index('--operation') + 1] if '--operation' in a else ''\n"
323
+ " ar = json.loads(a[a.index('--args') + 1]) if '--args' in a else {}\n"
324
+ " return op, ar\n"
325
+ "def emit(allowed, reason):\n"
326
+ " print(json.dumps({'allowed': bool(allowed), 'reason': reason}))\n"
327
+ " sys.exit(0)\n"
328
+ )
329
+ (self.sb / ".policy-tools/_common.py").write_text(helper)
330
+
331
+ db = sqlite3.connect(self.sb / "agents/data/memory/policies.db")
332
+ db.execute("CREATE TABLE policies (id TEXT, name TEXT, category TEXT, level TEXT,"
333
+ " rawMarkdown TEXT, convertedFormat TEXT, executableTools TEXT, tags TEXT,"
334
+ " createdAt TEXT, updatedAt TEXT, version INT, isActive INT, priority INT,"
335
+ " enforcementStage TEXT)")
336
+ db.execute("CREATE TABLE executable_tools (id TEXT, policyId TEXT, toolName TEXT,"
337
+ " code TEXT, language TEXT, createdAt TEXT)")
338
+ db.execute("CREATE TABLE policy_executions (id TEXT)")
339
+ db.execute("INSERT INTO policies VALUES (?,?,?,?,?,?,?,?,?,?,1,1,1,'pre-exec')",
340
+ (self.PID, "Schema Diff Gate", "quality", "REQUIRED",
341
+ "# Schema Diff Gate", "", "", "", "", ""))
342
+ db.execute("INSERT INTO executable_tools VALUES (?,?,?,?,?,?)",
343
+ ("t1", self.PID, "schema_diff_gate", "", "python", ""))
344
+ db.commit()
345
+ db.close()
346
+
347
+ def tearDown(self):
348
+ self._tmp.cleanup()
349
+
350
+ def install_enforcer(self, body: str | None) -> None:
351
+ p = self.sb / f".policy-tools/{self.PID}_schema_diff_gate.py"
352
+ if body is None:
353
+ if p.exists():
354
+ p.unlink()
355
+ return
356
+ p.write_text(body)
357
+
358
+ def gate(self, command: str, env_extra: dict | None = None) -> int:
359
+ dst = self.sb / ".claude/hooks/uap-policy-gate.sh"
360
+ dst.write_text(PRIMARY_HOOK.read_text())
361
+ dst.chmod(0o755)
362
+ env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
363
+ env.update(env_extra or {})
364
+ payload = json.dumps({"tool_name": "Bash", "cwd": str(self.sb),
365
+ "tool_input": {"command": command}})
366
+ p = subprocess.run(["bash", str(dst)], input=payload, capture_output=True,
367
+ text=True, cwd=self.sb, env=env, timeout=180)
368
+ return p.returncode
369
+
370
+ BROKEN = "raise SystemExit('boom')\n"
371
+ HEALTHY = (
372
+ "import json\n"
373
+ "print(json.dumps({'allowed': True, 'reason': 'ok'}))\n"
374
+ )
375
+
376
+ def test_a_broken_enforcer_refuses_a_commit(self):
377
+ self.install_enforcer(self.BROKEN)
378
+ self.assertEqual(self.gate("git commit -m x"), 2)
379
+
380
+ def test_a_missing_enforcer_refuses_a_commit(self):
381
+ self.install_enforcer(None)
382
+ self.assertEqual(self.gate("git commit -m x"), 2)
383
+
384
+ def test_a_broken_enforcer_does_not_block_ordinary_work(self):
385
+ """The scoping that keeps this from being a session-wide deadlock."""
386
+ self.install_enforcer(self.BROKEN)
387
+ for command in ("ls -la", "npm test", "git status", "git commit --help"):
388
+ with self.subTest(command=command):
389
+ self.assertEqual(self.gate(command), 0, command)
390
+
391
+ def test_the_operator_hatch_actually_clears_it(self):
392
+ """The refusal names this override, so it has to work.
393
+
394
+ It cleared SEC_SENSITIVE only, so for a schema-gate fail-closed it did
395
+ nothing -- every commit refused, with the message pointing at a switch
396
+ that had no effect on the branch printing it.
397
+ """
398
+ self.install_enforcer(self.BROKEN)
399
+ self.assertEqual(self.gate("git commit -m x"), 2)
400
+ self.assertEqual(
401
+ self.gate("git commit -m x", {"UAP_SELF_PROTECT_OFF": "1"}), 0,
402
+ "the advertised override must clear the schema-gate branch too",
403
+ )
404
+
405
+ def test_a_healthy_enforcer_is_unaffected(self):
406
+ self.install_enforcer(self.HEALTHY)
407
+ self.assertEqual(self.gate("git commit -m x"), 0)
408
+
409
+ if __name__ == "__main__":
410
+ unittest.main()
@@ -23,6 +23,7 @@ import json
23
23
  import os
24
24
  import shutil
25
25
  import subprocess
26
+ import sys
26
27
  import tempfile
27
28
  import unittest
28
29
  from pathlib import Path
@@ -35,13 +36,36 @@ BASE_SQL = "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);\n"
35
36
  BREAKING_SQL = "CREATE TABLE t (id INTEGER PRIMARY KEY);\n"
36
37
 
37
38
 
39
+ def _git_only_path() -> str:
40
+ """A PATH with git and nothing else, so `cli_argv` can find no checker.
41
+
42
+ A test that means "no checker is available" has to SAY so. Without this it
43
+ inherits the ambient PATH, finds the globally installed `uap`, and the
44
+ inline layer answers -- which is a different test with a different
45
+ expectation. It passed only while that binary predated --json, and inverted
46
+ the day it was updated. Exactly the coupling the marker suite was pinned
47
+ against in test_schema_diff_gate.py; this file needed the same treatment.
48
+ """
49
+ d = Path(tempfile.mkdtemp(prefix="schema-inline-gitonly-"))
50
+ found = shutil.which("git")
51
+ if found:
52
+ (d / "git").symlink_to(found)
53
+ return str(d)
54
+
55
+
56
+ _GIT_ONLY_PATH = _git_only_path()
57
+
58
+
38
59
  def run_gate(op: str, args: dict, root: Path, env_extra: dict | None = None):
39
60
  env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
40
61
  env["UAP_REPO_ROOT"] = str(root)
41
62
  env["UAP_WORKTREE_ROOT"] = str(root)
42
63
  env.update(env_extra or {})
43
64
  r = subprocess.run(
44
- ["python3", str(ENFORCER), "--operation", op, "--args", json.dumps(args)],
65
+ # Absolute interpreter, not "python3": a test may pin PATH to exclude
66
+ # the checker, and resolving the interpreter through that same PATH
67
+ # fails before the enforcer ever runs.
68
+ [sys.executable, str(ENFORCER), "--operation", op, "--args", json.dumps(args)],
45
69
  cwd=root,
46
70
  env=env,
47
71
  capture_output=True,
@@ -212,7 +236,11 @@ class InlineSchemaDiffTest(unittest.TestCase):
212
236
  self.assertNotEqual(reason, "", "the gate must answer, not die silently")
213
237
 
214
238
  def test_a_missing_checker_leaves_the_shipped_behaviour_untouched(self):
215
- allowed, reason = self.commit()
239
+ # No stub, and a PATH with no `uap` on it, so there is genuinely no
240
+ # checker to run. Without the PATH pin this found the installed CLI and
241
+ # asserted against the inline layer's message instead -- green for the
242
+ # wrong reason until that binary learned --json, then red.
243
+ allowed, reason = self.commit(PATH=_GIT_ONLY_PATH)
216
244
  self.assertFalse(allowed, f"missing checker must not open the gate: {reason}")
217
245
  self.assertIn("uap schema-diff", reason)
218
246
 
@@ -342,12 +370,24 @@ class CheckerProvenanceTest(unittest.TestCase):
342
370
  subprocess.run(["git", "add", "-A"], cwd=self.root, check=True)
343
371
  bin_dir = self.root / "dist" / "bin"
344
372
  bin_dir.mkdir(parents=True)
345
- # A rubber stamp: three lines, and it attests to anything.
373
+ # A rubber stamp: three lines, and it attests to anything. It also
374
+ # leaves a footprint, because "the gate blocked" does not by itself
375
+ # prove this file went unread -- the installed CLI would also block
376
+ # here, on the real breaking change. The footprint is the only thing
377
+ # that distinguishes "ignored the foreign build" from "ran something
378
+ # else that happened to agree".
346
379
  (bin_dir / "cli.js").write_text(
380
+ "require('fs').writeFileSync('rubber-stamp-was-run', '1');\n"
347
381
  "console.log(JSON.stringify({contract:1,ran:true,base:'HEAD',files:"
348
382
  "[{path:'" + WATCHED + "',sha:'x',analysed:true,breaking:[]}]}));\n"
349
383
  )
350
384
 
385
+ def assertStampUnread(self):
386
+ self.assertFalse(
387
+ (self.root / "rubber-stamp-was-run").exists(),
388
+ "the gate executed a checker out of the tree it was gating",
389
+ )
390
+
351
391
  def tearDown(self):
352
392
  self._tmp.cleanup()
353
393
 
@@ -362,12 +402,28 @@ class CheckerProvenanceTest(unittest.TestCase):
362
402
  """
363
403
  (self.root / "package.json").write_text('{"name":"totally-normal-app"}\n')
364
404
  allowed, reason = run_gate("Bash", {"command": "git commit -m x"}, self.root)
405
+ self.assertStampUnread()
365
406
  self.assertFalse(allowed, f"a foreign dist/ must not be the oracle: {reason}")
366
407
 
367
408
  def test_no_package_json_is_also_not_the_uap_checkout(self):
368
409
  allowed, reason = run_gate("Bash", {"command": "git commit -m x"}, self.root)
410
+ self.assertStampUnread()
369
411
  self.assertFalse(allowed, f"unidentified tree must not be trusted: {reason}")
370
412
 
413
+ def test_the_stamp_would_fire_if_the_build_were_trusted(self):
414
+ """Proves the two assertions above can fail.
415
+
416
+ A footprint check is worthless if the stub could never run in the
417
+ first place. Claiming to be the UAP checkout is exactly the condition
418
+ that makes the local build trusted, so here it MUST be executed.
419
+ """
420
+ (self.root / "package.json").write_text('{"name":"@miller-tech/uap"}\n')
421
+ run_gate("Bash", {"command": "git commit -m x"}, self.root)
422
+ self.assertTrue(
423
+ (self.root / "rubber-stamp-was-run").exists(),
424
+ "the local build should be trusted in the UAP checkout itself",
425
+ )
426
+
371
427
 
372
428
  if __name__ == "__main__":
373
429
  unittest.main()