admin: grant a token the admin scope from the tokens page

The API already took the flag; without this, granting it meant editing
auth.json by hand. Opt-in at creation, and a toggle per row.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent c6c1017c
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
<tr><th>Name</th><th>Token</th><th>Format</th><th>Created</th><th>Last used</th><th></th></tr> <tr><th>Name</th><th>Token</th><th>Format</th><th>Admin</th><th>Created</th><th>Last used</th><th></th></tr>
</thead> </thead>
<tbody id="tokens-body"> <tbody id="tokens-body">
<tr class="empty-row"><td colspan="6">No tokens — create one to get started</td></tr> <tr class="empty-row"><td colspan="6">No tokens — create one to get started</td></tr>
...@@ -43,6 +43,13 @@ ...@@ -43,6 +43,13 @@
<option value="custom">Custom</option> <option value="custom">Custom</option>
</select> </select>
</div> </div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer">
<input type="checkbox" id="t-admin" style="width:auto">
<span class="form-label" style="margin:0">Admin scope</span>
</label>
<span class="form-hint">Lets this key read spend and pod status from <code>GET /v1/runpod/spend</code>. Off by default — an ordinary key can call the models but not see what the install costs.</span>
</div>
<div class="form-actions"> <div class="form-actions">
<button class="btn btn-primary" onclick="createToken()">Generate</button> <button class="btn btn-primary" onclick="createToken()">Generate</button>
<button class="btn btn-ghost" onclick="closeModal('create-modal')">Cancel</button> <button class="btn btn-ghost" onclick="closeModal('create-modal')">Cancel</button>
...@@ -104,6 +111,9 @@ async function loadTokens() { ...@@ -104,6 +111,9 @@ async function loadTokens() {
<td class="td-name">${esc(t.name)}</td> <td class="td-name">${esc(t.name)}</td>
<td><code>${esc(t.token.substring(0,28))}…</code></td> <td><code>${esc(t.token.substring(0,28))}…</code></td>
<td><span class="badge badge-user">${esc(t.provider)}</span></td> <td><span class="badge badge-user">${esc(t.provider)}</span></td>
<td><label style="display:flex;align-items:center;gap:.3rem;cursor:pointer" title="Read spend over the served API">
<input type="checkbox" style="width:auto" ${t.admin ? 'checked' : ''} onchange="setTokenAdmin(${t.id}, this.checked)">
</label></td>
<td class="mono small dim">${fmt(t.created_at)}</td> <td class="mono small dim">${fmt(t.created_at)}</td>
<td class="dim small">${t.last_used ? fmt(t.last_used) : 'Never'}</td> <td class="dim small">${t.last_used ? fmt(t.last_used) : 'Never'}</td>
<td style="text-align:right"><button class="btn btn-danger btn-sm" onclick="delToken(${t.id})">Delete</button></td> <td style="text-align:right"><button class="btn btn-danger btn-sm" onclick="delToken(${t.id})">Delete</button></td>
...@@ -111,13 +121,25 @@ async function loadTokens() { ...@@ -111,13 +121,25 @@ async function loadTokens() {
} catch {} } catch {}
} }
async function setTokenAdmin(id, want) {
try {
const r = await fetch(ROOT_PATH + '/admin/api/tokens/' + id, {
method:'PATCH', headers:{'Content-Type':'application/json'},
body: JSON.stringify({admin: want})
});
if (!r.ok) { showAlert('Failed to change the admin scope'); }
} catch { showAlert('Failed to change the admin scope'); }
loadTokens();
}
async function createToken() { async function createToken() {
const name = document.getElementById('t-name').value.trim(); const name = document.getElementById('t-name').value.trim();
if (!name) { document.getElementById('t-name').focus(); return; } if (!name) { document.getElementById('t-name').focus(); return; }
try { try {
const r = await fetch(ROOT_PATH + '/admin/api/tokens', { const r = await fetch(ROOT_PATH + '/admin/api/tokens', {
method:'POST', headers:{'Content-Type':'application/json'}, method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({name, provider: document.getElementById('t-provider').value}) body: JSON.stringify({name, provider: document.getElementById('t-provider').value,
admin: document.getElementById('t-admin').checked})
}); });
if (r.ok) { if (r.ok) {
const d = await r.json(); const d = await r.json();
......
...@@ -7,7 +7,7 @@ public, pulled without credentials. There is nothing to build. ...@@ -7,7 +7,7 @@ public, pulled without credentials. There is nothing to build.
| Package | What it is | Pull | | Package | What it is | Pull |
|---|---|---| |---|---|---|
| `ghcr.io/nextime/coderai` | **The full CoderAI** — every capability, every engine, the Web Studio and admin UI, the cluster head/node, llama.cpp with the RPC backend and `rpc-server`. One layer, ~25 GB on disk (≈14 GB to pull). Tags: `latest`, `0.2.81`, … | `docker pull ghcr.io/nextime/coderai:latest` | | `ghcr.io/nextime/coderai` | **The full CoderAI** — every capability, every engine, the Web Studio and admin UI, the cluster head/node, llama.cpp with the RPC backend and `rpc-server`. One layer, ~25 GB on disk (≈14 GB to pull). Tags: `latest`, `0.2.82`, … | `docker pull ghcr.io/nextime/coderai:latest` |
| `ghcr.io/nextime/coderai-images` | Image generation only (SDXL, Flux, Z-Image …) | `docker pull ghcr.io/nextime/coderai-images:latest` | | `ghcr.io/nextime/coderai-images` | Image generation only (SDXL, Flux, Z-Image …) | `docker pull ghcr.io/nextime/coderai-images:latest` |
| `ghcr.io/nextime/coderai-video` | Video generation (Wan, LTX-2 …), upscale, interpolation | `…/coderai-video:latest` | | `ghcr.io/nextime/coderai-video` | Video generation (Wan, LTX-2 …), upscale, interpolation | `…/coderai-video:latest` |
| `ghcr.io/nextime/coderai-text` | LLMs with transformers / llama.cpp, incl. local LoRA adapters | `…/coderai-text:latest` | | `ghcr.io/nextime/coderai-text` | LLMs with transformers / llama.cpp, incl. local LoRA adapters | `…/coderai-text:latest` |
...@@ -34,13 +34,13 @@ cluster node that only needs one thing runs; the full image is what a ...@@ -34,13 +34,13 @@ cluster node that only needs one thing runs; the full image is what a
workstation runs. All of them expose the same API on port 8000 (capability workstation runs. All of them expose the same API on port 8000 (capability
images) or 8776 (full image), and all of them can be a cluster node. images) or 8776 (full image), and all of them can be a cluster node.
Every tag is also published with its version (`:0.2.81`); `:latest` moves Every tag is also published with its version (`:0.2.82`); `:latest` moves
only after the image has completed a real request on real hardware. only after the image has completed a real request on real hardware.
The capability images do not all move together: a profile is rebuilt when its The capability images do not all move together: a profile is rebuilt when its
own dependencies change, so `:latest` is the newest build *of that profile*, own dependencies change, so `:latest` is the newest build *of that profile*,
not of the release. At the time of writing the full image and most capability not of the release. At the time of writing the full image and most capability
images are `0.2.81`/`0.2.80`, while `coderai-llama`, `coderai-vllm`, images are `0.2.82`/`0.2.80`, while `coderai-llama`, `coderai-vllm`,
`coderai-engines`, `coderai-engines-kt`, `coderai-ocr-paddle`, `coderai-engines`, `coderai-engines-kt`, `coderai-ocr-paddle`,
`coderai-speaker`, `coderai-stt-crisper`, `coderai-stt-nemo` and `coderai-speaker`, `coderai-stt-crisper`, `coderai-stt-nemo` and
`coderai-tts-xtts` are still `0.2.18`. Pull `:latest` and check the version `coderai-tts-xtts` are still `0.2.18`. Pull `:latest` and check the version
...@@ -63,7 +63,7 @@ a `policy-controller` / `cosign` admission rule on a cluster, or simply the ...@@ -63,7 +63,7 @@ a `policy-controller` / `cosign` admission rule on a cluster, or simply the
command above in whatever pulls. command above in whatever pulls.
For an offline machine, pull the image where there is a connection and For an offline machine, pull the image where there is a connection and
move it: `docker save ghcr.io/nextime/coderai:0.2.81 | gzip > coderai.tar.gz` move it: `docker save ghcr.io/nextime/coderai:0.2.82 | gzip > coderai.tar.gz`
there, `docker load < coderai.tar.gz` here. there, `docker load < coderai.tar.gz` here.
## Linux ## Linux
......
...@@ -230,3 +230,14 @@ def test_admin_scope_is_opt_in_at_creation(): ...@@ -230,3 +230,14 @@ def test_admin_scope_is_opt_in_at_creation():
assert 'is_admin = bool(data.get("admin"))' in src assert 'is_admin = bool(data.get("admin"))' in src
assert '"admin": is_admin,' in src assert '"admin": is_admin,' in src
assert '@app.patch("/admin/api/tokens/{token_id}"' in src assert '@app.patch("/admin/api/tokens/{token_id}"' in src
def test_the_admin_scope_is_settable_from_the_tokens_page():
"""The API supports the flag; the page must actually expose it, or granting
it means hand-editing auth.json."""
html = (ROOT / "codai/admin/templates/tokens.html").read_text()
assert 'id="t-admin"' in html # opt-in at creation
assert "admin: document.getElementById('t-admin').checked" in html
assert "function setTokenAdmin" in html # toggle on an existing one
assert "method:'PATCH'" in html
assert "${t.admin ? 'checked' : ''}" in html # reflects current state
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment