@miller-tech/uap 1.78.0 → 1.79.0
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 +10 -1
- package/src/policies/enforcers/enforcement_self_protect.py +119 -0
- package/src/policies/schemas/policies/enforcement-self-protect.md +24 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/tests/test_enforcement_self_protect.py +71 -0
package/package.json
CHANGED
|
Binary file
|
|
@@ -100,7 +100,16 @@ def main() -> None:
|
|
|
100
100
|
|
|
101
101
|
mode = os.environ.get("UAP_ENFORCE_DELIVERY", "block").lower()
|
|
102
102
|
if mode == "block":
|
|
103
|
-
emit
|
|
103
|
+
# R1: emit a machine-actionable routing signal so a capable harness can
|
|
104
|
+
# auto-route the blocked change INTO `uap deliver` instead of leaving the
|
|
105
|
+
# agent to flail. `route`/`deliverHint` are advisory extra fields; harness
|
|
106
|
+
# adapters that understand them route, others just show `reason`.
|
|
107
|
+
emit(
|
|
108
|
+
False,
|
|
109
|
+
msg,
|
|
110
|
+
route="deliver",
|
|
111
|
+
deliverHint=f'uap deliver "implement the intended change to {rel_posix}"',
|
|
112
|
+
)
|
|
104
113
|
|
|
105
114
|
# Advisory (opt-out): never blocks. Surface the nudge, then allow.
|
|
106
115
|
print(f"[delivery-enforcement advisory] {msg}", file=sys.stderr)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""enforcement-self-protect enforcer: the model must not be able to disable
|
|
2
|
+
the delivery gate.
|
|
3
|
+
|
|
4
|
+
A weak local model that hits the delivery-enforcement block tends to try to
|
|
5
|
+
ESCAPE it — observed live (qwen <think>): "set enforcement to advisory mode",
|
|
6
|
+
"just use UAP_DELIVER_BYPASS=1". A single shell `export` does not persist across
|
|
7
|
+
tool calls, so the only way to actually weaken the gate is to PERSIST a bypass:
|
|
8
|
+
write it into a config/env file, or edit the policy/enforcer itself. This
|
|
9
|
+
enforcer blocks exactly those persistence paths.
|
|
10
|
+
|
|
11
|
+
Scope:
|
|
12
|
+
- Edit/Write/MultiEdit to the enforcement control surface (policy md/enforcers,
|
|
13
|
+
.policy-tools/, .uap.json, proxy env, the policy-gate hook scripts).
|
|
14
|
+
- Bash commands that set/export UAP_DELIVER_BYPASS or relax UAP_ENFORCE_DELIVERY,
|
|
15
|
+
write those to a file, or delete/chmod a policy enforcer.
|
|
16
|
+
|
|
17
|
+
There is intentionally NO model-reachable bypass: honoring UAP_DELIVER_BYPASS here
|
|
18
|
+
would reopen the hole (the model would just set it). Operator override is
|
|
19
|
+
out-of-band only: UAP_SELF_PROTECT_OFF=1 in the trusted launch env.
|
|
20
|
+
"""
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
27
|
+
from _common import emit, parse_cli, repo_root # noqa: E402
|
|
28
|
+
|
|
29
|
+
EDIT_OPS = {"Edit", "Write", "MultiEdit", "edit", "write", "multiedit"}
|
|
30
|
+
|
|
31
|
+
# Path substrings that control delivery enforcement. A write touching any of
|
|
32
|
+
# these can weaken the gate, so it is protected regardless of file extension.
|
|
33
|
+
PROTECTED_MARKERS = (
|
|
34
|
+
"/.policy-tools/",
|
|
35
|
+
"/src/policies/",
|
|
36
|
+
"/policies/", # the policy-definition markdown dir
|
|
37
|
+
"/.uap.json",
|
|
38
|
+
"/.uap/",
|
|
39
|
+
"anthropic-proxy.env",
|
|
40
|
+
"uap-policy-gate.sh",
|
|
41
|
+
"uap-reactor-prompt.sh",
|
|
42
|
+
"pre-tool-use",
|
|
43
|
+
)
|
|
44
|
+
# Normalize leading-slash matching for repo-relative paths.
|
|
45
|
+
PROTECTED_REL = tuple(m.lstrip("/") for m in PROTECTED_MARKERS)
|
|
46
|
+
|
|
47
|
+
# Bash patterns that persist a bypass or relax enforcement.
|
|
48
|
+
BYPASS_PATTERNS = (
|
|
49
|
+
re.compile(r"UAP_DELIVER_BYPASS\s*=\s*['\"]?1", re.I),
|
|
50
|
+
re.compile(r"UAP_ENFORCE_DELIVERY\s*=\s*['\"]?(advisory|off|0|false|no)", re.I),
|
|
51
|
+
re.compile(r"UAP_SELF_PROTECT_OFF\s*=\s*['\"]?1", re.I),
|
|
52
|
+
re.compile(r"UAP_NO_WORKTREE\s*=\s*['\"]?1", re.I),
|
|
53
|
+
re.compile(r"UAP_WORKDIR_SCOPE_OFF\s*=\s*['\"]?1", re.I),
|
|
54
|
+
)
|
|
55
|
+
# Destructive ops against the enforcer/policy surface.
|
|
56
|
+
DESTRUCTIVE = re.compile(
|
|
57
|
+
r"\b(rm|unlink|shred|truncate|mv|chmod)\b[^\n|;&]*"
|
|
58
|
+
r"(\.policy-tools|src/policies|/policies/|anthropic-proxy\.env)",
|
|
59
|
+
re.I,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
OVERRIDE = os.environ.get("UAP_SELF_PROTECT_OFF") == "1"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_protected_path(rel_posix: str) -> bool:
|
|
66
|
+
low = rel_posix.lower()
|
|
67
|
+
return any(m in ("/" + low) for m in PROTECTED_REL)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> None:
|
|
71
|
+
if OVERRIDE:
|
|
72
|
+
emit(True, "self-protect disabled by trusted operator override")
|
|
73
|
+
|
|
74
|
+
op, args = parse_cli()
|
|
75
|
+
|
|
76
|
+
if op in EDIT_OPS:
|
|
77
|
+
target = args.get("file_path") or args.get("path") or args.get("target") or ""
|
|
78
|
+
if not target:
|
|
79
|
+
emit(True, "no file path in args")
|
|
80
|
+
try:
|
|
81
|
+
rel = str(Path(target).resolve().relative_to(repo_root()))
|
|
82
|
+
except ValueError:
|
|
83
|
+
# Out-of-repo writes (e.g. the proxy env in ~/.config) — match by name.
|
|
84
|
+
rel = target
|
|
85
|
+
rel_posix = rel.replace(os.sep, "/")
|
|
86
|
+
if _is_protected_path(rel_posix):
|
|
87
|
+
emit(
|
|
88
|
+
False,
|
|
89
|
+
"BLOCKED: this file controls delivery enforcement and cannot be "
|
|
90
|
+
f"edited by the agent ('{rel_posix}'). Do not try to disable or "
|
|
91
|
+
"relax the gate — route your change through the `deliver` tool "
|
|
92
|
+
"instead. (Operator-only override: UAP_SELF_PROTECT_OFF=1.)",
|
|
93
|
+
)
|
|
94
|
+
emit(True, "not an enforcement-control file")
|
|
95
|
+
|
|
96
|
+
if op in {"Bash", "bash"}:
|
|
97
|
+
cmd = args.get("command") or ""
|
|
98
|
+
for pat in BYPASS_PATTERNS:
|
|
99
|
+
if pat.search(cmd):
|
|
100
|
+
emit(
|
|
101
|
+
False,
|
|
102
|
+
"BLOCKED: setting a delivery-enforcement bypass/advisory flag is "
|
|
103
|
+
"not allowed for the agent. Route your change through the "
|
|
104
|
+
"`deliver` tool instead of disabling the gate. "
|
|
105
|
+
"(Operator-only override: UAP_SELF_PROTECT_OFF=1.)",
|
|
106
|
+
)
|
|
107
|
+
if DESTRUCTIVE.search(cmd):
|
|
108
|
+
emit(
|
|
109
|
+
False,
|
|
110
|
+
"BLOCKED: modifying/removing the policy enforcers or proxy env is "
|
|
111
|
+
"not allowed for the agent.",
|
|
112
|
+
)
|
|
113
|
+
emit(True, "no enforcement-tampering in command")
|
|
114
|
+
|
|
115
|
+
emit(True, "not a protected operation")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Enforcement Self-Protect
|
|
2
|
+
|
|
3
|
+
The delivery gate exists to route coding through `uap deliver`. A weak local
|
|
4
|
+
model that hits the block tends to try to ESCAPE it rather than comply
|
|
5
|
+
(observed: "set enforcement to advisory", "use UAP_DELIVER_BYPASS=1"). This
|
|
6
|
+
policy makes the gate non-disableable by the agent.
|
|
7
|
+
|
|
8
|
+
## Rules
|
|
9
|
+
|
|
10
|
+
1. **The control surface is read-only to the agent.** Edits to policy
|
|
11
|
+
definitions, enforcers (`src/policies/`, `.policy-tools/`), `.uap.json`,
|
|
12
|
+
`.uap/`, the proxy env, and the policy-gate hook scripts are blocked.
|
|
13
|
+
2. **Bypass/relax flags cannot be set by the agent.** Bash commands that set or
|
|
14
|
+
persist `UAP_DELIVER_BYPASS`, relax `UAP_ENFORCE_DELIVERY`, or disable the
|
|
15
|
+
workdir/worktree gates are blocked. (A shell `export` alone does not persist
|
|
16
|
+
across tool calls; this blocks the persistence paths.)
|
|
17
|
+
3. **No model-reachable bypass.** Honoring a bypass here would reopen the hole.
|
|
18
|
+
Operator override is out-of-band only: `UAP_SELF_PROTECT_OFF=1` in the
|
|
19
|
+
trusted launch environment.
|
|
20
|
+
|
|
21
|
+
## Rationale
|
|
22
|
+
|
|
23
|
+
Compliance must be the easy path and non-compliance the blocked one. Route
|
|
24
|
+
substantive coding through the `deliver` tool; do not weaken the gate.
|
|
Binary file
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tests for the enforcement-self-protect enforcer (R2)."""
|
|
2
|
+
import importlib.util
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ENF = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "enforcement_self_protect.py"
|
|
10
|
+
REPO = Path(__file__).resolve().parents[3]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(op, args, env=None):
|
|
14
|
+
e = dict(os.environ)
|
|
15
|
+
e["UAP_REPO_ROOT"] = str(REPO)
|
|
16
|
+
e.pop("UAP_SELF_PROTECT_OFF", None)
|
|
17
|
+
if env:
|
|
18
|
+
e.update(env)
|
|
19
|
+
p = subprocess.run(
|
|
20
|
+
[sys.executable, str(ENF), "--operation", op, "--args", json.dumps(args)],
|
|
21
|
+
capture_output=True, text=True, env=e,
|
|
22
|
+
)
|
|
23
|
+
out = json.loads(p.stdout) if p.stdout.strip() else {}
|
|
24
|
+
return p.returncode, out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
import unittest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SelfProtectTest(unittest.TestCase):
|
|
31
|
+
def test_blocks_edit_to_policy_enforcer(self):
|
|
32
|
+
rc, out = run("Write", {"file_path": str(REPO / "src/policies/enforcers/delivery_enforcement.py")})
|
|
33
|
+
self.assertEqual(rc, 2)
|
|
34
|
+
self.assertFalse(out["allowed"])
|
|
35
|
+
|
|
36
|
+
def test_blocks_edit_to_uap_json(self):
|
|
37
|
+
rc, out = run("Edit", {"file_path": str(REPO / ".uap.json")})
|
|
38
|
+
self.assertEqual(rc, 2)
|
|
39
|
+
|
|
40
|
+
def test_blocks_edit_to_policy_tools(self):
|
|
41
|
+
rc, out = run("Write", {"file_path": str(REPO / ".policy-tools/abc_delivery_enforcement.py")})
|
|
42
|
+
self.assertEqual(rc, 2)
|
|
43
|
+
|
|
44
|
+
def test_blocks_bash_setting_bypass(self):
|
|
45
|
+
rc, out = run("Bash", {"command": "export UAP_DELIVER_BYPASS=1 && echo hi"})
|
|
46
|
+
self.assertEqual(rc, 2)
|
|
47
|
+
|
|
48
|
+
def test_blocks_bash_advisory_relax(self):
|
|
49
|
+
rc, out = run("Bash", {"command": "UAP_ENFORCE_DELIVERY=advisory uap deliver x"})
|
|
50
|
+
self.assertEqual(rc, 2)
|
|
51
|
+
|
|
52
|
+
def test_blocks_bash_rm_enforcer(self):
|
|
53
|
+
rc, out = run("Bash", {"command": "rm -f .policy-tools/abc_delivery_enforcement.py"})
|
|
54
|
+
self.assertEqual(rc, 2)
|
|
55
|
+
|
|
56
|
+
def test_allows_normal_source_edit(self):
|
|
57
|
+
rc, out = run("Write", {"file_path": str(REPO / "src/app/index.ts")})
|
|
58
|
+
self.assertEqual(rc, 0)
|
|
59
|
+
self.assertTrue(out["allowed"])
|
|
60
|
+
|
|
61
|
+
def test_allows_normal_bash(self):
|
|
62
|
+
rc, out = run("Bash", {"command": "npm test"})
|
|
63
|
+
self.assertEqual(rc, 0)
|
|
64
|
+
|
|
65
|
+
def test_operator_override_allows(self):
|
|
66
|
+
rc, out = run("Bash", {"command": "export UAP_DELIVER_BYPASS=1"}, env={"UAP_SELF_PROTECT_OFF": "1"})
|
|
67
|
+
self.assertEqual(rc, 0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
unittest.main()
|