# 密钥

了解 Beeuni 如何存储项目凭据，以及如何控制智能体代码是否可以读取每个值。

Canonical page: https://beeuni.ai/docs/zh/project/secrets

机密信息是每个项目独有的凭据——API 密钥、令牌或连接字符串——会话需要使用它，但不能将其存放在代码仓库中。
Beeuni 将机密信息存储在项目中，从不存储在账户中，并使用按项目派生的密钥，通过 AES-256-GCM 加密每个值。

<a id="identifier-and-name"></a>

## 标识符和名称

每个机密信息有两个名称。`identifier` 是你在 CLI 和代理的 `secrets` 授权中使用的句柄。`name` 是沙盒中的大写环境变量键，例如 `STRIPE_API_KEY`。在大多数项目中，标识符和名称相同。只有当一个项目为同一个名称保存多个候选值时，两者才会不同——例如，一个主 Google Maps 密钥和一个备用密钥都可以解析为 `GOOGLE_MAPS_API_KEY`。

每条访问规则都使用标识符，不使用名称。

<a id="exposure-and-usage"></a>

## 暴露方式和用途

机密信息有两个相互独立的设置。请分别查看它们；它们回答的是不同的问题。

| 设置 | 回答的问题 | 值 |
|---|---|---|
| **暴露方式** | 代理代码可以读取真实值吗？ | `environment`、`egress-enforced`、`none` |
| **用途** | 谁会使用它？ | 代理代码、LLM 网关、连接器、Git |

Beeuni 的旧文档曾将两者混在同一个列表中。该列表已移除。不存在“网络边界”和“HTTPS 代理”之间的选择：所有沙盒提供商上的每个受出口控制的机密信息都使用同一种机制。

<a id="the-three-exposures"></a>

### 三种暴露方式

| 暴露方式 | 沙盒中保存的内容 | 适用场景 |
|---|---|---|
| **环境变量** | 作为普通环境变量保存的真实值 | 默认方式。代理必须进行计算的值，以及非 HTTPS 协议 |
| **受出口控制** | 一个**句柄**——自描述的占位符，本身没有任何价值 | 实验性功能。`secrets_egress` 标志启用后用于 HTTPS 调用 |
| **无** | 什么也不保存 | 仅由 Beeuni 服务使用的凭据，或已保存但停用的值 |

> **工作规则**
>
> **环境变量**是默认的暴露方式。真实值会加载到沙盒中，代理可以读取、打印并转发它。**受出口控制**会将值保留在沙盒之外，但它是实验性功能：启用 `secrets_egress` 功能标志（Settings → Feature flags）后才能使用。在此之前，机密信息会加载到沙盒环境中。

<a id="usages"></a>

### 用途

大多数用途由 Beeuni 分配，而不是由你分配：

- **代理代码**——只要暴露方式不是 `none`，就会隐含分配此用途。
- **LLM 网关**——当该值被识别为模型提供商密钥时分配。网关会在服务器端验证提供商请求。
- **Connector:&lt;slug&gt;**——由连接器绑定流程分配。
- **Git**——Beeuni 为自身的 Git 访问分配。只读；你无法设置或清除它。

暴露方式为 `none` 且没有用途的机密信息会显示为**已禁用**：已存储并加密，但不会被任何对象使用。

<a id="sent-secrets-and-computed-secrets"></a>

## 传送型机密信息和计算型机密信息

机密信息可以使用哪种暴露方式，取决于**上游**，而不是 Beeuni。

- **传送型机密信息**——值会通过网络传输。API 密钥、Bearer 令牌、密码都属于此类，绝大多数机密信息也是如此。值会在出站请求中以字节形式存在一段时间，因此 Beeuni 可以在沙盒之外自行将其放入请求中。启用 `secrets_egress` 标志后，传送型机密信息可以改为**受出口控制**；默认情况下，它会加载到沙盒环境中。
- **计算型机密信息**——值是计算中的一个组成部分，从不会被传输。AWS SigV4 签名密钥、HMAC webhook 签名机密、JWT 客户端断言、SSH 私钥都属于此类。执行计算的对象必须持有该值。网络边界没有帮助，因为网络传输的内容中不包含凭据。计算型机密信息必须保持为**环境变量**。

