Files
zenord 9ae2639660 feat: run confirmed proposals continuously until a real decision point
A proposal still needs one confirmation before it starts, but once it is
working the worker now treats that as a single grant to carry out
routine low-risk execution without step-by-step reconfirmation. It only
returns pending early for high-risk actions, key business decisions,
external blockers, or dirty/unexpected targets. Assistant/worker
bootstrap copy and docs now align with that execution model.
2026-08-22 00:05:40 +08:00

22 KiB
Raw Permalink Blame History

gori-agent

gori-agent 是一个 Node.js 20+ / TypeScript 网关:从一个 IM 平台接收消息,通过官方 Agent Client Protocol(ACP)驱动一个 Coding Agent。

用户 → 单个平台 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;Worker 每轮只交回 result(pending),由用户决定 finish 结束还是继续说要求继续,系统不会自动开始下一个 Proposal。
  • 一个实例只绑定一个平台;多平台使用多个实例。
  • 不再有 roles[]、defaultRole、backends[]、动态 /role 切换、单主任务/近似 BTW 或 Config v1/v2 迁移。
  • configVersion 非 3 会明确失败,旧 state v1/v2 也不会自动改写。

一台机器上的实例统一位于:

${GORI_AGENT_ROOT:-$HOME/.gori-agent}/instances/<bot-id>/
├── 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。

构建与安装

npm install
npm run build
./install.sh

安装器只安装一个 launcher;不会为每个 Bot 复制源码或 dist/。默认 launcher 位于 $HOME/.gori-agent/bin/gori-agent。

确认 ACP agent 可用:

kimi --version
kimi doctor
kimi acp --help

模型和 provider 由 ACP agent 自身配置,例如 Kimi Code 使用 ~/.kimi-code/config.toml。gori-agent 不复制模型凭据。

实例管理

gori-agent init <bot-id>
gori-agent setup <bot-id>
gori-agent doctor <bot-id>
gori-agent start <bot-id>
gori-agent status <bot-id>
gori-agent logs <bot-id>
gori-agent restart <bot-id>
gori-agent stop <bot-id>
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 改变统一根目录:

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 作为初始化种子;其他命令只按 <bot-id> 加载统一实例目录中的 config.json,缺失即失败。

writeConfigFile() 使用同目录临时文件、fsync 和原子 rename,并强制最终文件为 0600。

Config v3

完整说明见 config.example.json。顶层只有:

configVersion  固定为 3
bot            Bot、workspace、persona、assistantPersona、agent、skills、permissions
gateway        HTTP server、入站 policy、唯一 platform
runtime.acp    ACP state、timeout 和 worker pool

Bot 与 ACP agent

