@miller-tech/uap 1.148.8 → 1.148.9
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 +91 -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 +84 -0
- package/tools/agents/tests/test_delivery_enforcement_worktree.py +3 -1
package/package.json
CHANGED
|
Binary file
|
|
@@ -22,6 +22,7 @@ files (deliver protects those itself), and tooling dot-dirs.
|
|
|
22
22
|
"""
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
import os
|
|
25
|
+
import re
|
|
25
26
|
import sys
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
|
|
@@ -35,6 +36,25 @@ SOURCE_EXTS = (
|
|
|
35
36
|
".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
|
|
36
37
|
".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".swift", ".kt",
|
|
37
38
|
".c", ".cc", ".cpp", ".h", ".hpp",
|
|
39
|
+
# WEB deliverables are source too. Omitting these let an ENTIRE class of
|
|
40
|
+
# deliverable (single-file web apps, static sites, templates) escape the gate:
|
|
41
|
+
# a 34KB single-file HTML app returned "not source code" -> allowed -> zero
|
|
42
|
+
# routing, zero deliver, zero validation (observed live). The completion gate's
|
|
43
|
+
# own code-change detector already counts html/css/vue/svelte as code, so the
|
|
44
|
+
# two halves of the system disagreed about what "source" means until now.
|
|
45
|
+
".html", ".htm", ".css", ".scss", ".sass", ".less",
|
|
46
|
+
".vue", ".svelte", ".astro",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# GUI-browser launchers. A model with no validation tooling in reach tries to
|
|
50
|
+
# "check its work" by opening a browser (observed: 11 xdg-open/firefox/chromium
|
|
51
|
+
# attempts in 40 min, spawning windows on the operator's desktop) — which proves
|
|
52
|
+
# nothing and burns turns. Redirect it to `uap verify`, which renders headlessly
|
|
53
|
+
# and runs the real visual + behavioral gates.
|
|
54
|
+
BROWSER_BINS = (
|
|
55
|
+
"xdg-open", "firefox", "chromium", "chromium-browser", "google-chrome",
|
|
56
|
+
"google-chrome-stable", "chrome", "sensible-browser", "x-www-browser",
|
|
57
|
+
"microsoft-edge", "open",
|
|
38
58
|
)
|
|
39
59
|
|
|
40
60
|
# NOTE: `.worktrees/` is deliberately NOT exempt here. A real `uap deliver` run
|
|
@@ -148,8 +168,79 @@ def _local_mode() -> str:
|
|
|
148
168
|
return "advisory" if adv not in {"0", "off", "false", "no", ""} else "block"
|
|
149
169
|
|
|
150
170
|
|
|
171
|
+
BASH_OPS = {"Bash", "bash", "run_bash", "shell"}
|
|
172
|
+
|
|
173
|
+
# A bash command that WRITES a source file: `> f.ts`, `>> f.ts`, `tee f.ts`,
|
|
174
|
+
# `sed -i ... f.ts`. Without this, Edit/Write gating is trivially bypassable —
|
|
175
|
+
# `cat > app.js <<EOF` writes source with no deliver run and no validation.
|
|
176
|
+
_EXT_ALT = "|".join(e.lstrip(".") for e in SOURCE_EXTS)
|
|
177
|
+
_BASH_WRITE_RE = re.compile(
|
|
178
|
+
r"(?:>>?\s*|tee\s+(?:-a\s+)?|sed\s+-i[^\s]*\s+(?:[^|;&]*\s)?)"
|
|
179
|
+
r"['\"]?([^\s'\"|;&>]+\.(?:" + _EXT_ALT + r"))\b",
|
|
180
|
+
re.IGNORECASE,
|
|
181
|
+
)
|
|
182
|
+
# A browser LAUNCH (not a lookup: `which firefox` / `command -v chrome` are fine).
|
|
183
|
+
_BROWSER_RE = re.compile(
|
|
184
|
+
r"(?:^|[;&|]|\&\&|\|\|)\s*(?:nohup\s+)?(" + "|".join(BROWSER_BINS) + r")\b",
|
|
185
|
+
re.IGNORECASE,
|
|
186
|
+
)
|
|
187
|
+
_LOOKUP_RE = re.compile(r"^\s*(?:which|command\s+-v|type|whereis)\b", re.IGNORECASE)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _handle_bash(args: dict) -> None:
|
|
191
|
+
"""Gate bash so it can't bypass delivery enforcement (source writes) and so a
|
|
192
|
+
model stops trying to 'verify' by opening a GUI browser."""
|
|
193
|
+
cmd = str(args.get("command") or args.get("cmd") or args.get("script") or "")
|
|
194
|
+
if not cmd.strip():
|
|
195
|
+
emit(True, "bash: empty command")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Browser launch → redirect to the real (headless) validation path. Always
|
|
199
|
+
# blocked: opening a window proves nothing and cannot gate a DONE claim.
|
|
200
|
+
if not _LOOKUP_RE.match(cmd) and _BROWSER_RE.search(cmd):
|
|
201
|
+
emit(
|
|
202
|
+
False,
|
|
203
|
+
"BLOCKED: do not open a GUI browser to check your work — it proves nothing "
|
|
204
|
+
"and cannot validate anything. Run `uap verify` instead: it renders the page "
|
|
205
|
+
"headlessly and runs the REAL visual + behavioral gates (screenshots land in "
|
|
206
|
+
".uap/visual). Use that to see whether the UI actually works.",
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Source write via shell → same rules as an Edit/Write: route through deliver.
|
|
211
|
+
if os.environ.get("UAP_DELIVER_ACTIVE") == "1":
|
|
212
|
+
emit(True, "bash: inside a deliver-driven run")
|
|
213
|
+
return
|
|
214
|
+
if os.environ.get("UAP_DELIVER_BYPASS") == "1":
|
|
215
|
+
emit(True, "bash: UAP_DELIVER_BYPASS override set")
|
|
216
|
+
return
|
|
217
|
+
m = _BASH_WRITE_RE.search(cmd)
|
|
218
|
+
if m:
|
|
219
|
+
target = m.group(1)
|
|
220
|
+
mode = os.environ.get("UAP_ENFORCE_DELIVERY", "block").lower()
|
|
221
|
+
if mode == "block" and _is_local_model_session() and _local_mode() == "advisory":
|
|
222
|
+
mode = "advisory"
|
|
223
|
+
msg = (
|
|
224
|
+
f"BLOCKED: do not write source via the shell ('{target}'). Writing files with "
|
|
225
|
+
"a redirect/heredoc/sed/tee bypasses the delivery gate. To create or change "
|
|
226
|
+
"code, call the `deliver` tool (or run: uap deliver \"<one-line description>\"). "
|
|
227
|
+
"Do NOT retry this command."
|
|
228
|
+
)
|
|
229
|
+
if mode == "block":
|
|
230
|
+
emit(False, msg, route="deliver", deliverHint=f"implement the intended change to {target}")
|
|
231
|
+
return
|
|
232
|
+
print(f"[delivery-enforcement advisory] {msg}", file=sys.stderr)
|
|
233
|
+
emit(True, "bash advisory: source-write nudge logged")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
emit(True, "bash: no source write or browser launch")
|
|
237
|
+
|
|
238
|
+
|
|
151
239
|
def main() -> None:
|
|
152
240
|
op, args = parse_cli()
|
|
241
|
+
if op in BASH_OPS:
|
|
242
|
+
_handle_bash(args)
|
|
243
|
+
return
|
|
153
244
|
if op not in EDIT_OPS:
|
|
154
245
|
emit(True, "not a file-edit operation")
|
|
155
246
|
return
|
|
Binary file
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""delivery-enforcement must gate WEB source (.html/.css/...) and BASH.
|
|
3
|
+
|
|
4
|
+
Two live escapes this closes:
|
|
5
|
+
1. `.html` was not in SOURCE_EXTS, so a single-file web app — the actual
|
|
6
|
+
deliverable — returned "not source code" and was allowed: zero routing, zero
|
|
7
|
+
deliver, zero validation (observed: a 34KB rubiks-cube.html built ungated).
|
|
8
|
+
2. Bash was ungated entirely, so `cat > app.js <<EOF` bypassed the whole gate;
|
|
9
|
+
and a model with no validation tooling in reach kept launching a GUI browser
|
|
10
|
+
(xdg-open/firefox) to "check its work", which proves nothing.
|
|
11
|
+
"""
|
|
12
|
+
import json, os, subprocess, sys, unittest
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "delivery_enforcement.py"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(op, args, env=None):
|
|
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"):
|
|
21
|
+
e.pop(k, None)
|
|
22
|
+
if env:
|
|
23
|
+
e.update(env)
|
|
24
|
+
p = subprocess.run([sys.executable, str(ENF), "--operation", op, "--args", json.dumps(args)],
|
|
25
|
+
capture_output=True, text=True, env=e, cwd=str(ENF.parents[3]))
|
|
26
|
+
return p.returncode, p.stdout + p.stderr
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WebSourceTest(unittest.TestCase):
|
|
30
|
+
def test_html_is_source_and_routes(self):
|
|
31
|
+
code, out = run("Write", {"file_path": "app.html", "content": "<html>" + "x" * 2000})
|
|
32
|
+
self.assertEqual(code, 2, out) # blocked
|
|
33
|
+
self.assertIn("route", out) # → deliver
|
|
34
|
+
|
|
35
|
+
def test_css_is_source(self):
|
|
36
|
+
code, _ = run("Write", {"file_path": "src/app.css", "content": "a{}" * 500})
|
|
37
|
+
self.assertEqual(code, 2)
|
|
38
|
+
|
|
39
|
+
def test_vue_svelte_are_source(self):
|
|
40
|
+
for f in ("src/App.vue", "src/App.svelte"):
|
|
41
|
+
code, _ = run("Write", {"file_path": f, "content": "x" * 1000})
|
|
42
|
+
self.assertEqual(code, 2, f)
|
|
43
|
+
|
|
44
|
+
def test_non_source_still_allowed(self):
|
|
45
|
+
code, out = run("Write", {"file_path": "notes.md", "content": "hello"})
|
|
46
|
+
self.assertEqual(code, 0, out)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BashGateTest(unittest.TestCase):
|
|
50
|
+
def test_gui_browser_launch_is_blocked_and_redirected_to_verify(self):
|
|
51
|
+
code, out = run("Bash", {"command": "xdg-open /tmp/app.html"})
|
|
52
|
+
self.assertEqual(code, 2, out)
|
|
53
|
+
self.assertIn("uap verify", out) # redirected to the real validation path
|
|
54
|
+
|
|
55
|
+
def test_firefox_launch_blocked(self):
|
|
56
|
+
code, _ = run("Bash", {"command": "nohup firefox /tmp/a.html &"})
|
|
57
|
+
self.assertEqual(code, 2)
|
|
58
|
+
|
|
59
|
+
def test_browser_LOOKUP_is_not_blocked(self):
|
|
60
|
+
# `which firefox` is a capability probe, not a launch — must not false-block.
|
|
61
|
+
code, _ = run("Bash", {"command": "which firefox chromium 2>/dev/null"})
|
|
62
|
+
self.assertEqual(code, 0)
|
|
63
|
+
|
|
64
|
+
def test_bash_source_write_routes_to_deliver(self):
|
|
65
|
+
code, out = run("Bash", {"command": "cat > src/app.js <<EOF\nconsole.log(1)\nEOF"})
|
|
66
|
+
self.assertEqual(code, 2, out)
|
|
67
|
+
self.assertIn("route", out)
|
|
68
|
+
|
|
69
|
+
def test_sed_inplace_on_source_blocked(self):
|
|
70
|
+
code, _ = run("Bash", {"command": "sed -i s/a/b/ src/main.ts"})
|
|
71
|
+
self.assertEqual(code, 2)
|
|
72
|
+
|
|
73
|
+
def test_benign_bash_allowed(self):
|
|
74
|
+
code, out = run("Bash", {"command": "ls -la && npm test"})
|
|
75
|
+
self.assertEqual(code, 0, out)
|
|
76
|
+
|
|
77
|
+
def test_deliver_active_bypasses_bash_gate(self):
|
|
78
|
+
code, _ = run("Bash", {"command": "cat > src/app.js <<EOF\nx\nEOF"},
|
|
79
|
+
env={"UAP_DELIVER_ACTIVE": "1"})
|
|
80
|
+
self.assertEqual(code, 0) # deliver's own shell writes must pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
unittest.main()
|
|
@@ -58,7 +58,9 @@ class TestDeliveryEnforcementWorktree(unittest.TestCase):
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
def test_worktree_nonsource_still_allowed(self):
|
|
61
|
-
|
|
61
|
+
# NOTE: .css is now SOURCE (web deliverables are gated) — use a genuinely
|
|
62
|
+
# non-source file to keep this test's intent (non-source is not routed).
|
|
63
|
+
self.assertTrue(run(".worktrees/001-x/docs/NOTES.md", self.root))
|
|
62
64
|
|
|
63
65
|
def test_direct_source_still_blocked(self):
|
|
64
66
|
self.assertFalse(run("src/game.js", self.root))
|