@miller-tech/uap 1.163.4 → 1.163.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.163.4",
3
+ "version": "1.163.5",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -225,12 +225,77 @@ _BASH_WRITE_RE = re.compile(
225
225
  # or relative path reaches it too. Anchoring on "name right after the separator"
226
226
  # let `nohup /home/u/.cloakbrowser/chromium-146/chrome file:///x.html &` walk
227
227
  # straight through the gate — the invocation the model actually reaches for.
228
+ #
229
+ # The trailing lookahead requires the bin to be a real COMMAND: followed by
230
+ # whitespace-then-an-argument (a URL/file/flag), or a terminator (`firefox &`),
231
+ # or end of input. Without it, `open` — a browser bin on macOS — also matched
232
+ # Python's `open()` BUILTIN: `json.load(open('f'))` read as "subshell-paren +
233
+ # open browser" and hard-blocked read-only diagnostics. A launch always has a
234
+ # space before its argument; `open(` is a function call, so it no longer matches.
235
+ # `\n` is a command separator too: without it `^` only anchored at offset 0, so a
236
+ # launch on the SECOND-or-later line of a multi-line command was invisible —
237
+ # `cd app\npython3 -m http.server &\nfirefox http://localhost:8080` walked
238
+ # through. Agents emit multi-line bash constantly, so this was a live bypass.
239
+ # `(?:\w+=[^\s]*\s+)*` skips leading env assignments: the path group rejects `=`
240
+ # and `:`, so `DISPLAY=:0 firefox /tmp/a.html` — the canonical incantation for
241
+ # putting a window on the operator's desktop, i.e. exactly what this gate exists
242
+ # to stop — was also walking through.
228
243
  _BROWSER_RE = re.compile(
229
- r"(?:^|[;&|(]|\|\||&&|\bnohup\b|\bsetsid\b|\bexec\b|\bxargs\b|\benv\b|\bnice\b|\btimeout\b|`|\$\()"
230
- r"\s*(?:[\w./~+-]*/)?(" + "|".join(BROWSER_BINS) + r")\b",
244
+ r"(?:^|[;&|(\n]|\|\||&&|\bnohup\b|\bsetsid\b|\bexec\b|\bxargs\b|\benv\b|\bnice\b|\btimeout\b|`|\$\()"
245
+ r"\s*(?:\w+=[^\s]*\s+)*(?:[\w./~+-]*/)?(" + "|".join(BROWSER_BINS) + r")(?=\s+[^\s(]|\s*[;&|]|\s*$)",
231
246
  re.IGNORECASE,
232
247
  )
233
- _LOOKUP_RE = re.compile(r"^\s*(?:which|command\s+-v|type|whereis)\b", re.IGNORECASE)
248
+ # A lookup is only exempt when the command is EXCLUSIVELY a lookup. Matching on
249
+ # PREFIX alone disabled the whole browser scan for the natural probe-then-launch
250
+ # sequence `which firefox && firefox /tmp/a.html` — the most likely real bypass
251
+ # in this file, and one an agent reaches for without trying to evade anything.
252
+ _LOOKUP_RE = re.compile(r"^\s*(?:which|command\s+-v|type|whereis)\b[^;&|\n]*$", re.IGNORECASE)
253
+
254
+ # Quoted spans and heredoc bodies — blanked before the browser scan so a browser
255
+ # NAME that is really a grep PATTERN, an interpreter payload
256
+ # (`python -c "...open(...)"`), echoed text, or a heredoc body is not misread as
257
+ # a LAUNCH. This was the other half of the false-positive class: a diagnostic
258
+ # like `grep -n "GUI browser\|open.*browser" f.py` matched `|open` as "pipe into
259
+ # the open browser". Only the shell SURFACE can express a launch.
260
+ # The leading `\\.` alternative CONSUMES a backslash-escaped character before it
261
+ # can open a span. A naive `'[^']*'` pairs the two ESCAPED apostrophes in
262
+ # `echo it\'s done ; firefox a.html ; echo that\'s all`, blanking straight across
263
+ # the launch and hiding it — reachable by accident, not just by an evader. The
264
+ # escape token is returned unchanged by the substitution (only real quoted spans
265
+ # are blanked); ordering matters, so keep `\\.` first.
266
+ _QUOTE_SPAN_RE = re.compile(r"\\.|'(?:\\.|[^'])*'|\"(?:\\.|[^\"])*\"", re.DOTALL)
267
+ _HEREDOC_RE = re.compile(r"<<-?\s*(['\"]?)([A-Za-z_]\w*)\1.*?^[ \t]*\2[ \t]*$", re.DOTALL | re.MULTILINE)
268
+ # Above this size, skip surface-stripping and scan raw: `_HEREDOC_RE`'s lazy
269
+ # `.*?` under DOTALL is O(n) per `<<TAG` with no terminator, so a pathological
270
+ # ~100KB command could stall the hook. Scanning raw fails toward BLOCKING.
271
+ _SURFACE_MAX = 65536
272
+
273
+
274
+ def _shell_surface(cmd: str) -> str:
275
+ """`cmd` with quoted spans and heredoc bodies replaced by spaces, so the
276
+ browser matcher sees only real shell command words.
277
+
278
+ Length-preserving ON PURPOSE (blanks, never deletes): `firefox "$URL"` must
279
+ still read as a launch, and it only does because the blanked argument leaves
280
+ whitespace for the trailing lookahead. Returning "" here would silently stop
281
+ blocking every quoted-argument launch — there are tests pinning this.
282
+
283
+ KNOWN GAP (pre-existing, not introduced here): a launch inside a quoted
284
+ interpreter payload — `bash -c "firefox url"`, `sh -c 'xdg-open x'` — is not
285
+ detected. The OLD matcher missed these too (the `"` before the bin was never
286
+ a separator), so this is not a regression; closing it needs a separate
287
+ interpreter-aware pass that does not re-fire on `python -c "...open('f')"`."""
288
+ if len(cmd) > _SURFACE_MAX or ("'" not in cmd and '"' not in cmd and "<<" not in cmd):
289
+ return cmd
290
+ def blank(m: "re.Match[str]") -> str:
291
+ return " " * (m.end() - m.start())
292
+
293
+ def blank_spans(m: "re.Match[str]") -> str:
294
+ # Escape tokens (`\'`) pass through untouched — blanking them is
295
+ # unnecessary and they must not be treated as quote spans.
296
+ return m.group(0) if m.group(0).startswith("\\") else blank(m)
297
+
298
+ return _QUOTE_SPAN_RE.sub(blank_spans, _HEREDOC_RE.sub(blank, cmd))
234
299
 
235
300
 
236
301
  def _handle_bash(args: dict) -> None:
@@ -243,7 +308,7 @@ def _handle_bash(args: dict) -> None:
243
308
 
244
309
  # Browser launch → redirect to the real (headless) validation path. Always
245
310
  # blocked: opening a window proves nothing and cannot gate a DONE claim.
246
- if not _LOOKUP_RE.match(cmd) and _BROWSER_RE.search(cmd):
311
+ if not _LOOKUP_RE.match(cmd) and _BROWSER_RE.search(_shell_surface(cmd)):
247
312
  emit(
248
313
  False,
249
314
  "BLOCKED: do not open a GUI browser to check your work — it proves nothing "
@@ -82,6 +82,83 @@ class BashGateTest(unittest.TestCase):
82
82
  code, _ = run("Bash", {"command": "which firefox chromium 2>/dev/null"})
83
83
  self.assertEqual(code, 0)
84
84
 
85
+ def test_browser_name_inside_code_and_quotes_is_not_blocked(self):
86
+ # The matcher must read the SHELL SURFACE only — a browser-bin NAME that
87
+ # appears inside a quoted grep pattern, an interpreter payload, an echoed
88
+ # string, or a heredoc body is NOT a launch. These are the real
89
+ # false-positives that repeatedly blocked read-only diagnostics:
90
+ # - Python's open() builtin: `(open` read as "subshell + open browser".
91
+ # - a grep pattern containing `\|open`: read as "pipe + open browser".
92
+ for cmd in (
93
+ # Python open() builtin in a -c payload (the .uap/ ledger reads).
94
+ "python3 -c \"import json; d=json.load(open('.uap/completion-ledger.json'))\"",
95
+ # Python open() for writing, inside a heredoc body.
96
+ "python3 - \"$HEAD\" <<'PY'\nopen('.uap/reviews/x.json', 'w').write(s)\nPY",
97
+ # A grep pattern that literally contains browser-bin words.
98
+ "grep -n \"GUI browser\\|open.*browser\\|BROWSER\" src/x.py",
99
+ # Echoed text mentioning a browser is not a launch.
100
+ "echo 'open the browser to check the page'",
101
+ # `open` as a grep pattern arg (not a command at a shell position).
102
+ "cat notes.txt | grep open",
103
+ ):
104
+ code, out = run("Bash", {"command": cmd})
105
+ self.assertEqual(code, 0, f"{cmd!r} must NOT be blocked — got {out}")
106
+
107
+ def test_quoted_argument_launch_still_blocked(self):
108
+ # The surface-stripper blanks quoted spans LENGTH-PRESERVINGLY; these
109
+ # only stay blocked because the blanked argument leaves whitespace for
110
+ # the trailing lookahead. If blank() ever returns "" instead of spaces,
111
+ # every quoted-argument launch silently stops being blocked — pin it.
112
+ for cmd in ("firefox '/tmp/a.html'", 'xdg-open "$F"', 'chromium "/tmp/a b.html"'):
113
+ code, out = run("Bash", {"command": cmd})
114
+ self.assertEqual(code, 2, f"{cmd!r} must be BLOCKED — got {out}")
115
+
116
+ def test_lookahead_branches_and_alternation_order(self):
117
+ # `chromium` precedes `chromium-browser` in BROWSER_BINS, so the engine
118
+ # must backtrack through the lookahead to reach the longer name.
119
+ # Also covers the bare-bin (`\s*$`) and terminator (`\s*[;&|]`) branches.
120
+ for cmd in ("chromium-browser /tmp/a.html", "google-chrome-stable /tmp/a.html",
121
+ "firefox", "chromium &"):
122
+ code, out = run("Bash", {"command": cmd})
123
+ self.assertEqual(code, 2, f"{cmd!r} must be BLOCKED — got {out}")
124
+
125
+ def test_probe_then_launch_is_blocked(self):
126
+ # A prefix `which` used to disable the ENTIRE browser scan, so the
127
+ # natural probe-then-launch sequence walked straight through.
128
+ for cmd in ("which firefox && firefox /tmp/a.html",
129
+ "which xdg-open >/dev/null && xdg-open /tmp/app.html"):
130
+ code, out = run("Bash", {"command": cmd})
131
+ self.assertEqual(code, 2, f"{cmd!r} must be BLOCKED — got {out}")
132
+ # A pure lookup is still exempt.
133
+ code, out = run("Bash", {"command": "which firefox chromium 2>/dev/null"})
134
+ self.assertEqual(code, 0, out)
135
+
136
+ def test_launch_on_a_later_line_is_blocked(self):
137
+ # `^` only anchored at offset 0, so a launch on line 2+ of a multi-line
138
+ # command was invisible — and agents emit multi-line bash constantly.
139
+ cmd = "cd /tmp/app\npython3 -m http.server 8080 &\nfirefox http://localhost:8080"
140
+ code, out = run("Bash", {"command": cmd})
141
+ self.assertEqual(code, 2, f"multi-line launch must be BLOCKED — got {out}")
142
+
143
+ def test_leading_env_assignment_launch_is_blocked(self):
144
+ # `DISPLAY=:0 firefox ...` is the canonical way to put a window on the
145
+ # operator's desktop — precisely what this gate exists to prevent.
146
+ for cmd in ("DISPLAY=:0 firefox /tmp/a.html", "FOO=bar BAZ=1 chromium /tmp/a.html"):
147
+ code, out = run("Bash", {"command": cmd})
148
+ self.assertEqual(code, 2, f"{cmd!r} must be BLOCKED — got {out}")
149
+
150
+ def test_escaped_quotes_do_not_hide_a_launch(self):
151
+ # Escaped apostrophes must not pair ACROSS a launch and blank it away.
152
+ cmd = "echo it\\'s done ; firefox /tmp/a.html ; echo that\\'s all"
153
+ code, out = run("Bash", {"command": cmd})
154
+ self.assertEqual(code, 2, f"escaped-quote span must not hide the launch — got {out}")
155
+
156
+ def test_open_function_call_is_not_a_browser_launch(self):
157
+ # `open(` is a function call (Python/JS), never a browser launch; a real
158
+ # `open <url>` launch has a space + argument, not a paren.
159
+ code, out = run("Bash", {"command": "node -e \"const f = open('a.txt'); void f;\""})
160
+ self.assertEqual(code, 0, out)
161
+
85
162
  def test_bash_source_write_routes_to_deliver(self):
86
163
  code, out = run("Bash", {"command": "cat > src/app.js <<EOF\nconsole.log(1)\nEOF"})
87
164
  self.assertEqual(code, 2, out)