proxyagent 0.8.0__tar.gz → 0.9.0__tar.gz
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.
- {proxyagent-0.8.0 → proxyagent-0.9.0}/PKG-INFO +5 -4
- {proxyagent-0.8.0 → proxyagent-0.9.0}/README.md +4 -3
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/__init__.py +1 -1
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/config.py +1 -1
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/providers.py +5 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/signers.py +66 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/ui/index.html +2 -2
- {proxyagent-0.8.0 → proxyagent-0.9.0}/pyproject.toml +1 -1
- {proxyagent-0.8.0 → proxyagent-0.9.0}/tests/test_proxy.py +25 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/.gitignore +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/aliases.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/cli.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/crypto.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/db.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/harness.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/pricing.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/security.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/server.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/store.py +0 -0
- {proxyagent-0.8.0 → proxyagent-0.9.0}/proxyagent/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxyagent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Run any agent (Claude, Codex, custom) on any machine — with no API key on the machine. A secure, self-hosted proxy for models and tools.
|
|
5
5
|
Project-URL: Homepage, https://github.com/teddyoweh/proxyagent
|
|
6
6
|
Author-email: Spawn Labs <teddy@spawnlabs.ai>
|
|
@@ -179,9 +179,9 @@ is to centralise *all* of them so the machine running the harness holds only a `
|
|
|
179
179
|
| **Gemini CLI** | Google | API key · OAuth · Vertex |
|
|
180
180
|
|
|
181
181
|
Connect each mode in the dashboard's **Harnesses** tab (or `proxyagent provider add … --kind`).
|
|
182
|
-
**API key, OAuth, AWS Bedrock
|
|
183
|
-
|
|
184
|
-
AWS
|
|
182
|
+
**Every auth mode is wired**: API key, OAuth, AWS Bedrock (the proxy SigV4-signs the Claude-on-Bedrock
|
|
183
|
+
request itself), Azure, and Google Vertex (service-account JSON → access token → Claude-on-Vertex).
|
|
184
|
+
For Bedrock/Vertex the proxy holds the AWS/GCP credentials and signs upstream, so the machine needs no cloud creds at all. The model providers below are the *backends* for model-agnostic
|
|
185
185
|
harnesses (aider, Cline…).
|
|
186
186
|
|
|
187
187
|
```bash
|
|
@@ -189,6 +189,7 @@ harnesses (aider, Cline…).
|
|
|
189
189
|
proxyagent provider add anthropic --kind bedrock --key <AWS_SECRET> # + meta: access_key, region
|
|
190
190
|
proxyagent provider add openai --kind azure --key <AZURE_KEY> # + meta: endpoint
|
|
191
191
|
proxyagent provider add anthropic --kind oauth --key <OAUTH_TOKEN>
|
|
192
|
+
proxyagent provider add anthropic --kind vertex --key "$(cat sa.json)" # + meta: region
|
|
192
193
|
```
|
|
193
194
|
|
|
194
195
|
## Credential pools & failover
|
|
@@ -147,9 +147,9 @@ is to centralise *all* of them so the machine running the harness holds only a `
|
|
|
147
147
|
| **Gemini CLI** | Google | API key · OAuth · Vertex |
|
|
148
148
|
|
|
149
149
|
Connect each mode in the dashboard's **Harnesses** tab (or `proxyagent provider add … --kind`).
|
|
150
|
-
**API key, OAuth, AWS Bedrock
|
|
151
|
-
|
|
152
|
-
AWS
|
|
150
|
+
**Every auth mode is wired**: API key, OAuth, AWS Bedrock (the proxy SigV4-signs the Claude-on-Bedrock
|
|
151
|
+
request itself), Azure, and Google Vertex (service-account JSON → access token → Claude-on-Vertex).
|
|
152
|
+
For Bedrock/Vertex the proxy holds the AWS/GCP credentials and signs upstream, so the machine needs no cloud creds at all. The model providers below are the *backends* for model-agnostic
|
|
153
153
|
harnesses (aider, Cline…).
|
|
154
154
|
|
|
155
155
|
```bash
|
|
@@ -157,6 +157,7 @@ harnesses (aider, Cline…).
|
|
|
157
157
|
proxyagent provider add anthropic --kind bedrock --key <AWS_SECRET> # + meta: access_key, region
|
|
158
158
|
proxyagent provider add openai --kind azure --key <AZURE_KEY> # + meta: endpoint
|
|
159
159
|
proxyagent provider add anthropic --kind oauth --key <OAUTH_TOKEN>
|
|
160
|
+
proxyagent provider add anthropic --kind vertex --key "$(cat sa.json)" # + meta: region
|
|
160
161
|
```
|
|
161
162
|
|
|
162
163
|
## Credential pools & failover
|
|
@@ -101,7 +101,7 @@ AUTH_LABELS = {"api_key": "API key", "oauth": "OAuth", "bedrock": "AWS Bedrock",
|
|
|
101
101
|
"vertex": "Google Vertex", "azure": "Azure"}
|
|
102
102
|
# Auth modes that are fully wired today (just a key swap). Others are surfaced in the
|
|
103
103
|
# UI as "available" and built out (Bedrock SigV4 / Vertex token / OAuth refresh).
|
|
104
|
-
AUTH_READY = {"api_key", "oauth", "bedrock", "azure"}
|
|
104
|
+
AUTH_READY = {"api_key", "oauth", "bedrock", "azure", "vertex"}
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
@dataclass
|
|
@@ -70,6 +70,11 @@ def build_plans(provider, store: Store | None, body: dict) -> list[tuple]:
|
|
|
70
70
|
plans.append(signers.bedrock_plan(c, body))
|
|
71
71
|
except Exception: # noqa: BLE001 — skip a malformed bedrock cred
|
|
72
72
|
pass
|
|
73
|
+
elif kind == "vertex":
|
|
74
|
+
try:
|
|
75
|
+
plans.append(signers.vertex_plan(c, body))
|
|
76
|
+
except Exception: # noqa: BLE001 — skip if SA invalid / token fetch fails
|
|
77
|
+
pass
|
|
73
78
|
if provider.key:
|
|
74
79
|
plans.append((provider.endpoint, {**JSON, **provider.auth_headers()}, raw))
|
|
75
80
|
return plans
|
|
@@ -9,10 +9,12 @@ Vertex (GCP service-account → access token) lands next.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import base64
|
|
12
13
|
import datetime
|
|
13
14
|
import hashlib
|
|
14
15
|
import hmac
|
|
15
16
|
import json
|
|
17
|
+
import time
|
|
16
18
|
from urllib.parse import quote
|
|
17
19
|
|
|
18
20
|
|
|
@@ -86,3 +88,67 @@ def bedrock_plan(cred: dict, body: dict):
|
|
|
86
88
|
access_key=meta.get("access_key", ""), secret_key=cred.get("secret", ""),
|
|
87
89
|
body=raw, session_token=meta.get("session_token"))
|
|
88
90
|
return f"https://{host}{path}", headers, raw
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------ #
|
|
94
|
+
# Google Vertex AI — service account → OAuth2 access token → Claude-on-Vertex.
|
|
95
|
+
# The proxy holds the service-account key; the machine holds none.
|
|
96
|
+
# ------------------------------------------------------------------ #
|
|
97
|
+
|
|
98
|
+
def _b64url(b: bytes) -> str:
|
|
99
|
+
return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_VERTEX_TOKEN_CACHE: dict[str, tuple[str, int]] = {} # client_email -> (token, expiry_ms)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def vertex_signed_assertion(sa: dict, *, now: int | None = None) -> str:
|
|
106
|
+
"""A signed RS256 JWT bearer assertion for the service account (no network)."""
|
|
107
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
108
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
109
|
+
iat = int(now if now is not None else time.time())
|
|
110
|
+
token_uri = sa.get("token_uri", "https://oauth2.googleapis.com/token")
|
|
111
|
+
header = {"alg": "RS256", "typ": "JWT"}
|
|
112
|
+
claims = {"iss": sa["client_email"], "scope": "https://www.googleapis.com/auth/cloud-platform",
|
|
113
|
+
"aud": token_uri, "iat": iat, "exp": iat + 3600}
|
|
114
|
+
signing_input = f"{_b64url(json.dumps(header).encode())}.{_b64url(json.dumps(claims).encode())}"
|
|
115
|
+
key = serialization.load_pem_private_key(sa["private_key"].encode(), password=None)
|
|
116
|
+
sig = key.sign(signing_input.encode(), padding.PKCS1v15(), hashes.SHA256())
|
|
117
|
+
return f"{signing_input}.{_b64url(sig)}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def vertex_access_token(sa: dict) -> str:
|
|
121
|
+
"""Exchange the SA assertion for a short-lived access token (cached ~1h)."""
|
|
122
|
+
import httpx
|
|
123
|
+
email = sa["client_email"]
|
|
124
|
+
nowms = int(time.time() * 1000)
|
|
125
|
+
cached = _VERTEX_TOKEN_CACHE.get(email)
|
|
126
|
+
if cached and cached[1] - 60_000 > nowms:
|
|
127
|
+
return cached[0]
|
|
128
|
+
token_uri = sa.get("token_uri", "https://oauth2.googleapis.com/token")
|
|
129
|
+
r = httpx.post(token_uri, data={
|
|
130
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
131
|
+
"assertion": vertex_signed_assertion(sa)}, timeout=20)
|
|
132
|
+
r.raise_for_status()
|
|
133
|
+
body = r.json()
|
|
134
|
+
tok, ttl = body["access_token"], body.get("expires_in", 3600)
|
|
135
|
+
_VERTEX_TOKEN_CACHE[email] = (tok, nowms + ttl * 1000)
|
|
136
|
+
return tok
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def vertex_url(project: str, region: str, model: str) -> str:
|
|
140
|
+
return (f"https://{region}-aiplatform.googleapis.com/v1/projects/{project}"
|
|
141
|
+
f"/locations/{region}/publishers/anthropic/models/{quote(model, safe='')}:rawPredict")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def vertex_plan(cred: dict, body: dict):
|
|
145
|
+
"""(url, headers, body_bytes) for a Claude-on-Vertex rawPredict call."""
|
|
146
|
+
sa = json.loads(cred["secret"]) # the full service-account JSON
|
|
147
|
+
meta = cred.get("meta") or {}
|
|
148
|
+
region = meta.get("region") or sa.get("region") or "us-east5"
|
|
149
|
+
project = meta.get("project_id") or sa.get("project_id")
|
|
150
|
+
payload = {k: v for k, v in body.items() if k != "model"}
|
|
151
|
+
payload["anthropic_version"] = "vertex-2023-10-16"
|
|
152
|
+
raw = json.dumps(payload).encode("utf-8")
|
|
153
|
+
headers = {"content-type": "application/json", "Authorization": f"Bearer {vertex_access_token(sa)}"}
|
|
154
|
+
return vertex_url(project, region, body.get("model", "")), headers, raw
|
|
@@ -144,7 +144,7 @@ function logo(name,color){color=color||"#888";
|
|
|
144
144
|
function fieldsFor(uid,kind){
|
|
145
145
|
if(kind==="bedrock")return `<div class="row"><input id="f1_${uid}" placeholder="AWS access key id" style="flex:1"/><input id="f3_${uid}" placeholder="region (us-east-1)" style="width:150px"/></div><input id="f2_${uid}" type="password" placeholder="AWS secret access key" style="width:100%;margin-top:8px"/>`;
|
|
146
146
|
if(kind==="azure")return `<input id="f1_${uid}" placeholder="Azure endpoint URL (…/chat/completions?api-version=…)" style="width:100%"/><input id="f2_${uid}" type="password" placeholder="api-key" style="width:100%;margin-top:8px"/>`;
|
|
147
|
-
if(kind==="vertex")return `<
|
|
147
|
+
if(kind==="vertex")return `<textarea id="f1_${uid}" placeholder="Paste the GCP service-account JSON" style="width:100%;min-height:92px;font:12px ui-monospace,monospace;border:none;border-radius:11px;background:var(--panel2);color:var(--txt);padding:10px 12px;outline:none"></textarea><input id="f3_${uid}" placeholder="region (default us-east5)" style="width:100%;margin-top:8px"/>`;
|
|
148
148
|
return `<input id="f1_${uid}" type="password" placeholder="${kind==='oauth'?'OAuth access token':'API key'}" style="width:100%"/>`;
|
|
149
149
|
}
|
|
150
150
|
function onKind(uid){document.getElementById("ff_"+uid).innerHTML=fieldsFor(uid,document.getElementById("kind_"+uid).value)}
|
|
@@ -153,7 +153,7 @@ async function submitCred(uid,provider){
|
|
|
153
153
|
let secret,meta={};
|
|
154
154
|
if(kind==="bedrock"){secret=g("f2_"+uid);meta={access_key:g("f1_"+uid),region:g("f3_"+uid)||"us-east-1"}}
|
|
155
155
|
else if(kind==="azure"){secret=g("f2_"+uid);meta={endpoint:g("f1_"+uid)}}
|
|
156
|
-
else if(kind==="vertex"){
|
|
156
|
+
else if(kind==="vertex"){secret=g("f1_"+uid);meta={region:g("f3_"+uid)||"us-east5"}}
|
|
157
157
|
else{secret=g("f1_"+uid)}
|
|
158
158
|
if(!secret)return;
|
|
159
159
|
await api("/admin/providers",{method:"POST",body:JSON.stringify({provider,secret,kind,meta})});refreshKeys();
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proxyagent"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Run any agent (Claude, Codex, custom) on any machine — with no API key on the machine. A secure, self-hosted proxy for models and tools."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -236,3 +236,28 @@ def test_bedrock_plan_and_build_plans():
|
|
|
236
236
|
s.add_credential("anthropic", "awssecret", kind="bedrock", meta={"access_key": "AKID", "region": "us-east-1"})
|
|
237
237
|
plans = build_plans(PROVIDERS["anthropic"], s, {"model": "claude-sonnet-4-5", "messages": []})
|
|
238
238
|
assert len(plans) == 2 and plans[0][0].endswith("/v1/messages") and "bedrock-runtime" in plans[1][0]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_vertex_assertion_and_url():
|
|
242
|
+
import base64, json
|
|
243
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
244
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
245
|
+
from proxyagent.signers import vertex_signed_assertion, vertex_url
|
|
246
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
247
|
+
pem = key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
|
|
248
|
+
serialization.NoEncryption()).decode()
|
|
249
|
+
sa = {"client_email": "svc@proj.iam.gserviceaccount.com", "private_key": pem,
|
|
250
|
+
"token_uri": "https://oauth2.googleapis.com/token"}
|
|
251
|
+
jwt = vertex_signed_assertion(sa, now=1700000000)
|
|
252
|
+
parts = jwt.split(".")
|
|
253
|
+
assert len(parts) == 3
|
|
254
|
+
b64d = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
|
|
255
|
+
header, claims = json.loads(b64d(parts[0])), json.loads(b64d(parts[1]))
|
|
256
|
+
assert header["alg"] == "RS256" and claims["iss"] == sa["client_email"]
|
|
257
|
+
assert claims["scope"].endswith("cloud-platform")
|
|
258
|
+
# signature must verify against the public key (raises on tamper)
|
|
259
|
+
key.public_key().verify(b64d(parts[2]), (parts[0] + "." + parts[1]).encode(),
|
|
260
|
+
padding.PKCS1v15(), hashes.SHA256())
|
|
261
|
+
assert vertex_url("myproj", "us-east5", "claude-sonnet-4-5") == (
|
|
262
|
+
"https://us-east5-aiplatform.googleapis.com/v1/projects/myproj/locations/us-east5"
|
|
263
|
+
"/publishers/anthropic/models/claude-sonnet-4-5:rawPredict")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|