Users, Roles and Access
Manage users from the admin console: search, roles, session and token revocation, closed registration and Astound SSO groups. Then provision in bulk from the CLI or the systemprompt MCP server.
On this page
The admin console at /admin/users manages accounts, roles, status, sessions and personal access tokens. Browser sign-in uses Astound SSO. Operators can also provision and recover access from the CLI. Public registration, browser invite redemption and passkey login are not offered by this installation.
The first half of this page is the console. The second half is provisioning from the CLI, in bulk, and through the systemprompt MCP server.
Managing users from the admin console
Open /admin/users as an admin. From this one page you can:
- Search and list users by name or email, with pagination.
- Create a user directly.
- Edit roles and status — grant or remove roles by hand (see Roles), activate or suspend an account.
- Disable or delete an account. Disabling keeps history; deleting removes the account.
- Revoke sessions — force sign-out everywhere with one action.
- Revoke personal access tokens — every PAT the user holds can be revoked from their detail page, or from
/admin/devices, which lists tokens across the instance. - See last activity — each user row shows last-active time, and the Inactive 30d filter lists accounts with no activity in thirty days, including accounts that were provisioned and never used.
No CLI or database access is required for any of these operations. The scripting equivalent is the CLI below.
The roster and a person's page
/admin/users is the roster. The KPI strip across the top doubles as filters:
Accounts, Enabled, Unassigned (no group), No role (no grant
beyond user), Idle 30d (not seen by any signal — provisioned but unused),
plus 30-day gateway cost and request counts. Every filter exports to CSV.
A user's detail page (/admin/users/<id>) holds their roles, groups and
projects, sessions (revoke from here), and a Usage tab with their
requests, cost and conversations.
New user creates an account from the browser only when
auto_provision: false in services/web/config/adfs.yaml. This installation
runs with auto_provision: true — the AD group is the invitation — so the
button is disabled and explains why. The CLI and the MCP route below work
either way.
Provisioning users
- Admin CLI — an operator creates the account directly (below). Full control, scriptable, works headless.
- Astound SSO just-in-time — first sign-in creates the account, with the
role and project the AD groups map to, provided the assertion passes the domain and group-claim gates and
auto_provisionis on. See Authentication.
Public self-registration, passkey login, and browser invite redemption are not available in this installation. See Authentication for operator recovery.
The login page offers Sign in with Astound SSO. A verified assertion must carry an allowed email domain and a group claim when deny_without_group is enabled. Mapped groups grant their configured roles; an unmapped group receives user. Directory role grants are refreshed at sign-in, while manual grants survive. Users may belong to zero, one, or several projects. See Set Up Astound SSO (AD FS).
Deprovisioning follows the directory: disabling the AD account stops sign-in immediately, and removing someone from a Systemprompt-* group strips the role it granted at their next sign-in. Mid-session revocation is bounded by the session TTL. An admin can revoke sessions and PATs immediately from /admin/users.
SCIM provisioning is not offered. AD FS does not push standards-based SCIM, so an endpoint would have no caller. See the Enterprise Roadmap for when this changes.
Roles
This is the one role table for the installation; other pages link here. The
set is closed — Role in extensions/web/shared/admin-core/src/types/role.rs
owns the spelling — and seven roles ship.
| Role | Kind | Admin console | What it opens |
|---|---|---|---|
user |
baseline | No (participation only) | Own profile, history, connectors and connected clients |
developer |
entitlement | No (participation only) | Whatever rules.yaml allows to role: [developer] — engineering marketplaces and MCP servers |
knowledge_worker |
entitlement | No (participation only) | Whatever rules.yaml allows to role: [knowledge_worker] — the Cowork workspace, business skills |
project_manager |
entitlement | No (participation only) | Whatever rules.yaml allows to role: [project_manager]; nothing on the admin plane |
super_admin |
entitlement | No | The astound-super-admin marketplace, its plugin, the admin skills and the systemprompt MCP server |
admin |
console | Yes, unscoped | Every admin page and write action |
platform_admin |
console | Yes, unscoped | Everything admin has, plus moving platform_admin / super_admin |
Only admin and platform_admin are console roles (ROLES_CONSOLE). They
see the whole instance, unscoped. Every other role is an entitlement role:
it decides which marketplaces, plugins, skills and MCP servers reach a person
through Access Control, and opens nothing on
the admin dashboard by itself.
Console access by marketplace participation
Anyone else reaches the console through marketplace participation: when a
marketplace's allow rule in services/access-control/rules.yaml names a group
or project the person belongs to, they participate in that marketplace. A
participant gets a scoped console:
- Analysis → Conversations and Analysis → Versions, narrowed to the people in the groups and projects their marketplaces name, and to those marketplaces' versions.
- Code sync (
/admin/sync) for the bundles that own their marketplaces, with each apply scoped to that marketplace. - The session, request, trace and conversation detail pages those views link to. A detail page outside their view returns 404.
Every other admin route redirects a participant to their profile. A person
who participates in no marketplace sees only their account pages (/admin/profile,
/admin/history, /admin/connect, /admin/connectors). The allowlists are
ACCOUNT_PATHS and PARTICIPANT_PATHS in
extensions/web/shared/admin-core/src/middleware/gates.rs.
So a project manager sees the conversations and versions of the marketplaces
their groups and projects participate in — not every project, and never a
write action on users, roles, access rules or gateway configuration. They do
not get the systemprompt MCP server, the astound-super-admin plugin or the
admin skills.
Where roles come from
Roles have two halves, recomputed together on users.roles:
- Directory half — projected from AD groups on every sign-in
(replace, not union).
Systemprompt-Admins→platform_admin, admin;Systemprompt-CommerceandSystemprompt-Core→developer;Systemprompt-Business→knowledge_worker;Systemprompt-ProjectManagers-*→project_manager. Everyone also getsuser. The map isgroup_rolesandgroup_role_patternsinservices/web/config/adfs.yaml; see Set Up Astound SSO (AD FS). - Manual half — granted by an admin on
/admin/roles, on a user's detail page (/admin/users/<id>), or from the CLI. A manual grant survives sign-in; a directory role cannot be revoked by hand — remove the AD group instead.
systemprompt admin users role promote jane@example.com # grant admin
systemprompt admin users role demote jane@example.com # revoke admin
systemprompt admin users role assign jane@example.com knowledge_worker
Only a platform admin may grant or remove platform_admin or super_admin,
and the last platform admin cannot be demoted (handlers/roles.rs).
Role checks read the user record on every request, so revocation is immediate. The OAuth scope in a session token is fixed when the token is issued — a freshly promoted admin must sign out and back in (and re-link the bridge) before admin surfaces appear.
Personal access tokens and devices
/admin/devices lists every personal access token on the instance: owner, prefix (so a leaked token can be identified without exposing it), expiry, and creation time. Any token can be revoked immediately, and revocation takes effect on the next request. Issue a token on the same page mints one for the signed-in account and shows the secret exactly once.
Every token has an expiry. security.max_pat_lifetime_days in services/web/config/security.yaml (default 90) is the longest a token may live from issuance, enforced on every path that mints one — the console form, POST /admin/devices/pats, and device enrolment. A request with no expiry gets the maximum; one asking for more is refused with a 400 rather than silently clamped. Tokens issued before the policy changed keep the expiry they were issued with.
Per-key limits and scope bindings
A key (personal access token) can carry its own policy, set when it is issued. Without it, a key inherits its owner's scope and counts against the owner's quota windows only. With it, the key is its own quota subject (api_key), on top of the owner's windows, so one application's key can be capped without capping the person.
| Limit | Effect |
|---|---|
| Model allowlist | A request for a model not on the list is refused with 403 (model '<id>' is not allowed for this API key) before routing. |
| Budget (microdollars) and request ceiling | Ceilings over the key's own window (request_window_seconds, required when either is set). They reserve, settle and answer the machine-readable 429 exactly like policy quota windows, and follow the instance's quota_mode, which is warn here: a crossing is recorded, not refused. |
| Scope bindings | One value per scope dimension (for example project=<id>). The key's requests are attributed to that value, with source api_key, unless the request names the dimension by header. The value is checked against the owner's memberships at issuance and on every request: a value the owner does not hold is refused with 403, an unregistered dimension with 400. |
From the CLI:
systemprompt admin users api-key issue --user <user> --name ci-runner \
--model claude-sonnet-5 \
--budget-microdollars 50000000 --max-requests 2000 --window-seconds 86400 \
--scope project=<project-id>
systemprompt admin users api-key list --user <user> # prints each key's limits and scopes
--model and --scope repeat. With none of these flags, issuance is unchanged.
From the API, POST /api/v1/admin/api-keys accepts model_allowlist, budget_microdollars, max_requests, request_window_seconds and scopes, with the same membership check, and GET /api/v1/admin/api-keys returns them. The Issue a token form on /admin/devices does not set limits; use the CLI or API for a limited key.
Device certificates can be given an expiry from the certificates tab (Set expiry). The window is recorded beside the certificate and the hourly sweep revokes the certificate once it passes.
Time-bound sharing
Share tokens honour an expiry timestamp, and setup tokens, session JWTs and personal access tokens are all time-bound, so no credential lives forever.
Access itself is time-bound too. A group membership, a project membership, a manual role grant and an access-control rule each carry an optional expiry, set from the console with a date picker — the Add member dialog on a group or project, the Roles section of a person's page (one window for every role granted by hand), and the rule editor's reason box on a group's or person's Access tab. Membership tables and the /admin/access-control ledger show an Expires column, badge anything that lapses inside seven days, and the ledger's Expiring in 7 days filter lists what needs renewing. Directory-sourced membership and directory-projected roles never carry a window: Active Directory decides when those end.
How an expiry binds differs by what it is on, because two of the four are decided by code this extension does not own:
| What expires | Takes effect | How |
|---|---|---|
| Group or project membership | Immediately | Every membership read, including the group/project authorization dimensions and the user_groups view, hides a row outside its window. The hourly access_expiry sweep then stamps the row revoked_at so it remains as the audit trail, and recomputes the person's primary scope. |
| Manual role | Within the hour | Roles are enforced from the effective set on the user record, which the sweep rewrites without the expired grant. If that removes a manage role, the sweep revokes the person's live sessions, tokens and certificates — the same teardown a demotion in the role editor performs. |
| Access-control rule | Within the hour | Core's resolver reads the rule table directly, so the sweep deletes the expired rule. The window round-trips through rules.yaml as valid_until: on the entity (an RFC 3339 instant): the loader treats a declaration already past it as not declared, the export writes it back, and a window that differs between code and database is reported as drift on the Sync page. |
| Device certificate | Within the hour | The sweep stamps the certificate revoked, which is what the device gate reads. |
Re-adding a member whose earlier membership expired revives the row with the new window. For contractors, combine a membership expiry with the PAT lifetime cap above; account-level disable and revocation remain available for the immediate case.
Archive instead of delete
Deleting a person archives them (NFR-5.2). The account stops signing in and leaves the roster. Their sessions, tokens and certificates are revoked at once, and who archived them, when and why is recorded. Their requests, decisions and usage stay. Restore on the person's page, or systemprompt admin users restore <user>, brings the account back within retention.archived_users_days (90 days by default). After that window, the nightly cleanup purges the account and its rows, unless it is under legal hold (systemprompt admin users legal-hold <user>). See Data Retention.
Context-aware access control
What a session can reach — marketplaces, plugins, skills, MCP servers, gateway routes — is decided per request from the person's current context: their role, the groups and projects the last SSO sign-in placed them in, and the accounts they have linked. The narrowest band that names them decides, a deny beats an allow inside it, and an entity with rules is closed to everyone they do not name. Every rule is declared once in services/access-control/rules.yaml with a stated reason and enforced from the database; /admin/access-control shows who reaches what and why, and its Sync page reconciles code and console in either direction. The full model, worked examples and how-tos are in Access Control.
Create one user, end to end
# 1. Create the account (idempotent with --if-not-exists)
systemprompt admin users create --name jane --email jane@example.com \
--full-name "Jane Doe" --if-not-exists
# 2. Optional: make them an admin
systemprompt admin users role promote jane@example.com
# 3. Issue a bridge exchange code so they can connect their laptop
systemprompt admin bridge issue-code --user-id jane@example.com
Send the code through a channel you trust — it is single-use with a 10-minute TTL. The user then installs the bridge with it: Install the Desktop Bridge.
Browser access requires Astound SSO. The CLI retains WebAuthn administration commands, but this installation has no browser passkey setup or sign-in flow.
Bulk creation
--if-not-exists makes creation idempotent, so a CSV loop is safe to re-run:
# users.csv contains name,email,full_name rows without a header or quoted commas
while IFS=, read -r name email full_name; do
systemprompt admin users create --name "$name" --email "$email" \
--full-name "$full_name" --if-not-exists
done < users.csv
Follow with role promote for the rows that need it, and bridge issue-code
per user as they onboard.
The full CLI surface
systemprompt admin users --help is the authoritative list. The highlights:
| Command | Purpose |
|---|---|
list / show / search / count / stats |
Inspect the user base |
create / update / delete |
Lifecycle |
export |
Dump users for reporting or migration |
merge |
Combine duplicate accounts |
bulk … |
Batch operations |
ban … |
Block access |
role promote / role demote / role assign |
Role management |
session … |
Inspect and revoke sessions |
webauthn … |
Passkey management and setup tokens |
apikey |
API key management |
Conversational admin: the systemprompt MCP server
Admins who have connected Claude Code through the bridge get the
systemprompt MCP server automatically. It exposes the admin surface as
tools, so user management becomes conversational:
"Create accounts for these 40 people from this spreadsheet, and give the team leads admin roles."
Three properties make this safe:
- Admin-gated — a non-admin token gets HTTP 403 on every tool; the server does not even appear in their client until promotion (and a re-link).
- Audited — every tool call lands in the same governance spine as inference calls, with identity, arguments, decision, and trace ID.
- Same surface — the tools wrap the same CLI commands documented above; there is no separate, wider API.
For non-admins the server is simply absent — nothing to misconfigure.
Check it yourself
/admin/users: the roster, with search, roles, status and last activity. Try the Inactive 30d filter./admin/users/<id>: role editing, session and token revocation, and memberships with their expiry./admin/devices: every personal access token and device certificate, with expiry and a revoke action./admin/access-control: the ledger of who reaches what, with the Expiring in 7 days filter.systemprompt admin users listsystemprompt admin users show <email>systemprompt admin users --help