Skip to main content

Platform and auth

This page documents the commands that manage who you are, who else can get in, and where your work goes: signing in and out, checking your identity, managing projects, granting and revoking access, issuing and revoking credentials, and reading the project's audit trail and limits.

Most of these commands change who can reach your data. Where a command refuses to take a secret as a flag value, or asks you to repeat yourself, that is deliberate and the reason is written next to it.

Command summary

Signing in

CommandWhat it does
platformctl loginSign in and cache a 12-hour session token
platformctl logoutDiscard the cached token
platformctl whoamiShow who you are and which credential the CLI is using

Projects and CLI config

CommandWhat it does
platformctl projects listList the projects you can access
platformctl projects getShow the selected project, including your two roles on it
platformctl projects createCreate a project
platformctl projects updateRename the selected project's slug and/or display name
platformctl projects deleteDelete the selected project (requires --confirm)
platformctl config set-projectSave a default project
platformctl config get-projectShow the saved default
platformctl config viewShow the config file path and contents

Your own credentials

CommandWhat it does
platformctl keys listList the API keys you hold, live and revoked
platformctl keys createCreate a key that acts as you and print its secret once
platformctl keys revokeRevoke one of your keys, effective immediately

Project access

CommandWhat it does
platformctl iam members listList everyone who holds a role on the project
platformctl iam members addGrant a role to an account that already exists, by email
platformctl iam members set-roleChange a member's role, by user id
platformctl iam members removeRevoke a member's role, by user id
platformctl iam service-accounts set-roleChange a service account's project role
platformctl iam service-accounts removeRevoke a service account's project role
platformctl iam break-glassTake temporary, recorded, tenant-visible access
platformctl invitations listList the project's invitations in every state
platformctl invitations createInvite an email address and print the link once
platformctl invitations revokeRevoke an invitation by id
platformctl invitations previewShow what a token grants, without redeeming it
platformctl invitations acceptRedeem a token and join its project

Machine principals

CommandWhat it does
platformctl service-accounts listList the project's service accounts
platformctl service-accounts createCreate a service account and grant it a project role
platformctl service-accounts deleteDelete it and revoke every key it holds
platformctl service-accounts keys listList a service account's keys
platformctl service-accounts keys createMint a key and print its secret once
platformctl service-accounts keys revokeRevoke one key

Platform administration

CommandWhat it does
platformctl users listList platform accounts
platformctl users createCreate an account, with a generated temporary password
platformctl users set-roleChange a user's platform role
platformctl users reset-passwordIssue a new temporary password
platformctl users deleteDelete an account
platformctl orgs listList organizations
platformctl orgs createCreate an organization
platformctl orgs deleteDelete an empty organization
platformctl orgs members listList an organization's members
platformctl orgs members addGrant an org-level role
platformctl orgs members set-roleChange an org member's role
platformctl orgs members removeRevoke an org-level role

Crusoe Cloud

CommandWhat it does
platformctl crusoe-cloud showReport whether the project is connected, and to what
platformctl crusoe-cloud connectStore the project's Crusoe Cloud credential — required before the project can deploy anything
platformctl crusoe-cloud disconnectRemove it
platformctl crusoe-cloud buckets list / createObject-storage buckets in the connected account
platformctl crusoe-cloud repositories list / createContainer-registry repositories in the connected account
platformctl crusoe-cloud models listManaged inference models the connected account can serve

Reading the record

CommandWhat it does
platformctl auditShow one page of the project's audit trail, newest first
platformctl quotaShow the project's limits and current usage

platformctl login

Signs you in with email and password and caches the session token so later commands can use it.

platformctl login [--email <email>] [--password <password>]
FlagDefaultWhat it does
--email$CAI_USERYour account email. Accounts are created by an administrator or via an invitation link — there is no self-service sign-up.
--password$CAI_PASSWORDYour password.

Example:

platformctl login --email you@example.com --password 'your-password'

You should see:

signed in as you@example.com; token cached

The token is written with owner-only permissions to <UserConfigDir>/crusoe-ai/token (macOS: ~/Library/Application Support/crusoe-ai/token; fallback: $TMPDIR/crusoe-ai-token). Sessions last 12 hours; after that, sign in again.

Errors you may hit:

  • Missing credentials:
need an email and password: pass --email/--password, or set $CAI_USER/$CAI_PASSWORD
  • Wrong email or password — the server returns one deliberate, undifferentiated message for both, so account existence cannot be probed:
invalid email or password
  • Too many attempts (the server throttles sign-ins per IP and per account):
too many sign-in attempts; try again in <duration>
Temporary passwords

If your account was created with a temporary password, login still caches a token but tells you the account must change its password before it can manage anything. Set a new password in the web console at https://console.codyhill.dev, then log in again. Until then, every management command returns 403 with password change required before using this API.


platformctl logout

Deletes the cached token.

platformctl logout
platformctl logout

You should see:

cached token discarded (note: $CAI_TOKEN and the automation token, if present, still apply)

