Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,36 @@ _Nothing yet._

---

## [0.5.31] - 2026-09-21

AINode gets a front door: a person signs in to the dashboard with a name and a password and stays signed in until they sign out, while programs keep using API keys; accounts live on the master and replicate to every node, sessions stay per node, admins manage users from the dashboard or the box, the doctor checks for an account, any client written for TypeSafe's Jev can point at a node through /v1/systemone, and the bench can present a key and no longer scores a refused run as a model that could not answer.

### Added
- **The bench can present an API key, and a node that refuses a run no longer scores the model at zero.** Every section of `scripts/ainode-bench.py` (speed, harness, agentic, decide, embed, speech) takes `--api-key` and falls back to `$AINODE_API_KEY`, and the key goes on every request a run makes: the timed completions, the placement reads, the telemetry poll and the harness token window. It is never printed and never written into a record, so a run reports only where it came from. A 401 stops the section before anything is scored and says `this node wants an API key (pass --api-key or set AINODE_API_KEY)`; a 429 stops it the same way and names the limit that refused it. Neither is ever recorded as a model that could not answer.
- **A login for people, beside the API key for machines** (#261). Named accounts with passwords live in `~/.ainode/users.json` (0600, hashed with `hashlib.scrypt` over a per-password salt), and `POST /api/auth/login` hands a browser an `HttpOnly`, `SameSite=Lax` session cookie that the middleware accepts exactly where it accepts a key. A session does not expire: you are in until you log out, or until somebody revokes the session, changes the password or disables the account. Only a hash of the session token reaches disk, so a copy of `users.json` logs nobody in.
- **Roles, and a node that cannot be locked away from its operator.** An `admin` may manage accounts, API keys and the auth switch; a `member` may use the node and gets a 403 from all three. The last enabled admin cannot be removed or disabled, an operator API key and the fleet key both count as an admin (which is how the CLI works and how the first admin is created on a fresh node), and `GET /api/auth/users/export` / `POST /api/auth/users/sync` are the fleet key's alone, because they move password hashes.
- **Session and account routes**: `POST /api/auth/login|logout|password`, `GET /api/auth/me|sessions|users`, `DELETE /api/auth/sessions/{id}`, and the account CRUD under `/api/auth/users`. `/api/auth/login` and `/api/auth/me` answer with no credential, because the caller with none is exactly who knocks on the first and the second only ever says `{"user": null}` to a stranger.
- **`ainode auth user add|list|remove|passwd|disable|enable` and `ainode auth session list|revoke|clear`.** Dashboard accounts and sign-ins from the box. `add` and `passwd` prompt twice with `getpass`; `--password-stdin` is the path where there is no TTY, which includes the installer's `docker exec -it` wrapper under `ssh` and every script. Removing or disabling the last admin is refused, because with auth on and no admin the dashboard can only be opened by pasting an API key.
- **`ainode auth status` prints the user, admin and session counts beside the key count, and names the fix when a protected node has no account.**
- **Account replication (`ainode/auth/replication.py`).** The cluster's master is the one authority for accounts. It pushes every change to its peers with the fleet key and retries a peer that is behind on a 60 second tick; a worker pulls the master's list at startup and every 5 minutes. Sessions are never replicated: a session is one browser's credential against one node. A node with no `cluster_secret`, or with no peers, does nothing.
- **`ainode doctor` gains a Login check.** FAIL when a key is required and no dashboard account exists, WARN with accounts but no enabled admin, WARN on a `users.json` wider than 0600 (`--fix` tightens it), and WARN on a worker whose account list is older than the master's.
- **The installer prints `ainode auth enable` and `ainode auth user add <name> --admin` after an install that requires a credential, and nothing when `AINODE_AUTH=off`.**
- **`POST /v1/systemone`, TypeSafe's System One (Jev) wire format over this node's own decision core, so a client written for the hosted endpoint answers off a model on this fleet with its endpoint changed and nothing else (Titanium's JDE, browser-use's jev-ultrafast, the TypeSafe SDK, the playground). Choice, noul and score questions in, typed answers out: a choice answers with the caller's own criteria key, a noul answers P(true), a score answers the expected level with a legend from position to level name. `confidence` is the hosted service's chance-corrected `(n * p_max - 1) / (n - 1)`, inferred from its published examples, with the raw distribution reported beside it. A malformed request is a 422 naming the field, no node serving the model is a 503, and a 200 always carries every question asked. Calibration is the model's: nothing on this path rescales what the engine reported. A question carries at most 20 criteria, which is how many labels one engine call reports a probability for.**

### Changed
- **The README describes 0.5.30.** The CLI reference gained `ainode tls renew`, `auth key list` and `revoke`, `prune-images`, the `doctor` statuses including INFO, and the note that `tls enable --tailscale` and `tls renew` run on the host side of the wrapper; feature rows for adoption at restart, the fit check, the Metrics view, Whisper verified and the V100 Flash-Next lane match the code.
- **A 401 now names both doors**: "This node requires a login or an API key." A person told only about a Bearer token pastes a machine key into a browser, which is the thing this release exists to stop.
- **A cookie-authenticated write must carry `X-AINode-Client: dashboard`.** A browser attaches a cookie to a cross-site request on its own, and a custom header cannot be set cross-origin without a preflight this node never approves, so the header is the CSRF proof. Without it the cookie is ignored and the 401 says so. A Bearer token needs nothing and keeps precedence, so the bench, the desktop app and every script behave exactly as before.
- **Failed logins are throttled per source address**: ten in five minutes, then a 429 with `Retry-After`. Per address and not per name, because a name-keyed lockout is one an attacker can aim at somebody else's account.
- **Sign in to the dashboard.** A node that requires auth now asks for a name and a password instead of an API key. You sign in once and stay signed in until you sign out: no more pasting a key meant for a script into a browser. Keys are still how programs authenticate, and a browser with no account on a node can still use one (Config > API access, "Use a key in this browser instead"). New in Config: **Account** (your name and role, change password, your sessions with a Revoke per browser) and **Users** (admins add, reset, disable and remove accounts; the node refuses to remove its last admin). The header shows who is signed in, with Sign out beside it. When a session ends or auth is switched on while you are reading, the dashboard drops back to the sign-in screen with one line saying why, rather than leaving a page of panels that all fail.
- **The docs describe how a person gets in, not only how a program does.** The docs site has a new page next to API keys in the Security group, [Sign in and accounts](https://docs.ainode.dev/security/sign-in): why there is a login, the first account on a fresh node and on a fleet that is already running (`ainode auth user add <name> --admin`, then `ainode auth enable`), signing in, the account page, the Users page and the two roles, the `auth user` and `auth session` commands, why a login belongs behind HTTPS with the TLS page linked, the two files and their modes, the guard rails (HttpOnly and Secure cookie, the `X-AINode-Client: dashboard` CSRF header, ten failed sign-ins in five minutes pausing an address, scrypt hashes), the paths that stay open, and troubleshooting for locked out, no accounts yet and a revoked session. The README's access control section says people sign in and programs use keys, carries the two setup commands, and lists `/api/auth/me` and `/api/auth/login` among the keyless paths. Docs PR getainode/ainode-docs#8.

### Fixed
- **The host wrapper the installer writes asks for a TTY only when it has one.** It ran every forwarded command with `docker exec -it`, so any piped input (now including `ainode auth user add --password-stdin`) died on "the input device is not a TTY" before the CLI in the container ran.
馃 Generated with [Claude Code](https://claude.com/claude-code)

---

## [0.5.30] - 2026-09-19

AINode grows up: a fresh install is protected by default and a fleet can run with auth on everywhere, a tailnet certificate is one command with renewal, a restart or an update keeps the engines that are serving, Whisper runs on the fleet with a speech bench behind its verified flag, the dashboard draws the metrics it keeps, a download is refused when it will not fit, the doctor stops warning about what it cannot see, and Castor's Flash-Next lane is a catalog entry.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ainode"
version = "0.5.30"
version = "0.5.31"
description = "Turn any NVIDIA GPU into a local AI platform. Inference + fine-tuning in your browser."
readme = "README.md"
license = {text = "Apache-2.0"}
Expand Down
Loading