@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 +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/delivery_enforcement.py +69 -4
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/tests/test_delivery_enforcement_web_and_bash.py +77 -0
package/package.json
CHANGED
|
Binary file
|
|
@@ -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")
|
|
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
|
-
|
|
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 "
|
|
Binary file
|
|
Binary file
|
|
@@ -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)
|