utoid is built from this repository. One binary, no runtime, nothing to configure beyond the two variables below.
# from a checkout of the identity repository go build -o /usr/local/bin/utoid ./cmd/utoid utoid --help
For an agent, unattended — the key comes from Agents & keys:
export UTOID_SERVER=https://identity.utohub.app export UTOID_API_KEY=utoid_... # mint one under Agents & keys utoid whoami
For a person, once, through the browser:
utoid --server https://identity.utohub.app login # opens a browser once; the credential is kept for later runs
utoid user list | get | create | delete utoid app list | get | create | configure | rotate-secret | token | delete utoid permission list | create | delete utoid role list | show | create | delete utoid grant list | add | remove utoid check <subject> <permission> utoid agent list | create | delete utoid key list | create | revoke utoid audit list utoid session list | revoke # --json on any command for the raw response # exit 0 done · 3 refused · 1 everything else
Hand this to an agent and it knows the two layers, the ceiling on granting, how to read a refusal, and how to do the usual things without being told twice. Drop it in as SKILL.md.
---
name: utohub-identity
description: Administer the UtoHub identity hub — accounts, applications and their OIDC clients, roles and permissions, role bindings, agent credentials, and the audit record — through the `utoid` CLI. Use when asked to grant or take back access, register an application, mint or revoke an API key, or find out why something was refused.
---
# Administering the UtoHub identity hub
Every management action goes through `utoid`. It is non-interactive: there is
no prompt to answer and nothing to confirm, so it runs unattended.
## Credential
```sh
export UTOID_SERVER=https://identity.utohub.app
export UTOID_API_KEY=utoid_... # an agent acts with a key
```
A key is a credential, not an authority. What you may do comes from the roles
bound to the service account the key belongs to — revoking a key takes the key,
not the roles.
Check what you are and what you hold before assuming anything:
```sh
utoid whoami
```
## Reading output
Add `--json` to any command to get the raw response instead of a summary.
Parse that, never the human summary.
Exit codes: `0` success, `3` **refused** (you lack the authority), `1`
everything else. A refusal is not a crash — branch on it rather than retrying.
## The two layers
Authority lives in two layers and never reaches across:
- `platform` — the hub itself: accounts, applications, platform roles.
- `app:<app-key>` — one per application, whose vocabulary that application
declares for itself. The hub presumes nothing about what an application can do.
**The ceiling.** Granting is itself a permission, and it is checked against the
scope of the role being granted. A credential that administers `app:A` cannot
bind anyone — including itself — to a platform role. Do not try to work around
a refusal here; it is the rule, not an obstacle.
## Command surface
```sh
utoid user list | get <id> | create --email <e> --password <p> [--name <n>] | delete <id>
utoid app list | get <key> | create --name <n> [--desc <t>] | delete <key>
utoid app configure <key> [--redirect-uri u] [--scope s] [--grant-type g] \
[--response-type r] [--allow-origin o] [--public]
utoid app rotate-secret <key>
utoid app token <key> --secret <secret> [--scope s] # prove the client works
utoid permission list --scope <scope>
utoid permission create --code <scope>:<resource>:<action> [--desc <t>]
utoid permission delete <id>
utoid role list --scope <scope> | show <id> | delete <id>
utoid role create --scope <scope> --name <n> [--desc <t>] [--permission <code>]...
utoid grant list --scope <scope> [--subject <type:id>]
utoid grant add --subject <type:id> --role <role-id>
utoid grant remove <grant-id>
utoid check <subject> <permission> # allow or deny, and why
utoid agent list | create <name> [--desc <t>] | delete <name>
utoid key list [--agent <name>] | create --agent <name> [--name <label>] [--ttl <seconds>]
utoid key revoke <key-id>
utoid audit list [--action a] [--actor <type:id>] [--result allow|deny] [--limit n]
utoid session list [--subject <id>] [--client <app-key>] | revoke <request-id>
```
Subjects are written `<type>:<id>`: `user:12`, `service_account:release-bot`.
## How to do the usual things
**Give someone access to an application.** Find the role, then bind it. Roles
are per-layer, so list within the application's own scope:
```sh
utoid --json role list --scope app:<app-key>
utoid grant add --subject user:12 --role <role-id>
```
**Stand up a new application end to end.**
```sh
KEY=$(utoid --json app create --name "Agile" | jq -r .app_key)
utoid app configure "$KEY" --redirect-uri https://agile.utohub.app/callback \
--scope openid,email,profile --grant-type authorization_code,refresh_token --response-type code
utoid permission create --code "app:$KEY:issue:write" --desc "write issues"
utoid role create --scope "app:$KEY" --name maintainer --permission "app:$KEY:issue:write"
```
**Answer "why was this refused?"** The refusal is on the record with its reason:
```sh
utoid audit list --result deny --limit 20
utoid check user:12 app:<app-key>:issue:write
```
## Rules
- Never invent a permission code. Declare it, or list what exists.
- Mint a key only when asked. It is shown once and cannot be shown again — hand
it back in the same reply, and do not write it to a file the user did not ask for.
- Prefer `check` over reasoning about what a role probably carries.
- Every change is audited under the credential that made it. Do not share one
agent identity between jobs that should be told apart.