@miller-tech/uap 1.170.0 → 1.170.1
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/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +14 -5
- package/tools/agents/tests/test_proxy_auth_headers.py +82 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -161,8 +161,9 @@ PROXY_PORT = int(os.environ.get("PROXY_PORT", "4000"))
|
|
|
161
161
|
# PROXY_AUTH_TOKEN so the exposure is credential-gated.
|
|
162
162
|
PROXY_HOST = os.environ.get("PROXY_HOST", "127.0.0.1")
|
|
163
163
|
# Optional shared secret. When set, every request to a model route must present
|
|
164
|
-
# it as `Authorization: Bearer <token
|
|
165
|
-
# (default) = no check, which is safe only because
|
|
164
|
+
# it as `Authorization: Bearer <token>`, `X-Uap-Proxy-Token: <token>`, or
|
|
165
|
+
# `X-Api-Key: <token>`. Unset (default) = no check, which is safe only because
|
|
166
|
+
# the default bind is loopback.
|
|
166
167
|
# The health probe stays open so liveness checks don't need the secret.
|
|
167
168
|
PROXY_AUTH_TOKEN = os.environ.get("PROXY_AUTH_TOKEN", "").strip()
|
|
168
169
|
PROXY_LOG_LEVEL = os.environ.get("PROXY_LOG_LEVEL", "INFO").upper()
|
|
@@ -3584,8 +3585,9 @@ async def _shared_secret_auth(request: Request, call_next):
|
|
|
3584
3585
|
No-op when the token is unset (the default; safe only because the default
|
|
3585
3586
|
bind is loopback). When set — the intended posture for a shared LAN service
|
|
3586
3587
|
(PROXY_HOST=0.0.0.0) — a request must present the token as
|
|
3587
|
-
`Authorization: Bearer <token
|
|
3588
|
-
401. Uses a constant-time compare to avoid a
|
|
3588
|
+
`Authorization: Bearer <token>`, `X-Uap-Proxy-Token: <token>`, or
|
|
3589
|
+
`X-Api-Key: <token>`; otherwise 401. Uses a constant-time compare to avoid a
|
|
3590
|
+
timing oracle.
|
|
3589
3591
|
"""
|
|
3590
3592
|
if PROXY_AUTH_TOKEN and request.url.path not in _PROXY_AUTH_OPEN_PATHS and request.method != "OPTIONS":
|
|
3591
3593
|
provided = request.headers.get("x-uap-proxy-token", "")
|
|
@@ -3593,6 +3595,13 @@ async def _shared_secret_auth(request: Request, call_next):
|
|
|
3593
3595
|
auth = request.headers.get("authorization", "")
|
|
3594
3596
|
if auth.lower().startswith("bearer "):
|
|
3595
3597
|
provided = auth[7:].strip()
|
|
3598
|
+
if not provided:
|
|
3599
|
+
# This proxy emulates the Anthropic Messages API, whose SDK clients
|
|
3600
|
+
# (opencode's @ai-sdk/anthropic, claude-code, factory, …) send their
|
|
3601
|
+
# credential in the x-api-key header rather than Authorization/
|
|
3602
|
+
# X-Uap-Proxy-Token. Accept it as an equivalent token source so any
|
|
3603
|
+
# Anthropic-native client can authenticate without custom headers.
|
|
3604
|
+
provided = request.headers.get("x-api-key", "").strip()
|
|
3596
3605
|
import hmac as _hmac
|
|
3597
3606
|
|
|
3598
3607
|
if not (provided and _hmac.compare_digest(provided, PROXY_AUTH_TOKEN)):
|
|
@@ -3602,7 +3611,7 @@ async def _shared_secret_auth(request: Request, call_next):
|
|
|
3602
3611
|
"type": "error",
|
|
3603
3612
|
"error": {
|
|
3604
3613
|
"type": "authentication_error",
|
|
3605
|
-
"message": "missing or invalid proxy token (set X-Uap-Proxy-Token
|
|
3614
|
+
"message": "missing or invalid proxy token (set X-Uap-Proxy-Token, Authorization: Bearer, or X-Api-Key)",
|
|
3606
3615
|
},
|
|
3607
3616
|
}
|
|
3608
3617
|
),
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Tests for the shared-secret auth middleware header sources.
|
|
2
|
+
|
|
3
|
+
The proxy emulates the Anthropic Messages API. Its clients authenticate in one
|
|
4
|
+
of three ways depending on the SDK: `X-Uap-Proxy-Token`, `Authorization: Bearer`,
|
|
5
|
+
or — for Anthropic-native SDKs like opencode's @ai-sdk/anthropic and claude-code
|
|
6
|
+
— `x-api-key`. All three must be accepted; a wrong or missing token must 401.
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import importlib.util
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_proxy_module():
|
|
15
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
16
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
17
|
+
module = importlib.util.module_from_spec(spec)
|
|
18
|
+
spec.loader.exec_module(module)
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
proxy = _load_proxy_module()
|
|
23
|
+
|
|
24
|
+
_TOKEN = "proxy-secret-token"
|
|
25
|
+
_SENTINEL = object()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _FakeURL:
|
|
29
|
+
def __init__(self, path):
|
|
30
|
+
self.path = path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _FakeReq:
|
|
34
|
+
def __init__(self, headers, path="/v1/messages", method="POST"):
|
|
35
|
+
self.headers = headers
|
|
36
|
+
self.url = _FakeURL(path)
|
|
37
|
+
self.method = method
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _call_next(_request):
|
|
41
|
+
return _SENTINEL
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run_auth(headers):
|
|
45
|
+
"""Drive the async middleware; return _SENTINEL when auth passes, else the 401 Response."""
|
|
46
|
+
req = _FakeReq(headers)
|
|
47
|
+
return asyncio.run(proxy._shared_secret_auth(req, _call_next))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProxyAuthHeaderTest(unittest.TestCase):
|
|
51
|
+
def setUp(self):
|
|
52
|
+
self._prev = proxy.PROXY_AUTH_TOKEN
|
|
53
|
+
proxy.PROXY_AUTH_TOKEN = _TOKEN
|
|
54
|
+
|
|
55
|
+
def tearDown(self):
|
|
56
|
+
proxy.PROXY_AUTH_TOKEN = self._prev
|
|
57
|
+
|
|
58
|
+
def test_x_api_key_accepted(self):
|
|
59
|
+
# NEW behavior: Anthropic-native SDKs send the token as x-api-key.
|
|
60
|
+
self.assertIs(_run_auth({"x-api-key": _TOKEN}), _SENTINEL)
|
|
61
|
+
|
|
62
|
+
def test_x_api_key_wrong_token_rejected(self):
|
|
63
|
+
resp = _run_auth({"x-api-key": "not-the-token"})
|
|
64
|
+
self.assertEqual(getattr(resp, "status_code", None), 401)
|
|
65
|
+
|
|
66
|
+
def test_x_uap_proxy_token_still_accepted(self):
|
|
67
|
+
self.assertIs(_run_auth({"x-uap-proxy-token": _TOKEN}), _SENTINEL)
|
|
68
|
+
|
|
69
|
+
def test_authorization_bearer_still_accepted(self):
|
|
70
|
+
self.assertIs(_run_auth({"authorization": f"Bearer {_TOKEN}"}), _SENTINEL)
|
|
71
|
+
|
|
72
|
+
def test_no_credentials_rejected(self):
|
|
73
|
+
resp = _run_auth({})
|
|
74
|
+
self.assertEqual(getattr(resp, "status_code", None), 401)
|
|
75
|
+
|
|
76
|
+
def test_no_op_when_token_unset(self):
|
|
77
|
+
proxy.PROXY_AUTH_TOKEN = ""
|
|
78
|
+
self.assertIs(_run_auth({}), _SENTINEL)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
unittest.main()
|