{
  "bot": {
    "id": "my-bot",
    "workspace": "/absolute/workspace",
    "persona": "Describe responsibilities, boundaries, and confirmation points.",
    "assistantPersona": "Optional speaking personality for the Assistant; falls back to persona when empty.",
    "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 必须是绝对路径。
  • Worker bootstrap 始终使用 bot.persona;Assistant 使用 bot.assistantPersona,为空时回退到 bot.persona。assistantPersona 只决定 Assistant 的讲话人格,可让运维 Bot 的 Assistant 说人话而 Worker 保持严格。
  • 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 示例:

{
  "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:<group_openid>;用户 ID 按 QQ 官方语义取值:群聊作者用 member_openid,C2C 私聊作者用 user_openid。

QQ 入站附件:image/* 附件会被下载(每条消息最多 3 张、单张超过 5MB 跳过、10 秒下载超时、缺 scheme 的 URL 自动补 https:)并转成 base64 随消息传给 Agent;agent 声明 promptCapabilities.image 时作为 ACP image content block 发送,否则降级为文本说明。视频/文件等非图片附件不下载,仅以 [视频] <url> / [文件] <url> 文本拼进消息,交给模型自由使用;纯图片消息使用占位文本「(发来一张图片)」。日志只记录附件类型/大小/数量,不记录 URL 全文或 base64。

QQ 出站图片:Worker/Assistant 报告的 workspace 内图片(png/jpg、≤10MB、最多 3 张)经 POST /v2/{groups|users}/{id}/files 上传(file_type: 1、file_data base64、srv_send_msg: false)后以 msg_type: 7 + media.file_info 发送;群上传的 file_info 只能发群、私聊上传的只能发私聊,按目标分别上传。无主动消息权限(40034105),图片与文本一样只能走被动回复窗口,且与文本共用同一 msg_id + 递增 msg_seq 计数器;Worker 落定事件先发文案再发图,窗口过期时按现有规则记录并下次入站补发,补发时按路径重读文件,文件不存在则降级为文本说明。图片上传/发送失败不阻断文本,降级为文本说明 + 安全日志。其他平台 adapter 不支持图片时把图片降级为 [图片] <文件名> 文本行。

ACP 生命周期

默认 runtime.acp.promptTimeoutMs 是 14400000(4 小时),适合长构建/部署任务。其他默认值:

  • initialize:10 秒
  • cancel grace:5 秒
  • idle assistant:30 分钟
  • assistant session reset idle:1 小时(assistantSessionResetIdleMs,0 禁用;Assistant 会话空闲超过该阈值且 owner 没有未完成 Proposal 时,下一条消息开新 session 而不是 resume)
  • 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_V2 envelope 结尾(reply + actions),action 只有 create_proposal、confirm、adjust_proposal、follow_up、finish、send_image、start_next、cancel、stop;格式错误只修复一次(envelope 缺闭合标签时先按花括号配平 salvage,失败才修复)。send_image { path } 把 Worker 报告过的 workspace 内图片发给用户,与 Worker 附件同样的路径/类型/大小校验。
  • Proposal:一份工作单,owner 是 chat + 发起用户;confirm / adjust / follow_up / start_next 仍只有发起人本人可操作(runtime 强校验),但为了避免单人把单 worker 队列卡死,finish / stop / cancel 是全板共享动作:任何用户都可对可见 proposal 执行它们。Proposal 板全局共享可见:Assistant prompt 的全板列表包含所有 chat/user 的未完成条目(自己的标 scope=own,他人的标 scope=other 并附 chat 类型,不暴露 openid 明文),Assistant 可如实向任何用户描述全板状态。状态流为 proposed → queued → working → pending → finished。proposed 只有用户确认后才进入 queued;proposal 一旦进入 working,worker 默认连续执行普通低风险步骤,不再逐步回问,只有碰到高风险动作、关键业务选择、外部阻塞或 dirty/异常目标时才返回 pending question;pending 就是「等用户决定」,不再区分 success/failure;只有 finish 把 pending 落定为 finished(done),cancel 落定为 finished(cancelled)。
  • Worker:同一时刻全实例只有一个,在 bot.workspace 用 bot.permissions policy 执行一个已确认 Proposal。每轮必须以隐藏 GORI_WORKER_RESULT_V2 envelope 收尾:PENDING(summary 必填,可带 question、workspaceDirty),不区分成功/失败,只把结果交给用户。Worker 给用户看的图片(png/jpg)必须保存在 workspace 内(建议 .gori-outbox/),并通过 attachments: [{ path, mimeType? }](最多 3 个)上报;runtime 校验路径必须在 workspace 内、magic bytes 为 png/jpg、单张 ≤10MB,违规的丢弃并在事件文本里说明。

确认语义是刻意的:

  • Worker 落定后 Proposal 进入 pending,记录 summary、可选 question 和 workspaceDirty;用户说 finish(可带 note)落定为 finished(done),或直接继续说要求:Assistant 发 follow_up,runtime 优先 resume 原 worker native session 继续(resume 失败则带 Proposal 上下文新起 session),用户输入的图片附件也随 prompt 给 Worker。
  • 系统不会在一个 Proposal 落定后自动开始下一个;只有用户确认新 Proposal 或 Assistant 发出 start_next 才会推进队列。
  • 阻塞规则:任何 working 全局拒绝 start_next/follow_up;owner 自己有 pending 时拒绝该 owner 的 start_next(提示先 finish 或 follow_up);任何 workspaceDirty 的 pending 全局拒绝 start_next(提示先处理 dirty 工作区)。非 dirty 的 pending 不挡其他用户。
  • stop 只对 working 生效:停掉 Worker(含进程组清理)并把 Proposal 标记为 pending(summary 为「被用户中止」、workspaceDirty: true)。
  • Assistant 有 pending 时每轮 prompt 注入 pending card(id/title/summary/question);Assistant 回复没提到 pending 时,runtime 在回复末尾追加一条人话兜底提醒。
  • Worker working 期间,runtime 把 session/update 的工具活动按类别(read/search/write/execute/delegate/other)聚合成实时快照(不含工具参数、输出或正文);下一轮 Assistant prompt 注入紧凑状态卡(title、运行时长、最后活动类别、各类别计数),Assistant 据此用人话描述进度。调试期状态卡对所有人可见(他人任务会标注 another user's 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 后清理旧进程组,并标记为 pending(worker_lost、workspaceDirty: true),交给用户 finish 或 follow_up。

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 2)保存 Proposal 记录:title/goal/steps、owner chat、发起用户、状态(proposed | queued | working | pending | finished)、pending(summary/question?/workspaceDirty?/receivedAt)、finished 的 finishKind(done | cancelled)/finishNote?、worker native session ID 与进程组 PGID/token、时间戳;不保存消息正文。两个 store 都做单 writer lock、串行持久化、临时文件 + fsync + 原子 rename;version 或 identity 不匹配(含旧 state v1/v2)一律拒绝启动并保留原文件,不清空。唯一例外是 proposals v1:打开时先在同目录写 proposals.json.v1-<timestamp>.bak(0600)备份,再按固定映射迁移为 v2(SUCCESS/FAILED/NEEDS_CONFIRMATION → pending,completed/failed → finished(done),cancelled → finished(cancelled))。

Bot fingerprint 包含 bootstrap schema version、Bot ID、workspace、persona、assistantPersona、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 命令

/help
/status
/list
/confirm
/finish
/stop
/cancel
  • /help:显示自然语言使用说明和兜底命令列表。
  • /status:显示固定 Bot、agent、workspace、Assistant 会话数、各状态 Proposal 计数、Worker 是否在执行及当前用户是否 owner;另含当前用户维度(myQueuedProposals、myPendingProposals、schedulerState、blockedReason、nextAction,他人 Proposal 只给脱敏原因,不暴露 title/id)与当前 chat 的事件投递状态(lastEventDelivery、lastEventError、lastEventAt)。
  • /list:全局 Proposal 板面板,按 待确认(pending 优先)→ 进行中 → 排队中 → 未确认(proposed)→ 最近结束 排列;自己的条目标注「你的」,他人的标注「其他成员」(不暴露 chat/user ID);最近结束也全局显示。
  • /confirm:确认当前用户最近待确认的 proposed Proposal(进入队列)。
  • /finish:把当前用户最近的 pending Proposal 落定为 finished(done)。
  • /stop:停止当前用户正在执行的 Worker,Proposal 转为 pending(被用户中止、dirty);只有 Proposal 发起人可用。
  • /cancel:取消当前用户最近的 proposed / queued / pending Proposal,落定为 finished(cancelled)。

日常操作以自然语言为主,Assistant 会自己生成 confirm/follow_up/finish/cancel/stop 等 action;这些命令是旁路 Assistant 的兜底入口。未识别的 / 命令按普通消息交给 Assistant。

Gateway 不维护普通消息队列或 per-chat task lock,也不再有固定时间(15/60/180/480 秒)的处理中提醒;每条 allowlist 普通消息按 conversation(chat + user)串行交给 AssistantManager。Worker 落定结果(成功、失败或提问)时,AssistantManager 生成事件文案并经 Gateway.sendEvent 发出。QQ 无主动消息权限(HTTP 400 / code 40034105)时事件走被动回复窗口:sendEvent 引用该 chat 最近一次入站消息(msg_id + 递增 msg_seq),群聊窗口 5 分钟(按 4.5 分钟保守判定)、C2C 窗口 60 分钟(按 55 分钟保守判定);超过窗口或没有 messageId 时不发主动消息,记录为 skipped 并在该 chat 下次入站时补发。发送失败只记录不含消息正文或 provider 错误详情的安全日志(仅 HTTP status / QQ code),不影响任务或后续发送。

每条异步入站使用独立、串行的回复流,replySequence 从 1 动态递增;同步 webhook 直接返回 JSON 结果。

HTTP 端点

  • GET /health:configVersion、botId、platform 和 aggregate ACP counters,不暴露凭据/native session ID。
  • GET /platforms:当前单一 Bot/platform 概览。
  • POST /webhook/<selected-platform>:只存在所选平台路由;generic 使用 /webhook/generic。

Doctor 与安全纪律

gori-agent doctor <bot-id> 检查:

  • 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 和日志不得提交。

开发验收

项目级部署/配置 skill 位于 .kimi-code/skills/gori-agent-deploy/SKILL.md,覆盖代码发布、实例模型/人格配置、state 备份和 QQ 事件投递边界。

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 协议(V2)、Proposal 状态流与 pending 语义、proposals v1→v2 迁移、timeout/cancel、worker_lost 恢复、state v3 identity、workspace 重叠扫描、Gateway 无队列语义、QQ normalization 与图片附件、ACP prompt content blocks 和单平台 route。真实 Kimi ACP 的执行层兼容性可用 node scripts/side-session-spike.mjs 复验(assistant no-tool spike:私有 cwd 在项目外、mcpServers: []、toolUpdates/permissionRequests/fsRequests 全为 0、GORI_ASSISTANT_ACTION_V2 envelope 可解析);图片输入链路可用 node scripts/acp-image-capability-spike.mjs 与 node scripts/acp-image-e2e-spike.mjs 复验;这些脚本使用临时 cwd/profile,不读取或输出真实配置、凭据、日志正文。

src/agents/* 与 src/core/session-store.ts 仅为 legacy compatibility/reference,不在当前运行链路。运行时没有单轮 CLI fallback。