proxyagent 0.7.0__tar.gz → 0.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyagent
3
- Version: 0.7.0
3
+ Version: 0.8.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 real dashboard at `/` (reveal the admin token with
102
+ `proxyagent serve` ships a dashboard at `/` (reveal the admin token with
103
103
  `proxyagent admin-token`):
104
104
 
105
- - **Providers** — a branded catalog of every supported provider; **connect/disconnect**
106
- with a key right from the UI, see which auth types each supports (api_key / oauth) and
107
- whether it's on via env or stored credentials.
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
 
@@ -67,13 +67,13 @@ claude -p "ship it"
67
67
  ```
68
68
 
69
69
  ## The dashboard
70
- `proxyagent serve` ships a real dashboard at `/` (reveal the admin token with
70
+ `proxyagent serve` ships a dashboard at `/` (reveal the admin token with
71
71
  `proxyagent admin-token`):
72
72
 
73
- - **Providers** — a branded catalog of every supported provider; **connect/disconnect**
74
- with a key right from the UI, see which auth types each supports (api_key / oauth) and
75
- whether it's on via env or stored credentials.
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
 
@@ -16,7 +16,7 @@ from typing import Optional
16
16
 
17
17
  from .harness import run # noqa: F401 (the headline SDK call)
18
18
 
19
- __version__ = "0.7.0"
19
+ __version__ = "0.8.0"
20
20
  __all__ = ["run", "serve", "create_app", "Config", "Admin", "__version__"]
21
21
 
22
22
 
@@ -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:1140px;margin:0 auto;padding:8px 28px 60px}
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
- .tab.on{color:var(--txt);background:var(--panel)}
23
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(258px,1fr));gap:16px}
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
- .models{color:var(--dim);font-size:11.5px;margin:8px 0 13px;min-height:16px}
34
- input,select{font:inherit;border-radius:11px;border:none;background:var(--panel2);color:var(--txt);padding:9px 12px;outline:none;box-shadow:inset 0 0 0 1px transparent}
35
- input:focus,select:focus{box-shadow:inset 0 0 0 1px rgba(52,211,158,.45)}
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
- .connect{margin-top:4px;display:none;gap:9px;flex-direction:column}.connect.open{display:flex}
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 12px}
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="harnesses" onclick="tab('harnesses')">Harnesses</div>
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="t_harnesses">
84
- <p class="muted" style="margin:0 0 14px">The agents you run. Connect each auth method once — the machine running the harness then holds only a <code>pa_</code> token.</p>
85
- <div class="grid" id="harngrid"></div>
86
- </section>
87
-
88
- <section id="t_providers" class="hide">
89
- <p class="muted" style="margin:0 0 14px">Raw model backends for model-agnostic harnesses (aider, Cline, Codex pointed elsewhere…).</p>
90
- <div class="grid" id="provgrid"></div>
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,84 +124,30 @@ 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));["harnesses","providers","tokens","models","activity"].forEach(s=>document.getElementById("t_"+s).classList.toggle("hide",s!==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
- // ---- provider logos (inline, brand-tinted) ----
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>`}
160
142
 
161
- async function boot(){
162
- try{
163
- const u=await(await api("/admin/usage")).json();
164
- document.getElementById("gate").classList.add("hide");document.getElementById("app").classList.remove("hide");
165
- document.getElementById("s_req").textContent=u.usage.requests;
166
- document.getElementById("s_tok").textContent=`${u.usage.prompt_tokens}/${u.usage.completion_tokens}`;
167
- document.getElementById("s_cost").textContent="$"+(u.usage.cost_usd||0).toFixed(4);
168
- document.getElementById("badge_backend").textContent=u.backend;
169
- document.getElementById("enc").textContent=u.encryption?"🔒 encrypted at rest":"⚠ encryption off";
170
- renderHarnesses();refreshProviders();refreshTokens();refreshAliases();refreshLogs();
171
- }catch(e){document.getElementById("gateerr").textContent="Invalid admin token."}
172
- }
173
- async function renderHarnesses(){
174
- const d=await(await api("/admin/harnesses")).json();
175
- document.getElementById("harngrid").innerHTML=d.harnesses.map(h=>{
176
- const chips=h.auth.map(a=>{
177
- const cls=a.connected?"badge on":(a.ready?"badge":"badge");
178
- const tag=a.connected?"✓ connected":(a.ready?"connect":"soon");
179
- return `<span class="${cls}" title="${a.ready?'available now':'coming soon'}">${a.label} · ${tag}</span>`}).join("");
180
- return `<div class="prov"><div class="top">${logo(h.provider,h.color)}<div style="flex:1"><h3>${h.label}</h3><div class="ep">runs on ${h.provider}</div></div>
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")}
143
+ // the auth-type fields (the "provider" is essentially the auth type)
201
144
  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"/>`;
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"/>`;
204
147
  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'}"/>`;
148
+ return `<input id="f1_${uid}" type="password" placeholder="${kind==='oauth'?'OAuth access token':'API key'}" style="width:100%"/>`;
206
149
  }
207
150
  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
151
  async function submitCred(uid,provider){
214
152
  const kind=document.getElementById("kind_"+uid).value,g=id=>{const e=document.getElementById(id);return e?e.value.trim():""};
215
153
  let secret,meta={};
@@ -218,10 +156,35 @@ async function submitCred(uid,provider){
218
156
  else if(kind==="vertex"){return}
219
157
  else{secret=g("f1_"+uid)}
220
158
  if(!secret)return;
221
- await api("/admin/providers",{method:"POST",body:JSON.stringify({provider,secret,kind,meta})});
222
- renderHarnesses();refreshProviders();
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()}
162
+
163
+ let CATALOG=[];
164
+ async function boot(){
165
+ try{
166
+ const u=await(await api("/admin/usage")).json();
167
+ document.getElementById("gate").classList.add("hide");document.getElementById("app").classList.remove("hide");
168
+ document.getElementById("s_req").textContent=u.usage.requests;
169
+ document.getElementById("s_tok").textContent=`${u.usage.prompt_tokens}/${u.usage.completion_tokens}`;
170
+ document.getElementById("s_cost").textContent="$"+(u.usage.cost_usd||0).toFixed(4);
171
+ document.getElementById("badge_backend").textContent=u.backend;
172
+ document.getElementById("enc").textContent=u.encryption?"🔒 encrypted at rest":"⚠ encryption off";
173
+ refreshKeys();refreshTokens();refreshAliases();refreshLogs();
174
+ }catch(e){document.getElementById("gateerr").textContent="Invalid admin token."}
175
+ }
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
- async function disconnect(id){await api("/admin/providers/"+id,{method:"DELETE"});renderHarnesses();refreshProviders()}
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.0"
7
+ version = "0.8.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"
File without changes
File without changes
File without changes