环境变量是默认的暴露方式，也是唯一可以为沙盒执行数学运算所需的凭据提供服务的方式。非 HTTPS 协议——例如 Postgres 连接字符串和 SMTP 凭据——处于同样的情况：必须保持为环境变量。

当你保存看起来像签名材料的值（例如 `AKIA…` 访问密钥对或 PEM/SSH 材料）时，Beeuni 会将其默认设置为**环境变量**，并说明原因：该密钥会在本地签署请求，因此无法应用出口控制。

<a id="egress-enforced-exposure"></a>

## 受出口控制的暴露方式

> **实验性功能——需要 secrets_egress 标志**
>
> 网络层面的控制是实验性功能。启用 `secrets_egress` 功能标志（Settings → Feature flags）后才能使用。在此之前，机密信息会加载到沙盒环境中。标志关闭时，创建受出口控制的机密信息会返回 `403` `feature_disabled`。

沙盒会接收一个环境变量，其值是**句柄**，而不是凭据。代理使用该变量的方式与使用真实密钥完全相同——可以放在标头、查询字符串或请求正文中。`Authorization: Bearer $VAR`、`Cookie: …=$VAR`、`X-Api-Key` 标头、查询参数，以及 JSON 或表单正文字段都可以使用：无论句柄出现在哪里（原始形式、URL 编码形式、独立的 base64 形式或 JSON 转义形式），Beeuni 都能找到它并替换为真实值。请求发出时，Beeuni 会将句柄替换为真实值，但仅限于发送到你批准的主机的请求。

> **按原样发送句柄——不要自行进行 base64 编码**
>
> 有一种方式不可用：将句柄隐藏在你自行构建的 base64 数据块中，而 **HTTP Basic auth** 就是这样做的（`curl -u $VAR:` → `Authorization: Basic <base64>`）。Beeuni 无法找到嵌入在你编码的 base64 中、未对齐的句柄，因此不会发生替换，上游会返回 `401`。请将句柄放在 Bearer/令牌标头、查询参数或正文字段中。如果 API 仅支持 Basic auth，请为该机密信息使用**环境变量**暴露方式。

由此可得：

- 真实值**永远不会**出现在沙盒中：不是环境变量，不是文件，也不是别名。
- 发送到未批准主机的句柄会以字面句柄字符串的形式到达。上游会拒绝它。它本身没有任何价值。
- 签名错误的句柄永远不会被接受，Beeuni 会将其记录为伪造句柄。对于本会话无权使用的机密信息，合法句柄会被记录为被盗句柄。
- 每个中继请求都会写入一条按请求记录的审计记录。
- 不在列表中的主机会在不读取内容的情况下通过隧道传输。连接到这些主机的固定 TLS 和 mTLS 客户端不受影响。

<a id="hosts-match-exactly"></a>

### 主机必须完全匹配

列出你调用的每个主机名，每行一个精确的主机名。

| 被拒绝的内容 | 原因 |
|---|---|
| 通配符主机、`*.example.com` | 代理绝不能选择目标 |
| HTTPS 以外的 URL 协议 | Beeuni 需要终止 TLS 才能进行替换 |

`api.example.com` 不涵盖 `uploads.api.example.com`。请将第二个主机添加到同一个机密信息中。保存时，如果策略无法执行，Beeuni 会以 `400` 拒绝该策略——它绝不会存储无法应用的规则。

两个实施了出口限制的密钥可以共用一个主机。每个句柄都映射到各自的
值，因此两者都会在同一请求中正确替换。

<a id="verify-it-with-two-probes"></a>

### 用两个探测进行验证

在沙箱内运行两个探测，目标是列表中的主机。
示例使用 `postman-echo.com`，它提供这两种类型的各一个端点。
在测试期间将其添加为允许的主机。

```bash
# 1. Reachability — an endpoint that does NOT echo request headers.
curl -s -o /dev/null -w '%{http_code}\n' https://postman-echo.com/status/200
# expected: 200

# 2. Substitution — an endpoint that DOES echo request headers.
curl -sS -H "authorization: Bearer $STRIPE_API_KEY" https://postman-echo.com/get
# expected: 200, with "Bearer [REDACTED]" in the echoed headers
```

