Files
Agentswarm/docs/integration/security-boundary.md
T
Songhaoz666andClaude Opus 4.8 0cbab3f750 模型 key 注入对接:定死 HM #60 参数(KV value JSON + per-user 吊销握手)(Refs #16 #60)
回应 HM「#60 落地前需 Swarm 定死的参数清单」。Swarm 侧逐条定死并落实现:

- A.2 KV secret value 格式:JSON {"openai_api_key":"sk-..."}(对齐 callback 签名密钥
  约定),解析字段 openai_api_key;裸 sk- 串兼容;解析不到/字段缺失不伪造。实现
  agent_launcher._extract_model_key + _resolve_secret_ref。
- A.5 吊销信号(事件驱动):sk- per-user 长存;stop 为唯一终态(completed/failed 经
  …/input 可重开故保 key)。某用户全部 run 被 stop(retained 集清空)时,运行时发
  恰好一次 swarm.pool_terminated{user_id, secret_ref},HM 据此吊销 sk- + 清 KV。
  单 run swarm.stopped 不触发吊销。实现 swarm_runtime.retain_run_for_user /
  release_run_and_maybe_terminate_pool(per-user retained 集 + 一次性 flag)。
- A.1 粒度:每用户一把、跨 run 复用;KV 命名 swarm-model-key-<user_id>(文档)。
- A.4 OPENAI_API_BASE:Swarm 部署常量(已实现),不经 create 下发(文档确认)。
- A.3 KV 读 RBAC:⚠ 待定(联调阻塞前置)——如实标注归属未敲定,不伪造已就绪。
- B.1:CLIENT_GUIDE §9 /events 游标改 after=<next_after>(HM 不透明游标,非 sequence)。

swarm.pool_terminated 为 HM 控制面生命周期事件,不入 FROZEN_CLIENT_EVENT_TYPES、
不渲染驾驶舱;payload secret_ref 为 azkv:// 引用(非明文 key)。

文档:runtime-contract §3.3.1(参数表)、event-schema(注册 + 说明)、
security-boundary §6(吊销握手 + RBAC 待定)。测试 scripts/test-key-injection-contract.py
(KV 格式解析 + 吊销握手:单 run/多 run 保留/不重复吊销/teardown 后重新武装)+ CI 步。

不动 Manager 面接口、HMAC 回调、审批链、计费/审计字段语义。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:57:15 +08:00

