Manage people and the tenancy tree, from the web and the CLI

Nothing in the tree could be renamed, moved or removed, and an approved
person stayed in the project they were first put in — from the web or
the CLI. Now:

- /admin/users is the tree, each project opening onto its people and
  keys, with approve, move, suspend, reinstate and remove for people,
  revoke for keys, and add, rename, move and remove at every level,
  plus a search over every account. Forms post and get the page back,
  so it works with no script; htmx 2.0.11 (0BSD, embedded, checked
  against npm's integrity hash) swaps only the changed part, keeping
  open projects open.
- llmgw org tree/add/rename/move/remove, users move/delete, and
  keys list --all.
- Projects, teams and organizations are named by id, path
  (Acme/platform/gateway) or unique name; users by id or email. With a
  single project --project can be left out, instead of defaulting to
  "default", which a tree built from the web does not have.

Moving a person moves the keys they made; operator keys stay with the
project. Only empty things are removed, and never a project that had a
key, since usage history refers to it.

Also: keys init no longer reparents an existing team or project;
reinstating an account with no project is refused (a rejected signup
used to come back active and fail at its first key); rejecting a
signup removes it; keys limits --budget takes whole units like the
quotas page.

Checked in headless Chrome: a move from an open project updates the
page in place, keeps the project open, and the search still filters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-09-28 16:26:39 +02:00
1 parent 5141e4b138
commit 3104f4b706
21 files changed
+2081 -217

No files matched your search

+1 -1
View File
@@ -141,7 +141,7 @@ not count the same free terabyte once per node.
Set budgets and limits on day one, not later:
```
llmgw keys limits --scope project --id <project> --rpm 60 --budget 50000000
llmgw keys limits --scope project --id <project> --rpm 60 --budget 50
```
The reason is not technical. A resource that appears free and unlimited becomes,
+41 -11
View File
@@ -88,11 +88,36 @@ means anything.
If you have no structure yet, one of each is fine:
```
llmgw keys init --store … --org mynodes --team default --project default
llmgw org add org mynodes
llmgw org add team default --in mynodes
llmgw org add project default --in mynodes/default
llmgw org tree --people
```
It is idempotent: running it again renames rather than duplicating, so it can
live in whatever provisions the machine.
Everything that takes an organization, team or project — here, in `users` and
in `keys` — accepts its id, its path (`mynodes/default/default`) or its name
when the name is unique, so an id never has to be copied off a page. When there
is only one project, `--project` can be left out.
The tree is not fixed once made:
```
llmgw org rename project Acme/platform/gateway api-gateway
llmgw org move project Acme/platform/gateway --to Scania/ml
llmgw org remove team Acme/old # only when nothing is in it
llmgw users move ayse@example.com --project Acme/ml/research
llmgw users delete ayse@example.com --yes # signs out, revokes their keys
llmgw keys list --all # every key, its project and owner
```
Moving a person takes the keys they made with them, so they stop spending the
old project's budget; keys an operator issued in the project stay with it. A
project that ever had a key cannot be removed, because usage history refers to
it — rename it instead.
`llmgw keys init` still works for provisioning scripts and is idempotent, but it
will no longer move a team or project that already exists under another parent;
it says so and stops.
Split it when you want separate limits or separate reporting, not before.
@@ -156,16 +181,18 @@ Allowlists compose by intersection: a key cannot add a model its team withheld.
A model's canonical name and any of its aliases name the same thing, so either
form works.
Budgets are counted in micro-units, and what a unit means is yours to decide.
Self-hosted models have no per-token price, so it can be GPU-seconds, a notional
rate, or 1 000 000 everywhere — which turns a budget into a plain token quota.
Budgets are given in whole units, as the quotas page shows them, and what a unit
means is yours to decide. Self-hosted models have no per-token price, so it can
be GPU-seconds, a notional rate, or one unit per thousand tokens everywhere —
a price of 1 000 000 in `auth.pricing`, which is in micro-units — and a budget
becomes a plain quota of thousands of tokens.
Models are priced in the server's `auth.pricing` section; a model priced at zero
is exempt from budget checks, which is what lets a key with nothing left still be
served a cheap fallback.
```
llmgw keys limits --store … --scope project --id default \
--budget 50000000 --period month # 50 units a month
llmgw keys limits --store … --scope project --id Acme/platform/gateway \
--budget 50 --period month # 50 units a month
```
## The landing page
@@ -286,9 +313,12 @@ page exists.
## Managing accounts from the browser
`/admin/users` lists who is waiting and who is active. Approving (into a project)
and suspending are there, because that is the work an operator does constantly
and it should not need a terminal.
`/admin/users` is the tree: organizations, teams and projects, each project
opening onto its people and its keys. From there an admin approves, moves,
suspends and removes people, revokes keys, and adds, renames, moves and removes
organizations, teams and projects. A searchable list of every account is at the
bottom. The page works as plain forms; with scripts on, a change updates it in
place and the projects you had open stay open.
Three things are deliberately not there: