Models occasionally drop the closing tag of the result envelope while the JSON itself is complete; the strict parser rejected these and one repair attempt could not always recover, cascading into worker_error and dropping valid attachments. The parser now falls back to brace-balanced salvage when the closing tag is missing, and worker lifecycle events (start, resume, invalid envelope, repair, settle, worker_error) are logged.
21 KiB
gori-agent 项目施工指南
本文件供 Coding Agent 在本仓库施工时使用。先理解 Config v3 和实例隔离契约,再做最小、可验证的修改。
1. 项目定位与固定边界
gori-agent 是 Node.js 20+ / TypeScript 项目,通过单个 IM 平台接收请求,并通过官方 Agent Client Protocol(ACP)驱动一个 Coding Agent。
用户 → 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. 实例目录契约
统一实例根:
${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/flockworkspace 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 参数。
公开入口仅有:
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 只是附加层。
- worker 在
src/acp/assistant-manager.ts- 每 conversation(chat + user)一个无工具 Assistant 会话,同群不同用户互相隔离(
GORI_ASSISTANT_ACTION_V2envelope:create_proposal/confirm/adjust_proposal/follow_up/finish/send_image/start_next/cancel/stop,格式只修复一次);runtime 执行 action 后在 reply 末尾追加人话纠正(如 start_next 被阻塞、无 owner 匹配),Assistant 不得自行宣称 action 已生效。 - 唯一活跃 Worker 执行已确认 Proposal(
GORI_WORKER_RESULT_V2:仅PENDING,summary 必填,可带 question/workspaceDirty/attachments);pending 不区分 success/fail,只有 finish 落定为 finished(done),cancel 落定为 finished(cancelled),不自动开始下一个。 - envelope 解析先严格匹配(闭合标签 + 文本末尾 anchor);缺闭合标签时从起始标签后做花括号配平(字符串/转义感知)salvage,配平点必须在文本末尾才接受,否则仍判无效走修复。worker 启动(new/resume session)、envelope 无效、repair 成败、settle(attachments/dropped 数)、worker_error、follow_up resume/fresh 兜底均有单行安全日志(proposal id 前 8 位,不含正文)。
- capacity(maxAssistantSessions/maxProcesses)、idle sweep、cancel/confirm/finish/stop、worker_lost 恢复、owner 事件通知。
- 每 conversation(chat + user)一个无工具 Assistant 会话,同群不同用户互相隔离(
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 2)保存 Proposal:title/goal/steps、owner chat、发起用户 requesterUserId、状态流(proposed/queued/working/pending/finished)、pending(summary/question?/workspaceDirty?/receivedAt)、finished(finishKind done|cancelled、finishNote?)、worker session 与进程组 PGID/token;同样的 lock 与原子写入纪律。打开 v1 文件时先写
proposals.json.v1-<timestamp>.bak(0600)备份再按固定映射迁移。
- proposals.json(version 2)保存 Proposal:title/goal/steps、owner chat、发起用户 requesterUserId、状态流(proposed/queued/working/pending/finished)、pending(summary/question?/workspaceDirty?/receivedAt)、finished(finishKind done|cancelled、finishNote?)、worker session 与进程组 PGID/token;同样的 lock 与原子写入纪律。打开 v1 文件时先写
src/core/workspace-scope.ts- canonical workspace + 跨实例 Config v3 扫描;相同或父子 workspace 在 doctor/start/runner fail closed。lease 已退役。
src/core/workspace-images.ts- 出站图片校验与读取:路径必须 resolved 在 canonical workspace 内、png/jpg magic bytes、单张 ≤10MB;发送时按路径重读。
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、/finish、/stop、/cancel;未识别命令按普通消息处理。
src/platforms/*- Adapter 依赖独立平台 config type,不依赖完整 AppConfig 路径。
src/cli/config-file.ts- fail-closed 加载、example seed 与原子
0600配置写入。
- fail-closed 加载、example seed 与原子
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。
- 构建产物;不手改,修改 TypeScript 后运行
4. Config v3 契约
仓库中唯一可提交配置说明是 config.example.json。它含明显占位符,可通过 schema,但不能启动实际 Bot。
顶层:
configVersion 固定 3
bot 固定 Bot、agent、skills、permissions
gateway server、入站 policy、唯一 platform
runtime.acp state 和 ACP 生命周期
4.1 bot
{
"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[] 每项直接声明:
{ "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:typediscriminated 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/adjust/follow_up/finish/stop/cancel/list,start_next 也只启动发起人自己的 queued Proposal。
Proposal 状态流:proposed → queued → working → pending → finished。proposed 必须用户确认才进入 queued;pending 就是「等用户决定」,不区分 success/failure;只有 finish 把 pending 落定为 finished(done),cancel(proposed/queued/pending)落定为 finished(cancelled);没有 working、owner 自己没有 pending、全局没有 workspaceDirty pending 时,最早确认的 queued Proposal 才可通过 confirm 或 start_next 开始。
Assistant 每条回复以隐藏 GORI_ASSISTANT_ACTION_V2 envelope 结尾(reply + actions),action 仅 create_proposal、confirm、adjust_proposal(仅 proposed/queued)、follow_up(pending → working,优先 resume 原 worker native session,失败则带 Proposal 上下文新起 session,用户图片附件随 prompt 给 Worker)、finish、send_image(把 Worker 报告过的 workspace 内图片发给用户)、start_next、cancel、stop,格式错误只修复一次。Worker 每轮以隐藏 GORI_WORKER_RESULT_V2 envelope 收尾:仅 PENDING(summary 必填,可选 question、workspaceDirty、attachments),同样只修复一次。envelope 缺闭合标签(模型截断)时先按花括号配平 salvage(字符串/转义感知,配平点必须在文本末尾),失败才进入修复流程。
确认语义:
- Worker 落定即进入
pending(记录 summary/question?/workspaceDirty?/receivedAt);finish(可带 note)落定为finished(done),cancel落定为finished(cancelled);finished 保留最后 summary 供面板展示。 - 系统不自动开始下一个 Proposal;只有新确认或
start_next推进队列。 - 阻塞规则:任何
working全局拒绝 start_next/follow_up;owner 自己有 pending 时拒绝该 owner 的 start_next(提示先 finish 或 follow_up);任何workspaceDirty: true的 pending 全局拒绝 start_next(提示先处理 dirty 工作区);非 dirty 的 pending 不挡其他用户。 stop只对 working 生效:停 worker(现有进程组清理)后 Proposal 转为 pending(summary「被用户中止」、workspaceDirty)。- Assistant 有 pending 时每轮 prompt 注入 pending card;回复没提到 pending 时 runtime 在 reply 末尾追加人话兜底提醒。
- 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 清理旧进程组后标记为 pending(worker_lost、workspaceDirty: true)。
state v3(acp-sessions.json)header 保存 botId 和 platform,只存 assistant binding(agent ID、assistant workspace、native session ID、Bot fingerprint、时间戳);proposals.json(version 2)存 Proposal 记录。state v3 打开时 version/identity 不匹配必须拒绝,不能清空、迁移或覆盖;旧 state v1/v2 也明确拒绝并保留原文件。proposals.json 唯一例外:v1 文件打开时先在同目录写 proposals.json.v1-<timestamp>.bak(0600)备份,再按固定映射迁移为 v2(SUCCESS → pending{summary};FAILED → pending{summary, workspaceDirty: true};NEEDS_CONFIRMATION → pending{summary, question};completed/failed → finished(done)(failed 保留失败说明为 finishNote);cancelled → finished(cancelled);proposed/queued/working 保留)。fingerprint 包含 bootstrap schema version、Bot ID、workspace/persona/assistantPersona、agent 定义、permission policy、skill 路径和内容 hash。prompt 失败不重放。
命令 /help、/status、/list、/confirm、/finish、/stop、/cancel 旁路 Assistant;全部 owner-only(chat + user):/confirm 对 proposed,/finish 对 pending,/stop 仅对 working(→pending),/cancel 对 proposed/queued/pending;/list 面板按 pending → working → queued → proposed → 最近 finished 排列。QQ 入站图片附件(image/*,单张 ≤5MB、每条最多 3 张、10 秒下载超时)下载为 base64 经 IncomingMessage.attachments 透传;agent 声明 promptCapabilities.image 时作为 ACP image content block 发给 Assistant/Worker,否则降级为文本说明;视频/文件附件不下载,仅以 [视频] <url> / [文件] <url> 文本拼接。
出站图片(QQ):Worker 在 GORI_WORKER_RESULT_V2 的 attachments(最多 3 个 {path, mimeType?})上报 workspace 内 png/jpg(建议 .gori-outbox/),Assistant 也可用 send_image { path } 主动发图;runtime 校验 resolved realpath 必须在 canonical workspace 内、png/jpg magic bytes、单张 ≤10MB,违规丢弃并在事件文本说明。发送走 POST /v2/{groups|users}/{id}/files 上传(file_type: 1 + base64 file_data + srv_send_msg: false)后 msg_type: 7 + media.file_info 发送;群/私聊上传的 file_info 不通用,按目标分别上传。图片与文本共用 Gateway 的 msg_id + 递增 msg_seq 计数器;落定事件先文案后图;被动窗口过期时照旧记录并下次入站补发,补发按路径重读文件、文件缺失降级为文本说明;上传/发送失败不阻断文本,降级文本说明 + 安全日志。其他平台 adapter 无 supportsImages 标记时图片降级为 [图片] <文件名> 文本。
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. 配置与实例施工流程
- 读取本文件、
config.example.json和src/config.ts。 - 明确 Bot ID、workspace、persona、平台、skills 和最小 permission policy。
- 确认 ACP agent executable 与 ACP 可用。
- 使用
gori-agent init <bot-id>或 Coding Agent 生成实例config.json;不要把 example 当运行配置。 - 交互确认并严格校验
gateway.server.host/port;对其他实例的声明端口冲突给出警告和建议,但仍由start做实际 bind 检查。 - 写配置必须同目录临时文件 + fsync + atomic rename,最终
0600;目录0700。 - setup 的 secret/token 输入必须不回显;secrets 不在对话、日志、测试输出、diff 中回显。
- 运行
gori-agent doctor <bot-id>,不发送真实平台消息。 - 只有用户明确授权时才启动/停止真实 Bot。
旧仓库本地 config.json、备份和 state 可供人工回退;改造实例时不要删除或覆盖。
8. 测试与验收
项目级部署/配置 skill 位于 .kimi-code/skills/gori-agent-deploy/SKILL.md;真实 Bot 部署、模型/persona 配置和 state 备份流程以该文件为准。
行为变化补测试,不削弱测试。配置 schema 变化同步:
src/config.tsconfig.example.json- setup/doctor/instance CLI
- README 和本文件
- config、session、store、Gateway、server 测试
最终运行:
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。实例默认在仓库外。安全性按内容判断,不能只依赖文件名。