289 lines
17 KiB
Markdown
289 lines
17 KiB
Markdown
# gori-agent 项目施工指南
|
||
|
||
本文件供 Coding Agent 在本仓库施工时使用。先理解 Config v3 和实例隔离契约,再做最小、可验证的修改。
|
||
|
||
## 1. 项目定位与固定边界
|
||
|
||
`gori-agent` 是 Node.js 20+ / TypeScript 项目,通过单个 IM 平台接收请求,并通过官方 Agent Client Protocol(ACP)驱动一个 Coding Agent。
|
||
|
||
```text
|
||
用户 → Platform Adapter → Gateway → AssistantManager(Assistant / Proposal / Worker)→ ACP Agent
|
||
```
|
||
|
||
每个运行实例是一个完整且固定的 Bot:
|
||
|
||
- 一个 Gateway 进程。
|
||
- 一份 Config v3 本地配置。
|
||
- 一个 `bot.id`、workspace、persona。
|
||
- 一个 ACP agent。
|
||
- 一组直接声明的 skills 与一个 permission policy。
|
||
- 一个平台身份。
|
||
- 独立 PID、日志和 ACP state。
|
||
|
||
不要重新引入 Config v1/v2 migration、`roles[]`、动态 role selection、`backends[]`、多平台同时启用或单轮 CLI fallback。
|
||
|
||
## 2. 实例目录契约
|
||
|
||
统一实例根:
|
||
|
||
```text
|
||
${GORI_AGENT_ROOT:-$HOME/.gori-agent}/instances/<bot-id>/
|
||
├── config.json
|
||
├── logs/gori-agent.log
|
||
└── state/
|
||
├── acp-sessions.json
|
||
├── proposals.json
|
||
├── assistant-workspaces/
|
||
├── kimi/assistant/
|
||
└── gori-agent.pid
|
||
```
|
||
|
||
实例目录本身就是该进程的 `GORI_AGENT_HOME`。约束:
|
||
|
||
- `bot.id` 只允许小写字母、数字和连字符,拒绝 `/`、`..` 等路径穿越。
|
||
- CLI 加载后必须校验目录名与 `config.bot.id` 相同。
|
||
- 实例目录、`logs/`、`state/` 为 `0700`。
|
||
- `config.json`、PID、state 为 `0600`。
|
||
- 多实例必须使用不同 `gateway.server.port` 和平台 credentials。
|
||
- 旧的 `/usr/bin/flock` workspace lease 与 `owner.json` 已退役,不要重新引入。同一 `GORI_AGENT_ROOT` 下由 workspace scope 兜底:`doctor`、外层 `start` 与内部 runner 扫描其他实例目录的完整 Config v3,任一目录/配置不可读或无效即 fail closed;canonical workspace 相同或互为父子一律拒绝。该机制不覆盖终端、IDE、其他 root 或未接入 gori-agent 的进程。
|
||
- `init` / `setup` 必须交互询问并校验 server host/port;扫描其他 Config v3 的声明端口,冲突时警告,新实例建议下一个未声明端口,已有配置不得静默改端口。
|
||
- `setup <bot-id>` 只重配已有实例:校验目录/config/PID,运行中或 foreign PID 拒绝,固定 Bot ID,保留 agent/skills/permissions/policy/runtime/secrets/publicBaseUrl,确认写入后运行 doctor。
|
||
- setup 的声明冲突提示不替代实际 bind 检查;`start` 对缺配置、ID 不匹配、错误权限、占位符、运行 PID、端口冲突 fail closed。
|
||
- start 只 spawn 构建后的内部 `dist/cli/instance-runner.js <config-path>`;PID identity 必须精确匹配 Node executable、runner 和唯一 config 参数。
|
||
|
||
公开入口仅有:
|
||
|
||
```bash
|
||
gori-agent init <bot-id>
|
||
gori-agent setup <bot-id>
|
||
gori-agent start <bot-id>
|
||
gori-agent stop <bot-id>
|
||
gori-agent restart <bot-id>
|
||
gori-agent status <bot-id>
|
||
gori-agent logs <bot-id>
|
||
gori-agent doctor <bot-id>
|
||
gori-agent list
|
||
```
|
||
|
||
不兼容旧 `instance` 前缀;不公开 debug 命令、`--config` 或 `--json`。
|
||
|
||
## 3. 核心代码结构
|
||
|
||
- `src/config.ts`
|
||
- Config v3 Zod schema、类型、交叉校验和 state 默认路径。
|
||
- 只接受 `configVersion: 3`。
|
||
- `src/server.ts`
|
||
- 组装 fixed Bot、state store、ProposalStore、AssistantManager、Gateway 和唯一 platform adapter。
|
||
- 只挂载所选平台 route;创建 AssistantManager 时传入 `runtime.acp.maxAssistantSessions`。
|
||
- `src/roles/role-registry.ts`
|
||
- 历史路径名保留,但实现是 `BotProfileResolver`,不是 role registry。
|
||
- 加载当前 Bot skills、计算 fingerprint、生成版本化 assistant/worker bootstrap。
|
||
- Assistant 用 `bot.assistantPersona || bot.persona` 并带人话风格规则;Worker 始终用 `bot.persona`,bootstrap 保持严格。
|
||
- `src/acp/client.ts`
|
||
- ACP initialize/new/resume/load/prompt/cancel 和 permission request。
|
||
- assistant session 一旦出现 tool update 或 permission request 必须 fail closed。
|
||
- `src/acp/worker.ts`
|
||
- worker 在 `bot.workspace` 启动;assistant 在实例私有 `state/assistant-workspaces/<hmac>` 启动,未显式配置时 `KIMI_CODE_HOME` 指向实例私有 `state/kimi/assistant/`。
|
||
- 每个 ACP worker 使用独立进程组和随机 `GORI_AGENT_WORKER_TOKEN`;cancel、timeout、crash 或 assistant 隔离违约必须清理同进程组工具后代;bootstrap 禁止 `setsid`、`nohup`、detached/daemon/background 遗留进程,主动脱组属于无 cgroup/Bubblewrap 方案的边界。
|
||
- assistant 只接受 Kimi Code ACP,并依赖项目级 `tools: []`、`subagents: []` profile;permission deny 只是附加层。
|
||
- `src/acp/assistant-manager.ts`
|
||
- 每 conversation(chat + user)一个无工具 Assistant 会话,同群不同用户互相隔离(`GORI_ASSISTANT_ACTION_V1` envelope:create_proposal/confirm/start_next/cancel/stop,格式只修复一次);runtime 执行 action 后在 reply 末尾追加人话纠正(如 start_next 被阻塞、confirm/stop/cancel 无 owner 匹配),Assistant 不得自行宣称 action 已生效。
|
||
- 唯一活跃 Worker 执行已确认 Proposal(`GORI_WORKER_RESULT_V1`:SUCCESS/FAILED/NEEDS_CONFIRMATION);success 与 failure 都需用户确认才收尾,不自动开始下一个;执行中 dirty 是正常的 NEEDS_CONFIRMATION 确认点。
|
||
- capacity(maxAssistantSessions/maxProcesses)、idle sweep、cancel/confirm/stop、worker_lost 恢复、owner 事件通知。
|
||
- `src/core/durable-session-store.ts`
|
||
- state v3 只保存 bot/platform identity 和 assistant binding(conversation key,即 chat + user、agent ID、native session ID、assistant workspace、fingerprint、时间戳);不保存消息正文。
|
||
- 单 writer lock、串行持久化、临时文件、fsync、原子 rename;version/identity 不匹配(含旧 v1/v2)拒绝并保留原文件。
|
||
- `src/core/proposal-store.ts`
|
||
- proposals.json(version 1)保存 Proposal:title/goal/steps、owner chat、发起用户 requesterUserId、状态流(proposed/queued/working/awaiting_user_confirmation/completed/failed/cancelled)、pending(step/success/failure)、worker session 与进程组 PGID/token;同样的 lock 与原子写入纪律。
|
||
- `src/core/workspace-scope.ts`
|
||
- canonical workspace + 跨实例 Config v3 扫描;相同或父子 workspace 在 doctor/start/runner fail closed。lease 已退役。
|
||
- `src/core/gateway.ts`
|
||
- 入站 allowlist、群 mention、命令和回复;不维护普通消息队列或 per-chat task lock,也没有固定时间处理中提醒。
|
||
- 每条异步入站使用独立串行 ReplyStream,`replySequence` 从 1 动态递增;发送失败只写安全日志且不阻止任务或后续发送。
|
||
- Worker 落定事件经 `sendEvent` 发送:优先引用该 chat 最近入站消息走被动回复窗口(群 4.5 分钟、C2C 55 分钟保守判定,`msg_id` + 递增 `msg_seq`);`msg_seq` 按 chat+messageId 共享计数(正常回复、事件、补发同一计数器,QQ 重复推送同一 msg_id 时沿用已用序号);无新鲜窗口或发送失败时不发主动消息,记录 lastEventDelivery/lastEventError/lastEventAt,补发失败保留队列并在下次入站时重试。
|
||
- `src/core/command-router.ts`
|
||
- `/help`、`/status`、`/list`、`/confirm`、`/stop`、`/cancel`;未识别命令按普通消息处理。
|
||
- `src/platforms/*`
|
||
- Adapter 依赖独立平台 config type,不依赖完整 AppConfig 路径。
|
||
- `src/cli/config-file.ts`
|
||
- fail-closed 加载、example seed 与原子 `0600` 配置写入。
|
||
- `src/cli/instance.ts`
|
||
- 实例目录、PID/log、端口和 lifecycle 管理。
|
||
- `src/cli/instance-runner.ts`
|
||
- 非公开进程入口;加载单实例配置、启动 server,并处理 SIGINT/SIGTERM 优雅关闭。
|
||
- `src/cli/doctor.ts`
|
||
- v3、Bot、agent、platform、state、permission 和 placeholder 检查。
|
||
- `src/agents/*`、`src/core/session-store.ts`
|
||
- legacy compatibility/reference,不是当前运行链路。
|
||
- `dist/`
|
||
- 构建产物;不手改,修改 TypeScript 后运行 `npm run build`。
|
||
|
||
## 4. Config v3 契约
|
||
|
||
仓库中唯一可提交配置说明是 `config.example.json`。它含明显占位符,可通过 schema,但不能启动实际 Bot。
|
||
|
||
顶层:
|
||
|
||
```text
|
||
configVersion 固定 3
|
||
bot 固定 Bot、agent、skills、permissions
|
||
gateway server、入站 policy、唯一 platform
|
||
runtime.acp state 和 ACP 生命周期
|
||
```
|
||
|
||
### 4.1 `bot`
|
||
|
||
```json
|
||
{
|
||
"id": "bot-id",
|
||
"workspace": "/absolute/path",
|
||
"persona": "明确职责、边界和确认点。",
|
||
"assistantPersona": "可选;Assistant 讲话人格,为空时回退 persona。Worker 始终用 persona。",
|
||
"agent": {
|
||
"id": "kimi",
|
||
"command": "/absolute/path/to/kimi",
|
||
"args": ["acp"],
|
||
"env": {}
|
||
},
|
||
"skills": [],
|
||
"permissions": {
|
||
"mode": "deny",
|
||
"allowedTools": [],
|
||
"allowedCommandPatterns": []
|
||
}
|
||
}
|
||
```
|
||
|
||
- workspace 必须绝对。
|
||
- 一个 Bot 只有一个 agent;不要添加 registry/reference。
|
||
- Kimi Code 应使用 `args: ["acp"]`。
|
||
- agent `shell: false`,cwd 为 workspace。
|
||
- 模型/provider 属于 ACP agent 自己的配置,不写入 gori-agent config。
|
||
- `env` 不得在模板、日志、diff 中泄漏 token。
|
||
|
||
### 4.2 Skills
|
||
|
||
`bot.skills[]` 每项直接声明:
|
||
|
||
```json
|
||
{ "id": "skill-id", "file": "/absolute/SKILL.md", "maxBytes": 256000 }
|
||
```
|
||
|
||
ID 唯一;文件须存在、可读、普通文件且未超限。skill 内容发送给 ACP,也进入 fingerprint;不得放 secret。
|
||
|
||
### 4.3 Permissions
|
||
|
||
- `deny`:默认,拒绝全部 permission request。
|
||
- `allowlist`:`toolCall.name` 与 `allowedTools` 精确匹配;bash/terminal 的 raw command 还要被 `allowedCommandPatterns` 完整覆盖。
|
||
- `auto`:高风险/yolo,只有用户明确授权才能设置。
|
||
|
||
permission policy 只审批 ACP request,不会注册工具。persona 不是安全边界。请求信息不足时拒绝。命令 regex 应锚定并限制参数,不使用任意 `.*` 全放行。
|
||
|
||
### 4.4 Gateway
|
||
|
||
- `gateway.server`: host、port、publicBaseUrl。
|
||
- `gateway.policy`: allowedUsers、allowedChats、requireMentionInGroup。
|
||
- `gateway.platform`: `type` discriminated union,只能是 qq、feishu、wecom、webhook、weixin 之一。
|
||
- 对象存在即启用,无 `enabled` 字段。
|
||
|
||
QQ 群 chat ID 为 `group:<group_openid>`;用户 ID 按 QQ 官方语义取值:群聊作者用 `member_openid`,C2C 私聊作者用 `user_openid`。正式运维 Bot 应配置入口 allowlist。
|
||
|
||
### 4.5 Runtime ACP
|
||
|
||
- `stateFile` 为空时为 `$GORI_AGENT_HOME/state/acp-sessions.json`;proposals 固定在同目录 `proposals.json`。
|
||
- `initializeTimeoutMs`: 默认 10000。
|
||
- `promptTimeoutMs`: 默认 14400000(4 小时)。
|
||
- `cancelGraceMs`: 默认 5000。
|
||
- `idleTimeoutMs`: 默认 1800000。
|
||
- `sweepIntervalMs`: 默认 60000。
|
||
- `maxProcesses`: 默认 8,包含 assistant + worker。
|
||
- `maxAssistantSessions`: 默认 4。
|
||
|
||
Kimi Code agent `args` 必须严格为 `["acp"]`,不能添加可能绕过 assistant no-tool profile 的启动参数。
|
||
|
||
## 5. Assistant / Proposal / Worker 与 state v3
|
||
|
||
运行链路是三层:每 conversation(chat + user)一个无工具 Assistant 会话负责对话与创建 Proposal;唯一活跃 Worker 在 `bot.workspace` 执行已确认 Proposal;Proposal 是两者之间的持久工作单,owner 是 chat + 发起用户,只有发起人本人可 confirm/stop/cancel/list,`start_next` 也只启动发起人自己的 queued Proposal。
|
||
|
||
Proposal 状态流:`proposed → queued → working → awaiting_user_confirmation → completed | failed | cancelled`。`proposed` 必须用户确认才进入 `queued`;没有活跃 worker、没有 working/awaiting_user_confirmation 时最早确认的 queued Proposal 才开始。
|
||
|
||
Assistant 每条回复以隐藏 `GORI_ASSISTANT_ACTION_V1` envelope 结尾(`reply` + `actions`),action 仅 `create_proposal`、`confirm`、`start_next`、`cancel`、`stop`,格式错误只修复一次。Worker 每轮以隐藏 `GORI_WORKER_RESULT_V1` envelope 收尾:`SUCCESS`、`FAILED`、`NEEDS_CONFIRMATION`(可选 `question`、`nextStep`、`dirty`),同样只修复一次。
|
||
|
||
确认语义:
|
||
|
||
- `SUCCESS` 与 `FAILED` 都进入 `awaiting_user_confirmation`,都需用户确认才落定为 `completed`/`failed`;failed 用 answer `retry` 确认可重试。
|
||
- 系统不自动开始下一个 Proposal;只有新确认或 `start_next` 推进队列。
|
||
- 执行中遇到 dirty/意外目标是正常的 `NEEDS_CONFIRMATION` 确认点,不是 blocked 终态;用户回答后同一 worker 继续。
|
||
- Gateway 无固定时间提醒;worker 落定后由 Assistant 生成事件文案发给 owner chat,QQ 无主动消息权限时优先使用被动回复窗口(群 5 分钟/私聊 60 分钟,按 4.5/55 分钟保守判定),过期或失败则不主动发、记录 lastEventDelivery 并在下次入站时补发;`/status` 输出当前 chat 的事件投递状态与 schedulerState/blockedReason/nextAction。
|
||
|
||
Assistant 隔离:cwd 在实例私有 `state/assistant-workspaces/`,目录 key 使用进程随机 salt 的 HMAC,不得与项目 workspace 重叠;Kimi 项目级 agent override 必须设置 `tools: []`、`subagents: []`,ACP `mcpServers: []`,未显式配置时 `KIMI_CODE_HOME` 指向实例私有 `state/kimi/assistant/`;任何 tool update 或 permission request 都视为隔离违约,fail closed、终止进程组并删除 binding。每个 ACP worker 独立进程组 + 随机 token;runner 重启时 working Proposal 校验 token 清理旧进程组后标记 failed(worker_lost)。
|
||
|
||
state v3(`acp-sessions.json`)header 保存 `botId` 和 platform,只存 assistant binding(agent ID、assistant workspace、native session ID、Bot fingerprint、时间戳);`proposals.json`(version 1)存 Proposal 记录。两者打开时 version/identity 不匹配必须拒绝,不能清空、迁移或覆盖;旧 state v1/v2 也明确拒绝并保留原文件。fingerprint 包含 bootstrap schema version、Bot ID、workspace/persona/assistantPersona、agent 定义、permission policy、skill 路径和内容 hash。prompt 失败不重放。
|
||
|
||
命令 `/help`、`/status`、`/list`、`/confirm`、`/stop`、`/cancel` 旁路 Assistant;`/stop` 仅 Proposal 发起人(chat + user)可用,且对等待确认的 Proposal 按 pending 类型兜底落定(success→completed、failure→failed、step→cancelled),不自动开始下一个。
|
||
|
||
## 6. 单平台装配
|
||
|
||
Server 只构造 `gateway.platform.type` 对应 adapter,只挂载对应 webhook:
|
||
|
||
- qq webhook 模式 → `/webhook/qq`;QQ WebSocket 模式不挂载该 route
|
||
- feishu → `/webhook/feishu`
|
||
- wecom → `/webhook/wecom`
|
||
- weixin → `/webhook/weixin`
|
||
- webhook → `/webhook/generic`
|
||
|
||
QQ WebSocket 仅在 qq + websocket 模式启动。`GET /health` 只输出 configVersion、botId、platform 和 aggregate ACP counters,不得输出 credentials 或 native session ID。`GET /platforms` 返回单 Bot/platform 概览。
|
||
|
||
## 7. 配置与实例施工流程
|
||
|
||
1. 读取本文件、`config.example.json` 和 `src/config.ts`。
|
||
2. 明确 Bot ID、workspace、persona、平台、skills 和最小 permission policy。
|
||
3. 确认 ACP agent executable 与 ACP 可用。
|
||
4. 使用 `gori-agent init <bot-id>` 或 Coding Agent 生成实例 `config.json`;不要把 example 当运行配置。
|
||
5. 交互确认并严格校验 `gateway.server.host/port`;对其他实例的声明端口冲突给出警告和建议,但仍由 `start` 做实际 bind 检查。
|
||
6. 写配置必须同目录临时文件 + fsync + atomic rename,最终 `0600`;目录 `0700`。
|
||
7. setup 的 secret/token 输入必须不回显;secrets 不在对话、日志、测试输出、diff 中回显。
|
||
8. 运行 `gori-agent doctor <bot-id>`,不发送真实平台消息。
|
||
9. 只有用户明确授权时才启动/停止真实 Bot。
|
||
|
||
旧仓库本地 `config.json`、备份和 state 可供人工回退;改造实例时不要删除或覆盖。
|
||
|
||
## 8. 测试与验收
|
||
|
||
行为变化补测试,不削弱测试。配置 schema 变化同步:
|
||
|
||
- `src/config.ts`
|
||
- `config.example.json`
|
||
- setup/doctor/instance CLI
|
||
- README 和本文件
|
||
- config、session、store、Gateway、server 测试
|
||
|
||
最终运行:
|
||
|
||
```bash
|
||
npm run typecheck
|
||
npm test
|
||
npm run build
|
||
git diff --check
|
||
bash -n install.sh gori-agent.sh bin/gori-agent
|
||
```
|
||
|
||
额外检查:
|
||
|
||
- example 可 parse,doctor 识别 placeholder。
|
||
- 不存在的运行配置明确失败。
|
||
- tracked diff 无 config、backup、state、log、credentials 或手改 dist。
|
||
- 不执行真实平台消息、pull、部署、远程机器修改或 Git mutation,除非用户当次明确授权。
|
||
|
||
## 9. 敏感信息与禁止提交
|
||
|
||
不得提交或粘贴:
|
||
|
||
- 任何真实 `config.json` 或备份
|
||
- `.env`、agent env token/API key
|
||
- platform credentials
|
||
- ACP state、PID、lock、session metadata
|
||
- 含真实 chat/user/message ID 或消息正文的日志
|
||
|
||
`.gitignore` 已覆盖仓库本地 `config.json`、备份、日志、`dist`、`node_modules` 和 coverage。实例默认在仓库外。安全性按内容判断,不能只依赖文件名。
|