That note matters: logout only removes the cached login. If $CAI_TOKEN is set, the CLI keeps authenticating with it — unset that variable too if you want the CLI to stop acting as you. (Some installs also carry a machine credential your administrator configured; that is the "automation token" the message mentions, and it is not something you create or manage.) Session tokens are stateless and cannot be revoked server-side before their 12-hour expiry.


platformctl whoami

Shows who the platform thinks you are and which credential the CLI found.

platformctl whoami
platformctl whoami

You should see:

you@example.com role=user (credential: cached login (~/Library/Application Support/crusoe-ai/token))

The role shown is your effective role, re-read from the server — not whatever your token claims. If no credential is found:

not authenticated (management commands will fail; invoking agents still works)

Projects

A project is the isolation unit everything you create belongs to. Four of these five commands act on the project you are scoped to--project, then $CAI_PROJECT, then the default saved by config set-project — and take no project argument. That is deliberate: naming a project on the line while a different one is selected would be two answers to the same question, and the losing one would be silent.

projects list

platformctl projects list
platformctl projects list

You should see a table with the columns SLUG, NAME, SHORT, ROLE, and ID:

SLUG NAME SHORT ROLE ID
ml-team ML Team ab12cd admin 1c9a...

ROLE is your resource role in the project (admin or member) — your authority over the project's contents. SHORT is the project's immutable short id, and it appears in your workloads' addresses; it never changes, even if the project is renamed.

projects get

platformctl projects get
platformctl projects get --project ml-team

Prints the selected project as field/value rows:

created_at 2026-03-04T18:22:07Z
id 1c9a4f7e-2b30-4c81-9f0a-5d7e6b21c944
name ML Team
org_id 7f0c1d2e-9a44-4f13-8c65-2e0b7a913dd1
short ab12cd
slug ml-team
your_metadata_role admin
your_role admin

The two roles are separate answers. your_role is your authority over the project's contents — agents, secrets, topics, indexes. your_metadata_role is your authority over the project object — renaming it, deleting it, changing its membership. They differ: a platform admin holds admin on the second for every project while holding nothing on the first, which is why a console can show you a project's page and still hide the resource pages behind it.

projects create

platformctl projects create <slug> [--name <display name>] [--org <org id>]
FlagDefaultWhat it does
--namethe slugDisplay name.
--orgyour own organizationOrganization id (a UUID, from platformctl orgs list) to create the project in.
platformctl projects create ml-team --name "ML Team"

The slug is the short, lowercase handle you pass to --project afterwards. This is the one command in the group that takes the project as an argument — it is creating one, so there is nothing for --project to select yet.

--org resolves the target; it does not authorize it. Creating a project requires the org admin role on that organization, or platform admin.

projects update

platformctl projects update [--slug <new slug>] [--name <new display name>]
FlagDefaultWhat it does
--slugunchangedNew slug — the handle you pass to --project.
--nameunchangedNew display name.
platformctl projects update --name "Machine Learning"

Only the flags you pass are sent, so renaming the display name cannot silently revert a slug someone else just changed. Passing neither is an error rather than a no-op request.

A rename is safe because of what it does not touch: the project's short id is immutable, and every resource path, hostname and running workload derives from it. It moves the label a person reads and nothing a machine resolves.

projects delete

platformctl projects delete --confirm <slug|short|id>
FlagDefaultWhat it does
--confirmnone — requiredThe slug, short id, or id of the project to delete, repeated as confirmation.
platformctl projects delete --project ml-team --confirm ml-team
deleted project ml-team

This is the only command in platformctl that asks you to repeat yourself, and it earns the exception twice over. The project comes from ambient state — --project, $CAI_PROJECT, or the saved default — so there is nothing on the command line to check before you press enter, and the deletion is not recoverable. --confirm is what puts the target back on the line where you can read it.

The server also refuses two cases outright: the default project cannot be deleted, and neither can a project that still has agents. Deleting one would destroy their code and logs, so they have to go first, deliberately.

If the project you deleted was your saved default, the CLI tells you on stderr that the saved default no longer resolves.


platformctl config

Manages the CLI's saved default project, so you do not have to pass --project on every command.

config set-project

platformctl config set-project <project>
platformctl config set-project ml-team

You should see:

default project set to ml-team

Pass an empty string to clear the default:

platformctl config set-project ""
Env still wins

$CAI_PROJECT overrides the saved default, and the --project flag overrides both — the same layering as gcloud, aws, and az.

config get-project

platformctl config get-project

Prints the saved default project, if any.

config view

platformctl config view

Prints the config file's path and the saved default_project. The file is <UserConfigDir>/crusoe-ai/config.json, written with 0600 permissions, containing {"default_project":"..."}.


Using an API key instead of signing in

For scripts and CI, skip login entirely. Create a key with platformctl keys create (or, for a machine principal that is not you, platformctl service-accounts keys create) and export it:

export CAI_TOKEN=cai_xxxxxxxxxxxx_...
platformctl whoami