114 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 安全边界(Secret / Workspace / Tool / Approval / Tenant / Sandbox)
> 状态:**已强制边界 FROZEN v1(可验证)+ 部分待接入**(依据 `heicode-mananger/docs/heicode.md §六/七`、`docs/heicode-runtime-auth-newapi-secret-design.md §三/五`、`docs/integration/heicode-am-contract.md §3.1/§4`)。回应 issue #19。
>
> **本仓强制并可验证的边界**(契约测试 `scripts/test-security-boundary.py`,CI 守护):① secret 仅 `azkv://` 引用、明文密钥入口拒绝、回调/持久化脱敏;② workspace 写入路径越界(绝对路径 / `..` 逃逸)拒绝;③ 代码沙箱 **fail-closed**(未确认隔离则拒绝执行)。
>
> **本次不扩展(确认的边界/后续层,非本仓可改)**:tool/MCP 权限引擎(**无 SK/MCP 工具层**可治理)、租户隔离(**有意**按 `user/channelId` 归因、不引入 tenant)、Pod 级强化沙箱(seccomp/只读根/NetworkPolicy,归 Infra/Security)。详见 §9。
>
> 配套:[`runtime-contract.md`](./runtime-contract.md)、[`usage-billing-schema.md`](./usage-billing-schema.md)。
## 1. 不可破坏原则
- 密钥/Token/云凭据/SSH 私钥/数据库密码/NewAPI key **不得**进入代码、日志、Markdown、前端响应或 Git。
- 凭据基线 = **Azure Key Vault**;引用一律 `azkv://<vault>/secrets/<name>`,**不向后兼容 `vault://`**。
- Manager DB 与 Swarm 只保存 `secret_ref`,真实凭证由 Secret Broker 写入 Key Vault。
- 子 Agent 不持有长期密钥;只接收角色、资源元数据、AGENT.md 与**短期、最小权限、可审计**凭证。
- 高危操作审批**只在客户端完成**;运行时只校验审批结果,不发起审批。
## 2. Secret 注入边界
| 项 | 标准 | 本仓状态 |
|---|---|---|
| `secret_ref` 前缀 `azkv://` 强校验 | 必须 | ✅ `swarm_runtime.validate_create_request` 校验 `billing_context.secret_ref` 等为 `azkv://` |
| 拒绝明文密钥进入请求 | 必须 | ✅ `_reject_plaintext_secrets`(`metadata`/`resource_grants`/`callback`) |
| 响应/事件/持久化脱敏 | 必须 | ✅ `_redact_sensitive` 将明文密钥键(`password`/`token`/`secret`/`private_key`/`access_key` 及 `*_token`/`*_secret`/`*_password`/`*_key`)脱敏为 `[redacted]`;**保留** `secret_ref`/`credential_ref`/`signing_secret_ref`(安全引用) |
| `.env`/密钥不入库 | 必须 | ✅ `.gitignore` 忽略 `.env`、`secrets/`、`*.pem/key/p12/pfx`、`id_rsa/ed25519` |
| 真实凭证写入 Key Vault(Secret Broker) | Manager 侧 | ⛔ 非本仓(Manager Secret Broker 负责) |
| 短期凭证派生与注入 | 客户端审批后 | 🟡 Swarm 接收 `secret_context`;K8s 派生/注入由 Agent 平台(AM)实现 |
`secret_context`(HM 下发,仅引用与审批结果):
```jsonc
"secret_context": {
"secret_refs": ["azkv://heicode-kv.vault.azure.net/secrets/res_git_1"],
"inject_short_lived_credentials": true,
"approval_id": "approval_123"
}
```
## 3. Workspace 隔离
- 每个任务在**独立按任务工作目录**执行(agent `task_workspace`)。
- 文件写入有**路径越界校验**(`task_executor._resolve_workspace_path`,拒绝绝对路径与逃逸 workspace)。
- Git 操作在仓库根(`repo_root`)执行,限定结果分支。
- 🟡 待接入:跨任务/跨租户的强隔离、只读挂载、allowed_paths 强制(当前由模型提示约束,未做运行时强制)。
## 4. Tool / MCP 权限边界
- 当前 Agent 工具能力 = 工作区内文件读写 + Git;**无** SK/MCP 工具权限引擎。
- 🟡 待接入:统一 tool/MCP permission boundary、allowed/denied 工具策略、敏感工具审批联动(`sk_tool.*` 事件已在 schema 预留)。
## 5. 审批门(Approval Gate)
- 高危操作审批**只在客户端**;Swarm 不发起审批。
- Swarm 实现:create 命中高危(`risk_level=high` 或 `requires_user_approval`)→ 进入 `waiting_approval`,发 `approval.requested`(`approval_id`/`operation`/`risk_level`);客户端经 Manager 审批后回 `POST …/approvals/{approval_id}`(approved/rejected)→ 恢复或阻断。
- 🟡 待接入:审批主体/范围/TTL/`credential_ref`/`lease_id` 的逐项校验与到期失效,需与 Manager 审批链对齐。
## 6. Agent 鉴权与租户隔离
- **Swarm 模型**:Agent 主动出站连编排器 WebSocket(`/ws/{agent_id}`),**不**对公网暴露每 Agent 子域名。AM 单 Agent 模型里的「客户端↔agent 直连 + `AGENT_ACCESS_TOKEN` 本地校验」**不适用于** swarm(无直连回路)。
- 服务间鉴权:HM→Swarm 用 `AGENT_RUNTIME_SERVICE_TOKEN`(Bearer);回调 HMAC 签名。
- **每用户并发 Agent 配额**:一个 `user_id` 同时连接的 Agent 数上限为 `MAX_AGENTS_PER_USER`(env,默认 10)。注册(WS `register` 消息携带 `user_id`)超额即被拒绝(回 `registration_rejected` 并关闭,code 1008),断开后释放名额。归因主轴仍为 `user.id`/`channelId`。未带 `user_id` 的 Agent 为 unbound,不计入该配额。实现:`ConnectionManager.can_bind_user/bind_user/unbind` + 注册处强制;测试 `scripts/test-max-agents-per-user.py`。
- **Swarm 拉起 agent + 服务端解析 key(team 决议,runtime-contract §3.3)**:由 **Swarm 运行时**(`orchestrator/agent_launcher.py`)拉起专家 agent 池(拉起数 `min(池大小, MAX_AGENTS_PER_USER − 已连)`,与上面的注册兜底一致)。模型 key 由 **Swarm 从 `billing_context.secret_ref`(`azkv://`)服务端解析**后注入被拉起 agent 的 env——**不入** create 请求体 / 回调 / 日志 / argv(`command` 后端的密钥经进程 env 传入,不上命令行)。azkv 真实解析为部署侧 SecretResolver;dev/CI 用 `HEICODE_SECRET_<name>`。解析不到即 keyless 启动并明确报错(不伪造)。KV secret value 为 JSON `{"openai_api_key":"sk-..."}`(HM #60;裸串兼容),解析字段 `openai_api_key`。
- **per-user key 吊销握手(HM #60,runtime-contract §3.3.1 A.5)**:`sk-` per-user 长存;`stop` 为唯一终态(completed/failed 经 `…/input` 可重开故保 key)。某用户全部 run 被 stop 时运行时发一次 `swarm.pool_terminated{user_id, secret_ref}`(HM 控制面事件,非客户端、非冻结集),HM 据此吊销 `sk-` + 清 KV,杜绝永不吊销 token。
- 🟡 **待定(联调阻塞前置)**:Swarm 运行时读 `heicode-kv` 的身份/RBAC(Workload Identity / MI + `Key Vault Secrets User`)**归属未敲定**(运维 / Swarm 部署侧),须先确认——不通则 `secret_ref` 解不出。
- 🟡 待接入:多租户运行时隔离(命名空间/网络/配额)由 Agent 平台(AKS Workload Identity)承载,非本仓编排器;归因主轴为 `user.id`/`channelId`(见 `usage-billing-schema.md`),不引入 tenant 概念。
## 7. 外部 API 与传输
- 模型调用统一走 HM `/v1`(OpenAI 兼容),用 HM 现签 `OPENAI_API_KEY`。
- 传输:编排器/Agent 接口与回调走 **HTTPS / 私网**;`env` 含明文密钥时启动接口必须 HTTPS(`heicode-am-contract §4`)。
- 🟡 待接入:统一 external API egress 策略(白名单/出网控制)。
## 8. 执行沙箱
- 当前执行单元为进程 / K8s Pod,隔离强度依赖部署(namespace/资源限额)。
- 🟡 待接入:强化沙箱(seccomp/只读根/网络策略/能力裁剪),由 Infra/Security Team 定义。
### 8.1 代码测试沙箱(benchmark Group B,`orchestrator/sandbox.py`)
为给「生成代码」算真实 TestPassRate,需**执行模型生成的代码**。安全模型与边界如下:
- **OS 级隔离边界 = K8s Pod / 容器**(非 root、只读根文件系统、NetworkPolicy 出网拒绝、CPU/内存/pids 限额、seccomp),由部署侧(Manager/release 清单)强制,**非本仓可改**。按 Owner 裁定,在该隔离 Pod 内运行测试代码可接受。
- **Pod 内纵深防御(本模块新增)**:每次运行用临时工作目录(结束即删);wall-clock 超时 + 进程组强杀;POSIX 资源限额(CPU 时间 / 地址空间 / 文件大小 / 子进程数,见 `SANDBOX_*` 环境变量);**环境变量清洗**(不向子进程泄漏任何 API Key/Token/云凭据/代理变量,仅放行 `PATH`/语言区域等白名单);写入路径**越界校验**(拒绝绝对路径与 `..` 逃逸);输出截断;计数从 JSON 结果文件读取,不信任 stdout。
- **Fail-closed 双重门控(PR #24 复审整改)**:代码执行不是「一个 env 开关就能开」的默认能力。需要**两道独立确认**:
1. `ENABLE_QUALITY_EVAL=1` —— 打开质量评测功能;
2. `HEICODE_SANDBOX_ISOLATED=1` —— **显式断言本进程运行在上述隔离 Pod 内**(由 Pod manifest / CI runner 设置)。
二者缺一:
- 启动时:若 `ENABLE_QUALITY_EVAL` 开但隔离未确认,orchestrator **拒绝启动**(`assert_quality_eval_safe()`,main 生命周期)——平台级硬失败,非运维口头约定。
- 运行时:`sandbox.run_tests()` 与 `quality.evaluate_run_quality()` 在执行任何代码前调用 `assert_isolated()`,未确认则 **抛 `SandboxIsolationError`,不写文件、不起子进程**。
评测仅用 `benchmark/fixtures/<id>/` 的**留出测试**(held-out)作权威评分。
- **明确不构成**:本模块**不是**独立安全边界,**不替代** §8 的强化沙箱(seccomp/只读根/网络策略仍由 Infra/Security 在 Pod 层强制)。`HEICODE_SANDBOX_ISOLATED` 是「操作者确认隔离已就位」的断言,**不自行创造隔离**;只允许在真正隔离的 Pod 或 ephemeral CI/test runner 中置 1。后续可演进为专用 sandbox worker/job(复审建议的另一路径)。
## 9. 覆盖与缺口
✅✔ = 已实现**且有契约测试**(`scripts/test-security-boundary.py`)。
| 边界 | 状态 |
|---|---|
| `azkv://` `secret_ref` 强校验 / 明文拒绝 / 脱敏 / `.gitignore` | ✅✔ 已实现 + 测试(`validate_create_request`/`_reject_plaintext_secrets`/`_redact_sensitive`) |
| Workspace 路径越界校验(绝对路径 / `..` 逃逸拒绝) | ✅✔ 已实现 + 测试(`task_executor._resolve_workspace_path`) |
| 代码沙箱 fail-closed(`assert_isolated`:未确认隔离拒绝执行;启动 `assert_quality_eval_safe`) | ✅✔ 已实现 + 测试(§8.1 双门控 `ENABLE_QUALITY_EVAL`+`HEICODE_SANDBOX_ISOLATED`) |
| 审批状态机(waiting_approval + approvals 回执) | ✅ 已实现(逐项 TTL/范围校验待加强) |
| 短期凭证派生注入、Workload Identity | 🟡 AM/K8s 侧 |
| Tool/MCP 权限引擎 | ⏸ 本次不做 —— **无 SK/MCP 工具层可治理**;待工具层落地后随其设计权限边界 |
| allowed_paths 运行时强制(按 grant 限定)、强隔离 | ⏸ 本次不做 —— 当前仅 workspace 根越界强制;按 grant 的 allowed_paths 待资源授权链接入 |
| 强化执行沙箱(seccomp/只读根/网络策略/能力裁剪,Pod 层) | ⏸ 本次不做 —— 归 Infra/Security(Pod manifest) |
| 租户隔离 | ⛔ **有意**不在本仓(归因按 `user/channelId`,不引入 tenant 概念) |
> ⏸ = 确认的后续/外部层,非本仓本次范围(按规则 #9 不伪造「已强制」)。本仓**已强制**的三类边界均可由 `scripts/test-security-boundary.py` 验证。
## 10. 待对齐对象
Security / Governance Team(tool/MCP 边界、沙箱、审批逐项校验)、Infra Team(Workload Identity、租户隔离、egress 策略)。