# Agent Identity Plugin

UserPool OIDC login, TIP (Trusted Identity Provider) token via Identity GetWorkloadAccessTokenForJWT, credential 3LO (GetResourceOauth2Token/Oauth2Callback), and session management for OpenClaw.

> 中文文档请参阅 [README-cn.md](README-cn.md)

Integrates with [Volcengine Agent Identity and Permission Management](https://www.volcengine.com/docs/86848).

## Features

- **OIDC Login**: `/identity login` returns IdP auth URL (no HTTP start endpoint). User opens URL, IdP redirects to `/identity/oauth/callback`.
- **Pre-LLM Auth Gate (`before_dispatch`)**: Blocks unauthenticated messages before the LLM is invoked, saving tokens. Returns an OIDC login URL directly in the static response. Falls back to `before_agent_start` on older OpenClaw versions.
- **TIP Token**: `before_dispatch` / `before_agent_start` hooks fetch TIP token when session has a logged-in user.
- **Credential 3LO**: `/identity fetch <provider>` returns auth URL. IdP redirects to Identity-provided callback (control-plane config).
- **Credential Binding**: `/identity set <provider> <envVar>` binds stored credential to env var. Credentials are securely injected per-tool-call, isolated between concurrent multi-user sessions.
- **Local Identity Server (UDS)**: Optional HTTP-over-Unix-Domain-Socket server exposing TIP tokens, OIDC session tokens, and all plugin tools to other local processes. Supports peer credential checking (Linux `SO_PEERCRED` + `/proc`) and configurable process allowlist.
- **Encrypted Session Storage**: `sessions.json` is encrypted at rest (AES-256-GCM). Plaintext sessions from older versions are auto-migrated on first load.
- **In-memory TIP Cache**: TIP tokens are stored only in memory (no disk persistence). They are short-lived and re-obtained from the user's session token on demand.
- **Dynamic UserPool**: Resolve OIDC config by `userPoolName` + `clientName` (no manual clientId).
- **Credentials**: Load AK/SK from env, file, or STS AssumeRole (veadk-style).

## HTTP Endpoints

Only the OIDC login callback is exposed. Credential OAuth uses Identity callback. All other logic runs in slash commands.

| Path                       | Method | Description                              |
| -------------------------- | ------ | ---------------------------------------- |
| `/identity/oauth/callback` | GET    | OIDC login callback (IdP redirects here) |

## Slash Commands

Single command `/identity` (alias `/id`) with subcommands. Default with no args: `status`.

| Subcommand                          | Description                                                                                                   |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| _(none)_                            | Show help.                                                                                                    |
| `whoami`                            | Show current session identity (sub, TIP status).                                                              |
| `login`                             | If logged in: refresh TIP. If not: return OIDC IdP URL to open.                                               |
| `status`                            | Show login status, TIP, credentials. Tries to refresh TIP when session exists.                                |
| `logout`                            | Clear session and TIP for current session.                                                                    |
| `list-tips`                         | List all valid TIP tokens with delegation chain, expiry, and env bindings.                                    |
| `config`                            | Show identity plugin config (sensitive values redacted).                                                      |
| `list-credentials` or `list [page]` | List providers from control plane (paginated) and your credentials with bound env. Use `list 2` to load more. |
| `list-roles`                        | List STS role credential providers (not OAuth/API key). Optional name prefix filter.                          |
| `get-role <provider> [--use-tip] [--show-secrets]` | Get temporary STS credentials for a role provider (masked by default).                         |
| `fetch <provider> [--flow=...]`     | Add credential. Flow auto-inferred from provider type (api_key/oauth2/m2m); override with `--flow`.           |
| `set <provider> <envVar>`           | Bind credential to env var for tool injection. If no credential, import from `process.env[envVar]`.           |
| `unset <provider>`                  | Remove env binding for provider.                                                                              |

## OIDC Login Flow

1. User sends `/identity login` in chat (e.g. Telegram, Discord)
2. Command derives sessionKey from channel/sender, builds IdP authorize URL, stores state
3. Command returns the IdP URL; user opens it in browser
4. User completes login at UserPool IdP
5. IdP redirects to `/identity/oauth/callback` with `code` and `state`
6. Plugin exchanges code, creates session, shows success page and sends message to chat

## Credential Fetch Flow

**OAuth2 (user federation or M2M):**

1. User sends `/identity fetch google` or `/identity fetch google --flow=oauth2-m2m` (after `/identity login`)
2. Command uses TIP to call Identity API; returns auth URL or direct token
3. If auth URL: user opens it; IdP redirects to Identity callback (control-plane provider config)
4. Identity handles callback; token obtained via Identity; user may re-run fetch to pull credential

**API Key:**

1. User sends `/identity fetch openai` (provider type api_key in control plane) or `/identity fetch openai --flow=apikey`
2. Command uses TIP to call GetResourceApiKey; API key stored directly

Flow is auto-inferred from ListCredentialProviders (Type + Flow). Override with `--flow=oauth2-user|oauth2-m2m|apikey` when needed.

## Enable Volcengine Agent Identity Service

Before using this plugin, you must enable **Agent Identity and Permission Management** in Volcengine and complete authorization. See:

- [Enable Agent Identity Service (Chinese)](https://www.volcengine.com/docs/86848/2123358?lang=zh)

## Get Volcengine AK/SK

The plugin requires Volcengine Access Key and Secret Key to call the Identity API. To create credentials:

- [Create Access Key - Volcengine Docs](https://www.volcengine.com/docs/6291/65568?lang=zh)

After creating AK/SK in the console, pass them via config or use environment variables `VOLCENGINE_ACCESS_KEY` and `VOLCENGINE_SECRET_KEY`.

## Installation

```bash
openclaw plugins install @m1a0rz/agent-identity
```

Or with link for development:

```bash
openclaw plugins install --link .
```

## Configuration

### Step 2: Configure plugin (minimal setup)

The plugin typically needs three types of config:

**A. Platform access (Identity)**: For TIP Token, credential fetch/hosting, and optional permission checks.

- `endpoint` (optional): Full Identity API base URL (e.g. `https://id.cn-beijing.volcengineapi.com`). **Highest priority** for the API host.
- `regionMetadataUrl` (optional): HTTP(S) URL that returns a **plain-text region id** (e.g. `cn-beijing`). Used only when `endpoint` is **unset**: the client builds `https://id.{region}.volcengineapi.com`. Request timeout ~10s; on failure or invalid body (e.g. `unknown`), falls back to `https://id.cn-beijing.volcengineapi.com`. Example metadata URL: `http://100.96.0.96/latest/region_id` (must be reachable from the gateway).
- `accessKeyId` / `secretAccessKey`: For Identity API access. **Optional** when using env vars or credential file (see below).
- `workloadPoolName` / `workloadName`: For issuing TIP Token. Defaults: `default`, `openclaw-agent`.
- `audience` / `durationSeconds`: Optional, token audience and validity.
- `credentialsFile`: Path to credential JSON. Default: `VOLCENGINE_CREDENTIALS_FILE` env or `/var/run/secrets/iam/credential`.
- `credentialsMetadataUrl`: Full URL for remote credential fetch. **Highest-priority credential source** — when set, attempted before explicit AK/SK and env vars so a freshly-rotated metadata-served credential cannot be silently overridden by stale config. When set with `roleTrn`, fetches from URL (response: `AccessKeyId`, `SecretAccessKey`, `SessionToken`), then AssumeRole with `roleTrn` to get final credentials. Same flow as AK/SK + roleTrn. Network / parse failures fall through to the next source. Cached by ExpiredTime. Must be explicitly configured.
- `roleTrn`: Role TRN for STS AssumeRole. When set (and `workloadName` not set), workload name is omitted; backend uses roleName. Priority: `workloadName` > `roleTrn` > params. Used with `credentialsMetadataUrl` (AssumeRole after fetch) or explicit AK/SK.
- `sessionToken`: STS session token (or use `VOLCENGINE_SESSION_TOKEN` env).

**Identity API host resolution**: `endpoint` if set → else region from `regionMetadataUrl` → else `https://id.cn-beijing.volcengineapi.com`. **SigV4 signing region** is inferred from the resolved host when it matches `id.{region}.volcengineapi.com`; otherwise `cn-beijing`.

**Workload pool (credentials / STS)**: `workloadPoolName` (default `default`) scopes `ListCredentialProviders`, `ListRoleCredentialProviders`, and control-plane calls such as `GetResourceOauth2Token`, `GetResourceApiKey`, `GetUserCredential`, and `GetRoleCredentials` via `PoolName`. Role provider listing also filters by `userpool.userPoolName` when configured.

**Credential resolution order** (AK/SK): 1) Remote metadata (`credentialsMetadataUrl`, optional `roleTrn` AssumeRole; falls through on fetch / parse failure) → 2) Explicit config → 3) Env vars (`VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY`, `VOLCENGINE_SESSION_TOKEN`) → 4) Credential file (`credentialsFile` config, or `VOLCENGINE_CREDENTIALS_FILE` env, or `/var/run/secrets/iam/credential`). The metadata URL is ranked highest so stale env/explicit AK/SK cannot silently override a metadata-served credential. Credential file format (VeFaaS): `access_key_id`, `secret_access_key`, `session_token` (optional), `role_trn` (optional for AssumeRole). `RUNTIME_IAM_ROLE_TRN` env can supply role TRN when loading from file.

**B. User login (UserPool / OIDC)**: For `/identity login` and session setup.

- `discoveryUrl` (or `userPoolName` + `clientName` for dynamic resolution)
- `clientId` / `clientSecret` (auto-resolved in dynamic mode)
- `callbackUrl`: Public callback URL for OpenClaw gateway, e.g. `http://127.0.0.1:18789/identity/oauth/callback`
- `scope`: Typically `openid profile email`
- `identityProvider` (optional): IdP name for the `identity_provider` authorize param. When omitted, the first entry from `ListIdentityProviders` is used.
- `useRelayCallback` (optional): UserPool relay / `redirect_relay_uri` flow for OIDC callback. Default false.

**C. AuthZ and risk approval (optional)**: For TIP + CheckPermission + risk evaluation. Each flag is independent; no single "enable" switch.

- `agentCheck`: Run CheckPermission for agents (resource type agent) in `before_agent_start`. Verifies the user can invoke the current agent. Uses the outermost actor from TIP delegation chain as resource id. Default false.
- `toolCheck`: Run CheckPermission for tools (resource type tool). Default false.
- `skillReadCheck`: Run CheckPermission for read of SKILL.md (resource type skill). Parses available_skills from system prompt. Default false.
- `requireRiskApproval`: Require user approval for high-risk tool calls. Default false.
- `namespaceName`: CheckPermission Cedar policy namespace. Default `default`.
- `lowRiskBypass`: Skip TIP+CheckPermission for built-in low-risk tools. Default true.
- `lowRiskTools`: Extra tool names treated as low-risk.
- `enableLlmRiskCheck`: Use LLM to re-evaluate when rules return medium. Default false.
- `llmRiskCheck`: LLM config (`endpoint`, `api`, `model`, `apiKey`, `timeoutMs`, `cacheTtlMs`). Required when `enableLlmRiskCheck` is true.
- `approvalTtlSeconds`: Approval link/command TTL (seconds). Default 300.

**Expected outcome**: After config, the plugin can initiate login and obtain TIP Token. With AuthZ flags enabled, agent/tool/skill permission checks and high-risk approvals apply. High-risk tool calls use OpenClaw's native `requireApproval` mechanism (UI overlay, Telegram buttons, Discord interactions, `/approve` CLI).

---

Add to `openclaw.json` under `plugins.entries.agent-identity.config`:

```json
{
  "plugins": {
    "entries": {
      "agent-identity": {
        "config": {
          "identity": {
            "endpoint": "https://id.cn-beijing.volcengineapi.com",
            "workloadPoolName": "default",
            "workloadName": "openclaw-agent"
          },
          "userpool": {
            "discoveryUrl": "https://userpool-xxx.userpool.auth.id.cn-beijing.volces.com",
            "clientId": "<client-id>",
            "clientSecret": "<client-secret>",
            "callbackUrl": "https://gateway.example.com/identity/oauth/callback",
            "scope": "openid profile email"
          },
          "authz": {
            "agentCheck": false,
            "toolCheck": false,
            "skillReadCheck": false,
            "requireRiskApproval": false,
            "namespaceName": "default",
            "lowRiskBypass": true,
            "enableLlmRiskCheck": false,
            "approvalTtlSeconds": 300
          },
          "localServer": false,
          "localServerAllowlist": [],
          "localServerFailOpen": true
        }
      }
    }
  }
}
```