You should see:

ci-deploy@ab12cd.cai.local role=user (credential: $CAI_TOKEN)

Keys have the format cai_<keyid>_<secret> — a fixed, greppable prefix, so a secret scanner can find one that leaked into a commit. $CAI_TOKEN is checked first, ahead of every other credential source. A key acts as its owner, re-resolved on every request: if the owner loses a grant, the key loses it on its next call. Revocation is immediate. The CI/CD tutorial walks through the full setup.

One more credential source sits below $CAI_TOKEN and the cached login: $CAI_USER plus $CAI_PASSWORD, which makes the CLI sign in on the spot and cache nothing. The full resolution order is on the CLI overview.


Your own API keys

platformctl keys manages the keys that act as you. They are not project-scoped, so --project and $CAI_PROJECT are ignored here.

keys list

platformctl keys list
KEY_ID DISPLAY_NAME CREATED EXPIRES LAST_USED STATUS
k7q2m4x9r1t0 ci 2026-05-02T09:14:22Z never 2026-08-12T06:41:03Z live
b3f8n2j5w6c1 laptop 2026-01-18T21:07:55Z 2026-07-18T21:07:55Z 2026-07-11T15:22:40Z expired

Revoked and expired keys are listed too — a credential's history is part of answering "what did this key do". STATUS is three-way and says which of the three you are looking at: live, expired (a lifetime lapsed; mint another), or revoked (someone cut this credential off on purpose; minting another undoes their decision).

keys create

platformctl keys create [--name <label>] [--expires-in-days <n>]
FlagDefaultWhat it does
--namenoneLabel so you can tell keys apart later, e.g. ci, laptop.
--expires-in-daysthe key does not expireLifetime in days, 1–3650.
platformctl keys create --name ci
key id: k7q2m4x9r1t0
name: ci
expires: never

secret (shown once):
cai_k7q2m4x9r1t0_9f3c1a...

copy this now - only a hash is stored, so it cannot be shown again. If it is lost, revoke this key and create another.

The secret gets its own line so it can be selected cleanly. Only a hash is stored, so nothing can show it to you again — a lost secret is replaced by revoking the key and creating another, never recovered.

No expiry is the default on purpose rather than out of laziness: a credential that silently stops working on an unremarked Tuesday is worse than one whose lifetime somebody chose.

keys revoke

platformctl keys revoke <key>
platformctl keys revoke k7q2m4x9r1t0
revoked k7q2m4x9r1t0

<key> is the KEY_ID from keys list; the row's full id also works. Revocation is effective immediately — the next request presenting the key is refused — and it is final. Rotating a credential means creating a new key and revoking the old one; the id is never reused.

These endpoints act on behalf of a person

keys is a person's command group. Run it with a machine credential and the server refuses with 403 rather than 401 — the caller did authenticate, there is simply no human for "sign in" to apply to:

this endpoint acts on behalf of a person and cannot be used by a automation credential; use a user session or a user API key

The credential's kind is named in the message, so a service account's own key reads service_account credential in the same sentence.

keys create and keys revoke refuse a service account earlier still, and say why:

a service account cannot create or manage credentials. Sign in as a user (or use your own API key) to issue keys - otherwise a leaked key could mint replacements and revoking it would achieve nothing

That is the whole point: a credential that could mint credentials would make revoking a leaked one pointless. To manage a machine principal's keys, use platformctl service-accounts keys from a user session.


Project access

platformctl iam manages who holds a role on the project you are scoped to. There are two roles: admin may change the project's membership as well as use its resources; member may use the resources but not change who else can.

A project always keeps at least one human admin. The server refuses any removal or demotion that would leave it with none.

iam members list

platformctl iam members list
EMAIL ROLE KIND EXTERNAL EXPIRES
ada@example.com admin user false -
grace@partner.example member user true -
ci-deploy@ab12cd.cai.local member service_account false -
sre@example.com admin break-glass false 2026-08-12T18:03:11Z

EXTERNAL marks a member whose home organization is not this project's — an invited outsider. That is the "who outside my org can see this?" question a project admin actually needs answered.

Break-glass grants appear in this list on purpose, expired ones included. Emergency access the tenant cannot observe would be a backdoor whatever the intent behind it. Use -o json to see who took the grant and the reason they gave.

The write commands key on the user id, not the email

iam members set-role and iam members remove take a user id — a UUID, not an email address. An address can be re-pointed at a different account; an id cannot.

The table view above shows addresses. Run platformctl iam members list -o json to get the ids: each row carries user_id, and a service-account row carries service_account_id for the iam service-accounts commands.

iam members add

platformctl iam members add <email> [--role admin|member]
FlagDefaultWhat it does
--rolememberRole to grant.
platformctl iam members add ada@example.com --role admin

This is the one member command that takes an email, because it looks the address up. The account is never created here: if nobody holds that address, the server says so and points you at the invitation endpoint, so the person chooses their own password instead of being handed one. See invitations below.

iam members set-role

platformctl iam members set-role <userID> --role admin|member
FlagDefaultWhat it does
--rolenone — requiredNew role: admin or member.
platformctl iam members set-role 6b2f0c31-4a7d-4e15-9b0e-1c8a5d33f902 --role member

Demoting the project's last human administrator is refused: it would leave a project only a platform admin could ever change again.

iam members remove

platformctl iam members remove <userID>
platformctl iam members remove 6b2f0c31-4a7d-4e15-9b0e-1c8a5d33f902

Only the grant goes. The account itself is untouched; it simply loses access here. Removing the project's last human administrator is refused.

This is also how a break-glass grant is handed back early, rather than waiting for it to expire.

iam service-accounts set-role and remove

platformctl iam service-accounts set-role <serviceAccountID> --role admin|member
platformctl iam service-accounts remove <serviceAccountID>

A service-account grant lives in the same membership list as a person's, but it is addressed by the service account's own id, so it has its own commands. Find the id with platformctl iam members list -o json — the service-account row carries service_account_id.

remove revokes the grant, not the account: the service account and its keys keep working elsewhere, they just no longer reach this project. To delete the account and revoke every key it holds, use platformctl service-accounts delete.

No last-admin guard applies here. A machine credential cannot manage membership, so a service account is never the administrator a project would be orphaned without.

iam break-glass

platformctl iam break-glass --reason <text> [--role admin|member] [--minutes <n>]
FlagDefaultWhat it does
--reasonnone — required, at least 8 charactersWhy you need this access. Shown to the project's members verbatim.
--roleserver default: adminRole to grant yourself.
--minutesserver default: 240 (four hours), maximum 1440How long the grant lasts.
platformctl iam break-glass --project ml-team \
--reason "INC-4412: project admins unreachable, restoring agent deploys" \
--minutes 60
grant.break_glass true
grant.email sre@example.com
grant.expires_at 2026-08-12T15:03:11Z
grant.granted_by sre@example.com
grant.reason INC-4412: project admins unreachable, restoring agent deploys
grant.role admin
grant.user_id 9d41b7a2-58ce-4f6b-b0a1-3f27e6c8a115
note this grant is visible to the project's members and recorded in its audit log. It expires on its own; remove it sooner with DELETE /v1/projects/1c9a4f7e-.../members/9d41b7a2-...

This is for platform administrators repairing a project whose own admins are gone or locked out. It is not a bypass, and four properties are what make that true:

  1. It is an ordinary membership row. There is no second authorization path anywhere.
  2. The tenant can see it. It appears in platformctl iam members list, which the project's own members read, carrying who took it, why, and until when.
  3. It is audited. Actor, project, reason, role and expiry land in the project's audit trail, which the project's members can also read with platformctl audit.
  4. It expires on the clock, enforced in the authorization query rather than by a cleanup job — so it ends whether or not anyone remembers.

--reason is refused under eight characters rather than defaulted. Say what you are fixing and name the ticket if there is one; a reason nobody can act on is worse than none.

The command is platform-admin-only. If you already hold a grant on the project, or hold none to escalate from, the server refuses:

break-glass access is for platform admins repairing a project they are not a member of; you already hold a grant here, or you hold none to escalate from

A machine credential cannot take break-glass access at all — the entire value of the row is that a named person is accountable for it.

Hand a grant back early with platformctl iam members remove <your user id>. Background on the design is in break-glass and audit.


Invitations

An invitation is for an email address that has no account yet — that is the whole feature. Inviting an address that already has an account grants it the role directly instead of minting a token nobody needs, and invitations create tells you that is what happened.

list, create and revoke are project-scoped and need project admin, since an invitation is a membership change. preview and accept need neither a project nor a session: the token is the credential.

invitations list

platformctl invitations list
ID EMAIL ROLE STATE EXPIRES
3a1c9f04-77b2-4d38-9e51-0c6a2b7d5e83 newhire@example.com member open 2026-08-19T11:02:44Z
be27d150-8f4a-4c19-a7d3-6b90e1f24c77 grace@partner.example admin accepted 2026-07-30T08:15:10Z

STATE is one of open, accepted, revoked, or expired. Accepted, revoked and expired invitations stay in the list as history — the state column is what separates them from the ones still worth chasing.

Tokens are never listed. An issued token exists only in the response that created it.

invitations create

platformctl invitations create <email> [--role admin|member]
FlagDefaultWhat it does
--rolememberRole the invitee gets on acceptance.
platformctl invitations create newhire@example.com --role member

When the address has no account, the token is minted and shown once:

invited newhire@example.com as member
id: 3a1c9f04-77b2-4d38-9e51-0c6a2b7d5e83
expires: 2026-08-19T11:02:44Z
accept url: /invite/2f9d7c14a8b3...
token: 2f9d7c14a8b3...