结合起来查看这两个结果，并先看探测 1：只有它能告诉你
主机是否可访问；只有它通过后，探测 2 才有意义。

| 探测 2 结果 | 含义 |
|---|---|
| `200`，回显的标头显示 `[REDACTED]` | 正常工作。真实值已发送到上游，返回时回显内容已被编辑。 |
| `200`，回显的标头显示句柄本身 | 未执行替换。检查主机列表和智能体授权。 |
| 来自真实 API 主机的 `401` | 未执行替换。检查同样的两项设置。 |
| 空响应或连接错误 | 确实发生了故障。这不是成功的表现。 |

确认真实值没有出现在来宾环境中的任何位置：

```bash
env | grep '^STRIPE_API_KEY='
# expected: the handle, not the credential
```

该标识符也会出现在 `BEEUNI_SECRET_CAPABILITIES` 中。这是一个不含值的
目录，告知智能体有哪些密钥、每个句柄由哪个变量保存，以及每个密钥适用于哪些
主机。它绝不包含任何值。

列表中的主机会提供 Beeuni 为此沙箱颁发的证书，而不是源站自己的证书；
沙箱已信任该证书：

```bash
curl -sv https://postman-echo.com/status/200 2>&1 | grep 'issuer:'
# issuer: CN=Beeuni Egress CA (<project prefix>); O=Beeuni
```

<a id="what-the-relay-changes"></a>

### 中继会带来哪些变化

经批准的主机通过 Beeuni 访问，因此其行为与直接调用略有不同：

- 响应不会以流式方式传输。服务器发送事件和 WebSocket 无法通过
  已批准的主机使用。
- 请求正文上限为 1 MiB，响应上限为 5 MiB，整个调用的时限为
  30 秒。Beeuni 最多跟随 3 次重定向。
- 返回的响应标头仅限固定的一组：内容类型和语言、
  缓存验证器、速率限制相关标头、`retry-after`、`x-request-id`。
- 只有遵循 `https_proxy` 的客户端才会被拦截。Beeuni 会在智能体的环境中设置该
  变量，并配置相应的 CA 信任。若进程在经过清理的环境中启动，就会直接调用主机，
  请求中会携带句柄。

对于 Beeuni 无法拦截的请求，智能体可以通过一个明确的入口访问相同主机，
并遵循相同策略：

```bash
beeuni secrets call STRIPE_API_KEY https://api.stripe.com/v1/customers
```

<a id="environment-exposure"></a>

## 环境变量暴露

沙箱会将真实值作为普通环境变量接收。智能体代码可以读取它、打印它，
并将它转发到任何地方。Beeuni 无法编辑该值、审计其使用情况，
也无法阻止它被传出。

环境变量是默认的暴露方式。每个新密钥都使用环境变量，除非你将其改为
实施出口限制的方式；此方式仍处于实验阶段，需要启用
`secrets_egress` 标志。计算得出的凭据以及任何非 HTTPS 协议，必须
继续使用环境变量。

<a id="who-can-use-a-secret"></a>

## 谁可以使用密钥

每个密钥值都有一个受众范围——与连接器账户的 **谁可以使用它** 选项相同：

| 选项 | 谁可以使用该值 |
| --- | --- |
| **项目中的所有人**（默认） | 项目中的每个会话，包括触发器、计划任务和共享会话。 |
| **仅你** | 你本人，以及你自己的私人会话。 |
| **指定人员、群组或智能体** | 你选择的人员和群组（本人可直接使用，也可在其私人会话中使用），以及你选择的智能体（可在该智能体的所有会话中使用）。 |

一个会话同时具有两种身份：它**始终是**其**智能体**，并且**仅在会话为私人会话时**才是其
**发起人**。

- **发起人**可以在由其本人启动的**私人**会话中使用某个值。其他任何人的提示都会清除该会话的
  发起人身份，因此该值不再提供给该会话。共享会话、触发器、
  计划任务、Webhook、电子邮件和 Telegram 会话都没有发起人身份。
