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 @@
<div class="table-wrap">
<table>
<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>
<tbody id="tokens-body">
<tr class="empty-row"><td colspan="6">No tokens — create one to get started</td></tr>
......@@ -43,6 +43,13 @@
<option value="custom">Custom</option>
</select>
</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">
<button class="btn btn-primary" onclick="createToken()">Generate</button>
<button class="btn btn-ghost" onclick="closeModal('create-modal')">Cancel</button>
......@@ -104,6 +111,9 @@ async function loadTokens() {
<td class="td-name">${esc(t.name)}</td>
<td><code>${esc(t.token.substring(0,28))}…</code></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="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>
......@@ -111,13 +121,25 @@ async function loadTokens() {
} 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() {
const name = document.getElementById('t-name').value.trim();
if (!name) { document.getElementById('t-name').focus(); return; }
try {
const r = await fetch(ROOT_PATH + '/admin/api/tokens', {
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) {
const d = await r.json();
......
......@@ -7,7 +7,7 @@ public, pulled without credentials. There is nothing to build.
| 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-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` |
......@@ -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
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.
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*,
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-speaker`, `coderai-stt-crisper`, `coderai-stt-nemo` and
`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
command above in whatever pulls.
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.
## Linux
......
......@@ -230,3 +230,14 @@ def test_admin_scope_is_opt_in_at_creation():
assert 'is_admin = bool(data.get("admin"))' in src
assert '"admin": is_admin,' 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