Agent access
no roles
Sign out
An agent is a subject here like anyone else. It acts with an API key, and what it may do comes from the roles bound to it — the key is the credential, never the authority.

Install

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

Point it at this hub

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

What it can do

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

The skill

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.