@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.
@@ -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" ]] && exit 0
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" ]] && continue
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("1")' 2>/dev/null || echo 1)"
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
@@ -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
- PROXY_HOST = os.environ.get("PROXY_HOST", "0.0.0.0")
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