@miller-tech/uap 1.179.5 → 1.179.7

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.
@@ -117,7 +117,23 @@ token is not echoed into the ride-along log. Binding beyond loopback
117
117
  `uap-anthropic-proxy.service`, reading its environment from
118
118
  `~/.config/uap/anthropic-proxy.env`; a plain spawned proxy seeds the same file
119
119
  for parity. The proxy also loads project-local `.uap/proxy.env` at startup
120
- (real environment always wins). PID reuse is defended with a `/proc/<pid>`
120
+ (real environment always wins). That search walks up from the working
121
+ directory and **stops at the repository root**, so a checkout nested inside an
122
+ unrelated checkout no longer inherits the outer repo's `PROXY_AUTH_TOKEN`; a
123
+ git *worktree* is the same repository, so its `.git` pointer is followed back
124
+ to the main checkout rather than terminating the search. If nothing is found
125
+ under the repo, `$XDG_CONFIG_HOME/uap/proxy.env` and `~/.uap/proxy.env` are
126
+ tried, matching the client-side resolver. The proxy logs which file it loaded
127
+ (or that it found none) plus its auth state at startup, so a discovery miss is
128
+ visible rather than silently degrading to defaults.
129
+
130
+ | Env (default) | Behavior |
131
+ |---|---|
132
+ | `UAP_PROXY_ENV_FILE` (unset) | Explicit path to a proxy.env, tried before the walk |
133
+ | `UAP_PROXY_ENV_AUTOLOAD` (**on**) | Set to `0` to make *importing* the proxy module inert — it then mutates no process environment. Used by the Python test suite so tests assert shipped defaults rather than the developer's config. Does not affect a server run |
134
+ | `PROXY_ALLOW_UNAUTHENTICATED_BIND` (**off**) | The proxy **refuses to start** on a non-loopback `PROXY_HOST` when `PROXY_AUTH_TOKEN` is empty, because the auth middleware treats an empty token as "no auth configured" and would leave every route open on the LAN. Set to `1` only if an open listener is genuinely intended |
135
+
136
+ PID reuse is defended with a `/proc/<pid>`
121
137
  start-time token so `release` never kills an unrelated process that inherited an
122
138
  old pid. Everything hook-driven **fails open** — if the proxy won't start, the
123
139
  agent just runs without it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.179.5",
3
+ "version": "1.179.7",
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": "python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer 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_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",
24
+ "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer 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_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_delivery_enforcement_worktree",
25
25
  "test:coverage": "vitest --coverage",
26
26
  "bench": "vitest --config vitest.bench.config.ts",
27
27
  "lint": "eslint src --ext .ts",
@@ -109,21 +109,92 @@ from fastapi.responses import StreamingResponse
109
109
  import uvicorn
110
110
 
111
111
 
112
+ # Path of the .uap/proxy.env actually loaded, or "" if none was found. Reported
113
+ # at startup so a silent discovery miss is visible in the journal.
114
+ _PROXY_ENV_FILE_LOADED = ""
115
+
116
+
117
+ def _git_common_root(gitfile: Path) -> "Path | None":
118
+ """Resolve a worktree/submodule `.git` FILE to its main checkout root.
119
+
120
+ A worktree's .git is a file containing `gitdir: <main>/.git/worktrees/<n>`.
121
+ It names the SAME repository — same object store, same operator, same trust
122
+ domain — so config discovery must follow it home rather than treat the
123
+ worktree as a foreign tree. Returns None if the file isn't a gitdir pointer.
124
+ """
125
+ try:
126
+ text = gitfile.read_text(errors="ignore").strip()
127
+ except OSError:
128
+ return None
129
+ if not text.startswith("gitdir:"):
130
+ return None
131
+ target = Path(text.split(":", 1)[1].strip())
132
+ if not target.is_absolute():
133
+ target = (gitfile.parent / target)
134
+ try:
135
+ target = target.resolve()
136
+ except OSError:
137
+ return None
138
+ for parent in [target, *target.parents]:
139
+ if parent.name == ".git":
140
+ return parent.parent
141
+ return None
142
+
143
+
112
144
  def _load_proxy_env_file() -> None:
113
145
  """Load .uap/proxy.env (KEY=VALUE) written by `uap setup` into os.environ so
114
146
  recipe / escalation / delivery selections reach the proxy without the user
115
147
  hand-exporting env. Existing process env ALWAYS wins (setdefault). Walks up
116
- from CWD to the filesystem root looking for a .uap/proxy.env; override the
117
- path with UAP_PROXY_ENV_FILE. Runs before the import-time env reads below.
118
- Fails open."""
148
+ from CWD looking for a .uap/proxy.env, stopping at the repository root;
149
+ override the path with UAP_PROXY_ENV_FILE. Runs before the import-time env
150
+ reads below. Fails open."""
119
151
  try:
120
152
  candidates = []
121
153
  explicit = os.environ.get("UAP_PROXY_ENV_FILE")
122
154
  if explicit:
123
155
  candidates.append(Path(explicit))
124
156
  d = Path.cwd()
157
+ try:
158
+ home = Path.home()
159
+ except Exception:
160
+ home = None
125
161
  for base in [d, *d.parents]:
126
162
  candidates.append(base / ".uap" / "proxy.env")
163
+ git = base / ".git"
164
+ # A real repository root: stop. The walk used to climb to the
165
+ # FILESYSTEM root, so a checkout nested under another repo silently
166
+ # loaded the outer repo's proxy.env — PROXY_AUTH_TOKEN included.
167
+ if git.is_dir():
168
+ break
169
+ # A worktree or submodule: its .git is a FILE naming the SAME
170
+ # repository. Same object store, same operator, same trust domain —
171
+ # so follow it to the main checkout instead of stopping. Stopping
172
+ # here would be worse than the leak: a worktree gets no .uap/ of its
173
+ # own, so the proxy would start with an EMPTY PROXY_AUTH_TOKEN, and
174
+ # the auth middleware treats empty as "no auth configured" rather
175
+ # than "misconfigured". Since PROXY_HOST=0.0.0.0 arrives from a
176
+ # different file that this walk never touched, that combination is
177
+ # an unauthenticated listener on the LAN.
178
+ if git.is_file():
179
+ main_root = _git_common_root(git)
180
+ if main_root is not None:
181
+ candidates.append(main_root / ".uap" / "proxy.env")
182
+ break
183
+ # Never climb above the user's home. With no repository anywhere
184
+ # above cwd the walk would otherwise reach "/", where a
185
+ # world-writable /tmp/.uap/proxy.env would be loaded unconditionally.
186
+ if home is not None and base == home:
187
+ break
188
+ # Machine-wide fallbacks, mirroring the TypeScript reader in
189
+ # src/models/openai-compat-client.ts. Without these the two halves
190
+ # disagree about where the token lives: the client finds one and sends
191
+ # it while the server has none and skips the check.
192
+ xdg = os.environ.get("XDG_CONFIG_HOME")
193
+ if xdg:
194
+ candidates.append(Path(xdg) / "uap" / "proxy.env")
195
+ if home is not None:
196
+ candidates.append(home / ".config" / "uap" / "proxy.env")
197
+ candidates.append(home / ".uap" / "proxy.env")
127
198
  for path in candidates:
128
199
  try:
129
200
  if not path.is_file():
@@ -139,12 +210,36 @@ def _load_proxy_env_file() -> None:
139
210
  val = val.strip().strip('"').strip("'")
140
211
  if key:
141
212
  os.environ.setdefault(key, val)
213
+ global _PROXY_ENV_FILE_LOADED
214
+ _PROXY_ENV_FILE_LOADED = str(path)
142
215
  break # first file found wins
143
216
  except Exception:
144
217
  pass
145
218
 
146
219
 
147
- _load_proxy_env_file()
220
+ def _proxy_env_autoload_enabled() -> bool:
221
+ """Whether importing this module may mutate the process environment.
222
+
223
+ On by default: the module-level constants below read os.environ at import,
224
+ so a server run needs the file loaded first. But `import anthropic_proxy`
225
+ is not always a server run — the test suite imports it ~6 times, and each
226
+ import used to setdefault the whole of .uap/proxy.env into the real
227
+ os.environ. Every enforcer subprocess spawned afterwards inherited it, so
228
+ tests asserted the developer's proxy config instead of the shipped
229
+ defaults, and the outcome depended on module order. Set
230
+ UAP_PROXY_ENV_AUTOLOAD=0 to import the module inertly; callers that want
231
+ the file can still invoke _load_proxy_env_file() explicitly.
232
+ """
233
+ return os.environ.get("UAP_PROXY_ENV_AUTOLOAD", "1").strip().lower() not in {
234
+ "0",
235
+ "off",
236
+ "false",
237
+ "no",
238
+ }
239
+
240
+
241
+ if _proxy_env_autoload_enabled():
242
+ _load_proxy_env_file()
148
243
 
149
244
  # ---------------------------------------------------------------------------
150
245
  # Configuration (all configurable via environment variables)
@@ -12588,7 +12683,58 @@ async def context_status(request: Request):
12588
12683
  # Entry Point
12589
12684
  # ===========================================================================
12590
12685
 
12686
+ _LOOPBACK_HOSTS = {"127.0.0.1", "::1", "localhost", ""}
12687
+
12688
+
12689
+ def _assert_bind_is_authenticated(host: str, token: str) -> None:
12690
+ """Refuse to expose an unauthenticated proxy beyond loopback.
12691
+
12692
+ The auth middleware is a no-op when PROXY_AUTH_TOKEN is empty — it reads
12693
+ "no auth configured", not "misconfigured". The token arrives from
12694
+ .uap/proxy.env while PROXY_HOST commonly arrives from the systemd
12695
+ EnvironmentFile, so any failure to locate the former leaves the latter
12696
+ intact: bind-all survives, require-a-credential does not. That asymmetry
12697
+ turns a silent config miss into an open LLM proxy on the LAN, fronting both
12698
+ the local model and cloud passthrough on the operator's credentials.
12699
+
12700
+ Fail closed instead. Set PROXY_ALLOW_UNAUTHENTICATED_BIND=1 to override
12701
+ when an open listener is genuinely intended.
12702
+ """
12703
+ if host in _LOOPBACK_HOSTS or token:
12704
+ return
12705
+ if os.environ.get("PROXY_ALLOW_UNAUTHENTICATED_BIND", "").strip().lower() in {
12706
+ "1",
12707
+ "on",
12708
+ "true",
12709
+ "yes",
12710
+ }:
12711
+ logger.warning(
12712
+ "PROXY AUTH DISABLED on a non-loopback bind (%s) — allowed only "
12713
+ "because PROXY_ALLOW_UNAUTHENTICATED_BIND is set.",
12714
+ host,
12715
+ )
12716
+ return
12717
+ raise SystemExit(
12718
+ f"refusing to bind {host} with no PROXY_AUTH_TOKEN: every route would be "
12719
+ f"unauthenticated. The token normally comes from .uap/proxy.env — if the "
12720
+ f"proxy cannot find that file, config discovery is broken rather than the "
12721
+ f"token being genuinely unset. Set PROXY_AUTH_TOKEN, bind 127.0.0.1, or "
12722
+ f"set PROXY_ALLOW_UNAUTHENTICATED_BIND=1 if an open listener is intended."
12723
+ )
12724
+
12725
+
12591
12726
  if __name__ == "__main__":
12727
+ # Config discovery is silent by design (it fails open so a missing file can
12728
+ # never stop the proxy). Say what it found, so a miss is visible in the
12729
+ # journal instead of showing up later as unexplained default behaviour.
12730
+ logger.info(
12731
+ "proxy env: %s | auth=%s | bind=%s:%d",
12732
+ f"loaded {_PROXY_ENV_FILE_LOADED}" if _PROXY_ENV_FILE_LOADED else "no proxy.env found",
12733
+ "enabled" if PROXY_AUTH_TOKEN else "DISABLED",
12734
+ PROXY_HOST,
12735
+ PROXY_PORT,
12736
+ )
12737
+ _assert_bind_is_authenticated(PROXY_HOST, PROXY_AUTH_TOKEN)
12592
12738
  uvicorn.run(
12593
12739
  app,
12594
12740
  host=PROXY_HOST,
@@ -0,0 +1,27 @@
1
+ """Test package init — keeps importing the proxy from mutating this process.
2
+
3
+ `anthropic_proxy` loads `.uap/proxy.env` into the real `os.environ` at import.
4
+ That is right for a server run and wrong for a test run: it makes tests assert
5
+ the developer's proxy config instead of the shipped defaults, and it leaks
6
+ UAP_DELIVER_* settings into every enforcer subprocess the suite spawns, so
7
+ results depend on module import order.
8
+
9
+ Setting the opt-out here rather than only in `conftest.py` covers every entry
10
+ path, because all of them import this package first:
11
+
12
+ - `python -m unittest tools.agents.tests.test_x` (what CI runs)
13
+ - `python -m unittest discover -s tools/agents/tests`
14
+ - `loadTestsFromNames(...)` in test_enforcer_suite_coverage
15
+ - pytest (which also reads conftest.py)
16
+
17
+ `python3 tools/agents/tests/test_x.py` — running a file directly by path — is
18
+ the one shape that does NOT import the package, which is why conftest.py and
19
+ the `test:enforcers` script both still set it too.
20
+
21
+ `setdefault`, so an explicit `UAP_PROXY_ENV_AUTOLOAD=1` in the environment
22
+ still wins for anyone who deliberately wants the real file loaded.
23
+ """
24
+
25
+ import os
26
+
27
+ os.environ.setdefault("UAP_PROXY_ENV_AUTOLOAD", "0")
@@ -0,0 +1,27 @@
1
+ """Keep importing the proxy from mutating the test process's environment.
2
+
3
+ `anthropic_proxy` loads `.uap/proxy.env` into the real `os.environ` at import
4
+ so a server run gets the operator's recipe/delivery/auth settings without
5
+ hand-exported env. Under test that is a liability, and it produced two real
6
+ failures:
7
+
8
+ 1. Tests that assert shipped defaults (timeouts, retry budgets) instead
9
+ asserted whatever the developer's `.uap/proxy.env` happened to contain —
10
+ green in CI, red locally, for no code reason.
11
+ 2. `UAP_DELIVER_LOCAL_MODE` from that file reached every enforcer subprocess
12
+ the suite spawns, so `test_delivery_enforcement_worktree` passed or failed
13
+ depending on whether a proxy-importing module had run before it.
14
+
15
+ Setting the opt-out here covers every pytest run from one place instead of
16
+ each module defending itself. `npm run test:enforcers` sets the same variable
17
+ for the unittest path, which does not read conftest.py.
18
+
19
+ This must run before any test module imports the proxy — conftest is imported
20
+ at collection time, ahead of the test modules, which is exactly that point.
21
+ Tests that specifically exercise the loader call `_load_proxy_env_file()`
22
+ directly and are unaffected.
23
+ """
24
+
25
+ import os
26
+
27
+ os.environ.setdefault("UAP_PROXY_ENV_AUTOLOAD", "0")
@@ -3,6 +3,7 @@
3
3
  import asyncio
4
4
  import importlib.util
5
5
  import json
6
+ import os
6
7
  import unittest
7
8
  import unittest.mock
8
9
  from pathlib import Path
@@ -91,36 +92,44 @@ class TestProxyConfigTuning(unittest.TestCase):
91
92
  finally:
92
93
  setattr(proxy, "PROXY_MAX_TOKENS_FLOOR", old_floor)
93
94
 
94
- def test_build_request_bypasses_floor_for_tool_turns_when_thinking_disabled(self):
95
- old_floor = getattr(proxy, "PROXY_MAX_TOKENS_FLOOR")
96
- old_disable = getattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS")
97
- try:
98
- setattr(proxy, "PROXY_MAX_TOKENS_FLOOR", 4096)
99
- setattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", True)
100
-
101
- body = {
102
- "model": "test",
103
- "max_tokens": 512,
104
- "messages": [{"role": "user", "content": "run pwd"}],
105
- "tools": [
95
+ def test_thinking_disabled_flag_no_longer_changes_the_tool_turn_budget(self):
96
+ # This test used to assert 512 while toggling
97
+ # PROXY_DISABLE_THINKING_ON_TOOL_TURNS. That gating was deliberately
98
+ # removed — it skipped the floor on every tool turn once thinking was
99
+ # off, which re-introduced truncated tool calls on long edits — and the
100
+ # flag now only feeds a log line. Asserting the post-removal number
101
+ # alone would just duplicate test_max_tokens_floor_bypassed_for_small_
102
+ # preflight, so pin the removal itself: the flag must not move the
103
+ # budget in either position. 512 lands on THINKING_MIN_FOR_TOOLS (2048)
104
+ # because Qwen emits <think> regardless of the flag, and on a tool turn
105
+ # those blocks alone eat ~400-1000 tokens, leaving nothing for the
106
+ # tool_call (observed live as ~5 required_tool_miss retries per turn).
107
+ def budget(disable_thinking):
108
+ with unittest.mock.patch.object(proxy, "PROXY_MAX_TOKENS_FLOOR", 4096), \
109
+ unittest.mock.patch.object(
110
+ proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", disable_thinking):
111
+ return proxy.build_openai_request(
106
112
  {
107
- "name": "Bash",
108
- "description": "run command",
109
- "input_schema": {"type": "object"},
110
- }
111
- ],
112
- }
113
+ "model": "test",
114
+ "max_tokens": 512,
115
+ "messages": [{"role": "user", "content": "run pwd"}],
116
+ "tools": [
117
+ {
118
+ "name": "Bash",
119
+ "description": "run command",
120
+ "input_schema": {"type": "object"},
121
+ }
122
+ ],
123
+ },
124
+ proxy.SessionMonitor(context_window=0),
125
+ ).get("max_tokens")
113
126
 
114
- openai = proxy.build_openai_request(
115
- body, proxy.SessionMonitor(context_window=0)
116
- )
117
- self.assertEqual(openai.get("max_tokens"), 512)
118
- finally:
119
- setattr(proxy, "PROXY_MAX_TOKENS_FLOOR", old_floor)
120
- setattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", old_disable)
127
+ self.assertEqual(budget(True), budget(False), "flag must no longer gate the floor")
128
+ self.assertEqual(budget(True), 2048)
121
129
 
122
- def test_build_request_skips_floor_for_non_tool_turns(self):
123
- """Non-tool requests should NOT have the max_tokens floor applied."""
130
+ def test_non_tool_turn_takes_thinking_floor_not_the_big_floor(self):
131
+ """Non-tool requests skip PROXY_MAX_TOKENS_FLOOR and land on
132
+ THINKING_MIN_NO_TOOLS instead — a different, larger mechanism."""
124
133
  old_floor = getattr(proxy, "PROXY_MAX_TOKENS_FLOOR")
125
134
  old_disable = getattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS")
126
135
  try:
@@ -133,11 +142,20 @@ class TestProxyConfigTuning(unittest.TestCase):
133
142
  "messages": [{"role": "user", "content": "say ok"}],
134
143
  }
135
144
 
136
- openai = proxy.build_openai_request(
137
- body, proxy.SessionMonitor(context_window=0)
138
- )
139
- # Floor should NOT inflate max_tokens for non-tool requests
140
- self.assertEqual(openai.get("max_tokens"), 512)
145
+ with unittest.mock.patch.dict(
146
+ os.environ, {"PROXY_THINKING_MIN_NO_TOOLS": "8192"}
147
+ ):
148
+ openai = proxy.build_openai_request(
149
+ body, proxy.SessionMonitor(context_window=0)
150
+ )
151
+ # The big PROXY_MAX_TOKENS_FLOOR (4096) is still skipped for
152
+ # non-tool turns — that is what this test guards. What the request
153
+ # lands on instead is THINKING_MIN_NO_TOOLS: Qwen spends small
154
+ # no-tool budgets entirely inside <think>, the EMPTY-OUTPUT GUARD
155
+ # then promotes truncated reasoning as the body, and evaluator
156
+ # callers get an unparseable verdict. Distinct mechanism, distinct
157
+ # value — assert it explicitly rather than the raw 512.
158
+ self.assertEqual(openai.get("max_tokens"), 8192)
141
159
  finally:
142
160
  setattr(proxy, "PROXY_MAX_TOKENS_FLOOR", old_floor)
143
161
  setattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", old_disable)
@@ -170,30 +188,56 @@ class TestProfileSelection(unittest.TestCase):
170
188
  )
171
189
  self.assertIn(suffix, openai_body["messages"][0]["content"])
172
190
 
191
+ @staticmethod
192
+ def _grammar_body():
193
+ return {
194
+ "model": "default",
195
+ "max_tokens": 128,
196
+ "messages": [{"role": "user", "content": "run pwd"}],
197
+ "tools": [
198
+ {
199
+ "name": "Bash",
200
+ "description": "run command",
201
+ "input_schema": {"type": "object"},
202
+ }
203
+ ],
204
+ }
205
+
173
206
  def test_build_request_uses_profile_grammar_override(self):
174
- old_flag = getattr(proxy, "PROXY_TOOL_CALL_GRAMMAR")
175
- setattr(proxy, "PROXY_TOOL_CALL_GRAMMAR", True)
176
- try:
177
- body = {
178
- "model": "default",
179
- "max_tokens": 128,
180
- "messages": [{"role": "user", "content": "run pwd"}],
181
- "tools": [
182
- {
183
- "name": "Bash",
184
- "description": "run command",
185
- "input_schema": {"type": "object"},
186
- }
187
- ],
188
- }
207
+ # PROXY_TOOL_CALL_GRAMMAR_REQUIRED_ONLY defaults to True and gates the
208
+ # whole grammar path on tool_choice == "required". It postdates this
209
+ # test, which is why the override looked broken: _apply_tool_call_grammar
210
+ # returned before ever consulting it. Relax the gate so this test covers
211
+ # what its name says — the override winning over the default GBNF — and
212
+ # let the test below cover the gate itself.
213
+ with unittest.mock.patch.object(proxy, "PROXY_TOOL_CALL_GRAMMAR", True), \
214
+ unittest.mock.patch.object(
215
+ proxy, "PROXY_TOOL_CALL_GRAMMAR_REQUIRED_ONLY", False):
189
216
  openai_body = proxy.build_openai_request(
190
- body,
217
+ self._grammar_body(),
191
218
  proxy.SessionMonitor(context_window=0),
192
219
  profile_grammar="grammar-test",
193
220
  )
194
- self.assertEqual(openai_body.get("grammar"), "grammar-test")
195
- finally:
196
- setattr(proxy, "PROXY_TOOL_CALL_GRAMMAR", old_flag)
221
+ self.assertEqual(openai_body.get("grammar"), "grammar-test")
222
+
223
+ def test_required_only_gate_is_what_decides_whether_grammar_attaches(self):
224
+ # The gate that broke the test above is real behaviour and was untested.
225
+ # Assert it on the DISCRIMINATING axis — tool_choice — rather than only
226
+ # the negative case: a bare assertIsNone would also pass if the grammar
227
+ # flag were off, if tools were stripped, or if the _apply_tool_call_grammar
228
+ # call site were deleted outright, so it would prove nothing.
229
+ def grammar_for(tool_choice):
230
+ body = {"tools": [{"type": "function"}], "tool_choice": tool_choice}
231
+ with unittest.mock.patch.object(proxy, "PROXY_TOOL_CALL_GRAMMAR", True), \
232
+ unittest.mock.patch.object(
233
+ proxy, "PROXY_TOOL_CALL_GRAMMAR_REQUIRED_ONLY", True):
234
+ proxy._apply_tool_call_grammar(
235
+ body, tool_choice=tool_choice, grammar_override="grammar-test"
236
+ )
237
+ return body.get("grammar")
238
+
239
+ self.assertEqual(grammar_for("required"), "grammar-test")
240
+ self.assertIsNone(grammar_for("auto"))
197
241
 
198
242
  def test_prune_target_fraction_uses_config_or_default(self):
199
243
  old_target = getattr(proxy, "PROXY_CONTEXT_PRUNE_TARGET_FRACTION")
@@ -408,7 +452,18 @@ class TestStreamGuardedPathSelection(unittest.TestCase):
408
452
 
409
453
 
410
454
  class TestMalformedToolGuardrail(unittest.TestCase):
411
- def test_detects_malformed_tool_payload(self):
455
+ def test_tolerates_orphan_parameter_closer_after_a_valid_answer(self):
456
+ # Reversed deliberately on 2026-05-12 (_strip_orphan_tool_xml): an orphan
457
+ # closer with no opener is no longer treated as a malformed tool call,
458
+ # because Qwen3.6 leaks bare </parameter> training residue after a valid
459
+ # answer when forced into tool_choice='required' with nothing to call, and
460
+ # rejecting those cost ~11 false rejections in 40 min on a related branch.
461
+ #
462
+ # Be honest about the trade-off this pins: THIS payload is the model
463
+ # regurgitating its own tool schema twice, which is a plausible genuine
464
+ # failure. It survives only because the separator is '= {' rather than
465
+ # '=\n{' — one whitespace character flips the verdict. We accept that
466
+ # false negative to kill a much larger false-positive class.
412
467
  openai_resp = {
413
468
  "choices": [
414
469
  {
@@ -427,9 +482,43 @@ class TestMalformedToolGuardrail(unittest.TestCase):
427
482
  "tools": [{"name": "Read", "input_schema": {"type": "object"}}],
428
483
  "messages": [{"role": "user", "content": "fix this"}],
429
484
  }
430
- self.assertTrue(proxy._is_malformed_tool_response(openai_resp, anthropic_body))
485
+ self.assertFalse(proxy._is_malformed_tool_response(openai_resp, anthropic_body))
431
486
 
432
- def test_detects_closing_function_tag_payload(self):
487
+ def test_still_detects_a_payload_that_retains_its_opener(self):
488
+ # The other half of the contract the strip promises: tolerating orphan
489
+ # closers must not blind the guardrail to a genuine malformed attempt,
490
+ # which keeps its opener. Without this, the test above alone would be
491
+ # satisfied by deleting the detector outright.
492
+ anthropic_body = {
493
+ "tools": [{"name": "Read", "input_schema": {"type": "object"}}],
494
+ "messages": [{"role": "user", "content": "fix this"}],
495
+ }
496
+ for payload in (
497
+ # The load-bearing case: an orphan closer AND a real opener in the
498
+ # same text. Only this one proves the strip removes the residue
499
+ # without also swallowing the genuine attempt beside it — the other
500
+ # payloads return early before _strip_orphan_tool_xml ever runs.
501
+ '</function> then <function=Bash><parameter name="c">ls</parameter></function>',
502
+ '<parameter name="cmd">ls</parameter>',
503
+ '<tool_call>{"name":"Read"}',
504
+ '<function=Bash>{"cmd":"ls"}',
505
+ ):
506
+ with self.subTest(payload=payload):
507
+ openai_resp = {
508
+ "choices": [
509
+ {
510
+ "finish_reason": "stop",
511
+ "message": {"content": payload, "tool_calls": []},
512
+ }
513
+ ]
514
+ }
515
+ self.assertTrue(
516
+ proxy._is_malformed_tool_response(openai_resp, anthropic_body)
517
+ )
518
+
519
+ def test_tolerates_orphan_function_closer_after_a_valid_answer(self):
520
+ # Same 2026-05-12 reversal as the </parameter> case above: a trailing
521
+ # </function> with no opener is training residue, not a tool call.
433
522
  openai_resp = {
434
523
  "choices": [
435
524
  {
@@ -449,7 +538,7 @@ class TestMalformedToolGuardrail(unittest.TestCase):
449
538
  "tools": [{"name": "Bash", "input_schema": {"type": "object"}}],
450
539
  "messages": [{"role": "user", "content": "list root docs/json files"}],
451
540
  }
452
- self.assertTrue(proxy._is_malformed_tool_response(openai_resp, anthropic_body))
541
+ self.assertFalse(proxy._is_malformed_tool_response(openai_resp, anthropic_body))
453
542
 
454
543
  def test_detects_think_tag_with_repeated_policy_phrase(self):
455
544
  openai_resp = {
@@ -1840,7 +1929,14 @@ class TestTurnCountFinalizeBreaker(unittest.TestCase):
1840
1929
  self._body(45), proxy.SessionMonitor(context_window=262144)
1841
1930
  )
1842
1931
  self.assertFalse(out.get("tools")) # tools stripped
1843
- self.assertIn("STOP now", out["messages"][-1]["content"])
1932
+ # The breaker's wording moved from a hard "STOP now" to a periodic
1933
+ # progress checkpoint that explicitly resumes next turn. Assert the
1934
+ # contract that survives rewording rather than the prose: the turn
1935
+ # count is cited, and the model is told not to emit a tool call —
1936
+ # which is the half that must agree with the stripped toolset.
1937
+ nudge = out["messages"][-1]["content"]
1938
+ self.assertIn("45", nudge)
1939
+ self.assertRegex(nudge, r"(?i)do NOT emit any tool call|no tools are available")
1844
1940
  finally:
1845
1941
  setattr(proxy, "PROXY_HARD_FINALIZE_TURNS", old)
1846
1942
 
@@ -3643,8 +3739,9 @@ class TestDegenerateRepetitionDetection(unittest.TestCase):
3643
3739
  self.assertFalse(truncated)
3644
3740
  self.assertEqual(result["choices"][0]["message"]["content"], text)
3645
3741
 
3646
- def test_max_tokens_floor_skipped_for_non_tool_requests(self):
3647
- """max_tokens floor should not inflate non-tool requests."""
3742
+ def test_non_tool_request_lands_below_the_big_floor(self):
3743
+ """The 16384 floor is skipped for non-tool turns; the much smaller
3744
+ THINKING_MIN_NO_TOOLS applies instead."""
3648
3745
  old_floor = getattr(proxy, "PROXY_MAX_TOKENS_FLOOR")
3649
3746
  old_disable = getattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS")
3650
3747
  try:
@@ -3656,11 +3753,17 @@ class TestDegenerateRepetitionDetection(unittest.TestCase):
3656
3753
  "max_tokens": 100,
3657
3754
  "messages": [{"role": "user", "content": "generate a title"}],
3658
3755
  }
3659
- openai = proxy.build_openai_request(
3660
- body, proxy.SessionMonitor(context_window=0)
3661
- )
3662
- # No tools = no floor inflation
3663
- self.assertEqual(openai.get("max_tokens"), 100)
3756
+ with unittest.mock.patch.dict(
3757
+ os.environ, {"PROXY_THINKING_MIN_NO_TOOLS": "8192"}
3758
+ ):
3759
+ openai = proxy.build_openai_request(
3760
+ body, proxy.SessionMonitor(context_window=0)
3761
+ )
3762
+ # No tools = the 16384 floor is skipped. The result is the much
3763
+ # smaller THINKING_MIN_NO_TOOLS, not the raw 100: a 100-token
3764
+ # no-tool budget is consumed entirely by Qwen's mandatory <think>,
3765
+ # leaving nothing for the answer. Still well under the floor.
3766
+ self.assertEqual(openai.get("max_tokens"), 8192)
3664
3767
  finally:
3665
3768
  setattr(proxy, "PROXY_MAX_TOKENS_FLOOR", old_floor)
3666
3769
  setattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", old_disable)
@@ -3826,8 +3929,10 @@ class EmptyOutputGuardTest(unittest.TestCase):
3826
3929
  self.assertFalse(any(b.get("type") == "text" for b in out["content"]))
3827
3930
 
3828
3931
 
3829
- if __name__ == "__main__":
3830
- unittest.main()
3932
+ # NB: the `unittest.main()` entrypoint is at the END of this file. It used to
3933
+ # sit here, above ~2200 further lines of TestCase classes, so running the file
3934
+ # directly exited after this point and silently skipped roughly 40% of the
3935
+ # module — including TestSendStreamWithRetry — while still printing OK.
3831
3936
 
3832
3937
 
3833
3938
  class TestCompletionContractGuardrails(unittest.TestCase):
@@ -45,7 +45,13 @@ NOT_GATED = ["package.json", "config.yaml", "data.xml", "README.md", "notes.txt"
45
45
 
46
46
  def run(path, content="x" * 2000):
47
47
  e = {**os.environ, "UAP_ENFORCE_DELIVERY": "block", "UAP_INFERENCE_ENDPOINT": "http://172.17.0.1:8080/v1"}
48
- for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE", "UAP_DELIVER_LOCAL_ADVISORY"):
48
+ # ANTHROPIC_BASE_URL / OPENAI_BASE_URL must be stripped too: the enforcer
49
+ # downgrades block -> advisory for a local-model session, so a developer
50
+ # with a loopback base URL exported (the normal shape of a local session)
51
+ # sees every block-expecting test here allow instead. CI has them unset, so
52
+ # this is green in CI and red on the developer's machine.
53
+ for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE",
54
+ "UAP_DELIVER_LOCAL_ADVISORY", "ANTHROPIC_BASE_URL", "OPENAI_BASE_URL"):
49
55
  e.pop(k, None)
50
56
  p = subprocess.run(
51
57
  [sys.executable, str(ENF), "--operation", "Write", "--args", json.dumps({"file_path": path, "content": content})],
@@ -21,7 +21,13 @@ ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "
21
21
 
22
22
  def run(path):
23
23
  e = {**os.environ, "UAP_ENFORCE_DELIVERY": "block", "UAP_INFERENCE_ENDPOINT": "http://172.17.0.1:8080/v1"}
24
- for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE", "UAP_DELIVER_LOCAL_ADVISORY"):
24
+ # ANTHROPIC_BASE_URL / OPENAI_BASE_URL must be stripped too: the enforcer
25
+ # downgrades block -> advisory for a local-model session, so a developer
26
+ # with a loopback base URL exported (the normal shape of a local session)
27
+ # sees every block-expecting test here allow instead. CI has them unset, so
28
+ # this is green in CI and red on the developer's machine.
29
+ for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE",
30
+ "UAP_DELIVER_LOCAL_ADVISORY", "ANTHROPIC_BASE_URL", "OPENAI_BASE_URL"):
25
31
  e.pop(k, None)
26
32
  p = subprocess.run(
27
33
  [sys.executable, str(ENF), "--operation", "Write",
@@ -9,7 +9,13 @@ ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "
9
9
 
10
10
  def run(args, env=None):
11
11
  e = {**os.environ, "UAP_ENFORCE_DELIVERY": "block", "UAP_INFERENCE_ENDPOINT": "http://172.17.0.1:8080/v1"}
12
- for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE", "UAP_DELIVER_LOCAL_ADVISORY"):
12
+ # ANTHROPIC_BASE_URL / OPENAI_BASE_URL must be stripped too: the enforcer
13
+ # downgrades block -> advisory for a local-model session, so a developer
14
+ # with a loopback base URL exported (the normal shape of a local session)
15
+ # sees every block-expecting test here allow instead. CI has them unset, so
16
+ # this is green in CI and red on the developer's machine.
17
+ for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE",
18
+ "UAP_DELIVER_LOCAL_ADVISORY", "ANTHROPIC_BASE_URL", "OPENAI_BASE_URL"):
13
19
  e.pop(k, None)
14
20
  if env:
15
21
  e.update(env)
@@ -17,7 +17,13 @@ ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "
17
17
 
18
18
  def run(op, args, env=None):
19
19
  e = {**os.environ, "UAP_ENFORCE_DELIVERY": "block", "UAP_INFERENCE_ENDPOINT": "http://172.17.0.1:8080/v1"}
20
- for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE", "UAP_DELIVER_LOCAL_ADVISORY"):
20
+ # ANTHROPIC_BASE_URL / OPENAI_BASE_URL must be stripped too: the enforcer
21
+ # downgrades block -> advisory for a local-model session, so a developer
22
+ # with a loopback base URL exported (the normal shape of a local session)
23
+ # sees every block-expecting test here allow instead. CI has them unset, so
24
+ # this is green in CI and red on the developer's machine.
25
+ for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS", "UAP_DELIVER_LOCAL_MODE",
26
+ "UAP_DELIVER_LOCAL_ADVISORY", "ANTHROPIC_BASE_URL", "OPENAI_BASE_URL"):
21
27
  e.pop(k, None)
22
28
  if env:
23
29
  e.update(env)
@@ -24,8 +24,17 @@ def run(path, root, env_extra=None):
24
24
  env = dict(os.environ)
25
25
  env["UAP_REPO_ROOT"] = str(root)
26
26
  env["UAP_ENFORCE_DELIVERY"] = "block"
27
- env.pop("UAP_DELIVER_ACTIVE", None)
28
- env.pop("UAP_DELIVER_BYPASS", None)
27
+ # Every ambient input that can turn "block" into "allow" must be cleared,
28
+ # or these tests assert the developer's shell rather than the enforcer.
29
+ # ANTHROPIC_BASE_URL is the subtle one: delivery_enforcement downgrades
30
+ # block -> advisory for a local-model session, so with a loopback base URL
31
+ # exported (the normal shape of a local session here) the three
32
+ # block-expecting tests below flip to allowed. CI has it unset, so this
33
+ # fails only for developers — presenting as "behaviour drifted".
34
+ for k in ("UAP_DELIVER_ACTIVE", "UAP_DELIVER_BYPASS",
35
+ "UAP_DELIVER_LOCAL_MODE", "UAP_DELIVER_LOCAL_ADVISORY",
36
+ "ANTHROPIC_BASE_URL", "OPENAI_BASE_URL"):
37
+ env.pop(k, None)
29
38
  if env_extra:
30
39
  env.update(env_extra)
31
40
  p = subprocess.run(
@@ -82,8 +91,12 @@ class TestDeliveryEnforcementWorktree(unittest.TestCase):
82
91
  )
83
92
 
84
93
 
85
- if __name__ == "__main__":
86
- unittest.main()
94
+ # NB: the `unittest.main()` entrypoint lives at the END of this file, not here.
95
+ # It used to sit at this point, above the two classes below, so running the file
96
+ # directly (`python3 tools/agents/tests/test_delivery_enforcement_worktree.py`)
97
+ # ran 7 tests and printed OK while `python -m unittest` ran 14 — the 7 it
98
+ # skipped being exactly LocalAdvisoryTest + LocalModeTest. Anyone verifying a
99
+ # change to those got a false green.
87
100
 
88
101
 
89
102
  import os as _os, subprocess as _sp, sys as _sys, json as _json, tempfile as _tf
@@ -96,7 +109,21 @@ class LocalAdvisoryTest(unittest.TestCase):
96
109
  root = _Path(td); (root/".git").mkdir()
97
110
  f = root/"src"/"a.ts"; f.parent.mkdir(parents=True); f.write_text("x")
98
111
  e = dict(_os.environ); e["UAP_REPO_ROOT"]=str(root)
99
- for k in ("ANTHROPIC_BASE_URL","UAP_DELIVER_LOCAL_ADVISORY","UAP_DELIVER_ACTIVE"): e.pop(k, None)
112
+ # This test asserts the DEFAULT for a local session, so every input
113
+ # that overrides that default has to be cleared:
114
+ # - UAP_DELIVER_BYPASS: run() already strips it. An agent shell
115
+ # commonly exports it, the enforcer then allows every write, and
116
+ # the block-expecting tests fail rc 0 != 2 — which reads as
117
+ # "behaviour drifted" rather than "your env leaked".
118
+ # - UAP_DELIVER_LOCAL_MODE: LocalModeTest._run already strips it;
119
+ # omitting it here was an inconsistency. Importing the proxy
120
+ # module (any test that does _load_proxy_module()) runs
121
+ # _load_proxy_env_file(), which loads .uap/proxy.env into the
122
+ # real os.environ — and that file sets UAP_DELIVER_LOCAL_MODE.
123
+ # So this test's result depended on whether a proxy-importing
124
+ # module ran before it in the same process.
125
+ for k in ("ANTHROPIC_BASE_URL","UAP_DELIVER_LOCAL_ADVISORY","UAP_DELIVER_ACTIVE",
126
+ "UAP_DELIVER_BYPASS","UAP_DELIVER_LOCAL_MODE"): e.pop(k, None)
100
127
  e.update(extra_env)
101
128
  p = _sp.run([_sys.executable, str(_ENF), "--operation","Write","--args",_json.dumps({"file_path":str(f)})], capture_output=True, text=True, env=e)
102
129
  return p.returncode, _json.loads(p.stdout) if p.stdout.strip() else {}
@@ -120,7 +147,8 @@ class LocalModeTest(unittest.TestCase):
120
147
  root = _Path(td); (root/".git").mkdir()
121
148
  f = root/"src"/"a.ts"; f.parent.mkdir(parents=True); f.write_text("x")
122
149
  e = dict(_os.environ); e["UAP_REPO_ROOT"]=str(root)
123
- for k in ("ANTHROPIC_BASE_URL","UAP_DELIVER_LOCAL_ADVISORY","UAP_DELIVER_LOCAL_MODE","UAP_DELIVER_ACTIVE"): e.pop(k, None)
150
+ # See the note in LocalAdvisoryTest._run — same ambient-bypass leak.
151
+ for k in ("ANTHROPIC_BASE_URL","UAP_DELIVER_LOCAL_ADVISORY","UAP_DELIVER_LOCAL_MODE","UAP_DELIVER_ACTIVE","UAP_DELIVER_BYPASS"): e.pop(k, None)
124
152
  e.update(env)
125
153
  p = _sp.run([_sys.executable, str(_ENF), "--operation","Write","--args",_json.dumps({"file_path":str(f)})], capture_output=True, text=True, env=e)
126
154
  return p.returncode, _json.loads(p.stdout) if p.stdout.strip() else {}
@@ -141,3 +169,7 @@ class LocalModeTest(unittest.TestCase):
141
169
  def test_default_is_advisory(self):
142
170
  rc, out = self._run({"ANTHROPIC_BASE_URL":"http://127.0.0.1:4000"})
143
171
  self.assertEqual(rc, 0)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ unittest.main()
@@ -28,8 +28,19 @@ TESTS = Path(__file__).resolve().parent
28
28
  # Verified failing for their own pre-existing reasons, not by omission. Each
29
29
  # entry is a debt with a stated cause — not a place to park a newly broken test.
30
30
  KNOWN_EXCLUDED = {
31
- "test_anthropic_proxy_streaming": "behaviour drifted from the assertions",
32
- "test_delivery_enforcement_worktree": "behaviour drifted from the assertions",
31
+ # test_anthropic_proxy_streaming and test_delivery_enforcement_worktree
32
+ # were both listed here as "behaviour drifted from the assertions" and are
33
+ # now listed in test:enforcers instead:
34
+ # - proxy_streaming had 7 genuinely stale assertions (max_tokens floors
35
+ # superseded by the thinking-floor, a grammar guard that postdated the
36
+ # test, the turn-count breaker's rewording, and two malformed-payload
37
+ # cases deliberately reversed by _strip_orphan_tool_xml). All rewritten
38
+ # against current intent. This is the module that covers the streaming
39
+ # 503 retry, so it was the gap that let that bug ship.
40
+ # - delivery_enforcement_worktree never drifted at all: two of its env
41
+ # helpers failed to strip an ambient UAP_DELIVER_BYPASS, so the
42
+ # enforcer allowed every write and four block-expecting tests read 0
43
+ # instead of 2. It passes clean; the helpers are now hermetic.
33
44
  "test_uap_compliance": "needs a populated DB; environment-bound",
34
45
  }
35
46
 
@@ -87,5 +98,50 @@ class TestEveryListedModuleActuallyCollects(unittest.TestCase):
87
98
  assert ghosts == set(), f"test:enforcers names modules that do not exist: {sorted(ghosts)}"
88
99
 
89
100
 
101
+ class TestTheGateCollectsWhatItClaims(unittest.TestCase):
102
+ """Every check above is structural — none pins how many tests actually run.
103
+
104
+ A listed module can define a TestCase and still contribute almost nothing.
105
+ Two real instances: a mid-file `unittest.main()` in
106
+ test_anthropic_proxy_streaming and test_delivery_enforcement_worktree cut
107
+ direct runs off partway through the file (273 -> 168 and 14 -> 7) while
108
+ still printing OK; and a mass deletion inside any listed module is invisible
109
+ to the listing checks. A floor on the collected count catches both.
110
+ """
111
+
112
+ # Deliberately below the current count so ordinary churn doesn't trip it.
113
+ # Raise it when the suite grows substantially; never lower it to make a
114
+ # failing gate pass — that is the deletion this test exists to catch.
115
+ MINIMUM_COLLECTED = 750
116
+
117
+ def test_listed_modules_collect_at_least_the_expected_test_count(self):
118
+ loader = unittest.TestLoader()
119
+ suite = loader.loadTestsFromNames(
120
+ [f"tools.agents.tests.{m}" for m in sorted(_listed_modules())]
121
+ )
122
+ # loadTestsFromNames turns an import failure into a _FailedTest that
123
+ # still counts, so surface those rather than let them pad the total.
124
+ broken = [
125
+ str(t) for t in _flatten(suite)
126
+ if type(t).__name__ == "_FailedTest"
127
+ ]
128
+ assert broken == [], f"listed modules that fail to even import: {broken}"
129
+
130
+ count = suite.countTestCases()
131
+ assert count >= self.MINIMUM_COLLECTED, (
132
+ f"the gate collects {count} tests, below the {self.MINIMUM_COLLECTED} floor. "
133
+ "Tests were deleted, a module stopped collecting, or an entrypoint "
134
+ "truncates the file. Investigate before adjusting this number."
135
+ )
136
+
137
+
138
+ def _flatten(suite):
139
+ for item in suite:
140
+ if isinstance(item, unittest.TestSuite):
141
+ yield from _flatten(item)
142
+ else:
143
+ yield item
144
+
145
+
90
146
  if __name__ == "__main__":
91
147
  unittest.main()
@@ -3,6 +3,7 @@ import importlib.util
3
3
  import os
4
4
  import tempfile
5
5
  import unittest
6
+ import unittest.mock
6
7
  from pathlib import Path
7
8
 
8
9
 
@@ -55,5 +56,144 @@ class ProxyEnvLoaderTest(unittest.TestCase):
55
56
  proxy._load_proxy_env_file() # must not raise
56
57
 
57
58
 
59
+ class ProxyEnvWalkStopsAtRepoRootTest(unittest.TestCase):
60
+ """The upward walk must stop at the repository, not the filesystem root.
61
+
62
+ It used to run to "/", so a checkout nested under an unrelated checkout
63
+ loaded the outer repo's proxy.env, PROXY_AUTH_TOKEN included. A WORKTREE is
64
+ not that case — its .git file names the same repository — so it must still
65
+ reach its own repo's config rather than start tokenless.
66
+ """
67
+
68
+ KEY = "UAP_WALK_PROBE_KEY"
69
+
70
+ def tearDown(self):
71
+ os.environ.pop(self.KEY, None)
72
+ os.environ.pop("UAP_PROXY_ENV_FILE", None)
73
+
74
+ def _tree(self, td):
75
+ # outer/ is the "parent checkout" and holds the env file; outer/inner/
76
+ # is the "worktree" — it has a .git FILE (as real worktrees do) and no
77
+ # .uap/ of its own.
78
+ outer = Path(td) / "outer"
79
+ (outer / ".uap").mkdir(parents=True)
80
+ (outer / ".git").mkdir()
81
+ (outer / ".uap" / "proxy.env").write_text(f"{self.KEY}=from_the_repo\n")
82
+ inner = outer / "inner"
83
+ inner.mkdir()
84
+ (inner / ".git").write_text("gitdir: ../.git/worktrees/inner\n")
85
+ return outer, inner
86
+
87
+ def _load_from(self, path):
88
+ # cwd is process-global; restore it even if the loader raises. Safe
89
+ # under unittest (serial) and pytest-xdist (separate processes); would
90
+ # not be under a thread-parallel runner, and nothing here runs threaded.
91
+ cwd = os.getcwd()
92
+ os.chdir(path)
93
+ try:
94
+ os.environ.pop(self.KEY, None)
95
+ proxy._load_proxy_env_file()
96
+ finally:
97
+ os.chdir(cwd)
98
+ return os.environ.get(self.KEY)
99
+
100
+ def test_a_worktree_still_reaches_its_own_repository_config(self):
101
+ # A worktree's .git is a FILE naming the same repository. Stopping there
102
+ # would leave the proxy with an empty PROXY_AUTH_TOKEN, and since the
103
+ # auth middleware treats empty as "no auth configured", a non-loopback
104
+ # bind would then be wide open. Same repo => follow the pointer home.
105
+ with tempfile.TemporaryDirectory() as td:
106
+ _outer, inner = self._tree(td)
107
+ self.assertEqual(self._load_from(inner), "from_the_repo")
108
+
109
+ def test_still_finds_the_env_file_at_the_repo_root_itself(self):
110
+ # The bound is inclusive — stopping AT the repo root must not stop
111
+ # BEFORE reading it, or the proxy loses its own config.
112
+ with tempfile.TemporaryDirectory() as td:
113
+ outer, _inner = self._tree(td)
114
+ self.assertEqual(self._load_from(outer), "from_the_repo")
115
+
116
+ def test_the_walk_actually_ascends(self):
117
+ # Both cases above sit ON a boundary directory, so an implementation
118
+ # that only ever looked at Path.cwd() would pass them. Start well below
119
+ # the root, in a dir with neither .git nor .uap.
120
+ with tempfile.TemporaryDirectory() as td:
121
+ outer, _inner = self._tree(td)
122
+ deep = outer / "a" / "b" / "c"
123
+ deep.mkdir(parents=True)
124
+ self.assertEqual(self._load_from(deep), "from_the_repo")
125
+
126
+ def test_does_not_climb_out_of_one_repository_into_another(self):
127
+ # The genuine cross-repo case the bound exists for: a checkout nested
128
+ # under an unrelated checkout must not inherit the outer repo's secret.
129
+ with tempfile.TemporaryDirectory() as td:
130
+ outer, _inner = self._tree(td)
131
+ nested = outer / "vendored"
132
+ (nested / ".git").mkdir(parents=True) # a real repo root, not a worktree
133
+ self.assertIsNone(
134
+ self._load_from(nested),
135
+ "walk escaped a nested repository into the enclosing checkout",
136
+ )
137
+
138
+
139
+ class ProxyBindAuthGuardTest(unittest.TestCase):
140
+ """A non-loopback bind with no token must fail closed.
141
+
142
+ PROXY_AUTH_TOKEN comes from .uap/proxy.env; PROXY_HOST commonly comes from
143
+ the systemd EnvironmentFile. So any failure to find the former leaves the
144
+ latter intact — bind-all survives, require-a-credential does not — and the
145
+ middleware reads an empty token as "no auth configured". Without this guard
146
+ a config-discovery bug silently produces an open LLM proxy on the LAN.
147
+ """
148
+
149
+ def test_loopback_without_a_token_is_allowed(self):
150
+ for host in ("127.0.0.1", "::1", "localhost"):
151
+ with self.subTest(host=host):
152
+ proxy._assert_bind_is_authenticated(host, "") # must not raise
153
+
154
+ def test_any_bind_with_a_token_is_allowed(self):
155
+ proxy._assert_bind_is_authenticated("0.0.0.0", "a-real-token")
156
+
157
+ def test_non_loopback_without_a_token_refuses_to_start(self):
158
+ for host in ("0.0.0.0", "192.168.1.50", "::"):
159
+ with self.subTest(host=host):
160
+ with unittest.mock.patch.dict(os.environ, {}, clear=False):
161
+ os.environ.pop("PROXY_ALLOW_UNAUTHENTICATED_BIND", None)
162
+ with self.assertRaises(SystemExit) as ctx:
163
+ proxy._assert_bind_is_authenticated(host, "")
164
+ self.assertIn("unauthenticated", str(ctx.exception))
165
+
166
+ def test_explicit_override_permits_an_open_listener(self):
167
+ with unittest.mock.patch.dict(
168
+ os.environ, {"PROXY_ALLOW_UNAUTHENTICATED_BIND": "1"}
169
+ ):
170
+ proxy._assert_bind_is_authenticated("0.0.0.0", "") # must not raise
171
+
172
+
173
+ class ProxyEnvAutoloadOptOutTest(unittest.TestCase):
174
+ """Importing the module must be inert when the opt-out is set."""
175
+
176
+ def test_autoload_enabled_by_default(self):
177
+ with unittest.mock.patch.dict(os.environ, {}, clear=False):
178
+ os.environ.pop("UAP_PROXY_ENV_AUTOLOAD", None)
179
+ self.assertTrue(proxy._proxy_env_autoload_enabled())
180
+
181
+ def test_opt_out_values_disable_autoload(self):
182
+ for val in ("0", "off", "false", "no", "OFF", " 0 "):
183
+ with self.subTest(val=val):
184
+ with unittest.mock.patch.dict(
185
+ os.environ, {"UAP_PROXY_ENV_AUTOLOAD": val}
186
+ ):
187
+ self.assertFalse(proxy._proxy_env_autoload_enabled())
188
+
189
+ def test_other_values_leave_autoload_on(self):
190
+ for val in ("1", "on", "true", "yes"):
191
+ with self.subTest(val=val):
192
+ with unittest.mock.patch.dict(
193
+ os.environ, {"UAP_PROXY_ENV_AUTOLOAD": val}
194
+ ):
195
+ self.assertTrue(proxy._proxy_env_autoload_enabled())
196
+
197
+
58
198
  if __name__ == "__main__":
59
199
  unittest.main()