现象(HM #92):蜂群"需绑定 git 仓库才能使用",但 agent 实际拿不到仓库。
根因:HM 已在 swarm create 的 resource_grants 下发 git 绑定(resource_type=git +
metadata.repo_url + secret_ref=azkv://),但 swarm 拉起侧只把 grant 用于校验/脱敏/审批,
launcher 从不消费它 → 被拉起 agent 的 env 没有 GIT_REPO_URL/凭据 → 无法 clone。
(agent 侧 agent/main.py + agent/git_operations.py 早已读取这些 env,缺的纯是注入。)
修复(仅 launcher 注入,比照模型 key 的服务端解析路径):
- 新增 resolve_git_grant(body):从 resource_grants(含 per-agent)取首个 git grant,
repo_url(metadata) → GIT_REPO_URL(非密文 inline);secret_ref(azkv) 服务端解析 →
GIT_USERNAME/GIT_PASSWORD。复用 azkv 读取(workload identity;dev/CI 用
HEICODE_SECRET_<name>)。有 repo 无凭据仍注入 GIT_REPO_URL(公有仓可 clone;私有仓
报错,不伪造)。
- plan_launch_specs 增加 git_env 合并;create 路径解析并透传。
- k8s 后端:GIT_PASSWORD 与模型 key 同走 per-swarm Secret 的 secretKeyRef,绝不内联
PodSpec;GIT_REPO_URL/GIT_USERNAME 为非密文 inline。SENSITIVE_ENV_KEYS 统一管控。
- git KV secret 值约定 JSON {"git_username","git_password"}(接受 git_token/token 形式
+ 裸 token),待 HM #92 对齐。
git 凭据不入 create 请求体/回调/日志/argv(_reject_plaintext_secrets 已覆盖
resource_grants)。
文档:runtime-contract §3.3 env 表 + 约束、security-boundary §6 增 git 绑定解析口径。
测试:scripts/test-agent-launcher.py 增 resolve_git_grant/凭据提取/k8s git secret 用例。
影响:仅 agent_swarm(Swarm/Agent + 密钥/secret_ref + 文档);不改 Manager/客户端/release/
契约状态机/计费/审计字段。HM 侧 binding_id(B 路径)解析另在 HM #92 处理,与本 PR 无关。
验收:
python scripts/test-runtime-contract.py
python scripts/test-contract-freeze.py
REDIS_FAKE=1 python scripts/test-key-injection-contract.py
python scripts/test-security-boundary.py
python scripts/test-agent-launcher.py
python scripts/test-git-workflow.py
python scripts/test-merge-smoke.py
python scripts/test-workflow-e2e.py
(全部通过)
Refs HM #92, Closes #63
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
12 KiB
Markdown
114 lines
12 KiB
Markdown
# 安全边界(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-vault.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——**不入** create 请求体 / 回调 / 日志 / argv。azkv 真实解析为部署侧 SecretResolver;dev/CI 用 `HEICODE_SECRET_<name>`。解析不到即 keyless 启动并明确报错(不伪造)。
|
||
- **Git 仓库绑定凭据服务端解析(agent_swarm#63 / HM #92)**:git 绑定经 `resource_grants` 里 `resource_type=git` 的 grant 下发。Swarm 同模型 key 方式**服务端解析**(`resolve_git_grant`):`metadata.repo_url`(非密文)→ `GIT_REPO_URL`;`secret_ref`(`azkv://` git 凭据引用)→ `GIT_USERNAME`/`GIT_PASSWORD`。git 凭据**不入** create 请求体 / 回调 / 日志 / argv;明文 git token/密码不得进 create 体(`_reject_plaintext_secrets` 已覆盖 `resource_grants`)。git KV secret 值约定 JSON `{"git_username","git_password"}`(接受 token 形式),**待 HM #92 对齐**。
|
||
- **K8s pod 边界(`AGENT_LAUNCH_BACKEND=kubernetes`,生产)**:**每 agent 一个 Pod**,带 CPU/内存 requests+limits、标签(`heicode-swarm-id`/`heicode-user-id`)。模型 key 与 **git 凭据(`GIT_PASSWORD`)** 均经**每-swarm k8s Secret**(manifest via stdin 应用)由 Pod `secretKeyRef` 引用,**绝不内联进 PodSpec env**(否则暴露于 etcd / `kubectl get pod -o yaml`);`GIT_REPO_URL`/`GIT_USERNAME` 为非密文,inline 注入。编排器需 `kubectl` + 一个仅对 `AGENT_POD_NAMESPACE` 有 pod/secret 权限的 **ServiceAccount(最小 RBAC)**;Pod 出网由 NetworkPolicy 限定到 HM `/v1` + git;停止/删除按标签 `kubectl delete pod,secret`。**更硬化(推荐 Infra 评估)**:azkv **CSI SecretProviderClass** 让 Pod 直接从 Key Vault 挂载密钥,编排器**全程不接触明文**。代码执行沙箱仍按 §8.1 在 Pod 层强制。
|
||
- 🟡 待接入:多租户运行时隔离(命名空间/网络/配额)由 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 策略)。
|