# gori-agent `gori-agent` 是一个 Node.js 20+ / TypeScript 网关:从一个 IM 平台接收消息,通过官方 Agent Client Protocol(ACP)驱动一个 Coding Agent。 ```text 用户 → 单个平台 Adapter → Gateway → AssistantManager(Assistant / Proposal / Worker)→ 单个 ACP Agent ``` ## Config v3 实例模型 Config v3 只支持以下边界: - 一份运行配置对应一个固定 Bot 身份。 - 一个 Bot 只有一个 workspace、persona、ACP agent、skill 列表和 permission policy。 - 每个 chat 一个无工具 Assistant 会话;它只对话、创建 Proposal 并解释 Worker 反馈,自己从不执行。 - 同一时刻全实例只有一个 Worker 在 `bot.workspace` 执行一个已确认 Proposal;success 与 failure 都需要用户确认后才收尾,系统不会自动开始下一个 Proposal。 - 一个实例只绑定一个平台;多平台使用多个实例。 - 不再有 `roles[]`、`defaultRole`、`backends[]`、动态 `/role` 切换、单主任务/近似 BTW 或 Config v1/v2 迁移。 - `configVersion` 非 `3` 会明确失败,旧 state v1/v2 也不会自动改写。 一台机器上的实例统一位于: ```text ${GORI_AGENT_ROOT:-$HOME/.gori-agent}/instances// ├── config.json ├── logs/gori-agent.log └── state/ ├── acp-sessions.json # state v3:Assistant binding ├── proposals.json # Proposal 记录 ├── assistant-workspaces/ # 每 chat 私有 Assistant cwd ├── kimi/assistant/ # Assistant 私有 KIMI_CODE_HOME └── gori-agent.pid ``` 实例目录就是该进程的 `GORI_AGENT_HOME`。实例目录、`state/`、`logs/` 使用 `0700`,配置与状态文件使用 `0600`。 ## 构建与安装 ```bash npm install npm run build ./install.sh ``` 安装器只安装一个 launcher;不会为每个 Bot 复制源码或 `dist/`。默认 launcher 位于 `$HOME/.gori-agent/bin/gori-agent`。 确认 ACP agent 可用: ```bash kimi --version kimi doctor kimi acp --help ``` 模型和 provider 由 ACP agent 自身配置,例如 Kimi Code 使用 `~/.kimi-code/config.toml`。gori-agent 不复制模型凭据。 ## 实例管理 ```bash gori-agent init gori-agent setup gori-agent doctor gori-agent start gori-agent status gori-agent logs gori-agent restart gori-agent stop gori-agent list ``` - `init` 交互生成 Config v3,本身不启动 Bot;它会询问并校验 `gateway.server.host/port`,扫描统一实例目录中其他 Config v3 的声明端口,并在默认端口冲突时建议下一个未占用端口。 - `setup` 只重配已有实例。它校验目录、配置身份、配置/PID 权限,并拒绝运行中实例或指向其他活进程的 PID;固定 `bot.id`,保留既有 agent(含 args/env)、skills、permissions、gateway policy、runtime、平台 secrets 与 publicBaseUrl,确认写入后自动运行 doctor。 - 平台 secret/token 使用不回显输入,空 generic webhook/weixin secret 会随机生成。 - `start` 会检查配置存在、目录名匹配 `bot.id`、目录/配置权限、模板占位符、workspace、agent executable、PID identity 和端口,并等待 `/health` 返回匹配的 Bot/platform identity;任一不满足即 fail closed。 - 实例进程由内部 `dist/cli/instance-runner.js` 承载;`status` 和 lifecycle 命令精确核对 Node executable、runner 路径及唯一 config 参数,再核对 `/health` 的 Bot/platform identity。 - 每个实例必须使用不同的 `gateway.server.port` 和平台凭据;setup 的声明冲突提示不替代 `start` 的实际端口检查。 - `stop` 发送 `SIGTERM`,runner 会优雅关闭 server,并等待最多 10 秒;不会自动 `SIGKILL`。 公开 CLI 不兼容旧 `instance` 前缀,也不提供 debug 命令、`--config` 或 `--json`。 可用 `GORI_AGENT_ROOT` 改变统一根目录: ```bash GORI_AGENT_ROOT=/srv/gori-agent gori-agent list ``` ## `config.example.json` 的定位 `config.example.json` 是仓库内唯一可提交、无密钥的配置说明,不是运行配置。它可被 Config v3 schema 解析,但保留 `BOT_ID`、`QQ_APP_ID` 等明显占位符。只有 `init` 会读取 example 作为初始化种子;其他命令只按 `` 加载统一实例目录中的 `config.json`,缺失即失败。 `writeConfigFile()` 使用同目录临时文件、`fsync` 和原子 rename,并强制最终文件为 `0600`。 ## Config v3 完整说明见 `config.example.json`。顶层只有: ```text configVersion 固定为 3 bot Bot、workspace、persona、agent、skills、permissions gateway HTTP server、入站 policy、唯一 platform runtime.acp ACP state、timeout 和 worker pool ``` ### Bot 与 ACP agent ```json { "bot": { "id": "my-bot", "workspace": "/absolute/workspace", "persona": "Describe responsibilities, boundaries, and confirmation points.", "agent": { "id": "kimi", "command": "/home/USER/.kimi-code/bin/kimi", "args": ["acp"], "env": {} }, "skills": [], "permissions": { "mode": "deny", "allowedTools": [], "allowedCommandPatterns": [] } } } ``` - `bot.id` 只允许小写字母、数字和连字符,最长 63 字符。 - `workspace` 与每个 skill `file` 必须是绝对路径。 - `bot.skills[]` 直接声明 `{ id, file, maxBytes }`;ID 必须唯一。 - agent 使用 `shell: false` 在 `bot.workspace` 启动。 - agent env 可能包含 secret,不能进入可提交模板、日志或 diff。 ### Permission policy ACP permission request 由 `bot.permissions` 决策,persona 不是安全边界: - `deny`:拒绝所有 permission request,默认值。 - `allowlist`:`toolCall.name` 必须与 `allowedTools` 精确匹配;bash/terminal 还必须由至少一个 `allowedCommandPatterns` 正则完整覆盖整条 raw command。名称或 command 缺失时拒绝。 - `auto`:自动允许,风险等价于 yolo;`doctor` 会告警。 `allowedTools` 只控制 ACP 审批,不会注册或创造工具。skills、agent 与 permission policy 是三个独立概念。 ### 单平台配置 `gateway.platform` 是按 `type` 区分的 union,只能选择一个: - `qq`:WebSocket 或 webhook。 - `feishu`:`/webhook/feishu`。 - `wecom`:`/webhook/wecom`;入站仍是 501 scaffold。 - `webhook`:`/webhook/generic`,同步 JSON + HMAC-SHA256。 - `weixin`:`/webhook/weixin`,外部 bridge scaffold。 对象存在即启用,不再使用 `enabled`。Server 只构造所选 adapter,也只挂载需要的 webhook route。QQ WebSocket 仅在 `type=qq` 且 `connectionMode=websocket` 时启动,此模式不挂载 `/webhook/qq`。 QQ websocket 示例: ```json { "type": "qq", "connectionMode": "websocket", "appId": "QQ_APP_ID", "clientSecret": "QQ_CLIENT_SECRET", "botSecret": "", "verifySignature": true, "botNames": ["QQ_BOT_NAME"], "intents": 33554432, "shard": [0, 1] } ``` 正式 Bot 应通过 `gateway.policy.allowedUsers` / `allowedChats` 限制入口。QQ 群 chat ID 为 `group:`;用户 ID 按 QQ 官方语义取值:群聊作者用 `member_openid`,C2C 私聊作者用 `user_openid`。 ### ACP 生命周期 默认 `runtime.acp.promptTimeoutMs` 是 `14400000`(4 小时),适合长构建/部署任务。其他默认值: - initialize:10 秒 - cancel grace:5 秒 - idle assistant:30 分钟 - sweep:60 秒 - max processes:8(Assistant + Worker 总和) - max assistant sessions:4 `stateFile` 为空时使用当前实例的 `$GORI_AGENT_HOME/state/acp-sessions.json`;Proposal 记录固定保存在同目录的 `proposals.json`。Kimi Code agent 的 `args` 必须严格为 `["acp"]`,避免额外启动参数绕过 Assistant 的 no-tool profile。 ## Assistant / Proposal / Worker 与 state v3 运行链路是三层: - **Assistant**:每个 conversation(chat + user)一个无工具 ACP 会话,只与用户对话;同群不同用户的会话互相隔离。它把用户意图整理成 Proposal(title、goal、steps),并解释 Worker 的反馈。Assistant 每条回复必须以隐藏 `GORI_ASSISTANT_ACTION_V1` envelope 结尾(`reply` + `actions`),action 只有 `create_proposal`、`confirm`、`start_next`、`cancel`、`stop`;格式错误只修复一次。 - **Proposal**:一份待确认的工作单,owner 是 chat + 发起用户;只有发起人本人可以 confirm/stop/cancel/list 它,`start_next` 也只启动发起人自己的 queued Proposal。状态流为 `proposed → queued → working → awaiting_user_confirmation → completed | failed | cancelled`。`proposed` 只有用户确认后才进入 `queued`;空闲时最早确认的 queued Proposal 才开始执行。 - **Worker**:同一时刻全实例只有一个,在 `bot.workspace` 用 `bot.permissions` policy 执行一个已确认 Proposal。每轮必须以隐藏 `GORI_WORKER_RESULT_V1` envelope 收尾:`SUCCESS`、`FAILED` 或 `NEEDS_CONFIRMATION`(可带 `question`、`nextStep`、`dirty`)。 确认语义是刻意的: - `SUCCESS` 和 `FAILED` 都进入 `awaiting_user_confirmation`,都需要用户确认后才分别落定为 `completed` / `failed`;对 failed Proposal 用 answer `retry` 确认可以重试。 - 系统**不会**在一个 Proposal 完成后自动开始下一个;只有用户确认新 Proposal 或 Assistant 发出 `start_next` 才会推进队列。 - Worker 执行中遇到 dirty 或意外目标时报告 `NEEDS_CONFIRMATION`,这是正常的执行中确认点,不是 blocked 终态;用户回答后同一 Worker 继续执行。 - `stop` 对 `awaiting_user_confirmation` 的 Proposal 有兜底语义:pending success 落定为 `completed`,pending failure 落定为 `failed`,pending step 落定为 `cancelled`(并清理 worker 状态);不会自动开始下一个 Proposal。 - Gateway 不再有 15/60/180/480 秒的固定时间提醒;Worker 落定结果时通过 Assistant 生成一条事件说明,由平台 adapter 作为**新消息**发给 owner chat(QQ 同样是新消息,不引用原消息)。 Assistant 隔离与旧 side Session 一致且更严格: - cwd 位于实例私有 `state/assistant-workspaces/`,目录名由进程随机 salt 和 conversation key(chat + user)计算 HMAC,不直接编码 chat 标识,并且不能与项目 workspace 重叠;重启后按 binding 恢复同一目录。 - 项目级 `agent.md` override 显式设置 `tools: []` 与 `subagents: []`;ACP 传入空 `mcpServers`;Assistant 只接受 Kimi Code ACP,并强制 permission deny 作为附加层。 - 除非 `bot.agent.env` 已显式设置,Assistant 进程的 `KIMI_CODE_HOME` 指向实例私有 `state/kimi/assistant/`(`0700`),与真实用户配置隔离。 - 一旦出现 tool update 或 permission request,即视为隔离违约:当前 turn fail closed、终止整个 ACP 进程组并删除 binding。Worker 的 cancel、timeout、crash 同样按进程组清理工具后代;bootstrap 禁止 `setsid`、`nohup`、detached/daemon/background 遗留进程,主动脱离进程组仍属于无 Bubblewrap/cgroup 方案的信任边界。 - 每个 ACP worker 以独立进程组运行并携带随机 `GORI_AGENT_WORKER_TOKEN`;runner 重启时会把仍处于 `working` 的 Proposal 校验 token 后清理旧进程组,并标记为 failed(`worker_lost`),交给用户确认或重试。 state v3(`acp-sessions.json`)只保存 header(version 3、`botId`、platform)和 Assistant binding:conversation key(chat + user)、agent ID、native session ID、assistant workspace、Bot fingerprint 与时间戳。`proposals.json`(version 1)保存 Proposal 记录:title/goal/steps、owner chat、发起用户、状态、pending(`step | success | failure`)、worker native session ID 与进程组 PGID/token、时间戳;不保存消息正文。两个 store 都做单 writer lock、串行持久化、临时文件 + fsync + 原子 rename;version 或 identity 不匹配(含旧 state v1/v2)一律拒绝启动并保留原文件,不清空、不迁移。 Bot fingerprint 包含 bootstrap schema version、Bot ID、workspace、persona、agent 定义、permission policy 和 skill 路径/内容 hash;fingerprint 或 agent 不匹配的 binding 会被丢弃并重建 Assistant 会话。失败 prompt 不会自动重放,因为工具操作可能已有副作用。 ## Workspace 独占 同一 `GORI_AGENT_ROOT` 下,旧的 `/usr/bin/flock` workspace lease 与 `owner.json` 已退役。`doctor`、外层 `start` 与内部 runner 都对其他实例目录和完整 Config v3 扫描 fail closed,canonical workspace 相同或互为父子都拒绝;不再做配置级 workspace 共享。`setup/start` 还通过实例 lifecycle flock 互斥,父 CLI 把已验证配置摘要传给 runner,配置发生换挡时 runner 拒绝启动。 ## IM 命令 ```text /help /status /list /confirm /stop /cancel ``` - `/help`:显示自然语言使用说明和兜底命令列表。 - `/status`:显示固定 Bot、agent、workspace、Assistant 会话数、各状态 Proposal 计数、Worker 是否在执行及当前用户是否 owner。 - `/list`:列出当前 chat 中当前用户拥有的 Proposal(id、状态、title)。 - `/confirm`:确认当前用户最近待确认的 Proposal(`proposed` 进入队列,`awaiting_user_confirmation` 落定完成/失败或带着回答继续)。 - `/stop`:停止当前用户正在执行的 Worker 并将其 Proposal 标记为 cancelled;对等待确认的 Proposal 按 pending 类型兜底落定(success→completed、failure→failed、step→cancelled);只有 Proposal 发起人可用。 - `/cancel`:取消当前用户最近未开始的 Proposal(`proposed` / `queued`)。 日常操作以自然语言为主,Assistant 会自己生成 confirm/cancel/stop 等 action;这些命令是旁路 Assistant 的兜底入口。未识别的 `/` 命令按普通消息交给 Assistant。 Gateway 不维护普通消息队列或 per-chat task lock,也不再有固定时间(15/60/180/480 秒)的处理中提醒;每条 allowlist 普通消息按 conversation(chat + user)串行交给 AssistantManager。Worker 落定结果(成功、失败或提问)时,AssistantManager 生成事件文案并经 `Gateway.sendEvent` 由平台 adapter 作为**新消息**发出(QQ 也是新消息,不引用原消息);发送失败只记录不含消息正文或 provider 错误详情的安全日志,不影响任务或后续发送。 每条异步入站使用独立、串行的回复流,`replySequence` 从 1 动态递增;同步 webhook 直接返回 JSON 结果。 ## HTTP 端点 - `GET /health`:`configVersion`、`botId`、platform 和 aggregate ACP counters,不暴露凭据/native session ID。 - `GET /platforms`:当前单一 Bot/platform 概览。 - `POST /webhook/`:只存在所选平台路由;generic 使用 `/webhook/generic`。 ## Doctor 与安全纪律 `gori-agent doctor ` 检查: - Config v3 与 example placeholder。 - 配置权限 `0600`。 - Bot ID、workspace、skills、permissions,以及 agent `args` 严格为 `["acp"]`。 - 同一实例根下相同或 parent/child workspace 重叠。 - ACP initialize 与 session restore capability。 - state 目录可读写。 - 单平台必需字段,但不打印 credential 值。 自动测试不会启动真实 Bot、发送平台消息、pull、部署或访问远程机器。真实 `config.json`、备份、state 和日志不得提交。 ## 开发验收 ```bash npm run typecheck npm test npm run build git diff --check bash -n install.sh gori-agent.sh bin/gori-agent ``` 测试使用 Node `node:test` + `tsx` 和本地 fake ACP 子进程,覆盖 Config v3、permission、Assistant/Worker envelope 协议、Proposal 状态流与确认语义、timeout/cancel、worker_lost 恢复、state v3 identity、workspace 重叠扫描、Gateway 无队列语义、QQ normalization 和单平台 route。真实 Kimi ACP 的执行层兼容性可用 `node scripts/side-session-spike.mjs` 复验(assistant no-tool spike:私有 cwd 在项目外、`mcpServers: []`、toolUpdates/permissionRequests/fsRequests 全为 0、`GORI_ASSISTANT_ACTION_V1` envelope 可解析);该脚本使用临时 cwd/profile,不读取或输出真实配置、凭据、日志正文。 `src/agents/*` 与 `src/core/session-store.ts` 仅为 legacy compatibility/reference,不在当前运行链路。运行时没有单轮 CLI fallback。