# Agent Identity 插件

UserPool OIDC 登录、TIP (Trusted Identity Provider) 令牌（通过 Identity GetWorkloadAccessTokenForJWT 获取）、凭据 3LO（GetResourceOauth2Token/Oauth2Callback）以及 OpenClaw 的会话管理。

集成 [火山引擎智能体身份和权限管理平台](https://www.volcengine.com/docs/86848)。术语映射：用户池 (UserPool)、入站授权 (OIDC login)、出站授权 (credential fetch)、工作负载令牌 (TIP)、凭据托管 (credential hosting)、权限管控 (CheckPermission)。

> For English documentation, see [README.md](README.md)

## 功能特性

- **OIDC 登录**：`/identity login` 返回 IdP 授权 URL。用户打开 URL 后，IdP 重定向到 `/identity/oauth/callback`。
- **LLM 前置认证拦截（`before_dispatch`）**：在 LLM 调用前拦截未认证消息，节省 token 开销。直接在静态回复中返回 OIDC 登录 URL。在不支持 `before_dispatch` 的旧版 OpenClaw 上自动回退到 `before_agent_start`。
- **TIP 令牌**：`before_dispatch` / `before_agent_start` 钩子在会话中有已登录用户时获取 TIP 令牌。
- **凭据 3LO**：`/identity fetch <provider>` 返回授权 URL。IdP 重定向到 Identity 提供的回调地址（控制台配置）。
- **凭据绑定**：`/identity set <provider> <envVar>` 将存储的凭据绑定到环境变量。凭据按工具调用粒度安全注入，并发多用户会话之间互相隔离。
- **本地身份服务（UDS）**：可选的 HTTP-over-Unix-Domain-Socket 服务，将 TIP 令牌、OIDC 会话令牌及所有插件工具暴露给本地其他进程。支持 Linux `SO_PEERCRED` + `/proc` 进程级校验和可配置进程白名单。
- **加密会话存储**：`sessions.json` 使用 AES-256-GCM 加密存储在磁盘上。旧版明文文件首次加载时自动迁移。
- **内存 TIP 缓存**：TIP 令牌仅存储在内存中（不持久化到磁盘）。TIP 是短效令牌，可随时从用户 session token 重新获取。
- **动态 UserPool**：通过 `userPoolName` + `clientName` 解析 OIDC 配置（无需手动配置 clientId）。
- **凭证加载**：从环境变量、文件或 STS AssumeRole 加载 AK/SK（veadk 风格）。

## HTTP 端点

仅暴露 OIDC 登录回调。凭据 OAuth 使用 Identity 回调。其余逻辑在 slash 命令中执行。

| 路径                       | 方法 | 描述                     |
| -------------------------- | ---- | ------------------------- |
| `/identity/oauth/callback` | GET  | OIDC 登录回调（IdP 重定向到此） |

## Slash 命令

单一命令 `/identity`（别名 `/id`）及其子命令。无参数时默认：`status`。

| 子命令                          | 描述                                                                 |
| ------------------------------- | -------------------------------------------------------------------- |
| _(无)_                          | 显示帮助。                                                           |
| `whoami`                        | 显示当前会话身份（sub、TIP 状态）。                                  |
| `login`                         | 已登录：刷新 TIP。未登录：返回需打开的 OIDC IdP URL。                |
| `status`                        | 登录状态、TIP、凭据。会话存在时尝试刷新 TIP。                         |
| `logout`                        | 清除当前会话的 session 和 TIP。                                      |
| `list-tips`                     | 列出所有有效 TIP 令牌及其委托链、过期时间和环境变量绑定。            |
| `config`                        | 显示 identity 插件配置（敏感信息脱敏）。                             |
| `list-credentials` 或 `list [page]` | 分页列出控制台 provider 及已绑定的凭据。使用 `list 2` 加载更多。 |
| `list-roles` | 列出 STS 角色类凭据提供方（非 OAuth/API key）。可选按名称前缀过滤。 |
| `get-role <provider> [--use-tip] [--show-secrets]` | 获取某角色提供方的 STS 临时凭据（默认脱敏）。 |
| `fetch <provider> [--flow=...]` | 添加凭据。flow 根据 provider 类型自动推断；可用 `--flow` 覆盖。       |
| `set <provider> <envVar>`       | 将凭据绑定到环境变量供工具注入。无凭据时从 `process.env[envVar]` 导入。 |
| `unset <provider>`              | 移除 provider 的环境变量绑定。                                       |

## OIDC 登录流程

1. 用户在聊天中发送 `/identity login`（如 Telegram、Discord）
2. 命令根据 channel/sender 推导 sessionKey，构建 IdP 授权 URL，保存 state
3. 命令返回 IdP URL，用户在浏览器中打开
4. 用户在 UserPool IdP 完成登录
5. IdP 携带 `code` 和 `state` 重定向到 `/identity/oauth/callback`
6. 插件交换 code，创建会话，显示成功页并向聊天发送消息

## 凭据获取流程

**OAuth2（用户联邦或 M2M）：**

1. 用户发送 `/identity fetch google` 或 `/identity fetch google --flow=oauth2-m2m`（在 `/identity login` 之后）
2. 命令使用 TIP 调用 Identity API，返回授权 URL 或直接返回 token
3. 若有授权 URL：用户打开，IdP 重定向到 Identity 回调（控制台 provider 配置）
4. Identity 处理回调，获取 token；用户可再次执行 fetch 拉取凭据

**API Key：**

1. 用户发送 `/identity fetch openai`（控制台 provider 类型为 api_key）或 `/identity fetch openai --flow=apikey`
2. 命令使用 TIP 调用 GetResourceApiKey，直接存储 API key

flow 根据 ListCredentialProviders 的 Type 和 Flow 自动推断。可通过 `--flow=oauth2-user|oauth2-m2m|apikey` 覆盖。

## 开通火山引擎 Agent Identity 服务

使用本插件前，需先在火山引擎开通 **智能体身份和权限管理平台** 并完成授权。详见：

- [开通智能体身份和权限管理平台并完成授权](https://www.volcengine.com/docs/86848/2123358?lang=zh)

## 获取火山引擎 AK/SK

插件调用 Identity API 需要火山引擎访问密钥（Access Key / Secret Key）。获取方式：

- [创建 Access Key - 火山引擎文档](https://www.volcengine.com/docs/6291/65568?lang=zh)

在控制台创建 AK/SK 后，可通过配置传入，或使用环境变量 `VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`。

## 安装

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

开发模式（link）：

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

## 配置

### 步骤二：配置插件（最小可用配置）

插件通常需要三类配置：

**A. 平台侧访问配置（Identity）**：用于获取 TIP Token、拉取/托管凭据、做权限校验（可选）。

- `endpoint`（可选）：Identity API 根地址（例如 `https://id.cn-beijing.volcengineapi.com`）。**优先级最高**。
- `regionMetadataUrl`（可选）：返回 **纯文本 region id**（如 `cn-beijing`）的 HTTP(S) URL。仅当 **未配置 `endpoint`** 时使用：客户端拼接 `https://id.{region}.volcengineapi.com`。单次请求约 10s 超时；失败或正文无效（如 `unknown`）时回退到 `https://id.cn-beijing.volcengineapi.com`。示例：`http://100.96.0.96/latest/region_id`（需网关能访问）。
- `accessKeyId` / `secretAccessKey`：用于访问 Identity API。**可选**，可使用环境变量或凭据文件（见下）。
- `workloadPoolName` / `workloadName`：用于签发 TIP Token。默认：`default`、`openclaw-agent`。
- `audience` / `durationSeconds`：可选，令牌受众与有效期。
- `credentialsFile`：凭据 JSON 文件路径。默认：`VOLCENGINE_CREDENTIALS_FILE` 环境变量或 `/var/run/secrets/iam/credential`。
- `credentialsMetadataUrl`：远程凭据拉取的完整 URL。**最高优先级凭据来源**——配置后优先于显式 AK/SK 与环境变量，避免因残留的过期凭据覆盖元数据下发的最新凭据。与 `roleTrn` 同时配置时，从 URL 拉取（响应：`AccessKeyId`、`SecretAccessKey`、`SessionToken`），再用 `roleTrn` 做 AssumeRole 获取最终凭据。流程与 AK/SK + roleTrn 一致。网络 / 解析失败时回退到下一优先级。按 ExpiredTime 缓存。需显式配置。
- `roleTrn`：STS AssumeRole 的 Role TRN。设置后（且未设置 `workloadName`）不传 workload name，后端使用 roleName。优先级：`workloadName` > `roleTrn` > params。与 `credentialsMetadataUrl` 配合时用于 AssumeRole，或与显式 AK/SK 配合。
- `sessionToken`：STS 会话令牌（或使用 `VOLCENGINE_SESSION_TOKEN` 环境变量）。

**Identity API 地址解析**：已配置 `endpoint` → 否则用 `regionMetadataUrl` 拉取 region 拼接 URL → 否则 `https://id.cn-beijing.volcengineapi.com`。**SigV4 签名用 region** 从解析后的主机名匹配 `id.{region}.volcengineapi.com` 推断；否则为 `cn-beijing`。

**工作负载池（凭据 / STS）**：`workloadPoolName`（默认 `default`）用于限定 `ListCredentialProviders`、`ListRoleCredentialProviders` 以及 `GetResourceOauth2Token`、`GetResourceApiKey`、`GetUserCredential`、`GetRoleCredentials` 等接口的 `PoolName`。若配置了 `userpool.userPoolName`，角色提供方列表还会在客户端按用户池再过滤。

**凭据解析顺序**（AK/SK）：1）远程元数据（`credentialsMetadataUrl`，可选 `roleTrn` AssumeRole；网络 / 解析失败时回退）→ 2）显式 config → 3）环境变量（`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`、`VOLCENGINE_SESSION_TOKEN`）→ 4）凭据文件（config 的 `credentialsFile`，或 `VOLCENGINE_CREDENTIALS_FILE` 环境变量，或 `/var/run/secrets/iam/credential`）。元数据 URL 排在最高优先级，避免残留的环境 / 显式 AK/SK 静默覆盖元数据下发的最新凭据。凭据文件格式（VeFaaS）：`access_key_id`、`secret_access_key`、`session_token`（可选）、`role_trn`（可选，用于 AssumeRole）。`RUNTIME_IAM_ROLE_TRN` 环境变量可在从文件加载时提供 role TRN。

**B. 用户登录配置（UserPool / OIDC）**：用于 `/identity login` 的用户登录与会话建立。

- `discoveryUrl`（或 `userPoolName` + `clientName` 动态解析模式）
- `clientId` / `clientSecret`（动态模式下可自动解析）
- `callbackUrl`：OpenClaw 网关对外可访问的回调地址，例如 `http://127.0.0.1:18789/identity/oauth/callback`
- `scope`：一般包含 `openid profile email`
- `identityProvider`（可选）：授权 URL 中 `identity_provider` 对应的 IdP 名称；不填则使用 `ListIdentityProviders` 返回的第一条。
- `useRelayCallback`（可选）：经 UserPool relay / `redirect_relay_uri` 的 OIDC 回调流程。默认 false。

**C. 权限校验与风险审批（AuthZ，可选）**：用于 TIP + CheckPermission + 风险评估与用户审批。各开关独立，无统一 `enable`。

- `agentCheck`：在 `before_agent_start` 中对 agent 执行 CheckPermission（resource type agent）。校验已认证用户是否有权限调用当前 agent。使用 TIP 委托链最外层 actor 作为 resource id。默认 false。
- `toolCheck`：对工具调用执行 CheckPermission（resource type tool）。默认 false。
- `skillReadCheck`：对 SKILL.md 读取执行 CheckPermission（resource type skill）。解析 system prompt 中的 available_skills。默认 false。
- `requireRiskApproval`：高风险工具调用需用户审批。默认 false。
- `namespaceName`：CheckPermission Cedar 策略命名空间。默认 `default`。
- `lowRiskBypass`：内置低风险工具是否跳过 TIP+CheckPermission。默认 true。
- `lowRiskTools`：额外视为低风险的工具名列表。
- `enableLlmRiskCheck`：规则返回 medium 时是否用 LLM 二次评估。默认 false。
- `llmRiskCheck`：LLM 配置（`endpoint`、`api`、`model`、`apiKey`、`timeoutMs`、`cacheTtlMs`）。`enableLlmRiskCheck` 为 true 时必填。
- `approvalTtlSeconds`：审批链接/命令的 TTL（秒）。默认 300。

**预期结果**：配置完成后，插件可正常发起登录、获取 TIP Token。开启 AuthZ 相关开关后，agent/工具/skill 权限检查与高风险审批生效。高风险工具调用通过 OpenClaw 原生 `requireApproval` 机制处理（UI 覆层、Telegram 按钮、Discord 交互、`/approve` CLI）。

---

在 `openclaw.json` 的 `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 凭据**：省略 `accessKeyId`/`secretAccessKey` 时，使用环境变量（`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`）或凭据文件（`VOLCENGINE_CREDENTIALS_FILE` 或 `/var/run/secrets/iam/credential`）。

### identity 配置（必填与可选）

| 参数 | 类型 | 必填 | 含义 |
|------|------|------|------|
| `endpoint` | string | 否 | Identity API 根地址。不填则通过 `regionMetadataUrl` 或默认 `https://id.cn-beijing.volcengineapi.com` |
| `regionMetadataUrl` | string | 否 | 返回纯文本 region 的 URL；在未配置 `endpoint` 时拼接 `https://id.{region}.volcengineapi.com` |
| `accessKeyId` | string | 否* | 火山引擎 Access Key。不填时从 `VOLCENGINE_ACCESS_KEY` 或 `credentialsFile` 读取 |
| `secretAccessKey` | string | 否* | 火山引擎 Secret Key。不填时从 `VOLCENGINE_SECRET_KEY` 或 `credentialsFile` 读取 |
| `workloadPoolName` | string | 否 | 工作负载池名称，默认 `default` |
| `workloadName` | string | 否 | 工作负载名称。设置时优先于 roleTrn。两者都未设置时默认：agentId 或 `openclaw-agent` |
| `audience` | string[] | 否 | TIP token 的 audience |
| `durationSeconds` | number | 否 | TIP token 有效期（秒），默认 3600 |
| `roleTrn` | string | 否 | STS AssumeRole 的 Role TRN。设置后（且未设置 workloadName）不传 workload name，后端使用 roleName。优先级：workloadName > roleTrn > params |
| `credentialsFile` | string | 否 | 凭证 JSON 文件路径。默认 `VOLCENGINE_CREDENTIALS_FILE` 或 `/var/run/secrets/iam/credential` |
| `credentialsMetadataUrl` | string | 否 | 远程凭据拉取的完整 URL。与 `roleTrn` 同时配置时拉取后做 AssumeRole。404 时回退到 `credentialsFile` |
| `sessionToken` | string | 否 | STS 临时会话令牌（或 `VOLCENGINE_SESSION_TOKEN`） |
| `subagentTipPropagation` | boolean | 否 | 将 TIP 和 session 传播到子 agent。默认 false |
| `webchatSessionExchange` | boolean | 否 | 启用 `identity.session.put` / `identity.session.get` gateway WS 方法供 webchat 客户端使用。默认 false |
| `personalSessionMode` | boolean | 否 | 个人/单用户模式：TIP、OIDC session、凭据仅存储在 `agent:main:main`（不做按发送者或 per-channel-peer 隔离）。子 agent 会话不变。默认 false；多租户或群聊共享场景勿开启。 |
| `credentialExchanges` | object[] | 否 | 由运维侧声明的 HTTP 换取规则，将当前会话的 TIP 换成下游凭据。通过 `identity_exchange_credential` 暴露。URL scheme 非法或 id 重复的 profile 在加载时被丢弃并记 warn 日志。详见[凭据换取 Profile](#凭据换取-profile)。 |
| `localServer` | boolean | 否 | **已废弃 — 值被忽略。** identity 配置存在时本地 UDS 身份服务始终启动（外部调用方依赖其常驻）。详见[本地身份服务（UDS）](#本地身份服务uds)。 |
| `localServerAllowlist` | string[] | 否 | 额外允许访问 UDS 服务的进程名/路径。`curl` 始终允许。 |
| `localServerFailOpen` | boolean | 否 | **已废弃 — 值被忽略。** 服务始终以 fail-open 模式运行（基础安全由 0600 socket 权限 + 对端白名单兜底）。 |

\* AK/SK 至少通过 `accessKeyId`+`secretAccessKey`、环境变量、`credentialsMetadataUrl`+`roleTrn` 或 `credentialsFile` 之一提供。

**环境变量**：`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY`、`VOLCENGINE_SESSION_TOKEN`、`VOLCENGINE_CREDENTIALS_FILE`、`RUNTIME_IAM_ROLE_TRN`（从文件加载时用于 AssumeRole）。设置 `IDENTITY_STS_DEBUG=1` 可打印完整 STS AssumeRole 请求/响应用于调试。

### userpool 配置（OIDC 登录）

**Explicit 模式**（必填）：`discoveryUrl`、`clientId`、`clientSecret`、`callbackUrl`、`scope`

**Dynamic 模式**（必填）：`userPoolName`、`clientName`、`callbackUrl`；`autoCreate` 默认 true

OAuth2 credential fetch 使用控制台配置的 redirect URL 和 scopes。可通过 `/identity fetch <provider> --redirectUrl` 和 `--scopes` 覆盖。

### authz 配置（可选，各开关独立）

| 参数 | 类型 | 含义 |
|------|------|------|
| `agentCheck` | boolean | 在 `before_agent_start` 中对 agent 执行 CheckPermission（resource type agent）。校验用户是否有权限调用当前 agent，使用 TIP 委托链最外层 actor 作为 resource id。默认 false。 |
| `toolCheck` | boolean | 对工具调用执行 CheckPermission（resource type tool）。默认 false。 |
| `skillReadCheck` | boolean | 对 SKILL.md 读取执行 CheckPermission（resource type skill）。默认 false。 |
| `requireRiskApproval` | boolean | 高风险工具调用需用户审批。默认 false。 |
| `namespaceName` | string | CheckPermission Cedar 命名空间。默认 `default`。 |
| `lowRiskBypass` | boolean | 内置低风险工具是否跳过 TIP+CheckPermission。默认 true。 |
| `lowRiskTools` | string[] | 额外视为低风险的工具名列表。 |
| `enableLlmRiskCheck` | boolean | 规则返回 medium 时用 LLM 二次评估。默认 false。 |
| `llmRiskCheck` | object | LLM 配置：`endpoint`、`api`、`model` 等。`enableLlmRiskCheck` 为 true 时必填。 |
| `approvalTtlSeconds` | number | 审批 TTL（秒）。默认 300。 |

### 工作负载与 TIP

TIP token 通过 `GetWorkloadAccessTokenForJWT` 获取。工作负载行为：

- **workloadName**（可选）：发送给 API 的工作负载名称。优先级：config.workloadName > config.roleTrn > params（会话 agentId 或 `"openclaw-agent"`）。当 config.workloadName 设置时，优先于 roleTrn。
- **roleTrn**（可选）：设置后（且未设置 workloadName），插件**不**传递 workload name，后端使用 roleName。用于委托执行（如 VeFaaS、K8s IRSA）。
- **自动创建工作负载**：当 `GetWorkloadAccessTokenForJWT` 返回 404（工作负载不存在）时，插件调用 `CreateWorkloadIdentity` 创建工作负载（Category: Agent），然后重试。仅在使用 workload name 时生效（设置了 workloadName 或两者都未设置）。并发创建产生的 Duplicated (409) 会被忽略。

### 凭据换取 Profile

部分下游服务不直接接受 TIP token，而是提供自己的换取端点：用 TIP 换出该服务原生凭据（如服务专用 JWT、不透明 token）。`identity.credentialExchanges` 用于声明这类换取规则，让 agent 通过 `identity_exchange_credential` 工具完成换取，不再需要为每个租户单独写代码。

换取按需触发、按 session 隔离：插件读取调用方会话的 TIP，请求配置的端点，按规则提取响应中的值，再按 TTL 缓存，命中缓存的后续调用直接复用、不走网络。**Per-session 隔离**保证不同用户换出的凭据永不交叉。

**Profile 字段**（`identity.credentialExchanges[]`）：

| 字段 | 类型 | 必填 | 含义 |
|------|------|------|------|
| `id` | string | 是 | 稳定标识；与 `identity_exchange_credential` 工具的 `id` 参数一一对应。重复 id 在加载时被丢弃。 |
| `request.method` | `"GET" \| "POST"` | 否 | HTTP 方法。默认 `GET`。 |
| `request.url` | string | 是 | 端点 URL。必须为 `https://`，本地测试可用 `http://localhost` / `http://127.0.0.1`，其他 scheme 在加载时被丢弃。**不**支持模板替换。 |
| `request.headers` | `Record<string,string>` | 否 | 请求头。值支持 `{{tipToken}}` 替换。 |
| `request.query` | `Record<string,string>` | 否 | Query 参数。值支持 `{{tipToken}}` 替换。 |
| `request.body` | string | 否 | 请求体。**仅** `method = "POST"` 时生效。支持 `{{tipToken}}` 替换。 |
| `request.contentType` | string | 否 | body 的 `Content-Type`，仅当 `headers` 中未设置时写入。 |
| `request.timeoutMs` | number | 否 | 请求超时（毫秒）。默认 `10000`。 |
| `extract.source` | `"responseHeader" \| "jsonBody" \| "textBody"` | 是 | 从响应的哪个位置读取换取值。 |
| `extract.name` | string | 条件必填 | 响应头名。`source = "responseHeader"` 时必填。 |
| `extract.path` | string | 条件必填 | 点号分隔的 JSON 路径（如 `data.token`）。`source = "jsonBody"` 时必填。**不是** RFC 6901 pointer，不要以 `/` 开头。 |
| `cache.ttlSeconds` | number | 否 | 缓存 TTL（秒）。默认 `300`。 |
| `cache.refreshBeforeSeconds` | number | 否 | 缓存条目在 `expiresAt` 前多少秒被视为过期。默认 `30`。 |

**模板替换**：仅识别字面量 `{{tipToken}}`，且仅在 `request.headers`（值）、`request.query`（值）、`request.body` 中生效。`request.url` 永不替换。Mustache 风格的 `{{ }}` 分隔符与 config-load 阶段的 `${VAR}` 环境变量渲染刻意区分，避免两个阶段的语法冲突。替换发生在换取时（会话的 TIP 已就绪 / 已刷新之后）。

**缓存 key**：`${profileId}::${sessionKey}`。即便 profile 相同，不同会话也绝不共享换取结果。

**安全要点**：

- URL scheme 在配置加载阶段强校验（仅 `https://`，本地回环用于测试除外）。
- 出站请求走 `noProxyFetch`，绕过全局代理。
- 换取值仅保存在内存缓存与工具结果中，不落盘。
- 工具结果**仅在**调用方传 `returnValue: true` 时包含 `value`。skill / agent 应将 `value` 视为一次性环境变量输入，**永远**不要在聊天回复、日志或命令回显里输出（每条工具结果末尾追加的 `Identity Credential Runtime Contract` 会强制约束这一点）。

**示例**——把 TIP 换成 partner JWT 并注入 `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 }
      }
    ]
  }
}
```

agent 调用 `identity_exchange_credential`，参数 `{ "id": "partner-jwt", "returnValue": true }`，再把返回的 `value` 写到 `PARTNER_JWT`（通常由 skill 中声明的 `Identity Credential Runtime Contract` 绑定项驱动）。

**通过 skill metadata 接入**——若希望 agent 自动完成换取并注入，在 SKILL.md frontmatter 的 `metadata.openclaw.identity` 中声明 `exchange` 绑定：

```yaml
---
name: my-partner-skill
metadata:
  openclaw:
    identity:
      - type: exchange
        id: partner-jwt          # 必须与 identity.credentialExchanges[].id 一致
        env: PARTNER_JWT         # 下游工具使用的目标环境变量
        required: true           # 换取失败时中止下游调用
---
```

插件会把该绑定渲染进每条工具结果末尾追加的 `Identity Credential Runtime Contract` 区块，agent 据此调用 `identity_exchange_credential`、参数 `{ id: "partner-jwt", returnValue: true }`，读取结果中的 `value`，在调用下游工具前赋值给 `PARTNER_JWT`。401/403 触发的刷新通过 `forceRefresh: true` 处理。

### 飞书通知

当用户从飞书聊天运行 `/identity` 时，登录成功和凭据获取的跟进消息会通过飞书发送。凭据从 openclaw.json 的 `channels.feishu` 读取（与 feishu 扩展相同：`appId`、`appSecret`，可选 `accounts`）。agent-identity 无需额外配置。

**审批消息**（当高风险工具被拦截时）：插件从 `before_tool_call` 返回 `requireApproval`，OpenClaw 框架在所有渠道原生处理审批 UI（exec 覆层、Telegram 按钮、Discord 交互、`/approve` CLI）。无需插件侧推送逻辑。

### WebChat Session Exchange（Gateway WS 方法）

当 `identity.webchatSessionExchange` 为 `true` 时，插件注册两个 gateway WebSocket 方法，允许 webchat 客户端直接注入和获取 session token，无需走 OIDC 重定向流程：

| 方法 | 参数 | 响应 | 描述 |
| --- | --- | --- | --- |
| `identity.session.put` | `{ sessionKey, idToken, refreshToken?, senderId?, channel? }` | `{ sub, expiresAt, effectiveSessionKey, hasTip }` | 将 OIDC id_token 注入到插件 session；可选传入 `refreshToken`（加密存储），用于静默续期。通过 `buildEffectiveSessionKey` 解析实际存储 key（与 hooks/commands 相同的隔离逻辑）。 |
| `identity.session.get` | `{ sessionKey, senderId?, channel? }` | `{ userToken, sub, expiresAt, effectiveSessionKey, hasRefreshToken }` | 获取指定 session 已存储的 user token。`hasRefreshToken` 表示是否存有 refresh token；响应中不会返回 refresh token 明文。 |

- `senderId` 默认值为 `"openclaw-control-ui"`。shared mode 下，实际存储 key 默认严格跟随传入的 OpenClaw `sessionKey`。
- `channel` 可选；当 canonical main session `agent:{id}:main` 来源于可发送消息的 IM 渠道（feishu、telegram 等）时，`channel + senderId` 会提升为 `agent:{id}:{channel}:direct:{senderId}`。群聊/channel session key 会追加 `:user:{senderId}` 做发送者隔离。

两个方法均**限制为 webchat WS 连接**（`isWebchatConnect` 检查），非 webchat 客户端会收到 `FORBIDDEN` 错误。

**配置：**

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

**典型流程（BFF → webchat → plugin）：**

1. BFF 完成 3LO 登录并获取用户的 OIDC `id_token`
2. Webchat 客户端调用 `identity.session.put`，传入 session key 和 `id_token`（若需静默续期，可一并传入 token 响应中的 `refresh_token` 作为 `refreshToken`）
3. 插件校验 token，存储 session，并获取 TIP
4. 后续该 session 中的 agent 运行拥有有效身份——无需手动登录

### WebChat / TUI

用户从 WebChat 或 TUI 运行 `/identity` 时，跟进消息不会投递；插件无 API 推送至这些渠道。请使用 `/identity status` 确认结果。

## 工具

面向 Agent 的用法见 [`skills/SKILL.md`](skills/SKILL.md)。本插件注册的工具有：

- **identity_whoami** — 会话身份（sub、TIP）
- **identity_status** — 登录、TIP、凭据、绑定
- **identity_login** / **identity_logout** — OIDC 登录或刷新 TIP；清除会话
- **identity_list_credentials** — OAuth/API key 类 provider 与已存凭据（支持 `page`、`name`、`flow`、`type` 过滤）
- **identity_list_roles** — STS 角色类凭据提供方（`name` 前缀过滤）
- **identity_fetch** — 添加凭据（`provider`、`flow`、`redirectUrl`、`scopes`、`returnValue`）
- **identity_get_role_credentials** — 某角色提供方的 STS 临时凭据（`providerName`、`useTip`）
- **identity_get_tip_token** / **identity_get_session_token** — 原始 TIP JWT 或会话 user token（进阶场景）
- **identity_exchange_credential** — 通过预配置的 profile 把当前会话的 TIP 换取下游凭据（`id`、`returnValue`、`forceRefresh`）。详见[凭据换取 Profile](#凭据换取-profile)。
- **identity_config** — 当前生效配置（脱敏）
- **identity_config_suggest** — 可合并的配置片段（`intent`、`lang`）
- **identity_set_binding** / **identity_unset_binding** — 工具注入用的环境变量绑定
- **identity_risk_check** / **identity_list_risk_patterns** — 风险评估（可选插件能力）
- **identity_list_tips** — 所有有效 TIP 与绑定（运维 / 多会话排查）

## 钩子

- **before_dispatch** - 无状态 LLM 前置认证拦截。在 LLM 调用前检查 TIP 令牌是否有效。未找到 TIP 时，以静态回复（包含 OIDC 登录 URL）阻断消息，LLM 零 token 消耗。开启 `authz.agentCheck` 后同时执行 agent 级 CheckPermission。设置特性标志使 `before_agent_start` 跳过冗余认证逻辑。
- **before_agent_start** - 面向不支持 `before_dispatch` 的旧版 OpenClaw 的回退认证拦截。当 `before_dispatch` 活跃时，此钩子的认证逻辑为空操作。仍负责获取 TIP 令牌和执行 agent CheckPermission。
- **subagent_spawned** - 在子 agent 创建时将 TIP 传播到子会话。
- **before_tool_call** - 群组上下文注入、可选 AuthZ（TIP 检查、CheckPermission、通过 `requireApproval` 处理风险审批）、工具调用级凭据注入。高风险工具调用返回 `requireApproval` 给 OpenClaw 框架，由框架原生处理审批 UI。
- **after_tool_call** - 清理工具调用级凭据注入状态。

## 本地身份服务（UDS）

只要 identity 完成配置，插件就会启动一个基于 Unix Domain Socket 的 HTTP 服务，允许本地其他进程（脚本、插件、CLI 工具）访问身份令牌、签发 egress 劫持地址、执行插件工具，无需经过 OpenClaw 消息通道。（`identity.localServer` 与 `identity.localServerFailOpen` 仍为兼容性保留但值被忽略 —— 服务始终常驻并以 fail-open 模式运行；访问控制由 0600 socket 权限 + 下面的对端白名单提供。）

### 配置

| 参数 | 类型 | 描述 |
|------|------|------|
| `localServer` | boolean | **已废弃 — 值被忽略。** identity 配置存在时常驻启动。 |
| `localServerAllowlist` | string[] | 额外允许连接的进程名或路径。支持精确匹配和 glob 后缀（如 `"python*"`）。`curl` 默认始终允许。 |
| `localServerFailOpen` | boolean | **已废弃 — 值被忽略。** 始终为 fail-open 模式。 |

**Socket 路径**：`~/.openclaw/plugins/identity/identity.sock`（权限 `0600`，仅 owner 可访问）。

### 端点

| 方法 | 路径 | 描述 |
|------|------|------|
| `GET` | `/token` | 主会话的 TIP 令牌 |
| `GET` | `/token?session=<key>` | 指定会话的 TIP 令牌 |
| `GET` | `/session` | 主会话的 OIDC session（id_token） |
| `GET` | `/status` | 服务健康状态与可用会话概览 |
| `GET` | `/tools` | 列出所有可用工具名称、描述及 JSON Schema |
| `POST` | `/tool/<name>` | 执行指定工具。Body：`{ "params": {...}, "session": "..." }` |

### 进程级校验（Linux）

在 Linux 上，服务通过以下方式校验连接进程：

1. **`SO_PEERCRED`**（内核级）：通过 `getsockopt` 零开销获取对端 PID/UID/GID。需注册原生 provider。
2. **`/proc` 文件系统**：从 `/proc/<pid>/exe`、`/proc/<pid>/comm`、`/proc/<pid>/status` 获取进程名和路径，无需 spawn 外部进程。

解析后的进程信息与内置白名单（`curl`）及 `localServerAllowlist` 自定义条目做匹配。无法识别的对端始终被放行（服务硬编码为 fail-open；socket 文件权限提供基础安全保障）。

### 使用示例

```bash
# 获取 TIP 令牌
curl --unix-socket ~/.openclaw/plugins/identity/identity.sock http://localhost/token

# 获取 OIDC session 令牌
curl --unix-socket ~/.openclaw/plugins/identity/identity.sock http://localhost/session

# 列出可用工具
curl --unix-socket ~/.openclaw/plugins/identity/identity.sock http://localhost/tools

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

更多语言示例见 [`demo/local-server/`](demo/local-server/)（Python、Go、TypeScript、Java、Rust）。

## 数据存储

插件数据位于 `~/.openclaw/plugins/identity/`：

| 文件 | 描述 |
|------|------|
| `sessions.json` | 加密存储的 sessionKey → userToken 映射。加载/保存时清理过期条目。 |
| `credential-env-bindings.json` | 按 session：`{ [sessionKey]: { [provider]: envVar } }` |

**仅内存存储（不持久化到磁盘）：**

| 数据 | 描述 |
|------|------|
| TIP 令牌 | sessionKey → TIP token 缓存。短效，按需从 session token 重新获取。 |
| 凭据 | 按 session（api_key、oauth2）。gateway 重启后丢失；logout 时清除。 |
| OIDC state | 临时数据，5 分钟 TTL。 |
