@miller-tech/uap 1.128.1 → 1.128.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.
- package/package.json +2 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/delivery_enforcement.py +10 -1
- package/src/policies/enforcers/schema_diff_gate.py +25 -8
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/tests/test_schema_diff_gate.py +115 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.128.
|
|
3
|
+
"version": "1.128.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": "python3 -m unittest tools.agents.tests.test_enforcement_self_protect 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",
|
|
24
|
+
"test:enforcers": "python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate 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",
|
|
25
25
|
"test:coverage": "vitest --coverage",
|
|
26
26
|
"bench": "vitest --config vitest.bench.config.ts",
|
|
27
27
|
"lint": "eslint src --ext .ts",
|
|
Binary file
|
|
@@ -86,32 +86,40 @@ def main() -> None:
|
|
|
86
86
|
op, args = parse_cli()
|
|
87
87
|
if op not in EDIT_OPS:
|
|
88
88
|
emit(True, "not a file-edit operation")
|
|
89
|
+
return
|
|
89
90
|
|
|
90
91
|
target = args.get("file_path") or args.get("path") or args.get("target") or ""
|
|
91
92
|
if not target:
|
|
92
93
|
emit(True, "no file path in args")
|
|
94
|
+
return
|
|
93
95
|
|
|
94
96
|
root = repo_root()
|
|
95
97
|
try:
|
|
96
98
|
rel = str(Path(target).resolve().relative_to(root))
|
|
97
99
|
except ValueError:
|
|
98
100
|
emit(True, "target outside repo")
|
|
101
|
+
return
|
|
99
102
|
|
|
100
103
|
rel_posix = rel.replace(os.sep, "/")
|
|
101
104
|
low = rel_posix.lower()
|
|
102
105
|
|
|
103
106
|
if not low.endswith(SOURCE_EXTS):
|
|
104
107
|
emit(True, "not source code")
|
|
108
|
+
return
|
|
105
109
|
if any(rel_posix.startswith(p) for p in EXEMPT_PREFIXES):
|
|
106
110
|
emit(True, f"exempt path: {rel_posix}")
|
|
111
|
+
return
|
|
107
112
|
if any(m in "/" + low for m in TEST_MARKERS):
|
|
108
113
|
emit(True, "test file (protected by deliver itself)")
|
|
114
|
+
return
|
|
109
115
|
|
|
110
116
|
# Escape hatches.
|
|
111
117
|
if os.environ.get("UAP_DELIVER_ACTIVE") == "1":
|
|
112
118
|
emit(True, "inside a deliver-driven run")
|
|
119
|
+
return
|
|
113
120
|
if os.environ.get("UAP_DELIVER_BYPASS") == "1":
|
|
114
121
|
emit(True, "UAP_DELIVER_BYPASS override set")
|
|
122
|
+
return
|
|
115
123
|
|
|
116
124
|
# #3-F: terse, imperative, model-parseable. Weak local models otherwise
|
|
117
125
|
# retry the blocked edit or hallucinate completion ("the files exist") when
|
|
@@ -143,6 +151,7 @@ def main() -> None:
|
|
|
143
151
|
route="deliver",
|
|
144
152
|
deliverHint=f'implement the intended change to {rel_posix}',
|
|
145
153
|
)
|
|
154
|
+
return
|
|
146
155
|
|
|
147
156
|
# Advisory (opt-out): never blocks. Surface the nudge, then allow.
|
|
148
157
|
print(f"[delivery-enforcement advisory] {msg}", file=sys.stderr)
|
|
@@ -150,4 +159,4 @@ def main() -> None:
|
|
|
150
159
|
|
|
151
160
|
|
|
152
161
|
if __name__ == "__main__":
|
|
153
|
-
main()
|
|
162
|
+
main()
|
|
@@ -16,7 +16,10 @@ WATCHED_RE = re.compile(
|
|
|
16
16
|
r"infra/helm_charts/[^/]*envoy|infra/helm_charts/[^/]*sentinel)",
|
|
17
17
|
re.I,
|
|
18
18
|
)
|
|
19
|
-
|
|
19
|
+
# NOTE: bare "Bash" used to be in this set, which made EVERY shell command a
|
|
20
|
+
# gate point — including the `uap schema-diff` remedy itself (self-deadlock).
|
|
21
|
+
# Gate only actual commit/push commands (main() also inspects cmd content).
|
|
22
|
+
COMMIT_OPS = {"git-commit", "git commit"}
|
|
20
23
|
RECENT_SEC = 3600
|
|
21
24
|
|
|
22
25
|
|
|
@@ -35,17 +38,31 @@ def schema_diff_ok(root: Path) -> bool:
|
|
|
35
38
|
return False
|
|
36
39
|
try:
|
|
37
40
|
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=1.0)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
|
|
41
|
+
# `uap memory store` writes to `memories` (type 'action'), while older
|
|
42
|
+
# UAP wrote session rows to `session_memories` — accept the marker from
|
|
43
|
+
# either table so the documented remedy actually clears the gate.
|
|
44
|
+
row = None
|
|
45
|
+
for table in ("memories", "session_memories"):
|
|
46
|
+
try:
|
|
47
|
+
cur = con.execute(
|
|
48
|
+
f"SELECT timestamp FROM {table} "
|
|
49
|
+
"WHERE content LIKE '%schema-diff%pass%' "
|
|
50
|
+
"ORDER BY id DESC LIMIT 1"
|
|
51
|
+
)
|
|
52
|
+
r = cur.fetchone()
|
|
53
|
+
if r and (row is None or r[0] > row[0]):
|
|
54
|
+
row = r
|
|
55
|
+
except sqlite3.Error:
|
|
56
|
+
continue
|
|
44
57
|
con.close()
|
|
45
58
|
if not row:
|
|
46
59
|
return False
|
|
47
60
|
try:
|
|
48
|
-
|
|
61
|
+
# Timestamps are stored as UTC ISO-8601 (trailing 'Z'); parse them
|
|
62
|
+
# as UTC — time.mktime would misread them as local time and expire
|
|
63
|
+
# the marker hours early (or late) depending on the host TZ.
|
|
64
|
+
import calendar
|
|
65
|
+
ts = calendar.timegm(time.strptime(row[0][:19], "%Y-%m-%dT%H:%M:%S"))
|
|
49
66
|
except Exception: # noqa: BLE001
|
|
50
67
|
return False
|
|
51
68
|
return (time.time() - ts) < RECENT_SEC
|
|
Binary file
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Tests for the schema-diff-gate enforcer fixes (self-deadlock + marker read).
|
|
2
|
+
|
|
3
|
+
Covers the 2026-07-08 mid-session incident: bare "Bash" in COMMIT_OPS gated
|
|
4
|
+
EVERY shell command (including the `uap schema-diff` remedy — self-deadlock),
|
|
5
|
+
and the pass-marker check read only the legacy session_memories table and
|
|
6
|
+
parsed UTC timestamps with time.mktime (host-timezone skew).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sqlite3
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
import unittest
|
|
17
|
+
from datetime import datetime, timedelta, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
REPO = Path(__file__).resolve().parents[3]
|
|
21
|
+
ENFORCER = REPO / "src" / "policies" / "enforcers" / "schema_diff_gate.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_gate(op: str, args: dict, root: Path, tz: str | None = None):
|
|
25
|
+
env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
|
|
26
|
+
env["UAP_REPO_ROOT"] = str(root)
|
|
27
|
+
env["UAP_WORKTREE_ROOT"] = str(root)
|
|
28
|
+
if tz:
|
|
29
|
+
env["TZ"] = tz
|
|
30
|
+
r = subprocess.run(
|
|
31
|
+
["python3", str(ENFORCER), "--operation", op, "--args", json.dumps(args)],
|
|
32
|
+
cwd=root,
|
|
33
|
+
env=env,
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
timeout=30,
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
payload = json.loads(r.stdout or "{}")
|
|
40
|
+
except json.JSONDecodeError:
|
|
41
|
+
payload = {}
|
|
42
|
+
return r.returncode, payload.get("allowed", False), payload.get("reason", "")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SchemaDiffGateTest(unittest.TestCase):
|
|
46
|
+
def setUp(self):
|
|
47
|
+
self._tmp = tempfile.TemporaryDirectory(prefix="schema-gate-")
|
|
48
|
+
self.root = Path(self._tmp.name)
|
|
49
|
+
subprocess.run(["git", "init", "-q"], cwd=self.root, check=True)
|
|
50
|
+
subprocess.run(
|
|
51
|
+
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "--allow-empty", "-m", "init"],
|
|
52
|
+
cwd=self.root,
|
|
53
|
+
check=True,
|
|
54
|
+
)
|
|
55
|
+
(self.root / "migrations").mkdir()
|
|
56
|
+
(self.root / "migrations" / "001_add_table.sql").write_text("CREATE TABLE t (id int);")
|
|
57
|
+
subprocess.run(["git", "add", "migrations"], cwd=self.root, check=True)
|
|
58
|
+
|
|
59
|
+
def tearDown(self):
|
|
60
|
+
self._tmp.cleanup()
|
|
61
|
+
|
|
62
|
+
def write_marker(self, iso_timestamp: str, table: str = "memories"):
|
|
63
|
+
mem = self.root / "agents" / "data" / "memory"
|
|
64
|
+
mem.mkdir(parents=True)
|
|
65
|
+
con = sqlite3.connect(mem / "short_term.db")
|
|
66
|
+
con.execute(f"CREATE TABLE {table} (id INTEGER PRIMARY KEY, content TEXT, timestamp TEXT)")
|
|
67
|
+
con.execute(
|
|
68
|
+
f"INSERT INTO {table} (content, timestamp) VALUES (?, ?)",
|
|
69
|
+
("schema-diff pass: verified", iso_timestamp),
|
|
70
|
+
)
|
|
71
|
+
con.commit()
|
|
72
|
+
con.close()
|
|
73
|
+
|
|
74
|
+
def test_ordinary_bash_is_not_a_gate_point(self):
|
|
75
|
+
"""Regression: bare Bash op must not gate — that deadlocked the remedy itself."""
|
|
76
|
+
code, allowed, reason = run_gate("Bash", {"command": "uap schema-diff"}, self.root)
|
|
77
|
+
self.assertEqual(code, 0)
|
|
78
|
+
self.assertTrue(allowed)
|
|
79
|
+
self.assertIn("not a commit/push gate point", reason)
|
|
80
|
+
|
|
81
|
+
def test_commit_command_still_gated(self):
|
|
82
|
+
code, allowed, reason = run_gate("Bash", {"command": 'git commit -m "add migration"'}, self.root)
|
|
83
|
+
self.assertEqual(code, 2)
|
|
84
|
+
self.assertFalse(allowed)
|
|
85
|
+
self.assertIn("uap schema-diff", reason)
|
|
86
|
+
|
|
87
|
+
def test_fresh_marker_in_memories_table_clears_gate(self):
|
|
88
|
+
"""`uap memory store` writes to `memories`; the gate must accept it."""
|
|
89
|
+
self.write_marker(datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
|
|
90
|
+
code, allowed, reason = run_gate("git-commit", {}, self.root)
|
|
91
|
+
self.assertEqual(code, 0)
|
|
92
|
+
self.assertTrue(allowed)
|
|
93
|
+
self.assertIn("recent schema-diff pass", reason)
|
|
94
|
+
|
|
95
|
+
def test_legacy_session_memories_table_still_accepted(self):
|
|
96
|
+
self.write_marker(
|
|
97
|
+
datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), table="session_memories"
|
|
98
|
+
)
|
|
99
|
+
code, allowed, _ = run_gate("git-commit", {}, self.root)
|
|
100
|
+
self.assertEqual(code, 0)
|
|
101
|
+
self.assertTrue(allowed)
|
|
102
|
+
|
|
103
|
+
def test_stale_marker_blocks_regardless_of_host_timezone(self):
|
|
104
|
+
"""Pre-fix, time.mktime read UTC timestamps as local: east-of-UTC hosts
|
|
105
|
+
saw stale markers shifted into the future and the gate cleared on a
|
|
106
|
+
2h-old pass."""
|
|
107
|
+
stale = (datetime.now(timezone.utc) - timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
108
|
+
self.write_marker(stale)
|
|
109
|
+
code, allowed, _ = run_gate("git-commit", {}, self.root, tz="Australia/Sydney")
|
|
110
|
+
self.assertEqual(code, 2)
|
|
111
|
+
self.assertFalse(allowed)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
unittest.main()
|