**Identity credentials**: Omit `accessKeyId`/`secretAccessKey` to use env vars (`VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY`) or credential file (`VOLCENGINE_CREDENTIALS_FILE` or `/var/run/secrets/iam/credential`).

### identity config (required vs optional)

| Param | Type | Required | Description |
|-------|------|----------|--------------|
| `endpoint` | string | No | Identity API base URL. Omit to use `regionMetadataUrl` or default `https://id.cn-beijing.volcengineapi.com` |
| `regionMetadataUrl` | string | No | Plain-text region id URL; builds `https://id.{region}.volcengineapi.com` when `endpoint` unset |
| `accessKeyId` | string | No* | Volcengine Access Key. Omit to load from `VOLCENGINE_ACCESS_KEY` or `credentialsFile` |
| `secretAccessKey` | string | No* | Volcengine Secret Key. Omit to load from `VOLCENGINE_SECRET_KEY` or `credentialsFile` |
| `workloadPoolName` | string | No | Workload pool name, default `default` |
| `workloadName` | string | No | Workload name for TIP. When set, takes precedence over roleTrn. Default when neither set: agentId or `openclaw-agent` |
| `audience` | string[] | No | TIP token audience |
| `durationSeconds` | number | No | TIP token TTL (seconds), default 3600 |
| `roleTrn` | string | No | Role TRN for STS AssumeRole. When set (and workloadName not set), workload name is omitted; backend uses roleName. Priority: workloadName > roleTrn > params |
| `credentialsFile` | string | No | Path to credential JSON. Default: `VOLCENGINE_CREDENTIALS_FILE` or `/var/run/secrets/iam/credential` |
| `credentialsMetadataUrl` | string | No | Full URL for remote credential fetch. When set with `roleTrn`, fetches then AssumeRole. 404 falls through to `credentialsFile` |
| `sessionToken` | string | No | STS session token (or `VOLCENGINE_SESSION_TOKEN`) |
| `subagentTipPropagation` | boolean | No | Propagate TIP and session to subagents. Default false |
| `webchatSessionExchange` | boolean | No | Enable `identity.session.put` / `identity.session.get` gateway WS methods for webchat clients. Default false |
| `personalSessionMode` | boolean | No | Single-user mode: TIP, OIDC session, and credentials are stored only under `agent:main:main` (no per-sender or per-channel-peer keys). Subagent sessions unchanged. Default false — do not enable for multi-tenant or shared groups. |
| `credentialExchanges` | object[] | No | Operator-declared HTTP exchanges that turn the current session's TIP token into a downstream credential. Surfaced via `identity_exchange_credential`. Profiles with invalid url scheme or duplicate ids are dropped at load time with a warn log. See [Credential Exchange Profiles](#credential-exchange-profiles). |
| `localServer` | boolean | No | **Deprecated — value ignored.** The local UDS identity server is always started whenever identity is configured (external callers depend on it). See [Local Identity Server (UDS)](#local-identity-server-uds). |
| `localServerAllowlist` | string[] | No | Additional process names/paths allowed to access the UDS server. `curl` always allowed. |
| `localServerFailOpen` | boolean | No | **Deprecated — value ignored.** The local server is always run in fail-open mode (baseline security from 0600 socket + peer allowlist). |

\* AK/SK must be provided via `accessKeyId`+`secretAccessKey`, environment variables, `credentialsMetadataUrl`+`roleTrn`, or `credentialsFile`.

**Environment variables**: `VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY`, `VOLCENGINE_SESSION_TOKEN`, `VOLCENGINE_CREDENTIALS_FILE`, `RUNTIME_IAM_ROLE_TRN` (for AssumeRole when loading from file). Set `IDENTITY_STS_DEBUG=1` to log full STS AssumeRole request/response for debugging.

### userpool config (OIDC login)

**Explicit mode** (required): `discoveryUrl`, `clientId`, `clientSecret`, `callbackUrl`, `scope`

**Dynamic mode** (required): `userPoolName`, `clientName`, `callbackUrl`; `autoCreate` defaults to true

OAuth2 credential fetch uses control-plane redirect URL and scopes. Override via `/identity fetch <provider> --redirectUrl` and `--scopes`.

### authz config (optional, each flag independent)

| Param | Type | Description |
|-------|------|-------------|
| `agentCheck` | boolean | Run CheckPermission for agents (resource type agent) in `before_agent_start`. Verifies the user can invoke the current agent. Uses the outermost actor from TIP delegation chain as resource id. Default false. |
| `toolCheck` | boolean | Run CheckPermission for tools (resource type tool). Default false. |
| `skillReadCheck` | boolean | Run CheckPermission for read of SKILL.md (resource type skill). Default false. |
| `requireRiskApproval` | boolean | Require user approval for high-risk tools. Default false. |
| `namespaceName` | string | CheckPermission Cedar namespace. Default `default`. |
| `lowRiskBypass` | boolean | Skip TIP+CheckPermission for built-in low-risk tools. Default true. |
| `lowRiskTools` | string[] | Extra tool names treated as low-risk. |
| `enableLlmRiskCheck` | boolean | Re-evaluate with LLM when rules return medium. Default false. |
| `llmRiskCheck` | object | LLM config: `endpoint`, `api`, `model`, etc. Required when `enableLlmRiskCheck` is true. |
| `approvalTtlSeconds` | number | Approval TTL (seconds). Default 300. |

### Workload and TIP

TIP token is obtained via `GetWorkloadAccessTokenForJWT`. Workload behavior:

- **workloadName** (optional): Workload name sent to the API. Priority: config.workloadName > config.roleTrn > params (session agentId or `"openclaw-agent"`). When config.workloadName is set, it takes precedence over roleTrn.
- **roleTrn** (optional): When set (and workloadName not set), the plugin does **not** pass workload name; the backend uses the role name. Use this for delegated execution (e.g. VeFaaS, K8s IRSA).
- **Auto-create workload**: When `GetWorkloadAccessTokenForJWT` returns 404 (workload not found), the plugin calls `CreateWorkloadIdentity` to create the workload (Category: Agent), then retries. Only applies when a workload name is used (workloadName set or neither workloadName nor roleTrn set). Duplicated (409) from concurrent create is ignored.

### Credential Exchange Profiles

Some downstream services do not accept the TIP token directly — they expose their own exchange endpoint that takes the TIP and returns their native credential (e.g. a service-specific JWT, an opaque token). `identity.credentialExchanges` declares these exchanges so the agent can perform them through the `identity_exchange_credential` tool, without operators having to write per-tenant code.

The exchange runs on demand, per session: the plugin reads the calling session's TIP, calls the configured endpoint, extracts the value, and caches it so subsequent calls within the TTL skip the network roundtrip. Per-session isolation guarantees that one user's exchanged credential is never reused for another.

**Profile schema** (`identity.credentialExchanges[]`):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Stable identifier; passed verbatim to `identity_exchange_credential`'s `id` argument. Duplicate ids are dropped at load time. |
| `request.method` | `"GET" \| "POST"` | No | HTTP method. Default `GET`. |
| `request.url` | string | Yes | Endpoint URL. Must be `https://`, or `http://localhost` / `http://127.0.0.1` for local tests. Other schemes are dropped at load time. **Not** templated. |
| `request.headers` | `Record<string,string>` | No | Request headers. Values support `{{tipToken}}` substitution. |
| `request.query` | `Record<string,string>` | No | Query parameters. Values support `{{tipToken}}` substitution. |
| `request.body` | string | No | Request body. Used **only** when `method` is `POST`. Supports `{{tipToken}}` substitution. |
| `request.contentType` | string | No | `Content-Type` for the body. Written only if not already set in `headers`. |
| `request.timeoutMs` | number | No | Request timeout in milliseconds. Default `10000`. |
| `extract.source` | `"responseHeader" \| "jsonBody" \| "textBody"` | Yes | Where to read the exchanged value from the response. |
| `extract.name` | string | Conditional | Header name. Required when `source = "responseHeader"`. |
| `extract.path` | string | Conditional | Dotted JSON path (e.g. `data.token`). Required when `source = "jsonBody"`. NOT an RFC 6901 pointer — do not prefix with `/`. |
| `cache.ttlSeconds` | number | No | Cache TTL in seconds. Default `300`. |
| `cache.refreshBeforeSeconds` | number | No | Treat the cached entry as stale this many seconds before `expiresAt`. Default `30`. |

**Templating**: only the literal placeholder `{{tipToken}}` is recognized, and only inside `request.headers` (values), `request.query` (values), and `request.body`. `request.url` is never templated. The Mustache-style delimiters (`{{ }}`) are deliberately distinct from the `${VAR}` syntax used by config-load env rendering, so the two stages never collide. Substitution happens at exchange time, after the session's TIP has been fetched (or refreshed).

**Cache key**: `${profileId}::${sessionKey}`. Two different sessions never share an exchanged credential, even with the same profile.

**Security posture**:

- Endpoint URL scheme is enforced at config load (`https://` only, except loopback for tests).
- Outbound requests bypass the global proxy via `noProxyFetch`.
- The exchanged value is held only in the in-memory cache and the tool result; nothing is persisted to disk.
- The tool result includes `value` **only** when the caller passes `returnValue: true`. The skill / agent should treat `value` as a one-shot env var input — never echo it in chat, logs, or command echoes (the `Identity Credential Runtime Contract` patched onto every tool result enforces this convention).

**Example** — exchange the TIP for a partner JWT and inject as `PARTNER_JWT`:

```json
{
  "identity": {
    "credentialExchanges": [
      {
        "id": "partner-jwt",
        "request": {
          "method": "GET",
          "url": "https://jwt.partner.example/api/exchange",
          "headers": { "x-ve-tip-token": "{{tipToken}}" },
          "timeoutMs": 5000
        },
        "extract": { "source": "responseHeader", "name": "X-Jwt-Token" },
        "cache": { "ttlSeconds": 300, "refreshBeforeSeconds": 30 }
      }
    ]
  }
}
```

The agent then calls `identity_exchange_credential` with `{ "id": "partner-jwt", "returnValue": true }` and writes the returned `value` to `PARTNER_JWT` (typically driven by a skill-declared `Identity Credential Runtime Contract` binding).

**Wiring via skill metadata** — to have an agent automatically call the exchange and inject the result, declare an `exchange` binding under `metadata.openclaw.identity` in a SKILL.md frontmatter:

```yaml
---
name: my-partner-skill
metadata:
  openclaw:
    identity:
      - type: exchange
        id: partner-jwt          # must match identity.credentialExchanges[].id
        env: PARTNER_JWT         # target env var for the downstream tool
        required: true           # abort the downstream call if the exchange fails
---
```

The plugin renders this binding into the `Identity Credential Runtime Contract` block appended to every tool result, so the agent knows to call `identity_exchange_credential` with `{ id: "partner-jwt", returnValue: true }`, read `value` from the result, and assign it to `PARTNER_JWT` before invoking the downstream tool. Refresh on 401/403 is handled with `forceRefresh: true`.

### Feishu notifications

Login success and credential fetch follow-up messages (e.g. "✓ Credential for `google` added.") are sent via Feishu when the user runs `/identity` from a Feishu chat. Credentials are read from `channels.feishu` in openclaw.json (same as feishu extension: `appId`, `appSecret`, optional `accounts`). No extra config in agent-identity is required.

**Approval messages** (when a high-risk tool is blocked): The plugin returns `requireApproval` from `before_tool_call`, and OpenClaw handles the approval UI natively across all channels (exec overlay, Telegram buttons, Discord interactions, `/approve` CLI). No plugin-side delivery logic is needed.

### WebChat Session Exchange (Gateway WS Methods)

When `identity.webchatSessionExchange` is `true`, the plugin registers two gateway WebSocket methods for webchat clients to inject and retrieve session tokens without going through the OIDC redirect flow:

| Method | Params | Response | Description |
| --- | --- | --- | --- |
| `identity.session.put` | `{ sessionKey, idToken, refreshToken?, senderId?, channel? }` | `{ sub, expiresAt, effectiveSessionKey, hasTip }` | Inject an OIDC id_token into a plugin session; optional `refreshToken` is stored encrypted for silent token renewal. Resolves effective storage key via `buildEffectiveSessionKey` (same sender isolation as hooks/commands). |
| `identity.session.get` | `{ sessionKey, senderId?, channel? }` | `{ userToken, sub, expiresAt, effectiveSessionKey, hasRefreshToken }` | Retrieve the stored user token for a session. `hasRefreshToken` indicates whether a refresh token is stored; the refresh token value is never returned. |

- `senderId` defaults to `"openclaw-control-ui"`. In shared mode, the effective storage key normally follows the supplied OpenClaw `sessionKey`.
- `channel` is optional. For the canonical main session `agent:{id}:main` from a sendable IM channel (feishu, telegram, etc.), `channel + senderId` promotes the storage key to `agent:{id}:{channel}:direct:{senderId}`. Group/channel session keys are isolated by appending `:user:{senderId}`.

Both methods are **restricted to webchat WS connections only** (`isWebchatConnect` check). Non-webchat clients receive a `FORBIDDEN` error.

**Config:**

```json
{
  "identity": {
    "webchatSessionExchange": true
  }
}
```

**Typical flow (BFF → webchat → plugin):**

1. BFF completes 3LO login and obtains an OIDC `id_token` for the user
2. Webchat client calls `identity.session.put` with the session key and `id_token` (optionally `refreshToken` from the token response if silent renewal is desired)
3. Plugin verifies the token, stores the session, and acquires TIP
4. Subsequent agent runs in that session have a valid identity — no manual login needed

### WebChat / TUI

Follow-up messages (login success, credential fetch done) are not delivered when the user runs `/identity` from WebChat or TUI; the plugin has no API to push to those channels. Use `/identity status` to confirm results.

## Tools

Agent-facing behavior is summarized in [`skills/SKILL.md`](skills/SKILL.md). Registered tools:

- **identity_whoami** — Session identity (sub, TIP)
- **identity_status** — Login, TIP, credentials, bindings
- **identity_login** / **identity_logout** — OIDC login or refresh TIP; clear session
- **identity_list_credentials** — OAuth/API key providers and stored credentials (`page`, `name`, `flow`, `type` filters)
- **identity_list_roles** — STS role credential providers (`name` prefix filter)
- **identity_fetch** — Add credential (`provider`, `flow`, `redirectUrl`, `scopes`, `returnValue`)
- **identity_get_role_credentials** — STS credentials for a role provider (`providerName`, `useTip`)
- **identity_get_tip_token** / **identity_get_session_token** — Raw TIP JWT or session user token (advanced)
- **identity_exchange_credential** — Exchange the current session's TIP token for a downstream credential via a configured profile (`id`, `returnValue`, `forceRefresh`). See [Credential Exchange Profiles](#credential-exchange-profiles).
- **identity_config** — Effective plugin config (redacted)
- **identity_config_suggest** — Config merge snippets (`intent`, `lang`)
- **identity_set_binding** / **identity_unset_binding** — Env var bindings for tool injection
- **identity_risk_check** / **identity_list_risk_patterns** — Risk evaluation (optional plugin)
- **identity_list_tips** — All valid TIP tokens and bindings (ops / multi-session debug)

## Hooks

- **before_dispatch** - Stateless pre-LLM authentication gate. Checks for a valid TIP token before the LLM is invoked. If no TIP is found, blocks the message with a static response containing an OIDC login URL. Zero LLM token cost for unauthenticated users. When `authz.agentCheck` is enabled, also runs agent-level CheckPermission. Sets a feature flag so `before_agent_start` skips redundant auth logic.
- **before_agent_start** - Fallback auth gate for older OpenClaw versions that do not support `before_dispatch`. When `before_dispatch` is active, this hook is a no-op for authentication. Still fetches TIP token and runs agent CheckPermission when applicable.
- **subagent_spawned** - Propagate TIP to child session on subagent spawn.
- **before_tool_call** - Group context injection, optional AuthZ (TIP check, CheckPermission, risk via `requireApproval`), and per-tool-call credential injection. High-risk tool calls return `requireApproval` to the OpenClaw framework, which handles approval UI natively.
- **after_tool_call** - Clean up per-tool-call credential injection state.

## Local Identity Server (UDS)

Whenever identity is configured, the plugin starts an HTTP server over a Unix Domain Socket so other local processes (scripts, plugins, CLIs) can access identity tokens, mint egress hijack URLs and execute plugin tools without going through OpenClaw's messaging layer. (`identity.localServer` and `identity.localServerFailOpen` are accepted for back-compat but ignored — the server is always-on and always fail-open; access control comes from the 0600 socket permissions and the peer allowlist below.)

### Configuration

| Param | Type | Description |
|-------|------|-------------|
| `localServer` | boolean | **Deprecated — value ignored.** Server is always-on when identity is configured. |
| `localServerAllowlist` | string[] | Additional process names or paths allowed to connect. Supports exact match and glob suffix (e.g. `"python*"`). `curl` is always allowed by default. |
| `localServerFailOpen` | boolean | **Deprecated — value ignored.** Always fail-open. |

**Socket path**: `~/.openclaw/plugins/identity/identity.sock` (permissions `0600`, owner-only access).

### Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/token` | TIP token for the main session |
| `GET` | `/token?session=<key>` | TIP token for a specific session |
| `GET` | `/session` | OIDC session (id_token) for main session |
| `GET` | `/status` | Server health and available sessions summary |
| `GET` | `/tools` | List all available tool names with descriptions and JSON schemas |
| `POST` | `/tool/<name>` | Execute a registered tool. Body: `{ "params": {...}, "session": "..." }` |

### Peer Credential Checking (Linux)

On Linux, the server validates connecting processes via:

1. **`SO_PEERCRED`** (kernel-level): Zero-overhead retrieval of peer PID/UID/GID via `getsockopt`. Requires a registered native provider.
2. **`/proc` filesystem**: Derives process name and path from `/proc/<pid>/exe`, `/proc/<pid>/comm`, `/proc/<pid>/status`. No process spawning.

Resolved peer info is checked against the built-in allowlist (`curl`) plus any custom entries in `localServerAllowlist`. Unresolvable peers are always allowed (the server is hard-coded to fail-open; socket file permissions provide baseline security).

### Usage Examples

```bash
# Get TIP token
curl --unix-socket ~/.openclaw/plugins/identity/identity.sock http://localhost/token

# Get OIDC session token
curl --unix-socket ~/.openclaw/plugins/identity/identity.sock http://localhost/session

# List available tools
curl --unix-socket ~/.openclaw/plugins/identity/identity.sock http://localhost/tools

# Execute a tool
curl --unix-socket ~/.openclaw/plugins/identity/identity.sock \
  -X POST http://localhost/tool/identity_whoami \
  -H 'Content-Type: application/json' \
  -d '{"params": {}, "session": ""}'
```

See [`demo/local-server/`](demo/local-server/) for examples in Python, Go, TypeScript, Java, and Rust.

## Data Storage

Plugin data at `~/.openclaw/plugins/identity/`:

| File | Description |
|------|-------------|
| `sessions.json` | Encrypted sessionKey → userToken mapping. Expired entries pruned on load/save. |
| `credential-env-bindings.json` | Per-session: `{ [sessionKey]: { [provider]: envVar } }` |

**In-memory only (not persisted to disk):**

| Data | Description |
|------|-------------|
| TIP tokens | sessionKey → TIP token cache. Short-lived, re-obtained from session token on demand. |
| Credentials | Per-session (api_key, oauth2). Lost on gateway restart; cleared on logout. |
| OIDC state | Ephemeral, 5 min TTL. |