- **智能体**可在其所有会话中使用某个值，包括触发器、计划任务和共享会话。若无人值守的运行需要某个值，
  请将其共享给该智能体。任何可以运行该智能体的人都可以通过它使用该值，因此运行该智能体就是访问关卡（谁可以运行智能体由
  智能体上的设置决定）。
- 服务端使用者遵循相同规则。绑定到密钥的连接器仅会为受众范围内的调用者使用该密钥。其他人会收到
  `credential_not_shared`，且不会有请求离开 Beeuni。
- 不代表任何人行事的 Beeuni 服务——Git 代理、Webhook 签名
  检查、频道安装和 Beeuni Apps——只能使用共享给所有人的值。

多个值可以使用同一个名称。与会话发起人共享的值优先于与其智能体共享的值，后者又优先于与所有人共享的值。
例如，`DEEL_API_TOKEN` 保存一个团队值，另一个值使用标识符
`DEEL_API_TOKEN-finance`，且仅共享给 Finance 群组。Finance 成员会在其私人会话中获得 Finance 值；
其他所有人以及每个触发器都会获得团队值。

受众范围由人员设置。智能体会话无法更改它（`403`），因此智能体不能扩大
对其已不可用的值的访问范围，也不能将自己添加为受众。管理项目密钥的人员仍可以看到未与其共享的值，
这些值会标记为 **未与你共享**，以便他们重新扩大受众范围。

<a id="sharing-a-session-that-holds-a-personal-value"></a>

### 共享包含个人值的会话

如果某个仅与你共享的值作为**环境变量**提供给了你的私人会话，该值就以明文形式存在于该沙箱中。共享会话
会让每位新查看者都能读取它，而且无法从正在运行的沙箱中移除环境变量。因此，Beeuni 会拒绝共享该会话，
也会拒绝为其创建公开链接，并显示 `409 PERSONAL_SECRET_REQUIRES_PRIVATE_SESSION`
以及其中包含的名称。若要共享，请启动新会话，或先将该值共享给所有人。通过智能体获得的值，以及实施出口限制的
值或服务端值（每次请求都会检查），不会阻止共享。

缩小受众范围时，Beeuni 会重新推送到每个正在运行的沙箱。作为环境变量发放的值会一直留在沙箱中，直到沙箱
重新启动。如果该值可能已被读取，请轮换它。

<a id="when-you-enter-a-value-through-a-link"></a>

### 通过链接输入值时

需要某个你尚未提供的值的智能体会发送一个链接。链接页面会提供 **谁可以使用它** 选项：项目中的所有人（默认），或仅限提出请求的人员。
对于任何个人信息，请选择第二项，例如工资单令牌或个人登录信息。页面只会显示请求者的显示名称，
不会显示电子邮件，因为任何拥有链接的人都可以打开页面。

<a id="personal-override"></a>

### 个人覆盖值

个人覆盖值是你为某个名称设置的专属值，在你启动的会话中会取代共享值。目前，Beeuni 仅将此功能用于一个 OAuth
登录凭据。Beeuni 绝不会让 LLM 提供商密钥（例如
`ANTHROPIC_API_KEY`）成为个人覆盖值。

<a id="add-a-secret"></a>

## 添加密钥

### 通过 CLI 设置值
```bash
beeuni secrets set STRIPE_API_KEY=sk_live_...
```

Beeuni 会将其保存为项目中的所有人可用。添加 `--only-me`，即可将其
设为仅你可用。若要在同一名称下存储多个值，请添加
`--identifier <id>`。名称不能以 `BEEUNI_` 开头——Beeuni 将该
前缀保留给平台值。

之后可更改使用权限：

```bash
beeuni secrets share DEEL_API_TOKEN --user me
beeuni secrets share DEEL_API_TOKEN --user finance@example.com --group <group-id>
beeuni secrets share NIGHTLY_REPORT_TOKEN --agent reporter
beeuni secrets share DEEL_API_TOKEN --everyone
```

### 或使用控制面板
打开项目的 Secrets 页面，输入密钥和值，选择 **Who can use
it**，然后保存。Beeuni 会立即加密该值，并将每个新密钥默认设为 **项目中的所有人**，且采用 **environment** 暴露方式——实际值会加载到沙箱中。**Who can use it** 列会显示每个值的受众。

