proxyagent 0.7.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.7.0 → proxyagent-0.9.0}/PKG-INFO +10 -9
- {proxyagent-0.7.0 → proxyagent-0.9.0}/README.md +9 -8
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/__init__.py +1 -1
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/config.py +1 -1
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/providers.py +5 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/signers.py +66 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/ui/index.html +64 -101
- {proxyagent-0.7.0 → proxyagent-0.9.0}/pyproject.toml +1 -1
- {proxyagent-0.7.0 → proxyagent-0.9.0}/tests/test_proxy.py +25 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/.gitignore +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/aliases.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/cli.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/crypto.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/db.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/harness.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/pricing.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/security.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/server.py +0 -0
- {proxyagent-0.7.0 → proxyagent-0.9.0}/proxyagent/store.py +0 -0
- {proxyagent-0.7.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>
|
|
@@ -99,13 +99,13 @@ claude -p "ship it"
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
## The dashboard
|
|
102
|
-
`proxyagent serve` ships a
|
|
102
|
+
`proxyagent serve` ships a dashboard at `/` (reveal the admin token with
|
|
103
103
|
`proxyagent admin-token`):
|
|
104
104
|
|
|
105
|
-
- **
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
- **Machine tokens** — mint (scoped/TTL), list, revoke.
|
|
105
|
+
- **Access keys** — the credentials you create. Each is a provider + an auth type
|
|
106
|
+
(Anthropic · API key, Anthropic · Bedrock, OpenAI · Azure, …); pick the type, enter the
|
|
107
|
+
key/fields, done. Listed with provider logo · auth type · masked key · remove.
|
|
108
|
+
- **Machine tokens** — mint (scoped / TTL / budget), list, revoke.
|
|
109
109
|
- **Model routing** — add/remove model remaps (e.g. `* → mock` for offline).
|
|
110
110
|
- **Activity** — live request log with usage + cost, and headline stats.
|
|
111
111
|
|
|
@@ -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
|
|
@@ -67,13 +67,13 @@ claude -p "ship it"
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## The dashboard
|
|
70
|
-
`proxyagent serve` ships a
|
|
70
|
+
`proxyagent serve` ships a dashboard at `/` (reveal the admin token with
|
|
71
71
|
`proxyagent admin-token`):
|
|
72
72
|
|
|
73
|
-
- **
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- **Machine tokens** — mint (scoped/TTL), list, revoke.
|
|
73
|
+
- **Access keys** — the credentials you create. Each is a provider + an auth type
|
|
74
|
+
(Anthropic · API key, Anthropic · Bedrock, OpenAI · Azure, …); pick the type, enter the
|
|
75
|
+
key/fields, done. Listed with provider logo · auth type · masked key · remove.
|
|
76
|
+
- **Machine tokens** — mint (scoped / TTL / budget), list, revoke.
|
|
77
77
|
- **Model routing** — add/remove model remaps (e.g. `* → mock` for offline).
|
|
78
78
|
- **Activity** — live request log with usage + cost, and headline stats.
|
|
79
79
|
|
|
@@ -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
|
|
@@ -12,42 +12,33 @@
|
|
|
12
12
|
header{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:16px 28px;background:rgba(8,9,11,.72);backdrop-filter:blur(14px)}
|
|
13
13
|
.brand{display:flex;align-items:center;gap:10px;font-weight:650;font-size:16px}
|
|
14
14
|
.logo{width:27px;height:27px;border-radius:8px;background:linear-gradient(135deg,#34d39e,#3b82f6);display:grid;place-items:center;color:#04130d;font-weight:800}
|
|
15
|
-
main{max-width:
|
|
15
|
+
main{max-width:1040px;margin:0 auto;padding:8px 28px 60px}
|
|
16
16
|
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin-bottom:26px}
|
|
17
17
|
.card{background:var(--panel);border-radius:18px;padding:20px}
|
|
18
18
|
.stat .n{font-size:31px;font-weight:700;letter-spacing:-.02em}.stat .l{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.09em;margin-top:6px}
|
|
19
19
|
.tabs{display:flex;gap:6px;margin-bottom:20px}
|
|
20
20
|
.tab{padding:8px 15px;color:var(--dim);cursor:pointer;font-weight:560;border-radius:10px}
|
|
21
|
-
.tab:hover{color:var(--txt);background:var(--panel)}
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.prov{background:var(--panel);border-radius:18px;padding:18px;transition:background .15s,transform .15s}
|
|
25
|
-
.prov:hover{background:#1a1d23}
|
|
26
|
-
.prov .top{display:flex;align-items:center;gap:13px}
|
|
27
|
-
.tile{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;flex:none;font-weight:800;font-size:18px;overflow:hidden}
|
|
28
|
-
.tile img{width:23px;height:23px;display:block}.tile .fbm{display:none}.tile.fb img{display:none}.tile.fb .fbm{display:flex;align-items:center;justify-content:center}
|
|
29
|
-
.prov h3{margin:0;font-size:15px;font-weight:640}.prov .ep{color:var(--dim);font-size:11.5px;margin-top:1px}
|
|
30
|
-
.badges{display:flex;gap:6px;flex-wrap:wrap;margin:13px 0}
|
|
21
|
+
.tab:hover,.tab.on{color:var(--txt);background:var(--panel)}
|
|
22
|
+
.tile{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;flex:none;font-weight:800;font-size:16px;overflow:hidden}
|
|
23
|
+
.tile img{width:21px;height:21px;display:block}.tile .fbm{display:none}.tile.fb img{display:none}.tile.fb .fbm{display:flex;align-items:center;justify-content:center}
|
|
31
24
|
.badge{font-size:10.5px;padding:3px 9px;border-radius:8px;background:var(--soft);color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
|
|
32
25
|
.badge.on{background:rgba(52,211,158,.14);color:var(--grn)}
|
|
33
|
-
|
|
34
|
-
input,select{
|
|
35
|
-
|
|
36
|
-
input::placeholder{color:#5a606b}
|
|
37
|
-
button{font:inherit;background:var(--grn);color:#04130d;border:none;border-radius:11px;padding:9px 14px;font-weight:660;cursor:pointer}button:hover{filter:brightness(1.07)}
|
|
26
|
+
input,select{font:inherit;border-radius:11px;border:none;background:var(--panel2);color:var(--txt);padding:10px 12px;outline:none;box-shadow:inset 0 0 0 1px transparent}
|
|
27
|
+
input:focus,select:focus{box-shadow:inset 0 0 0 1px rgba(52,211,158,.45)}input::placeholder{color:#5a606b}
|
|
28
|
+
button{font:inherit;background:var(--grn);color:#04130d;border:none;border-radius:11px;padding:10px 15px;font-weight:660;cursor:pointer}button:hover{filter:brightness(1.07)}
|
|
38
29
|
button.ghost{background:var(--soft);color:var(--dim)}button.ghost:hover{color:var(--txt)}
|
|
39
30
|
button.danger{background:rgba(246,120,138,.12);color:var(--red)}button.danger:hover{background:rgba(246,120,138,.2)}
|
|
40
31
|
button.sm{padding:7px 12px;font-size:12.5px;border-radius:9px}
|
|
41
32
|
.row{display:flex;gap:9px;flex-wrap:wrap;align-items:center}
|
|
42
|
-
|
|
43
|
-
table{width:100%;border-collapse:collapse}td,th{text-align:left;padding:11px 12px;font-size:13px}
|
|
33
|
+
table{width:100%;border-collapse:collapse}td,th{text-align:left;padding:11px 12px;font-size:13px;vertical-align:middle}
|
|
44
34
|
thead th{color:var(--dim);font-weight:520;font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
|
45
35
|
tbody tr{transition:background .12s}tbody tr:hover{background:#1a1d23}
|
|
46
36
|
.pill{padding:3px 10px;border-radius:8px;font-size:11px}.pill.ok{background:rgba(52,211,158,.13);color:var(--grn)}.pill.no{background:rgba(246,120,138,.13);color:var(--red)}
|
|
47
37
|
.gate{max-width:430px;margin:100px auto;text-align:center}.gate input{width:100%;margin:14px 0}
|
|
48
38
|
.tok{background:var(--panel2);padding:13px;border-radius:12px;word-break:break-all;margin-top:13px;font-size:13px}
|
|
49
|
-
.hide{display:none}.muted{color:var(--dim)}.h{display:flex;justify-content:space-between;align-items:center;margin:0 0
|
|
39
|
+
.hide{display:none}.muted{color:var(--dim)}.h{display:flex;justify-content:space-between;align-items:center;margin:0 0 14px}
|
|
50
40
|
h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin:0}
|
|
41
|
+
.empty{text-align:center;color:var(--dim);padding:46px}
|
|
51
42
|
</style>
|
|
52
43
|
</head>
|
|
53
44
|
<body>
|
|
@@ -66,28 +57,30 @@
|
|
|
66
57
|
</header>
|
|
67
58
|
<main>
|
|
68
59
|
<div class="stats">
|
|
60
|
+
<div class="card stat"><div class="n" id="s_keys">0</div><div class="l">Access keys</div></div>
|
|
69
61
|
<div class="card stat"><div class="n" id="s_req">0</div><div class="l">Requests</div></div>
|
|
70
|
-
<div class="card stat"><div class="n" id="s_tok">0</div><div class="l">Tokens (in/out)</div></div>
|
|
62
|
+
<div class="card stat"><div class="n" id="s_tok">0/0</div><div class="l">Tokens (in/out)</div></div>
|
|
71
63
|
<div class="card stat"><div class="n" id="s_cost" style="color:var(--grn)">$0</div><div class="l">Cost</div></div>
|
|
72
|
-
<div class="card stat"><div class="n" id="s_prov">0</div><div class="l">Providers connected</div></div>
|
|
73
64
|
</div>
|
|
74
65
|
|
|
75
66
|
<div class="tabs">
|
|
76
|
-
<div class="tab on" data-t="
|
|
77
|
-
<div class="tab" data-t="providers" onclick="tab('providers')">Model endpoints</div>
|
|
67
|
+
<div class="tab on" data-t="keys" onclick="tab('keys')">Access keys</div>
|
|
78
68
|
<div class="tab" data-t="tokens" onclick="tab('tokens')">Machine tokens</div>
|
|
79
69
|
<div class="tab" data-t="models" onclick="tab('models')">Model routing</div>
|
|
80
70
|
<div class="tab" data-t="activity" onclick="tab('activity')">Activity</div>
|
|
81
71
|
</div>
|
|
82
72
|
|
|
83
|
-
<section id="
|
|
84
|
-
<
|
|
85
|
-
<div class="
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
<section id="t_keys">
|
|
74
|
+
<div class="h"><h2>Access keys</h2><button onclick="openCreate()">+ Create access key</button></div>
|
|
75
|
+
<div class="card hide" id="createForm" style="margin-bottom:16px">
|
|
76
|
+
<div class="row" style="margin-bottom:11px">
|
|
77
|
+
<select id="cf_provider" onchange="onProvider()" style="flex:1"></select>
|
|
78
|
+
<select id="kind_cf" onchange="onKind('cf')" style="width:140px"></select>
|
|
79
|
+
</div>
|
|
80
|
+
<div id="ff_cf"></div>
|
|
81
|
+
<div class="row" style="margin-top:11px"><button onclick="createKey()">Create access key</button><button class="ghost" onclick="openCreate()">Cancel</button></div>
|
|
82
|
+
</div>
|
|
83
|
+
<div id="keylist"></div>
|
|
91
84
|
</section>
|
|
92
85
|
|
|
93
86
|
<section id="t_tokens" class="hide">
|
|
@@ -109,8 +102,7 @@
|
|
|
109
102
|
<div class="card" style="margin-bottom:16px">
|
|
110
103
|
<div class="h"><h2>Remap a model</h2></div>
|
|
111
104
|
<div class="row">
|
|
112
|
-
<input id="al_match" placeholder="match: * or gpt-4o"
|
|
113
|
-
<span class="muted">→</span>
|
|
105
|
+
<input id="al_match" placeholder="match: * or gpt-4o"/><span class="muted">→</span>
|
|
114
106
|
<input id="al_target" placeholder="target: mock or anthropic:claude-sonnet-4-5" style="flex:1"/>
|
|
115
107
|
<button onclick="setAlias()">Map</button>
|
|
116
108
|
</div>
|
|
@@ -132,32 +124,43 @@ async function api(p,o={}){const r=await fetch(p,{...o,headers:{...H(),...(o.hea
|
|
|
132
124
|
function val(id){return document.getElementById(id).value.trim()}
|
|
133
125
|
function saveAdmin(){const v=document.getElementById("admintok").value.trim();if(v){localStorage.setItem("pa_admin",v);boot()}}
|
|
134
126
|
function logout(){localStorage.removeItem("pa_admin");document.getElementById("app").classList.add("hide");document.getElementById("gate").classList.remove("hide")}
|
|
135
|
-
function tab(t){document.querySelectorAll(".tab").forEach(e=>e.classList.toggle("on",e.dataset.t===t));["
|
|
127
|
+
function tab(t){document.querySelectorAll(".tab").forEach(e=>e.classList.toggle("on",e.dataset.t===t));["keys","tokens","models","activity"].forEach(s=>document.getElementById("t_"+s).classList.toggle("hide",s!==t))}
|
|
136
128
|
|
|
137
|
-
//
|
|
129
|
+
// logos: real brand marks via the Simple Icons CDN, tinted; fall back to a drawn mark offline.
|
|
138
130
|
const MARK={
|
|
139
131
|
anthropic:'<path d="M9.6 3 3 21h4.1l1.2-3.5h5.9L15.4 21H20L13.4 3H9.6Zm-.4 10.5 1.9-5.4 2 5.4H9.2Z"/>',
|
|
140
132
|
openai:'<path d="M12 2.6c1.9 0 3.5 1.3 4 3a4.2 4.2 0 0 1 2.3 6.8 4.2 4.2 0 0 1-4 5.8 4.2 4.2 0 0 1-8.6-1A4.2 4.2 0 0 1 5.7 12 4.2 4.2 0 0 1 8 5.5a4.2 4.2 0 0 1 4-2.9Zm0 4.6a4.8 4.8 0 1 0 0 9.6 4.8 4.8 0 0 0 0-9.6Z"/>',
|
|
141
133
|
gemini:'<path d="M12 2c.4 4.6 3.4 7.6 8 8-4.6.4-7.6 3.4-8 8-.4-4.6-3.4-7.6-8-8 4.6-.4 7.6-3.4 8-8Z"/>',
|
|
142
|
-
groq:'<path d="M12 3a6 6 0 1 0 4.2 10.3l-2-2A3.2 3.2 0 1 1 12 6.2c.9 0 1.6.3 2.2.8L16.3 5A6 6 0 0 0 12 3Zm2 7v3.3h2.6V10H14Z"/>',
|
|
143
|
-
openrouter:'<path d="M3 8h6l3 4 3-4h6M3 16h6l3-4M21 8l-3 8h-3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
144
|
-
mistral:'<g><rect x="3" y="4" width="3.4" height="3.4"/><rect x="17.6" y="4" width="3.4" height="3.4"/><rect x="7.3" y="8.3" width="3.4" height="3.4"/><rect x="13.3" y="8.3" width="3.4" height="3.4"/><rect x="3" y="12.6" width="3.4" height="3.4"/><rect x="17.6" y="12.6" width="3.4" height="3.4"/><rect x="3" y="16.9" width="18" height="3.1"/></g>',
|
|
145
|
-
deepseek:'<path d="M4 9c3 0 4 2 7 2s4-3 8-2c-1 4-5 7-9 7-3 0-6-2-6-5 0-1 0-2 0-2Zm12 1.5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/>',
|
|
146
134
|
xai:'<path d="M4 4h3.2l4 5.6L15.8 4H19l-6 8 6.2 8h-3.2l-4.4-6L7 20H4l6.4-8.4L4 4Z"/>',
|
|
147
|
-
together:'<path d="M8 5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm8 7a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7ZM10.5 12.5l3-1.5" fill="none" stroke="currentColor" stroke-width="2"/>',
|
|
148
135
|
};
|
|
149
|
-
// real brand logos via the Simple Icons CDN, tinted to the brand colour; falls back to
|
|
150
|
-
// the drawn mark / letter if a slug is missing or the CDN is unreachable (offline).
|
|
151
136
|
const SLUG={anthropic:"anthropic",openai:"openai",gemini:"googlegemini",groq:"groq",mistral:"mistralai",deepseek:"deepseek",xai:"x"};
|
|
152
|
-
function logo(name,color){
|
|
153
|
-
const mark=MARK[name]?`<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor">${MARK[name]}</svg>`:`<b>${(name[0]||"?").toUpperCase()}</b>`;
|
|
154
|
-
const inner=SLUG[name]
|
|
155
|
-
?`<img src="https://cdn.simpleicons.org/${SLUG[name]}/${color.replace("#","")}" alt="${name}" onerror="this.closest('.tile').classList.add('fb')"/><span class="fbm">${mark}</span>`
|
|
156
|
-
:`<span class="fbm" style="display:flex">${mark}</span>`;
|
|
157
|
-
return `<div class="tile" style="background:${hexa(color,.14)};color:${color}">${inner}</div>`;
|
|
158
|
-
}
|
|
159
137
|
function hexa(h,a){const n=h.replace("#","");const x=parseInt(n.length===3?n.split("").map(c=>c+c).join(""):n,16);return `rgba(${(x>>16)&255},${(x>>8)&255},${x&255},${a})`}
|
|
138
|
+
function logo(name,color){color=color||"#888";
|
|
139
|
+
const mark=MARK[name]?`<svg viewBox="0 0 24 24" width="21" height="21" fill="currentColor">${MARK[name]}</svg>`:`<b>${(name[0]||"?").toUpperCase()}</b>`;
|
|
140
|
+
const inner=SLUG[name]?`<img src="https://cdn.simpleicons.org/${SLUG[name]}/${color.replace("#","")}" alt="${name}" onerror="this.closest('.tile').classList.add('fb')"/><span class="fbm">${mark}</span>`:`<span class="fbm" style="display:flex">${mark}</span>`;
|
|
141
|
+
return `<div class="tile" style="background:${hexa(color,.14)};color:${color}">${inner}</div>`}
|
|
142
|
+
|
|
143
|
+
// the auth-type fields (the "provider" is essentially the auth type)
|
|
144
|
+
function fieldsFor(uid,kind){
|
|
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
|
+
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 `<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
|
+
return `<input id="f1_${uid}" type="password" placeholder="${kind==='oauth'?'OAuth access token':'API key'}" style="width:100%"/>`;
|
|
149
|
+
}
|
|
150
|
+
function onKind(uid){document.getElementById("ff_"+uid).innerHTML=fieldsFor(uid,document.getElementById("kind_"+uid).value)}
|
|
151
|
+
async function submitCred(uid,provider){
|
|
152
|
+
const kind=document.getElementById("kind_"+uid).value,g=id=>{const e=document.getElementById(id);return e?e.value.trim():""};
|
|
153
|
+
let secret,meta={};
|
|
154
|
+
if(kind==="bedrock"){secret=g("f2_"+uid);meta={access_key:g("f1_"+uid),region:g("f3_"+uid)||"us-east-1"}}
|
|
155
|
+
else if(kind==="azure"){secret=g("f2_"+uid);meta={endpoint:g("f1_"+uid)}}
|
|
156
|
+
else if(kind==="vertex"){secret=g("f1_"+uid);meta={region:g("f3_"+uid)||"us-east5"}}
|
|
157
|
+
else{secret=g("f1_"+uid)}
|
|
158
|
+
if(!secret)return;
|
|
159
|
+
await api("/admin/providers",{method:"POST",body:JSON.stringify({provider,secret,kind,meta})});refreshKeys();
|
|
160
|
+
}
|
|
161
|
+
async function disconnect(id){await api("/admin/providers/"+id,{method:"DELETE"});refreshKeys()}
|
|
160
162
|
|
|
163
|
+
let CATALOG=[];
|
|
161
164
|
async function boot(){
|
|
162
165
|
try{
|
|
163
166
|
const u=await(await api("/admin/usage")).json();
|
|
@@ -167,61 +170,21 @@ async function boot(){
|
|
|
167
170
|
document.getElementById("s_cost").textContent="$"+(u.usage.cost_usd||0).toFixed(4);
|
|
168
171
|
document.getElementById("badge_backend").textContent=u.backend;
|
|
169
172
|
document.getElementById("enc").textContent=u.encryption?"🔒 encrypted at rest":"⚠ encryption off";
|
|
170
|
-
|
|
173
|
+
refreshKeys();refreshTokens();refreshAliases();refreshLogs();
|
|
171
174
|
}catch(e){document.getElementById("gateerr").textContent="Invalid admin token."}
|
|
172
175
|
}
|
|
173
|
-
async function
|
|
174
|
-
const d=await(await api("/admin/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
${h.configured?'<span class="pill ok">ready</span>':'<span class="pill no">connect a key</span>'}</div>
|
|
182
|
-
<div class="badges">${chips}</div>
|
|
183
|
-
<div class="models mono" style="font-size:11px">${h.install}</div>
|
|
184
|
-
<div class="row"><button class="sm" onclick="openConnect('h_${h.name}')">+ Connect auth</button></div>
|
|
185
|
-
<div class="connect" id="c_h_${h.name}">${connectForm('h_'+h.name, h.auth.map(a=>a.mode), h.provider)}</div></div>`}).join("");
|
|
186
|
-
}
|
|
187
|
-
async function refreshProviders(){
|
|
188
|
-
const d=await(await api("/admin/catalog")).json();const g=document.getElementById("provgrid");
|
|
189
|
-
let connected=0;
|
|
190
|
-
g.innerHTML=d.providers.map(p=>{const creds=p.creds||[];const on=p.via_env||creds.length;if(on)connected++;
|
|
191
|
-
const credrows=creds.map(c=>`<div class="row" style="justify-content:space-between;gap:6px"><span class="badge on">${c.kind} · ${c.masked||""}</span><button class="danger sm" onclick="disconnect('${c.id}')">remove</button></div>`).join("");
|
|
192
|
-
return `<div class="prov"><div class="top">${logo(p.name,p.color)}<div style="flex:1"><h3>${p.label}</h3><div class="ep">${p.shape} · ${p.name}</div></div>
|
|
193
|
-
${on?'<span class="pill ok">on</span>':'<span class="pill no">off</span>'}</div>
|
|
194
|
-
<div class="badges">${p.kinds.map(k=>`<span class="badge">${k}</span>`).join("")}${p.via_env?'<span class="badge on">env</span>':''}</div>
|
|
195
|
-
<div style="display:flex;flex-direction:column;gap:6px;margin:6px 0 11px">${credrows||'<span class="muted" style="font-size:11.5px">No keys yet</span>'}</div>
|
|
196
|
-
<div class="row"><button class="sm" onclick="openConnect('${p.name}')">+ Add credential</button></div>
|
|
197
|
-
<div class="connect" id="c_${p.name}">${connectForm(p.name,p.kinds,p.name)}</div></div>`}).join("");
|
|
198
|
-
document.getElementById("s_prov").textContent=connected;
|
|
199
|
-
}
|
|
200
|
-
function openConnect(n){document.getElementById("c_"+n).classList.toggle("open")}
|
|
201
|
-
function fieldsFor(uid,kind){
|
|
202
|
-
if(kind==="bedrock")return `<input id="f1_${uid}" placeholder="AWS access key id"/><input id="f2_${uid}" type="password" placeholder="AWS secret access key"/><input id="f3_${uid}" placeholder="region (default us-east-1)"/>`;
|
|
203
|
-
if(kind==="azure")return `<input id="f1_${uid}" placeholder="Azure endpoint URL (…/chat/completions?api-version=…)"/><input id="f2_${uid}" type="password" placeholder="api-key"/>`;
|
|
204
|
-
if(kind==="vertex")return `<span class="muted" style="font-size:12px">Vertex (service-account JSON) — coming next.</span>`;
|
|
205
|
-
return `<input id="f1_${uid}" type="password" placeholder="${kind==='oauth'?'OAuth access token':'API key'}"/>`;
|
|
206
|
-
}
|
|
207
|
-
function onKind(uid){document.getElementById("ff_"+uid).innerHTML=fieldsFor(uid,document.getElementById("kind_"+uid).value)}
|
|
208
|
-
function connectForm(uid,kinds,provider){
|
|
209
|
-
return `<select id="kind_${uid}" onchange="onKind('${uid}')">${kinds.map(k=>`<option value="${k}">${k}</option>`).join("")}</select>
|
|
210
|
-
<div id="ff_${uid}">${fieldsFor(uid,kinds[0])}</div>
|
|
211
|
-
<div class="row"><button class="sm" onclick="submitCred('${uid}','${provider}')">Add credential</button><button class="ghost sm" onclick="openConnect('${uid}')">Cancel</button></div>`;
|
|
212
|
-
}
|
|
213
|
-
async function submitCred(uid,provider){
|
|
214
|
-
const kind=document.getElementById("kind_"+uid).value,g=id=>{const e=document.getElementById(id);return e?e.value.trim():""};
|
|
215
|
-
let secret,meta={};
|
|
216
|
-
if(kind==="bedrock"){secret=g("f2_"+uid);meta={access_key:g("f1_"+uid),region:g("f3_"+uid)||"us-east-1"}}
|
|
217
|
-
else if(kind==="azure"){secret=g("f2_"+uid);meta={endpoint:g("f1_"+uid)}}
|
|
218
|
-
else if(kind==="vertex"){return}
|
|
219
|
-
else{secret=g("f1_"+uid)}
|
|
220
|
-
if(!secret)return;
|
|
221
|
-
await api("/admin/providers",{method:"POST",body:JSON.stringify({provider,secret,kind,meta})});
|
|
222
|
-
renderHarnesses();refreshProviders();
|
|
176
|
+
async function refreshKeys(){
|
|
177
|
+
const d=await(await api("/admin/catalog")).json();CATALOG=d.providers;
|
|
178
|
+
const keys=[];d.providers.forEach(p=>{(p.creds||[]).forEach(c=>keys.push({...c,provider:p.name,plabel:p.label,color:p.color}));if(p.via_env)keys.push({provider:p.name,plabel:p.label,color:p.color,kind:"api_key",masked:"env",id:null})});
|
|
179
|
+
document.getElementById("s_keys").textContent=keys.length;
|
|
180
|
+
document.getElementById("keylist").innerHTML=keys.length
|
|
181
|
+
?`<div class="card"><table><thead><tr><th></th><th>Provider</th><th>Auth type</th><th>Key</th><th></th></tr></thead><tbody>${keys.map(k=>`<tr><td style="width:50px">${logo(k.provider,k.color)}</td><td>${k.plabel}</td><td><span class="badge on">${k.kind}</span></td><td class="mono">${k.masked||""}</td><td>${k.id?`<button class="danger sm" onclick="disconnect('${k.id}')">remove</button>`:'<span class="muted" style="font-size:11px">from env</span>'}</td></tr>`).join("")}</tbody></table></div>`
|
|
182
|
+
:`<div class="card empty">No access keys yet.<br/><span style="font-size:12.5px">Click <b style="color:var(--txt)">+ Create access key</b> to add one.</span></div>`;
|
|
183
|
+
const ps=document.getElementById("cf_provider");if(ps&&!ps.dataset.init){ps.dataset.init="1";ps.innerHTML=CATALOG.map(p=>`<option value="${p.name}">${p.label}</option>`).join("");onProvider()}
|
|
223
184
|
}
|
|
224
|
-
|
|
185
|
+
function onProvider(){const p=CATALOG.find(x=>x.name===val("cf_provider"))||{};const ks=document.getElementById("kind_cf");ks.innerHTML=(p.kinds||["api_key"]).map(k=>`<option value="${k}">${k}</option>`).join("");onKind("cf")}
|
|
186
|
+
function openCreate(){document.getElementById("createForm").classList.toggle("hide")}
|
|
187
|
+
async function createKey(){await submitCred("cf",val("cf_provider"));document.getElementById("createForm").classList.add("hide")}
|
|
225
188
|
|
|
226
189
|
async function refreshTokens(){const d=await(await api("/admin/tokens")).json();
|
|
227
190
|
document.getElementById("toks").innerHTML=d.tokens.map(t=>`<tr><td class="mono">${t.id}</td><td>${t.label||""}</td><td class="mono">${t.masked||""}</td><td class="mono">${(t.scope||[]).join(", ")}</td><td class="mono">${t.budget_usd?('$'+(t.spent_usd||0).toFixed(4)+' / $'+(+t.budget_usd).toFixed(2)):'—'}</td><td>${t.revoked?'<span class="pill no">revoked</span>':'<span class="pill ok">active</span>'}</td><td>${t.revoked?"":`<button class="danger sm" onclick="revokeTok('${t.id}')">revoke</button>`}</td></tr>`).join("")}
|
|
@@ -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
|