Files
optimclaw/docs/USER_MANAGEMENT_API.md
T
OutBack Dingo 6d9dbbb3b9 fork: rename IronClaw → OptimClaw
Full rename of all identifiers, filenames, and references:
  ironclaw → optimclaw
  IronClaw → OptimClaw
  IRONCLAW → OPTIMCLAW
  ironclaw_common → optimclaw_common
  ironclaw_safety → optimclaw_safety

Upstream: nearai/ironclaw
2026-03-29 06:27:52 +07:00

14 KiB

User Management API

DB-backed user management for multi-tenant OptimClaw deployments. Covers admin user CRUD, per-user secrets provisioning, self-service profile, API token management, and usage reporting.

Authentication

All endpoints require Authorization: Bearer <token>. Tokens are either:

  • Env-var tokens — configured via GATEWAY_AUTH_TOKEN (single-user) at startup
  • DB-backed tokens — created via POST /api/tokens or POST /api/admin/users

DB tokens are SHA-256 hashed at rest; plaintext is returned exactly once at creation time.

Auth is cached in a bounded LRU (1024 entries, 60s TTL). Suspending a user or revoking a token may take up to 60s to take effect.

Roles

Role Scope
admin Full access to all endpoints
member Self-service profile + own token management only

Endpoints marked Admin return 403 Forbidden for member role.


Admin: Users

POST /api/admin/users

Create a new user. Returns the user record and a one-time plaintext API token.

Auth: Admin

Request body:

{
  "display_name": "Alice Smith",
  "email": "[email protected]",
  "role": "member"
}
Field Type Required Default Notes
display_name string yes
email string no null Must be unique if provided
role string no "member" "admin" or "member"

Response: 200 OK

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "[email protected]",
  "display_name": "Alice Smith",
  "status": "active",
  "role": "member",
  "token": "a1b2c3d4e5f6...64-char hex...",
  "created_at": "2026-03-25T12:00:00+00:00",
  "created_by": "admin-user-id"
}

The token field is the plaintext API token. It is shown only once — store it securely.

Errors: 400 (missing display_name, invalid role), 403 (not admin), 503 (no database)


GET /api/admin/users

List all users.

Auth: Admin

Response: 200 OK

{
  "users": [
    {
      "id": "550e8400-...",
      "email": "[email protected]",
      "display_name": "Alice Smith",
      "status": "active",
      "role": "member",
      "created_at": "2026-03-25T12:00:00+00:00",
      "updated_at": "2026-03-25T12:00:00+00:00",
      "last_login_at": "2026-03-25T14:30:00+00:00",
      "created_by": "admin-user-id"
    }
  ]
}

GET /api/admin/users/{id}

Get a single user by ID.

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "email": "[email protected]",
  "display_name": "Alice Smith",
  "status": "active",
  "role": "member",
  "created_at": "2026-03-25T12:00:00+00:00",
  "updated_at": "2026-03-25T12:00:00+00:00",
  "last_login_at": "2026-03-25T14:30:00+00:00",
  "created_by": "admin-user-id",
  "metadata": {}
}

Errors: 404 (user not found), 403 (not admin)


PATCH /api/admin/users/{id}

Update a user's display name and/or metadata. Omitted fields are left unchanged.

Auth: Admin

Request body:

{
  "display_name": "Alice Johnson",
  "metadata": {"department": "engineering"}
}
Field Type Required Notes
display_name string no
role string no "admin" or "member"
metadata object no Replaces entire metadata object (full replacement; keys not included are removed)

Response: 200 OK — returns the full updated user record (same shape as GET detail, without last_login_at/created_by).

Errors: 404 (user not found), 403 (not admin)


POST /api/admin/users/{id}/suspend

Suspend a user. Suspended users cannot authenticate (DB auth checks user status).

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "status": "suspended"
}

Errors: 404 (user not found), 403 (not admin)


POST /api/admin/users/{id}/activate

Re-activate a suspended user.

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "status": "active"
}

Errors: 404 (user not found), 403 (not admin)


DELETE /api/admin/users/{id}

Permanently delete a user and all associated data (tokens, jobs, conversations, memory, routines, settings, secrets).

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "deleted": true
}

Errors: 404 (user not found), 403 (not admin)