若要将密钥改为受出口强制策略约束的暴露方式，请先启用 `secrets_egress`
功能标志（Settings → Feature flags）；该功能仍处于实验阶段。启用标志后，Secrets 页面会显示 **“Can your code read this value?”** 控件：
选择 *no* 会将密钥改为受出口强制策略约束，并显示主机列表；选择 *yes* 则保留 environment 暴露方式。关闭标志时，密钥会保持为 environment 暴露方式。

### 将密钥授权给代理
在 Secrets 页面选择一个代理，或自行将标识符添加到该代理在 `beeuni.yaml` 中的 `secrets` 列表。一个会话只能获取其代理获准使用的密钥。请参阅[将密钥授权给代理](#grant-a-secret-to-an-agent)。

<a id="grant-a-secret-to-an-agent"></a>

## 将密钥授权给代理

只有当代理在其 `secrets` 列表中列出密钥标识符时，该代理运行的会话才能获取该密钥。匹配依据是标识符，而非名称，且不区分大小写。

> **受出口强制策略约束的密钥和服务使用的密钥需要明确授权**
>
> `secrets: all` 仅授予 environment 暴露权限。受出口强制策略约束的密钥，以及 Beeuni 服务使用的任何密钥，都必须明确写入代理的列表中。如果项目的 `beeuni.yaml` 中没有 `agents:` 块，则不会收到任何密钥。未获授权的密钥会被静默丢弃：会话仍会正常启动，但首次调用主机时会失败，就像凭据有误一样。

<a id="from-the-dashboard"></a>

### 通过控制面板

Secrets 页面会标记没有代理可以获取的密钥：**No agent can receive this secret**。在页面上选择一个代理并确认。Beeuni 会编辑 `beeuni.yaml`，并将其提交为 `chore(agents): grant <IDENTIFIER> to <agent>`。

无论清单中是否已声明该代理，授权都会生效。如果清单中未声明该代理，Beeuni 会为其添加一个新条目，其中包含此 `secrets` 列表。如果代理已声明，则会保留其他所有字段——模型、工具、连接器——并将标识符添加到现有列表中。如果代理已经允许使用该标识符，则无需提交，Beeuni 也不会提交。

`secrets: all` 上的代理属于特殊情况。`all` 无法携带受出口强制策略约束的密钥，因此 Beeuni 会写入一个明确的列表：包含项目当前拥有的所有标识符，以及这个新标识符。代理当前能获取的内容不会改变。之后添加的密钥需要单独授权。

以下两种情况无法授权：

- 使用 `beeuni_version: 1`（`beeuni.toml`）的项目没有可供编辑的 agents 映射。
  请求会以 `400` 和 `manifest_v1_unsupported` 失败。请手动编辑清单，或将项目迁移到 `beeuni_version: 2`。
- **Disabled** 状态的密钥无内容可供传递。请求会以
  `409` 和 `secret_not_grantable` 失败。请先为其设置一种暴露方式。

如果项目还没有 `agents:` 块，请在确认前阅读[第一个 `agents:` 块会改变整个项目](#the-first-agents-block-changes-the-whole-project)。
这一次编辑会改变其他所有代理的密钥访问权限。

<a id="by-hand"></a>

### 手动操作

直接写入的授权方式如下：

```yaml
beeuni_version: 2
agents:
  my-agent:
    secrets: [STRIPE_API_KEY]
```

<a id="when-a-session-asks-for-a-secret-its-agent-is-not-granted"></a>

### 会话请求代理未获授权的密钥时

需要凭据的代理会创建一个密钥链接（`beeuni secrets request`）。
即使会话的代理未获准使用该密钥，你输入的值仍会保存。该会话永远不会收到这个值，因此 Beeuni 会在所有界面上说明这一点：

- 链接表单会显示 **已保存，但 `<agent>` 尚不可读取**，并标出密钥名称。
- 创建密钥的响应和发送给会话的后续消息都会列出 `withheld` 名称及解决方法。
- 在会话中，`beeuni secrets ls` 会将清单中已声明、但未获授权的密钥显示为 `not granted`，而非 `missing`。

代理无法自行扩大授权范围。请打开 **Customize → Agents → `<agent>` →
Secrets** 并启用该密钥。保存后，Beeuni 会将更改推送到该代理正在运行的会话。代理也可以通过 `beeuni secrets sync` 立即拉取更改；每次提示都会重新同步会话的密钥。无需重启。

<a id="the-first-agents-block-changes-the-whole-project"></a>

### 第一个 `agents:` 块会改变整个项目

> **声明一个代理会拒绝其他所有代理**
>
> 没有 `agents:` 块，或完全没有 `beeuni.yaml` 的项目处于未受管控状态：每个代理都会收到所有采用 environment 暴露方式的密钥，而没有代理会收到受出口强制策略约束的密钥。
>
> 项目一旦声明第一个代理，所有**未列出**的代理都将无法获取任何项目密钥——包括一分钟前还可使用的 environment 密钥。列出一个代理，就会撤销其他代理的权限。

因此，请列出所有需要密钥的代理，而不只是你正在处理的那个。这就是为什么控制面板会在首次操作时要求你确认：提交后，`agents:` 就是项目的允许列表，未列入其中的代理运行时将无法获取任何项目密钥。

<a id="exposing-capabilities-to-third-party-users"></a>

## 向第三方用户开放功能

> **Beeuni 密钥策略不是多租户授权系统**
>
> 密钥策略用于保护**你自己项目中的代理**，避免其泄露你自己的凭据。它无法决定你的哪些终端用户可以使用这些凭据。

如果你正在构建的功能会让**不受信任的第三方用户**使用某项能力——例如公开聊天界面、共享应用，或任何互联网上的人都能提示的代理——请勿只依赖密钥策略。使用该界面的所有用户共享同一个项目代理、同一份授权和同一个主机列表。Beeuni 无法区分你的不同客户，因此任何用户的提示都能访问的受出口强制策略约束的密钥，都是所有用户都能使用的凭据，其权限上限取决于上游密钥本身的完整范围。

请在你自己的系统中建立所需的边界：

1. 部署自己的授权和代理服务，由该服务持有上游凭据。
2. 让代理连接你的服务，而不是上游服务。只向代理提供用于访问你服务的凭据——该凭据可设置为仅允许访问你自己的主机。
3. 你的服务识别终端用户，应用自己的授权规则和每用户配额，然后才使用其持有的凭据调用上游服务。

每用户规则应由该服务负责：谁可以调用什么、调用频率如何、可访问哪些记录。Beeuni 的密钥暴露控制只是更底层的一层，不能取代它。

<a id="list-your-secrets"></a>

## 列出密钥

运行 `beeuni secrets ls`，查看项目声明了哪些密钥，以及哪些密钥已设置值。

此列表是配置元数据，不会返回密钥值。范围受限的代理令牌只能看到其代理获准使用的标识符。特定会话的 `secrets_allowlist` 控制密钥是否传递到该会话，但不会隐藏代理授权范围内的配置元数据。

<a id="rotate-a-secret"></a>

## 轮换密钥

### 设置新值
使用新值运行相同的命令，或在项目的 Secrets 页面中重新设置：

```bash
beeuni secrets set STRIPE_API_KEY=sk_live_new...
```

### Beeuni 将其推送到正在运行的会话
Beeuni 会尽力将更改推送到该项目中所有有活跃会话的沙箱。对于模型或网关凭据，Beeuni 会重启标准运行器兼容进程。

轮换强制出口策略的密钥时，完全不必推送密钥值：沙箱只持有一个句柄，下一次请求到来时，Beeuni 会读取当前值。

<a id="remove-a-secret"></a>

## 移除密钥

运行 `beeuni secrets unset STRIPE_API_KEY`（或 `unset <identifier>`），或者
在项目的 Secrets 页面中将其删除。

> **移除会立即生效，但传播不会**
>
> Beeuni 会立即删除共享密钥。向已运行沙箱推送更新与轮换一样，尽力而为。

<a id="share-a-value-without-seeing-it"></a>

## 在看不到值的情况下分享

运行 `beeuni secrets request STRIPE_API_KEY` 以创建链接。任何拥有链接的人都可以输入值。你不会看到该值。链接默认有效期为 7 天；使用 `--expires <minutes>` 调整（最长 30 天）。链接过期后会显示明确的“已过期”页面——运行相同命令即可创建新链接。

<a id="give-an-agent-a-value-directly"></a>

## 直接向智能体提供值

如果你在会话中粘贴密钥，智能体会立即使用 `beeuni secrets set` 保存。它不会向你发送链接。智能体需要拥有项目的密钥写入权限。没有该权限时，智能体会收到 `403`，并改为创建密钥链接。智能体可以保存环境密钥或连接器密钥（`--scope connector`）。强制投递方式和主机列表仅限人工操作。

<a id="end-to-end-example"></a>

## 完整示例

`beeuni.yaml`：

```yaml
beeuni_version: 2
default_agent: my-agent
agents:
  my-agent:
    secrets: [STRIPE_API_KEY]
```

密钥配置。第一条命令使用默认的环境暴露方式保存值。第二条命令将其改为强制出口策略暴露方式，该功能仍处于实验阶段，需要启用 `secrets_egress` 标志（Settings → Feature flags）；标志关闭时会返回 `403` `feature_disabled`。

```bash
beeuni secrets set STRIPE_API_KEY=sk_live_...
beeuni secrets delivery STRIPE_API_KEY egress --allow-host api.stripe.com
```

随后，智能体使用收到的变量调用 Stripe：

```bash
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "authorization: Bearer $STRIPE_API_KEY" \
  https://api.stripe.com/v1/customers
# expected: 200
```

`$STRIPE_API_KEY` 持有的是句柄。Stripe 收到真实密钥，因为 `api.stripe.com` 在允许列表中。同一请求发往不在列表中的主机时，会发送句柄，上游服务会拒绝该请求。

如果智能体的 `secrets` 列表中没有 `STRIPE_API_KEY`，则发出相同请求会返回 `401`。该会话仍会正常启动——未获授权的密钥不会导致错误，只是不会被投递。

<a id="cli-commands"></a>

## CLI 命令

| 命令 | 功能 |
|---|---|
| `beeuni secrets ls` | 列出项目中已声明并设置的密钥 |
| `beeuni secrets set KEY=VALUE [--identifier <id>] [--scope runtime\|connector]` | 创建或更新密钥。`KEY=-` 从标准输入读取值。`--scope connector` 将其保留在服务器端 |
| `beeuni secrets unset IDENTIFIER` | 移除密钥 |
| `beeuni secrets delivery IDENTIFIER egress --allow-host <host>` | 对列出的主机强制采用出口策略暴露方式。处于实验阶段；需要启用 `secrets_egress` 标志 |
| `beeuni secrets delivery IDENTIFIER runtime` | 环境暴露方式（默认） |
| `beeuni secrets delivery IDENTIFIER denied` | 已停用 |
| `beeuni secrets call IDENTIFIER URL` | 通过 Beeuni 发送一个受策略约束的 HTTPS 请求。处于实验阶段；需要启用 `secrets_egress` 标志 |
| `beeuni secrets sync` | 在智能体会话中：拉取此会话的密钥并立即授权。在人工操作时：重新推送到项目中所有活跃的沙箱 |
| `beeuni secrets request NAME [--scope runtime\|connector] [--expires <min>]` | 创建链接，让其他人输入值 |
| `beeuni env push --from <path>` | 将 `.env` 文件上传为密钥 |
| `beeuni env pull [--out <path>] [--force]` | 将密钥名称（不含值）导出到 `.env` 文件 |

CLI 使用已保存的词汇：`runtime` 表示环境暴露，`egress` 表示强制出口策略，`denied` 表示已停用。运行 `beeuni secrets --help` 查看所有标志。

设置链接默认为 `connector`。只有当智能体的 shell 必须接收该值时，才使用 `--scope runtime`。绑定到连接器的密钥会保留在服务器端。

<a id="names-and-permissions"></a>

## 名称和权限

两种名称的格式规则：

| 名称 | 格式 |
|---|---|
| `identifier` | `^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$` |
| `name` | `^[A-Z_][A-Z0-9_]{0,63}$` |

读取或写入密钥需要 `project.secret.read` 或 `project.secret.write` 权限。项目管理员同时拥有这两项权限。自定义角色只能添加权限——Beeuni 不支持拒绝规则——因此，任何角色都无法从管理员身上移除其中任一权限。若要限制管理员，请移除其管理员角色。

会话收到的密钥由启动会话者的角色权限判定，并与已启动智能体清单中的授权取交集。两者都必须允许该密钥。请参阅
[一种词汇，两种绑定](/docs/zh/accounts#one-vocabulary-two-bindings)。

<a id="rest-routes"></a>

## REST 路由

所有路由均位于 `/v1/projects/{projectId}` 下。

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/secrets` | 列出密钥。如果调用方使用范围受限的智能体令牌，则按其授权范围筛选。 |
| POST | `/secrets` | 创建或更新共享值。请求体：`{name, identifier?, value}`，以及可选的投递策略。 |
| PUT | `/secrets/{identifier}/strategy` | 更改暴露方式及其主机列表。 |
| POST | `/secrets/{identifier}/grant` | 将标识符添加到 `beeuni.yaml` 中某个智能体的 `secrets` 列表。请求体：`{agent}`。 |
| DELETE | `/secrets/{name}` | 删除共享值。个人覆盖设置保持不变。 |
| PUT | `/secrets/{name}/personal` | 设置或启用调用方的个人覆盖设置。 |
| DELETE | `/secrets/{name}/personal` | 移除调用方的个人覆盖设置。 |

`POST /secrets` 会拒绝以 `BEEUNI_` 开头的名称。如果 `identifier` 已存在且其 `name` 不同，则返回 `409`。对于确切名称 `CODEX_AUTH_JSON`，会返回 `400`——Beeuni 通过 ChatGPT 订阅引导流程管理该密钥。

如果更改需要推送到正在运行的沙箱，两个写入路由都会返回 `delivery_sync` 对象。`ok: false` 表示值已保存，但至少有一个活跃会话仍在使用旧值；列出的会话会在重启时获取新值。

列表中的密钥带有 `delivery_blocked_reason`。值为 `no_agent_grant` 表示任何智能体都无法收到此密钥。`null` 表示该密钥已获授权、无需授权即可使用此暴露方式，或 Beeuni 无法读取清单。

`POST /secrets/{identifier}/grant` 会清除该原因。如果智能体列表中已允许此标识符，则返回 `already_granted: true`，此时 Beeuni 不会提交任何更改。如果此次编辑添加了项目的第一个 `agents:` 区块，则返回 `adopted_governance: true`——即[上文所述](#grant-a-secret-to-an-agent)的更改。对于 `beeuni.toml` 项目，会返回 `400` `manifest_v1_unsupported`；对于已停用的密钥，会返回 `409` `secret_not_grantable`。

<a id="rotation-and-propagation"></a>

## 轮换和传播

写入密钥时不会等待会话重启。Beeuni 会将更改推送到项目中的每个活跃沙箱：

1. Beeuni 会使用正在运行的智能体的 `secrets` 授权来构建新的环境快照。
2. 沙箱会将快照写入智能体的实时环境。新的工具调用会立即使用更改。
3. 如果更改的密钥是 LLM 提供商凭据，Beeuni 会重启标准运行器。

此推送尽力而为。更改密钥的 API 调用会在推送完成前返回。推送失败只会记录日志，不会重试。推送失败的沙箱会保留旧值，直到下次推送成功或会话重启。

<a id="model-credentials"></a>

## 模型凭据

使用 Beeuni 托管模型访问的项目不需要自己的密钥。若要使用自己的密钥，请设置标准运行器提供商配置引用的提供商变量。已识别的模型密钥会被分配 **LLM gateway** 用途，并由服务器端使用；沙箱无需接收该密钥。

不要将通用提供商验证结果作为运行时证明。它无法证明所选模型、区域、权限和 API 方言。请通过确切的模型发送真实提示词进行验证。
