---
name: admin-user-management
description: "Add, remove, list admins on this account, and update an admin PIN. Triggers when the owner asks to invite another admin, remove an admin, see who has admin access, or change a PIN."
---

# Admin user management

Invoked by the admin agent directly.

This skill manages who has admin access to this account. It wraps six MCP tools and carries the discipline that keeps the three identity stores in lockstep. The owner (or any current admin) can manage admins; roles are labels only, with no capability differences enforced.

## The tools

| Tool | Purpose |
|---|---|
| `admin-add` | Add a new admin. Requires a name. PIN is optional: omit it and a unique 4-digit PIN is generated. PIN must be at least 4 digits and unique across all users on the device. |
| `admin-remove` | Remove an admin by `userId` (find IDs via `admin-list`). The last admin on the account cannot be removed. The user's device-level entry is retained so they can still administer other accounts. |
| `admin-list` | List all admins on this account with names and roles. |
| `admin-update-pin` | Update an admin's PIN. Defaults to the calling admin if no `userId` is given. PIN must be at least 4 digits and unique across all users on the device. |
| `remote-auth-set-password` | Set an admin's outer remote-access password — the one the tunnel asks for before the PIN screen. Per-admin: defaults to the calling admin, or pass `userId` from `admin-list`. Strength: 8+ chars, a digit, a special character, no spaces. |
| `remote-auth-revoke-password` | Revoke an admin's remote-access password by `userId`. They can no longer start a new remote session (an open one lapses within 24h). Refuses to revoke the last remaining one. |

## Remote access passwords vs PINs

The remote-access password is the *outer* gate — the password the Cloudflare tunnel asks for before the PIN screen — and it is independent of the PIN. Each admin has their own; setting or revoking one never touches another admin's password or anyone's PIN. The PIN still does identity (it names the admin in session rows); the access password just decides who can reach a tunnel-exposed install at all. On an install upgraded from the old single shared password, the owner's existing password migrates onto their own record automatically on first boot, so nobody is locked out.

## The three-store invariant

Admin identity lives in three places that must stay in lockstep:

1. `account.json` `admins[]`: account-level role record.
2. `users.json`: device-level PIN authentication. This is the source of truth at login.
3. The Neo4j graph: `:AdminUser` + `:Person` + `OWNS` + `ADMIN_OF` edges. Display and graph identity.

`admin-add` writes all three. `admin-update-pin` writes `users.json` only (the other stores carry no PIN). If any leg fails, the tool returns `is_error: true` and `server.log` carries a `[admin-auth-store] action=… userId=… result=fail store=…` line naming which leg failed and what was already written. When you see that line, tell the owner the record is half-written and may need manual reconciliation.

## Never write `account.json` directly

All account-level mutations (`tier`, `outputStyle`, `thinkingView`, `effort`, `enabledPlugins`, `admins[]`, agent settings) go through the dedicated MCP tools: `account-update`, `plugin-toggle-enabled`, `admin-add`, `admin-remove`. Direct `Edit` or `Write` on `account.json` is forbidden by IDENTITY.md doctrine — there is no server-side gate, so the doctrine line is the only thing standing between an agent slip and a silently corrupted account file. `tier` and `purchasedPlugins` derive from a Rubytech-signed entitlement payload on commercial installs, so a hand-edit is silently void.

`tier` is the one account-level field with no admin-agent surface — `account-update` excludes it deliberately, because tier is privilege-bearing and the agent must not self-elevate. When refusing a tier-cap violation (`admin-add` against a full slot, etc.), point the owner at the founder-side path: `npx @rubytech/create-<brand>-code@latest --tier <solo|family|pro>` run on the device by the founder. On commercial installs the founder delivers a signed payload instead: `--entitlement-base64 <base64>`.

## `admin-add` retries: re-pass any user-stated PIN

If `admin-add` returns an error (tier cap reached, PIN collision) and the owner resolves the blocker (upgrade tier, remove an existing admin), the retried `admin-add` MUST re-pass the PIN the owner originally stated as the `pin` parameter. Omitting `pin` on the retry auto-generates a different 4-digit PIN, silently substituting what the owner asked for. The resulting `admin-update-pin` correction loop is exactly the failure mode this rule exists to prevent.

## Standard flow

1. Read the request. Identify which of the four operations applies.
2. For `admin-add`, gather the name. The PIN is optional; if the owner stated one, use it; otherwise let the tool generate one. For `admin-remove`, call `admin-list` first if the owner did not give a `userId`.
3. Call the tool.
4. If the tool succeeds, confirm the result in plain English. Include the PIN in the reply for `admin-add` so the owner can share it with the new admin.
5. If the tool returns an error, surface the failure and the `[admin-auth-store]` line from `server.log` if present. Do not retry the same call; resolve the underlying blocker first.

## What this skill does not do

It does not manage tier upgrades. It does not change account-level settings. It does not manage public agents or plugins. Each of those has its own skill.
