@miller-tech/uap 1.127.0 → 1.128.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/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +1 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +4 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +3 -0
- package/dist/cli/deliver.js.map +1 -1
- package/dist/delivery/agentic-executor.d.ts +11 -0
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +16 -3
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/docs/INDEX.md +7 -1
- package/docs/architecture/OVERVIEW.md +6 -1
- package/docs/guides/SANDBOX.md +99 -0
- package/package.json +3 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/uap-policy-gate.sh +62 -3
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +50 -1
|
Binary file
|
|
@@ -42,18 +42,73 @@ ARGS="$(printf '%s' "$PAYLOAD" | python3 -c 'import json,sys; d=json.load(sys.st
|
|
|
42
42
|
|
|
43
43
|
[[ -z "$TOOL" ]] && exit 0
|
|
44
44
|
|
|
45
|
+
# FAIL-CLOSED for the enforcement control surface. The gate is fail-SOFT by
|
|
46
|
+
# design (a broken/absent enforcer must not wedge ALL work) — but that same
|
|
47
|
+
# fail-open makes anything that BREAKS the enforcer a silent bypass of the
|
|
48
|
+
# self-protect control. So: if THIS operation touches the enforcement surface
|
|
49
|
+
# (policy DB/enforcers, .uap.json, proxy env, hook scripts) or sets a
|
|
50
|
+
# bypass/relax flag, the gate fails CLOSED (exit 2) whenever the self-protect
|
|
51
|
+
# enforcer cannot actually run and make the call. Normal ops keep failing open.
|
|
52
|
+
SEC_SENSITIVE="$(printf '%s' "$ARGS" | TOOL="$TOOL" python3 -c '
|
|
53
|
+
import json, os, re, sys
|
|
54
|
+
try: a = json.loads(sys.stdin.read() or "{}")
|
|
55
|
+
except Exception: a = {}
|
|
56
|
+
markers = ("/.policy-tools/", "/src/policies/", "/policies/", "/.uap.json",
|
|
57
|
+
".uap.json", "/.uap/", "anthropic-proxy.env", "uap-policy-gate.sh",
|
|
58
|
+
"uap-reactor-prompt.sh", "pre-tool-use")
|
|
59
|
+
target = a.get("file_path") or a.get("path") or a.get("target") or ""
|
|
60
|
+
cmd = a.get("command") or ""
|
|
61
|
+
low = ("/" + str(target)).lower()
|
|
62
|
+
hit = any(m in low for m in markers)
|
|
63
|
+
bypass = re.search(
|
|
64
|
+
r"UAP_DELIVER_BYPASS\s*=\s*[\x27\"]?1|UAP_ENFORCE_DELIVERY\s*=\s*[\x27\"]?(advisory|off|0|false|no)"
|
|
65
|
+
r"|UAP_SELF_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_NO_WORKTREE\s*=\s*[\x27\"]?1|UAP_WORKDIR_SCOPE_OFF\s*=\s*[\x27\"]?1",
|
|
66
|
+
cmd, re.I)
|
|
67
|
+
print("1" if (hit or bypass) else "0")
|
|
68
|
+
' 2>/dev/null || echo 1)"
|
|
69
|
+
|
|
70
|
+
# Operator out-of-band override disables the fail-closed guard too.
|
|
71
|
+
[[ "${UAP_SELF_PROTECT_OFF:-}" == "1" ]] && SEC_SENSITIVE=0
|
|
72
|
+
|
|
73
|
+
fail_closed() {
|
|
74
|
+
echo "[UAP policy gate] FAIL-CLOSED: this operation touches the enforcement control surface but the self-protect enforcer could not run (${1:-machinery unavailable}). Blocked so a broken/absent gate can't become a bypass. (Operator override: UAP_SELF_PROTECT_OFF=1.)" >&2
|
|
75
|
+
exit 2
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
DB="$MAIN_ROOT/agents/data/memory/policies.db"
|
|
46
|
-
[[ ! -f "$DB" ]]
|
|
79
|
+
if [[ ! -f "$DB" ]]; then
|
|
80
|
+
[[ "$SEC_SENSITIVE" == "1" ]] && fail_closed "policies.db not found"
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
if ! command -v sqlite3 >/dev/null 2>&1; then
|
|
84
|
+
[[ "$SEC_SENSITIVE" == "1" ]] && fail_closed "sqlite3 not on PATH"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Did the self-protect enforcer actually run and make a decision this call?
|
|
89
|
+
sec_enforcer_ran=0
|
|
47
90
|
|
|
48
91
|
# Iterate active policies with attached executable tools
|
|
49
92
|
while IFS='|' read -r pid pname tool; do
|
|
50
93
|
[[ -z "$pid" ]] && continue
|
|
51
94
|
enforcer="$MAIN_ROOT/.policy-tools/${pid}_${tool}.py"
|
|
52
|
-
[[ ! -f "$enforcer" ]]
|
|
95
|
+
if [[ ! -f "$enforcer" ]]; then
|
|
96
|
+
# A missing self-protect enforcer on a sensitive op = fail closed.
|
|
97
|
+
[[ "$SEC_SENSITIVE" == "1" && "$tool" == "enforcement_self_protect" ]] && fail_closed "enforcer file missing"
|
|
98
|
+
continue
|
|
99
|
+
fi
|
|
53
100
|
out="$(python3 "$enforcer" --operation "$TOOL" --args "$ARGS" 2>/dev/null || true)"
|
|
54
101
|
allowed="$(printf '%s' "$out" | python3 -c 'import json,sys;
|
|
55
102
|
try: d=json.loads(sys.stdin.read()); print("1" if d.get("allowed",True) else "0")
|
|
56
|
-
except: print("
|
|
103
|
+
except: print("2")' 2>/dev/null || echo 2)"
|
|
104
|
+
# allowed=2 => enforcer errored / emitted unparseable output. For a sensitive
|
|
105
|
+
# op via the self-protect enforcer, that error must NOT default to allow.
|
|
106
|
+
if [[ "$tool" == "enforcement_self_protect" ]]; then
|
|
107
|
+
[[ "$SEC_SENSITIVE" == "1" && "$allowed" == "2" ]] && fail_closed "enforcer errored"
|
|
108
|
+
sec_enforcer_ran=1
|
|
109
|
+
fi
|
|
110
|
+
# For all other enforcers, an error still fails open (unchanged behavior).
|
|
111
|
+
[[ "$allowed" == "2" ]] && allowed=1
|
|
57
112
|
if [[ "$allowed" == "0" ]]; then
|
|
58
113
|
# R1: consume the enforcer's route:deliver signal (log intent, opt-in
|
|
59
114
|
# background auto-route to `uap deliver`). Falls back to the plain reason if
|
|
@@ -73,4 +128,8 @@ except: print("")' 2>/dev/null || echo "")"
|
|
|
73
128
|
fi
|
|
74
129
|
done < <(sqlite3 "$DB" "SELECT p.id, p.name, t.toolName FROM policies p JOIN executable_tools t ON t.policyId=p.id WHERE p.isActive=1;")
|
|
75
130
|
|
|
131
|
+
# A sensitive op that no self-protect enforcer ever evaluated = the control
|
|
132
|
+
# surface is unguarded (self-protect not registered/active). Fail closed.
|
|
133
|
+
[[ "$SEC_SENSITIVE" == "1" && "$sec_enforcer_ran" == "0" ]] && fail_closed "self-protect not registered/active"
|
|
134
|
+
|
|
76
135
|
exit 0
|
|
Binary file
|
|
@@ -155,7 +155,16 @@ ANTHROPIC_API_BASE = os.environ.get(
|
|
|
155
155
|
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
156
156
|
ANTHROPIC_PASSTHROUGH_MODELS = os.environ.get("ANTHROPIC_PASSTHROUGH_MODELS", "")
|
|
157
157
|
PROXY_PORT = int(os.environ.get("PROXY_PORT", "4000"))
|
|
158
|
-
|
|
158
|
+
# Bind loopback by DEFAULT (security audit: an unauthenticated 0.0.0.0 listener
|
|
159
|
+
# let any LAN host drive the local model and reach cloud passthrough). To run
|
|
160
|
+
# the proxy as a shared LAN service, set PROXY_HOST=0.0.0.0 AND set
|
|
161
|
+
# PROXY_AUTH_TOKEN so the exposure is credential-gated.
|
|
162
|
+
PROXY_HOST = os.environ.get("PROXY_HOST", "127.0.0.1")
|
|
163
|
+
# Optional shared secret. When set, every request to a model route must present
|
|
164
|
+
# it as `Authorization: Bearer <token>` or `X-Uap-Proxy-Token: <token>`. Unset
|
|
165
|
+
# (default) = no check, which is safe only because the default bind is loopback.
|
|
166
|
+
# The health probe stays open so liveness checks don't need the secret.
|
|
167
|
+
PROXY_AUTH_TOKEN = os.environ.get("PROXY_AUTH_TOKEN", "").strip()
|
|
159
168
|
PROXY_LOG_LEVEL = os.environ.get("PROXY_LOG_LEVEL", "INFO").upper()
|
|
160
169
|
PROXY_READ_TIMEOUT = float(os.environ.get("PROXY_READ_TIMEOUT", "180"))
|
|
161
170
|
PROXY_GENERATION_TIMEOUT = float(os.environ.get("PROXY_GENERATION_TIMEOUT", "300"))
|
|
@@ -3051,6 +3060,46 @@ app = FastAPI(
|
|
|
3051
3060
|
lifespan=lifespan,
|
|
3052
3061
|
)
|
|
3053
3062
|
|
|
3063
|
+
|
|
3064
|
+
# Open paths that never require the shared secret (liveness / discovery), so a
|
|
3065
|
+
# LAN health check or an SDK model-list probe works without the token.
|
|
3066
|
+
_PROXY_AUTH_OPEN_PATHS = frozenset({"/health", "/", "/v1/models"})
|
|
3067
|
+
|
|
3068
|
+
|
|
3069
|
+
@app.middleware("http")
|
|
3070
|
+
async def _shared_secret_auth(request: Request, call_next):
|
|
3071
|
+
"""Gate every request behind PROXY_AUTH_TOKEN when it is set.
|
|
3072
|
+
|
|
3073
|
+
No-op when the token is unset (the default; safe only because the default
|
|
3074
|
+
bind is loopback). When set — the intended posture for a shared LAN service
|
|
3075
|
+
(PROXY_HOST=0.0.0.0) — a request must present the token as
|
|
3076
|
+
`Authorization: Bearer <token>` or `X-Uap-Proxy-Token: <token>`; otherwise
|
|
3077
|
+
401. Uses a constant-time compare to avoid a timing oracle.
|
|
3078
|
+
"""
|
|
3079
|
+
if PROXY_AUTH_TOKEN and request.url.path not in _PROXY_AUTH_OPEN_PATHS and request.method != "OPTIONS":
|
|
3080
|
+
provided = request.headers.get("x-uap-proxy-token", "")
|
|
3081
|
+
if not provided:
|
|
3082
|
+
auth = request.headers.get("authorization", "")
|
|
3083
|
+
if auth.lower().startswith("bearer "):
|
|
3084
|
+
provided = auth[7:].strip()
|
|
3085
|
+
import hmac as _hmac
|
|
3086
|
+
|
|
3087
|
+
if not (provided and _hmac.compare_digest(provided, PROXY_AUTH_TOKEN)):
|
|
3088
|
+
return Response(
|
|
3089
|
+
content=json.dumps(
|
|
3090
|
+
{
|
|
3091
|
+
"type": "error",
|
|
3092
|
+
"error": {
|
|
3093
|
+
"type": "authentication_error",
|
|
3094
|
+
"message": "missing or invalid proxy token (set X-Uap-Proxy-Token or Authorization: Bearer)",
|
|
3095
|
+
},
|
|
3096
|
+
}
|
|
3097
|
+
),
|
|
3098
|
+
status_code=401,
|
|
3099
|
+
media_type="application/json",
|
|
3100
|
+
)
|
|
3101
|
+
return await call_next(request)
|
|
3102
|
+
|
|
3054
3103
|
# NOTE: Concurrency control is enforced by _acquire_upstream_slot() inside
|
|
3055
3104
|
# _post_with_retry (the single point where we hit llama.cpp). An earlier
|
|
3056
3105
|
# implementation also added an HTTP middleware that acquired the same
|