Cascade: Deletes from api_tokens, agent_jobs, conversations, memory_documents, routines, secrets, settings, wasm_tools, and related tables. On PostgreSQL this uses FK cascades; on libSQL it uses explicit deletes.


Admin: Per-User Secrets

Provision secrets on behalf of individual users. The primary use case is an application backend (acting as admin) that configures per-user credentials so each user's OptimClaw agent can call back to external services.

Secrets are encrypted at rest with AES-256-GCM using a per-secret HKDF-derived key. Plaintext values are never returned by any endpoint — they can only be used by the agent's tool system at runtime.

PUT /api/admin/users/{user_id}/secrets/{name}

Create or update a secret for the specified user. If a secret with the same name already exists, it is overwritten.

Auth: Admin

Path parameters:

Param Type Notes
user_id string The user's ID
name string Secret name (normalized to lowercase)

Request body:

{
  "value": "sk-live-abc123...",
  "provider": "my-app-backend",
  "expires_in_days": 90
}
Field Type Required Notes
value string yes The secret value (encrypted at rest, never returned)
provider string no Tag for grouping (e.g. "stripe", "my-app")
expires_in_days integer no Auto-expire after N days; null = never

Response: 200 OK

{
  "user_id": "550e8400-...",
  "name": "my_app_callback_token",
  "status": "created"
}

Errors: 400 (missing value), 403 (not admin), 503 (secrets store not available)

Example — application backend provisioning a callback token:

# Admin creates a user
curl -X POST https://optimclaw.example.com/api/admin/users \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"display_name": "Alice", "role": "member"}'
# Response includes: {"id": "alice-uuid", "token": "alice-bearer-token", ...}

# Admin provisions a per-user callback secret
curl -X PUT https://optimclaw.example.com/api/admin/users/alice-uuid/secrets/app_callback_token \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"value": "per-user-jwt-for-alice", "provider": "my-app"}'

# Now Alice's OptimClaw agent can use the "app_callback_token" secret
# when calling tools that need to authenticate back to the app backend.

GET /api/admin/users/{user_id}/secrets

List a user's secrets. Returns names and providers only — never values or hashes.

Auth: Admin

Response: 200 OK

{
  "user_id": "550e8400-...",
  "secrets": [
    {"name": "app_callback_token", "provider": "my-app"},
    {"name": "openai_api_key", "provider": "openai"}
  ]
}

DELETE /api/admin/users/{user_id}/secrets/{name}

Delete a specific secret for a user.

Auth: Admin

Response: 200 OK

{
  "user_id": "550e8400-...",
  "name": "app_callback_token",
  "deleted": true
}

Errors: 404 (secret not found), 403 (not admin), 503 (secrets store not available)


Admin: Usage

GET /api/admin/usage

Per-user LLM usage statistics aggregated from llm_calls via agent_jobs.user_id.

Auth: Admin

Query parameters:

Param Type Default Notes
user_id string all users Filter to a single user
period string "day" "day" (24h), "week" (7d), or "month" (30d)

Response: 200 OK

{
  "period": "week",
  "since": "2026-03-18T12:00:00+00:00",
  "usage": [
    {
      "user_id": "alice-id",
      "model": "claude-sonnet-4-5-20250514",
      "call_count": 42,
      "input_tokens": 150000,
      "output_tokens": 30000,
      "total_cost": "1.23"
    }
  ]
}

Self-Service: Profile

GET /api/profile

Get the authenticated user's own profile.

Auth: Any authenticated user

Response: 200 OK

{
  "id": "550e8400-...",
  "email": "[email protected]",
  "display_name": "Alice Smith",
  "status": "active",
  "role": "member",
  "created_at": "2026-03-25T12:00:00+00:00",
  "last_login_at": "2026-03-25T14:30:00+00:00"
}

PATCH /api/profile

Update the authenticated user's own display name and/or metadata.

Auth: Any authenticated user

Request body:

{
  "display_name": "Alice Johnson",
  "metadata": {"theme": "dark"}
}

Response: 200 OK

{
  "id": "550e8400-...",
  "display_name": "Alice Johnson",
  "updated": true
}

Self-Service: Tokens

POST /api/tokens

Create a new API token for the authenticated user. Admins can optionally create tokens for other users by including user_id.

Auth: Any authenticated user

Request body:

{
  "name": "CI pipeline",
  "expires_in_days": 90,
  "user_id": "other-user-id"
}
Field Type Required Notes
name string yes Human-readable label
expires_in_days integer no null = never expires
user_id string no Admin-only; create token for another user

Response: 200 OK

{
  "token": "a1b2c3d4...64-char hex...",
  "id": "token-uuid",
  "name": "CI pipeline",
  "token_prefix": "a1b2c3d4",
  "expires_at": "2026-06-23T12:00:00+00:00",
  "created_at": "2026-03-25T12:00:00+00:00"
}

The token field is shown only once.


GET /api/tokens

List the authenticated user's tokens. Token hashes are never returned.

Auth: Any authenticated user

Response: 200 OK

{
  "tokens": [
    {
      "id": "token-uuid",
      "name": "CI pipeline",
      "token_prefix": "a1b2c3d4",
      "expires_at": "2026-06-23T12:00:00+00:00",
      "last_used_at": "2026-03-25T14:00:00+00:00",
      "created_at": "2026-03-25T12:00:00+00:00",
      "revoked_at": null
    }
  ]
}

DELETE /api/tokens/{id}

Revoke one of the authenticated user's tokens. Users can only revoke their own tokens.

Auth: Any authenticated user

Path: id — UUID of the token to revoke

Response: 200 OK

{
  "status": "revoked",
  "id": "token-uuid"
}

Errors: 400 (invalid UUID), 404 (token not found or belongs to another user)


Error Format

All error responses return a plain text body with the error message and the corresponding HTTP status code:

Code Meaning
400 Bad request (missing fields, invalid input)
401 Missing or invalid bearer token
403 Authenticated but insufficient role (member accessing admin endpoint)
404 Resource not found
503 Database or secrets store not available
500 Internal server error

Security Model

Secrets Encryption

  • Algorithm: AES-256-GCM with per-secret HKDF-SHA256 derived keys
  • Master key: 32+ bytes, resolved from SECRETS_MASTER_KEY env var or OS keychain
  • Storage format: nonce (12B) || ciphertext || tag (16B) in encrypted_value column
  • Per-secret salt: 32 random bytes stored alongside the ciphertext
  • Zero-exposure: Plaintext never appears in logs, debug output, API responses, or LLM conversations

Auth Cache

  • Bounded LRU cache (1024 entries max)
  • 60-second TTL per entry
  • Suspending a user or revoking a token takes up to 60s to propagate

Database Schema

users

Column Type (PG / libSQL) Notes
id TEXT / TEXT Primary key; typically UUID v4 strings (bootstrap admin may use a custom ID)
email TEXT UNIQUE Nullable
display_name TEXT NOT NULL
status TEXT NOT NULL "active" or "suspended"
role TEXT NOT NULL "admin" or "member"
created_at TIMESTAMPTZ / TEXT
updated_at TIMESTAMPTZ / TEXT
last_login_at TIMESTAMPTZ / TEXT Nullable
created_by TEXT Nullable, references users.id
metadata JSONB / TEXT Default {}

api_tokens

Column Type (PG / libSQL) Notes
id UUID / TEXT Primary key
user_id TEXT NOT NULL FK to users.id (PG cascades; libSQL explicit cleanup)
token_hash BYTEA / BLOB SHA-256 of hex-encoded plaintext
token_prefix TEXT NOT NULL First 8 chars for identification
name TEXT NOT NULL Human-readable label
expires_at TIMESTAMPTZ / TEXT Nullable
last_used_at TIMESTAMPTZ / TEXT Nullable
created_at TIMESTAMPTZ / TEXT
revoked_at TIMESTAMPTZ / TEXT Nullable; set on revocation

secrets

Column Type (PG / libSQL) Notes
id UUID / TEXT Primary key
user_id TEXT NOT NULL Scoped to user
name TEXT NOT NULL Unique per user (lowercase normalized)
encrypted_value BYTEA / BLOB AES-256-GCM (nonce + ciphertext + tag)
key_salt BYTEA / BLOB Per-secret HKDF salt
provider TEXT Optional grouping tag
expires_at TIMESTAMPTZ / TEXT Nullable
last_used_at TIMESTAMPTZ / TEXT Audit: last injection time
usage_count BIGINT / INTEGER Audit: total injections
created_at TIMESTAMPTZ / TEXT
updated_at TIMESTAMPTZ / TEXT