The accept url is a path, not a full link: join it to your console's base URL
(the console serves the acceptance page at /#/invite/<token>).

The token is not stored in the clear, so it can never be retrieved again. Send it to the invitee now; if it is lost, revoke the invitation and issue a new one.

When the address already has an account, no token is minted at all:

no invitation was created: grace@partner.example already has an account, so the member role was granted to it directly

That is the correct outcome, and it avoids somebody deciding the address is "taken" and inventing a variant of it.

The invitee may be outside your organization — inviting them is how they get in.

invitations revoke

platformctl invitations revoke <invitationID>
platformctl invitations revoke 3a1c9f04-77b2-4d38-9e51-0c6a2b7d5e83
revoked invitation 3a1c9f04-77b2-4d38-9e51-0c6a2b7d5e83

The id is the ID column of invitations list. The token stops working immediately, which is the move when a link went to the wrong address or was forwarded somewhere it should not have been. The invitation stays in the list with state revoked: the record that it was issued is worth more than a clean list.

invitations preview

platformctl invitations preview <token>
account_exists false
email newhire@example.com
expires_at 2026-08-19T11:02:44Z
project_name ML Team
project_slug ml-team
role member

Unauthenticated, because the person holding the link may not have an account yet. For the same reason it deliberately shows nothing about the project beyond its name and slug — whoever holds a forwarded link is not a member of anything.

account_exists is the field to read before accepting. false means the address has no account, so accepting also creates it and accept needs a password. An expired, revoked or already-accepted token reports that it is no longer valid rather than what it used to grant.

invitations accept

platformctl invitations accept <token> [--password-file <path>]
FlagDefaultWhat it does
--password-filenoneFile holding the password for the new account this invitation creates (at least 12 characters, mixing at least 3 of 4 character types). - or an empty value reads stdin. Omit it entirely when the invited address already has an account.

A password is needed only when the invited address has no account yet. Accepting is then also the moment that account is created — this is the platform's only self-service account-creation path, reachable exclusively by presenting a valid, project-admin-issued token.

The password is read from a file or from stdin. It is never accepted as a flag value, so it cannot land in your shell history or in the process table of a shared machine:

platformctl invitations accept 2f9d7c14a8b3... --password-file ./new-password
read -rs pw && printf %s "$pw" | platformctl invitations accept 2f9d7c14a8b3... --password-file -
accepted true
email newhire@example.com
note sign in with this email to use the project
project_id 1c9a4f7e-2b30-4c81-9f0a-5d7e6b21c944
role member

If the address already has an account, accepting grants the role and nothing else: it never sets or resets that account's password, so a stolen link cannot become a password reset. Leave --password-file off entirely in that case — it is then neither read nor sent.

Then sign in with platformctl login.


Service accounts

A service account is a machine principal that belongs to exactly one project and holds its role through the same grant table users do. Creating, deleting or keying one requires the project admin role — and none of it can be done with a service account's own credential.

The group is also available as platformctl sa.

service-accounts list

platformctl service-accounts list
NAME EMAIL ROLE DISABLED ID
ci-deploy ci-deploy@ab12cd.cai.local member false 4e81c2a6-...

service-accounts create

platformctl service-accounts create <name> [--display-name <label>] [--role admin|member]
FlagDefaultWhat it does
--display-namenoneHuman-readable label for the account.
--descriptionnoneAlias for --display-name, accepted by the server.
--roleserver default: memberRole to grant on this project.
platformctl service-accounts create ci-deploy --display-name "GitHub Actions deploy"

The name is the local part of the account's email — <name>@<project>.cai.local — and follows the same DNS-1035 rule as project and agent names.

Names are reserved permanently, including after a delete, so a new principal can never inherit a deleted one's grants or audit history. A name that comes back "taken" will not become free again.

Creating the account does not create a credential. Follow with service-accounts keys create.

Pass --role admin only when the workload genuinely has to change who else can reach the project.

service-accounts delete

platformctl service-accounts delete <name>
platformctl service-accounts delete ci-deploy
deleted service account ci-deploy (its keys were revoked; the name stays reserved)

Every key the account holds is revoked in the same transaction, which is what makes this the right lever when a workload's credentials are in doubt: one call, no enumeration. The delete is soft and the name stays reserved, so the audit rows and log lines naming this principal keep resolving.

service-accounts keys list

platformctl service-accounts keys list <name>
ID KEY_ID NAME STATUS EXPIRES LAST_USED
c0d4... m2x8k5q7n3v1 github-actions live never 2026-08-12T04:19:58Z
a71b... t9r4w6z2p8j5 old-runner revoked never 2026-06-02T13:44:07Z

STATUS is the same three-way column as your own keys, and for a machine credential the distinction is most of the answer. expired means a rotation schedule lapsed and the fix is to mint another; revoked means somebody cut this workload off on purpose, and minting another undoes their decision.

service-accounts keys create

platformctl service-accounts keys create <name> [--display-name <label>] [--expires-in-days <n>]
FlagDefaultWhat it does
--display-namenoneHuman-readable label, e.g. where the key is deployed.
--expires-in-daysthe key does not expireLifetime in days, 1–3650.
platformctl service-accounts keys create ci-deploy --display-name "github-actions"
key id: m2x8k5q7n3v1
id: c0d47f92-1e6b-4a58-8d03-b2c9e0714a6f (pass this to 'service-accounts keys revoke')
expires: never

cai_m2x8k5q7n3v1_4b1e9d...

copy this now - only a hash is stored, so it cannot be shown again. If it is lost, revoke this key and create another.

This is the only moment the secret exists. Only a hash is stored, so a lost key is replaced, never recovered.

The expiry line is printed even when the server sends none, so you never have to guess whether the key never expires or the response simply did not say — "never" is the answer you have to plan a rotation around.

service-accounts keys revoke

platformctl service-accounts keys revoke <name> <keyID>
platformctl service-accounts keys revoke ci-deploy m2x8k5q7n3v1
revoked key m2x8k5q7n3v1 on ci-deploy - the next request presenting it is refused

<keyID> is the ID or the KEY_ID from service-accounts keys list. The key_id is the public half that appears in audit lines and in the response that created the key, so whichever you copied is accepted and resolved for you.

Revocation is per-key. To cut off every credential an account holds at once, delete the account instead.


Platform users

platformctl users manages the accounts that can sign in to the platform. These are platform accounts, not project memberships: "role" here is admin or user across the whole platform, while who may do what inside one project is managed with platformctl iam.

These routes are global rather than project-scoped, so no --project is needed and none is sent. Listing and editing accounts requires a platform admin; creating one is also open to an org or project admin, for their own org.

users list

platformctl users list
EMAIL ROLE AGENTS PROJECTS MUST_CHANGE
ada@example.com admin 4 ml-team,web no
newhire@example.com user 0 ml-team yes

The AGENTS count is scoped to the projects you hold a grant in — administering accounts does not by itself let you enumerate another tenant's workloads — so a platform admin with no grant in a project sees 0 there rather than a number they are not entitled to.

Agents with no owner (deployed before accounts existed, or by a machine credential) belong to nobody and are reported on stderr after the table, so the listing accounts for every agent you can see and -o json still pipes cleanly.

users create

platformctl users create <email> [--role admin|user] [--org <org id>] [--password -]
FlagDefaultWhat it does
--roleserver default: userPlatform role. Pass admin only for someone who should administer the platform itself.
--orgyour own orgOrg id to create the account in. Naming an org you may not write into is refused.
--passwordthe platform generates a temporary oneInitial password, or - to read it from stdin.
platformctl users create newhire@example.com
agent_count 0
agents -
created_at 2026-08-12T14:31:09Z
email newhire@example.com
external false
id 2c7f9a10-6b48-4d92-a3e1-70c5f8b41d26
must_change_password true
org_id 7f0c1d2e-9a44-4f13-8c65-2e0b7a913dd1
projects -
role user

temporary password (shown once, hand it over now):
Yb7-qz2K4mNv

shown once - the user must change it at first login

Omitting --password is the expected path: the platform generates a strong temporary password and returns it once, so an admin never has to invent a password on someone else's behalf. Either way the account must change its password at first login.

The password is printed on its own line, to stdout and only stdout. An admin who redirected stdout to a file asked for the secret to land there; echoing it to stderr as well would copy it into whatever log the other stream is pointed at.

If you must choose the password yourself, pass --password - and give it on stdin. As a flag value it would be left behind in your shell history and in the process's argv, where anyone else on the same host can read it off ps.

users set-role

platformctl users set-role <email> --role admin|user
FlagDefaultWhat it does
--rolenone — requiredNew platform role: admin or user.

The change is effective immediately, not at token expiry: authorization reads the role on every privileged request. Two edits are refused by design — removing your own admin role (ask another admin) and demoting the last admin.

users reset-password

platformctl users reset-password <email>
platformctl users reset-password newhire@example.com
email newhire@example.com

temporary password (shown once, hand it over now):
Qr4-mv8T2xKd

shown once - the user must change it at next login

Replaces the user's password with a freshly generated temporary one, presented exactly the way users create presents its own. This is the way back in for someone locked out — there is no reset-by-email.

users delete

platformctl users delete <email>
deleted newhire@example.com

Deleting an account stops future logins for it. Two things it deliberately does not do:

  • Agents the user deployed are not deleted. They keep running and show up as unowned in users list. Cascading a workload delete off a user removal would be a destructive surprise.
  • An already-issued session token is signed and stateless, so it remains valid until it expires.

Organizations

An organization is the top of the hierarchy: it owns projects and users, and an org admin inherits the admin role on every project in the org. Creating, deleting, and re-grading membership are platform-admin actions.

These routes are not project-scoped, so --project and $CAI_PROJECT are ignored here.

orgs list

platformctl orgs list
SLUG NAME PROJECTS USERS ID
acme Acme Inc 3 12 7f0c1d2e-9a44-4f13-8c65-2e0b7a913dd1

The ID column is the org id that projects create --org, users create --org, and every orgs members command take.

orgs create

platformctl orgs create <slug> [--name <display name>]
FlagDefaultWhat it does
--namethe slugDisplay name.

The slug is the org's stable, unique handle; the server lowercases and trims it.

orgs delete

platformctl orgs delete <orgID>

The server refuses rather than cascades: an org that still has projects or users returns a conflict naming which, and the default organization cannot be deleted at all.

orgs members

platformctl orgs members list <orgID>
platformctl orgs members add <orgID> <email> [--role admin|member]
platformctl orgs members set-role <orgID> <userID> --role admin|member
platformctl orgs members remove <orgID> <userID>
CommandFlagDefaultWhat it does
add--rolememberOrg role to grant.
set-role--rolenone — requiredNew org role.

list shows EMAIL, ROLE and CREATED. As with project members, add takes an email because it looks the account up, while set-role and remove take a user id; get it from platformctl orgs members list <orgID> -o json, which carries user_id.

Adding is not upserting. Re-adding someone who already holds the same role is a no-op, and re-adding at a different role is refused, so an accidental repeat can never silently re-grade them. Use set-role to change a role.

Removing or demoting an organization's only admin is refused. A removal is effective immediately: the removed member loses the project access they inherited from the org and keeps only the project memberships they hold in their own right.


The model credential

There is no platform-wide model credential, and platformctl profile has been removed along with it. The profile show, profile set-model-key and profile clear-model-key commands documented here previously wrote one key shared by every tenant and copied into every project. Any agent could read it out of its own environment, spend could not be attributed to the project that caused it, and nothing capped it — which stopped being tolerable once a project could publish a genuinely public agent.

A model credential now belongs to a project, and it is the customer's own key from Crusoe Intelligence Foundry:

  • In the console: Project Settings, which holds both of a project's bring-your-own credentials — the Foundry key for inference, and the Crusoe Cloud key for the container registry and object storage.
  • Over the API: GET/PUT/DELETE /v1/projects/{id}/inference. Writes are project admin. The value is write-only — a read reports whether a key is mapped and its last four characters, never the key.

A single agent that must use a different key, or bill separately, still overrides its project with platformctl secrets set <agent> MODEL_API_KEY=….

platformctl audit

Shows recent audit entries for the project, newest first.

platformctl audit [--limit <n>] [--page-token <token>]
FlagDefaultWhat it does
--limitthe server's page sizeEntries to show, 1–200.
--page-tokennoneContinue from the token printed by a previous run.
platformctl audit --limit 20
TIME ACTOR ACTION TARGET
2026-08-12T14:03:11Z sre@example.com project.break-glass ml-team
2026-08-12T09:41:52Z ada@example.com apikey.create k7q2m4x9r1t0
2026-08-11T17:20:03Z ada@example.com member.add grace@partner.example

Every member of a project can read its audit trail. The entries that matter most are the ones you did not cause — a platform admin taking break-glass access, membership changing, credentials being issued or revoked.

Paging

One page is shown per invocation, deliberately. The audit trail is the one list on this platform that grows without bound, so following the cursor to exhaustion could pull years of rows for a command whose normal use is "what just happened".

When older entries exist, the command prints the continuation token to stderr:

older entries exist; continue with --page-token eyJ0cyI6IjIwMjYtMDgtMTFUMTc6MjA6MDNaIn0

Pass it back to continue:

platformctl audit --page-token eyJ0cyI6IjIwMjYtMDgtMTFUMTc6MjA6MDNaIn0

The token goes to stderr so that stdout stays exactly the entries — platformctl audit -o json | jq ... pipes cleanly, and the truncation notice still fires in the machine formats, where a silently short read is most dangerous.

The -o json and -o yaml views carry the full row for each entry, including the detail object the table has no room for.

Background on what is recorded and why is in break-glass and audit.


platformctl quota

Shows the project's resource limits and how much of each is in use.

platformctl quota
RESOURCE USED LIMIT USED_PCT
Workloads 3 50 6%
Running instances 7 unlimited -
Storage volumes 2 20 10%

The figures are read live from the platform's own accounting, so they are exactly what is being enforced — there is no second copy that could drift.

A limit of unlimited means no cap is set for that resource, and the percentage column shows - there rather than a misleading 0%.

-o json and -o yaml carry the whole envelope, including the raw resource keys and the server's note about how to read the figures.


platformctl change-password

Replaces the password of the account you are signed in as.

This is usually the first command a new account needs. An administrator-created account is issued a temporary password, and until you replace it every management call is refused — deploy, status, logs, secrets, all of it — with a message saying a password change is owed.

platformctl change-password

The command prompts for your current password and then the new one. Your current password is required even though you are already signed in: a session proves who you are, not that you are still the person who chose the password, and asking for it is what stops a stolen session from locking the real owner out of their own account.

Two related commands do different jobs, and mixing them up is easy:

CommandWho runs itWhat it does
platformctl change-passwordYouReplaces your own password.
platformctl users reset-passwordAn administratorIssues someone else a new temporary password, which that person then replaces with the command above.

Passwords must be at least 12 characters, and at least 3 of these 4: an uppercase letter, a lowercase letter, a number, a symbol. See create an account.

Finds a resource by a case-insensitive substring of its name, across every kind the platform knows about — agents, functions, serverless services, MCP servers, service accounts, secrets, and projects. Use it when you know part of a name but not which kind of thing it is or which project it lives in.

platformctl search checkout

Each match comes back with its kind, its project, and the console route that opens it.

FlagWhat it does
--all-projectsSearch every project you can see, ignoring the selected project.
--limit <n>Stop after this many matches. 0 means every match, following pages as needed.

platformctl crusoe-cloud

Connects the project to a Crusoe Cloud project and browses what that connection reaches. cc is an alias for the whole group.

This group is a prerequisite for deploying, not an optional extra. Every image the platform builds for you is pushed to a repository in your own Crusoe Cloud container registry, so a project with no stored credential is refused at submit:

this project cannot deploy yet: its container images are built into your own Crusoe Cloud
container registry, and no Crusoe Cloud credential is mapped to this project.

Connecting is a one-time, project-admin action. Reading the connection and the three discovery lists is open to any project member. Disconnecting, and creating buckets or repositories, are admin-only too.

crusoe-cloud show

platformctl crusoe-cloud show
connected.
crusoe cloud project: ml-platform
project id: b6f1a0c2-1f2e-4a55-9a4e-2c0a7f8d3e11
region: us-east1-a

An unconnected project is a normal answer here, not an error — the output says so and names the command that fixes it:

not connected: this project has no Crusoe Cloud credential.
connect one with 'platformctl crusoe-cloud connect --access-key-id <id>'.

A blank region is not a failure either. The platform only infers one from a deployed model or an existing bucket, and a brand-new account has neither, so the line reads not known yet (pass --region when creating a bucket).

crusoe-cloud connect

printf %s "$CRUSOE_SECRET_KEY" | platformctl crusoe-cloud connect --access-key-id 'CRUSOEEXAMPLEKEYID'
platformctl crusoe-cloud connect --access-key-id 'CRUSOEEXAMPLEKEYID' --secret-key-file ./secret-key
connected - the credential was accepted by Crusoe Cloud and stored.
crusoe cloud project: ml-platform
project id: b6f1a0c2-1f2e-4a55-9a4e-2c0a7f8d3e11
region: us-east1-a
FlagWhat it does
--access-key-id <id>The access key's public identifier. Safe as a flag: it authenticates nothing on its own.
--access-key-file <path>Read the id from a file instead, for when you keep the pair on disk.
--secret-key-file <path>Read the secret key from a file, or - for standard input. Standard input is the default.
--cc-project-id <id>Which Crusoe Cloud project to map to. Required only when the key can reach more than one.
--region <region>Pin the region instead of letting the platform infer one.

There is no --secret-key flag, and that is the point. A credential passed as an argument lands in your shell history, in the process table while the command runs, and in any shell tracing or audit rule watching arguments. A file or a pipe is none of those things.

The key is checked before it is stored: the platform calls Crusoe Cloud with it and refuses the connection if it is rejected, so a typo fails here rather than as a mystery three commands later. Once accepted it goes into the project secret store and is never returned by any endpoint — not by crusoe-cloud show, not by the project secret commands, which cannot see it at all.

Re-running connect replaces the whole mapping, not just the key, and re-provisions the project's image-pull credential — which is also how you refresh a rotated key. A --region pinned on an earlier connect is not carried over: pass it again, or the platform goes back to inferring one.

crusoe-cloud disconnect

platformctl crusoe-cloud disconnect
disconnected - the stored credential is destroyed and image pulls from the connected registry will stop working

Nothing in Crusoe Cloud is touched: buckets, repositories, and images all keep existing. But the project stops being able to deploy until you connect a credential again, and workloads pulling private images fail on their next pull. Reconnect with crusoe-cloud connect.

crusoe-cloud buckets, repositories, models

platformctl crusoe-cloud buckets list
platformctl crusoe-cloud buckets create incoming-docs --region us-east1-a # --region is required
platformctl crusoe-cloud repositories list
platformctl crusoe-cloud repositories create checkout-api [--region us-east1-a]
platformctl crusoe-cloud models list

These are live reads of Crusoe Cloud, briefly cached. On an unconnected project they refuse with a message saying so rather than returning an empty list — "you have no buckets" and "there is no key to ask with" are different facts, and only one of them is worth acting on.

You do not need repositories create for a deploy: the platform creates the repository a workload needs on its first build. --region is required for a bucket (its region is where the bytes physically live) and optional for a repository. The task-oriented guide is Crusoe Cloud integration.

Everything else has a command

Two areas belong to sibling pages rather than this one: MCP servers and the project secret store are on MCP and secrets, and agents, functions and the data services are on agents and functions and data and messaging.

For the HTTP endpoints behind all of this, see the platform API reference.