Skip to content

Commit 68c91cc

Browse files
committed
文档对齐实现:CLI 认证机制、逐实例隔离、实例键与镜像内置 CLI
实例键改为 computer_instance.id,组织绑定身份改为服务用户;lifecycle 目录树补 /usr/local/bin/apemind 与 .dsh/AGENTS.md 及所有权分界; integration 文档记录 env 即认证的三个时机、逐实例凭证隔离论证,以及 「不把 key 写进 CLI profile 状态文件」的决策与备选路径。 Closes #38
1 parent 8941f35 commit 68c91cc

5 files changed

Lines changed: 109 additions & 34 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ node dist/host-agent.mjs
6969
## 镜像
7070

7171
镜像只在 GitHub Actions 构建(推 tag `v*.*.*` 触发),不在本地构建。dsh 版本在
72-
Dockerfile 的 `DSH_VERSION` 中锁定,升级 dsh 一律走新镜像 tag 加回归验证。
72+
Dockerfile 的 `DSH_VERSION` 中锁定;`apemind` CLI 同样构建时锁版本 + sha256,
73+
装到 `/usr/local/bin/apemind`(运行期零下载)。升级 dsh 或 CLI 一律走新镜像 tag
74+
加回归验证。租户 HOME 与 CLI 身份注入见 [docs/lifecycle.md](docs/lifecycle.md) §1。
7375

7476
## Kubernetes
7577

docs/apemind-integration.md

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,68 @@ apemind CLI 现状已经具备关键性质,**不需要重写**:Go 单二进
7272

7373
### 认证与身份(已就绪)
7474

75-
CLI 收到 `APEMIND_API_KEY` 即用 Bearer 认证,身份就是 key 的属主:个人实例是
76-
用户本人,组织实例是服务用户。组织服务用户对 CLI 无特殊性——它就是一个有
77-
membership 和角色的用户,`whoami``org list``collection list --org-id`
78-
权限正常工作。无需为「隐藏用户」做任何 CLI 侧适配。
75+
**托管 dsh 里的 CLI 没有「登录」这个步骤——认证发生在每次命令执行时。**
76+
CLI 的凭证查找顺序是:`APEMIND_API_KEY`/`APEMIND_BASE_URL` 环境变量优先,
77+
其次才是 profile 状态文件(`$XDG_CONFIG_HOME/apemind`,且仅当保存的 base_url
78+
与目标一致)。托管实例走的是第一条:dsh 进程 spawn 时环境里就有这两个变量,
79+
agent 的 bash 及所有子进程天然继承,`apemind whoami` 开箱即返回绑定身份。
80+
身份就是 key 的属主:个人实例是用户本人,组织实例是服务用户。组织服务用户对
81+
CLI 无特殊性——它就是一个有 membership 和角色的用户,`whoami``org list`
82+
`collection list` 按权限正常工作。
83+
84+
三个时机各司其职(目录细节见 [lifecycle.md](lifecycle.md) §1):
85+
86+
- **open(写盘)**`POST /open` → 控制面 ensure 把绑定身份的 key、
87+
`APEMIND_BASE_URL`、组织时的 `APEMIND_ORG_ID` 整体写入该实例
88+
`.apemind/env.json`(0600)。这是身份的唯一权威投影,key 轮换也走这条。
89+
- **spawn(进环境)**:每次拉起 dsh(冷启动、闲置唤醒、宿主重启后首次触达)
90+
`env.json` 白名单构造进程环境。进程已 running 时 open 只改磁盘不改环境,
91+
生效等下一次冷启动。
92+
- **执行(读 env)**:CLI 每次调用读环境变量完成 Bearer 认证。没有会话状态,
93+
没有过期刷新,key 有效即恒可用。
94+
95+
注入的前提:部署配置了 MCP 端点(`APEMIND_BASE_URL` 由 MCP URL 推导)。
96+
未配 MCP 的部署不注入 CLI 上下文,agent 会看到 `base URL is required`
97+
98+
### 为什么不把 key 写进 CLI 配置文件
99+
100+
评估过「open/spawn 时由宿主把 key 写入 CLI profile 状态文件」的方案——确实是
101+
纯本地文件写入(api-key 认证无需服务端握手),但被拒,理由:
102+
103+
- **没有增量收益**:dsh 里 agent 能触达的一切工具(bash、web 终端、脚本)都是
104+
dsh 的后代进程,POSIX 环境继承在这条链上不会丢。env 认证已恒可用。
105+
- **格式耦合**:profile 状态文件(`config.json` + `profiles/<name>/state.json`
106+
是 CLI 的内部实现,host-agent(TypeScript)硬编码 Go CLI 的私有 schema,
107+
CLI 重构一次就悄悄破一次。
108+
- **密钥双份落盘**`env.json` 之外再多一份拷贝,轮换一致性要多管一处,
109+
威胁模型上没有任何改善(同一 HOME、同 0600、同 uid)。
110+
111+
若将来出现 env 传播真实断裂的场景,正确姿势是给 CLI 加一个官方写入命令
112+
(如 `apemind profile` 系列 + `--api-key-stdin`),由宿主在 spawn 前以租户 uid
113+
执行一次——schema 归 CLI 所有,宿主只调公开命令面。目前不做。
114+
115+
### 逐实例隔离:每个 dsh 一份独立凭证
116+
117+
「一个大容器 N 个 dsh」下的不越权由三层保证,CLI 不需要任何额外机制:
118+
119+
1. **env 是 per-process 的**:spawn 用白名单构造环境(不继承 host-agent 的
120+
env),每个 dsh 进程只带自己实例 `env.json` 里的 key。租户 A 的进程环境里
121+
没有 B 的任何东西。
122+
2. **key 本身是实例绑定身份签的**:个人=本人、组织=服务用户。就算 key 泄露给
123+
同实例里的 agent(本来就是给它用的),权限边界也在服务端 RBAC,越不出
124+
绑定身份的角色。
125+
3. **HOME/uid 隔离**:每实例 HOME 0700 + 独立 uid + 回环 iptables。CLI 的
126+
状态目录跟随 `XDG_CONFIG_HOME`(spawn 时已指向各实例自己的
127+
`$HOME/.config`),即使 agent 主动 `apemind login` 落了状态,也只落在
128+
自己 HOME 里,别的实例读不到。
129+
130+
(CLI 没有 cwd 级「目录认证」——它的状态定位是 XDG/HOME 级。托管布局恰好
131+
利用了这一点:XDG 指到哪,状态就隔离到哪。)
132+
133+
### 二进制位置
134+
135+
镜像内 `/usr/local/bin/apemind`,全租户共用只读层,不在租户 HOME、不随实例删除。
136+
dsh 进程继承宿主 `PATH`,agent 直接跑 `apemind`。升级 = 换镜像 tag。
79137

80138
### 上下文缺省(CLI 小改)
81139

docs/architecture.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
本文回答 computer-host 作为独立多租户 dsh 服务,如何与 ApeMind 协作、数据怎么走、身份怎么签、镜像和隔离怎么做。
44

5-
落地口径:个人一人一台;组织一组织一台(实例键 `org-{org_id}`成员共用同一 HOME 与进程)。ApeMind 只做控制面(签票 + 调启停),`user_id` host 完全不透明,零 dsh plugin 代码。
5+
落地口径:个人一人一台;组织一组织一台(实例键是 ApeMind `computer_instance.id`,形如 `ci` + 16 hex,成员共用同一 HOME 与进程)。ApeMind 只做控制面(签票 + 调启停),实例键对 host 完全不透明,零 dsh plugin 代码。
66

77
## 0. 一句话架构
88

@@ -69,9 +69,10 @@ flowchart TB
6969
- 路由:`POST /api/v2/computer/open``POST /api/v2/computer/stop``GET /api/v2/computer`;可选查询参数 `org_id` 切到组织实例。
7070
- `ticket.py`:HMAC 签票(`v1.<b64url>.<hmac>`,无 DB)。
7171
- `host_client.py`:httpx 薄客户端。
72-
- managed key 签发(复用 `api_key``is_managed`,无新表)。打开时注入当前操作者的 Computer 托管密钥。
72+
- managed key 签发(复用 `api_key``is_managed`)。打开时注入**绑定身份**的托管密钥:个人=用户本人,组织=该组织的服务用户(不是点开的那个成员)。
73+
- 实例表 `computer_instance``id` 即宿主租户键;`data_plane_user_id` 指向绑定身份。
7374
- `web/src/app/workspace/computer/`:单卡片页(状态 / 打开 / 停止);组织工作区带 `org_id`
74-
- 实例状态 source of truth 在 hostaperag 实时查询。零新业务表
75+
- 实例状态 source of truth 在 hostaperag `computer_instance` 记绑定身份与租户键,打开时按行投影 env
7576

7677
**apemind-computer(本仓库)**
7778

@@ -114,7 +115,7 @@ v1.<body>.<sig>
114115
- `sig``HMAC-SHA256(secret, body)` 的十六进制小写摘要。
115116
- 短票 payload `{"t":"ticket","u":"<user_id>","e":<unix秒>,"n":"<nonce>"}`,60 秒,网关内存防重放。
116117
- 会话 cookie payload `{"t":"session","u":"<user_id>","e":<unix秒>}`,默认 12 小时,HttpOnly + Secure + SameSite=Lax。
117-
- `u` 对 host 不透明,须匹配 `^[A-Za-z0-9_-]{1,64}$`个人实例用原始用户 id;组织实例用 `org-{org_id}`
118+
- `u` 对 host 不透明,须匹配 `^[A-Za-z0-9_-]{1,64}$`值是 `computer_instance.id``ci` + 16 hex),不是 ApeMind 用户 id,也不是 `org-{org_id}`
118119

119120
```mermaid
120121
sequenceDiagram
@@ -136,7 +137,7 @@ sequenceDiagram
136137
- dsh 零登录零账号:只见到回环 Host 的请求,天然过它的 /api fence;不配 trustedHosts、不改 dsh。
137138
- 带 Origin 的请求先校验 `Origin == https://computer.apemind.ai`(或对应 staging Origin)再剥头。
138139
- 唤醒语义:cookie 有效 + 实例存在但闲置回收 → 网关自己拉起;用户主动停止的不自动唤醒 → 302 主站;实例从未创建 → 302 主站。
139-
- 组织实例:任一在职成员可打开/停止同一 `org-{org_id}`;停止影响该组织所有成员。MCP 身份跟随最近一次真正拉起进程的操作者
140+
- 组织实例:任一在职成员可打开/停止同一 `ci*` 实例;停止影响该组织所有成员。MCP / CLI / 模型网关的身份是组织服务用户,与谁点开无关
140141

141142
跨语言一致性靠 `tests/vectors/` golden vectors 单测锁住。
142143

@@ -147,7 +148,7 @@ sequenceDiagram
147148
- `POST /v1/pair`:未配对时绑定控制面,换出长期令牌与签票密钥(唯一不要求 Bearer 的写接口,防浏览器校验见 pairing.md §5.1)。
148149
- `GET /v1/runtime`:未配对无鉴权回 `{state, public_origin, version}`;已配对需 Bearer,另回 `main_url` / `paired_at`
149150
- `PUT /v1/runtime`:更新 `main_url``POST /v1/unpair`:解除配对。
150-
- `PUT /v1/instances/{user_id}`:幂等 ensure。body `{desired: running|stopped, env?: {APEMIND_API_KEY?...}}`。同步返回 `{status, port, started_at, last_activity}`
151+
- `PUT /v1/instances/{user_id}`:幂等 ensure。路径参数名仍是 `user_id`,值是不透明实例键(ApeMind 的 `computer_instance.id`)。body `{desired: running|stopped, env?: {APEMIND_API_KEY, APEMIND_BASE_URL, …}}`。同步返回 `{status, port, started_at, last_activity}`
151152
- `GET /v1/instances/{user_id}``GET /v1/instances`:状态(running/stopped/error、RSS、last_activity)。
152153
- `DELETE /v1/instances/{user_id}`:停进程 + 删工作区(重置)。
153154
- `POST /v1/instances/{user_id}/revoke-sessions`:会话代数 +1,立刻废掉该实例全部存量网关会话(成员被移出组织等撤权场景由控制面调用;被踢的合法用户重新 open 即恢复)。
@@ -157,15 +158,15 @@ sequenceDiagram
157158

158159
## 7. host-agent 行为细节
159160

160-
- **supervisor 状态机**`created → running ⇄ idle-stopped → deleted`,另有 `error(backoff)`。spawn 命令模板 `dsh {patch} --profile web --no-open --port {port}``{patch}` 在租户存在 `~/.apemind/managed.cordis.yml` 时展开为 `--patch <该文件>`);每租户 `HOME=/data/users/<key>``DSH_HOME=$HOME/.dsh`、独立 XDG;崩溃指数退避,闲置自动 stop(默认 1800s);端口重启后重新分配(cookie 只含 user_id,与端口无关)。HOME 目录 0700。逐状态转换、目录所有权与数据流细节见 [lifecycle.md](lifecycle.md)
161+
- **supervisor 状态机**`created → running ⇄ idle-stopped → deleted`,另有 `error(backoff)`。spawn 命令模板 `dsh {patch} --profile web --no-open --port {port}``{patch}` 在租户存在 `~/.apemind/managed.cordis.yml` 时展开为 `--patch <文件>`);每租户 `HOME=/data/users/<key>``DSH_HOME=$HOME/.dsh`、独立 XDG;崩溃指数退避,闲置自动 stop(默认 1800s);端口重启后重新分配(cookie 只含实例键,与端口无关)。HOME 目录 0700。镜像内置 `/usr/local/bin/apemind`逐状态转换、目录所有权、CLI 身份注入与数据流细节见 [lifecycle.md](lifecycle.md)
161162
- **网关转发卫生**:剥 `Origin/Referer/sec-fetch-*``Accept-Encoding: identity`、响应加 `X-Accel-Buffering: no`;WS 重写 `Host` 后原始 socket 对拷。
162163
- **可观测**:结构化 JSON 日志(不落 prompt/key/文档内容),实例数/RSS/活跃度进 `/healthz`
163164

164165
## 8. Docker 镜像设计
165166

166167
AIO 底座(Xvfb/Chromium/VNC/noVNC/supervisord/nginx/gem-server/tinyproxy/bubblewrap)整体弃用。托管 dsh WebUI 用不到桌面沙箱,却带来体积、架构限制和多余攻击面。若未来要浏览器自动化/桌面,另起独立镜像轨道。
167168

168-
全新镜像(node:22-bookworm-slim,amd64+arm64):系统层提供租户 shell 环境与隔离工具;全局安装锁定版本的 `@deepseek-ai/dsh`;host-agent esbuild 单文件;`tini` 作 PID 1。暴露 8080/9090,数据卷 `/data`
169+
全新镜像(node:22-bookworm-slim,amd64+arm64):系统层提供租户 shell 环境与隔离工具;全局安装锁定版本的 `@deepseek-ai/dsh`构建时锁版本 + sha256 校验装入 `apemind` CLI(`/usr/local/bin/apemind`,运行期零下载);host-agent esbuild 单文件;`tini` 作 PID 1。暴露 8080/9090,数据卷 `/data`
169170

170171
- host-agent 以 root 运行(需要 setuid 切租户 uid 与 iptables);容器保持尽可能少的 capability,P2 回环隔离时加 `NET_ADMIN`
171172
- 私有化扩展点:客户 `FROM apecloud/apemind-computer` 再 apt 加自己的工具链。

0 commit comments

Comments
 (0)