Accounts, subaccounts and CLI profiles¶
Editions: OSS, Cloud, Enterprise. Unless stated otherwise, everything on this page ships in OSS.
Multi-account, subaccounts, sharing and access rules are served by an
extension plugin. The open-source server does not have these endpoints. The
console and the CLI show the related views and commands only when
GET /api/v1/features reports the matching capability:
| Capability | Console | CLI |
|---|---|---|
multi_account |
Header account switcher, account chooser at sign-in | accounts works against the server's memberships |
account_hierarchy |
Settings > Subaccounts, Settings > Access grants, sharing on resource pages, read-only shared views, usage and attention by subaccount | subaccounts, share |
abac_rules |
Tags on resource pages, Policies > Access rules | tags, access |
All three are false unless a plugin sets them. With all three off, none of
the gated routes, nav items or panels is registered and their code is never
downloaded. If an account endpoint answers 404 anyway (for example after the
plugin was removed), the view hides without an error toast. A 404 on one item,
such as a sibling's subaccount id, shows "not found".
CLI profiles and accounts¶
~/.preloop/config.yaml can hold several named profiles, and each profile
can hold one token pair per account. A file written before profiles existed
keeps working unchanged: its top-level keys are the default profile.
# The default profile, exactly as before profiles existed.
access_token: <token>
refresh_token: <token>
api_url: https://preloop.ai
current_profile: work # optional; used when no flag or env var picks one
profiles:
work:
api_url: https://preloop.example.com
access_token: <login token>
refresh_token: <login token>
current_account: north # set by `preloop accounts switch`
accounts:
north:
account_id: 6c1f...
name: North site
access_token: <token for north>
refresh_token: <token for north>
Profile and account names are lowercase letters, digits, - and _.
Which session a command uses¶
- Profile:
--profile, thenPRELOOP_PROFILE, thencurrent_profile, thendefault. - Account:
--account, thenPRELOOP_ACCOUNT, then the profile'scurrent_account. With no account, the profile's own token pair is used. - Token:
--tokenandPRELOOP_TOKENstill override everything.
If an account is asked for and the profile holds no tokens for it, the
command stops with a hint to run preloop accounts switch <slug>. It never
falls back to another account's tokens.
Commands¶
preloop accounts listlists the accounts you belong to and marks the current one.preloop accounts switch <slug|id>asks the server for a token pair for that account, stores both tokens under the profile and makes it current.preloop accounts currentprints the profile and account in use;preloop auth statusshows them too.preloop subaccounts list|create|rename|detach|delete(account_hierarchy).preloop share list|add|rm(account_hierarchy).share addadds a share and leaves existing shares alone;share rm <share-id>stops one.preloop tags list|set|rm <kind> <id>(abac_rules). Keys the parent account governs are read-only.preloop access rules list,preloop access rules apply -f <file|->andpreloop access explain --subject --action --resource(abac_rules).applyreplaces this account's rules with the file, so the file is the source of truth.
The gated groups are hidden from preloop --help unless the server reports
their capability. Running one anyway prints which capability is off.
Endpoint contract for the extension¶
All paths are under /api/v1. A 404 on a collection means "capability off" to
both clients. {kind} is one of ai_model, mcp_server, managed_agent,
flow, runner_pool or policy. For runner_pool the id is the pool name,
and for policy it is the id of the active baseline version.
| Method | Path | Body and notes |
|---|---|---|
| GET | /me/memberships |
{items:[{account_id, account_name, slug, parent_account_id, last_used_at}]} |
| POST | /auth/switch-account |
{account_id}, returns {access_token, refresh_token} |
| GET, POST | /accounts/{id}/subaccounts |
POST {name, tags} |
| GET, PATCH, DELETE | /accounts/{id}/subaccounts/{sub_id} |
PATCH {name, tags}; 404 when sub_id is not a child of id |
| POST | /accounts/{id}/subaccounts/{sub_id}/detach |
|
| GET, POST | /accounts/{id}/access-grants |
{subject_type, subject_id, level, target, subaccount_ids} |
| DELETE | /accounts/{id}/access-grants/{grant_id} |
|
| GET | /accounts/{id}/shares?resource_type=&resource_id= |
A resource can have several shares |
| POST | /accounts/{id}/shares |
{resource_type, resource_id, target}; target is {type: all}, {type: selected, subaccount_ids} or {type: tag, key, value} |
| DELETE | /accounts/{id}/shares/{share_id} |
|
| GET | /accounts/{id}/shared-resources/{kind}/{resource_id} |
{kind, id, name, provider, identifier, description, price, shared_from}; never credentials |
| GET | /accounts/{id}/usage/rollup?subaccount_id=&start=&end= |
{rows:[{subaccount_id, subaccount_name, model, day, requests, cost_usd}]} |
| GET | /accounts/{id}/attention/rollup |
{items:[{subaccount_id, subaccount_name, count}]} |
| GET, PUT | /tags/{kind}/{resource_id} |
GET {tags, governed_keys, version}; PUT {tags, version} |
| GET, PUT | /access/rules |
GET {rules, inherited, modes, version}; PUT {rules, version} |
| GET | /access/rules/export |
{yaml} |
| POST | /access/rules/apply |
{yaml} |
| POST | /access/explain |
{subject, action, resource}, returns {effect, reason, rule_ids} |
| POST | /access/modes/preview |
{action, mode}, returns {losing_access, preview_token} |
| PUT | /access/modes/{action} |
{mode, preview_token} |
Tag and rule writes carry the version the client read. The server should
answer 409 when the stored set has a different version, and the clients then
reload instead of overwriting. A client that sends version: null has not
read a version (for example against a server that does not return one).
Moving an action to require_permit can remove access. The console enables
the save only after it has shown the preview, and sends the preview's
preview_token so the server can check that the preview was seen.