9b4a25799f
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
12 KiB
Studio Goose 权限与 Session 排障 Runbook
目的:以后再遇到“库里有权限,但 Agent 在对话里说自己没有某能力”的问题,直接按这份 runbook 查,不要从头摸。
2026-06-22 已确认的稳定事实:
- Studio 上 Goose 是双实例:
18006主、18007备用/第二实例。 - 105 不跑 Goose;105 只是无状态 H5 前端,代理回 Studio。
- Studio 可直接 SSH:
ssh john@58.38.22.103。不要为了查 Studio 再绕 105。 - H5 会话权限不是只看数据库,还要看会话启动时实际下发给 Goose 的
extension_overrides。
这次案例结论
用户:
username:wx_ul610et8display_name:唐user_id:a70ff537-8908-486e-9b6c-042e07cc25db
线上生产结论:
h5_capability_grants里该用户有aider=1- 角色默认
role:user也有aider=1 h5_user_policies里该用户没有单独禁用项- 角色策略是:
workspace_access=readwritenetwork_egress=allowgoose_mode=autoapi_lockdown=true
按当前代码实时计算 getAgentSessionPolicy(userId) 的结果:
capabilities.aider = truedeveloper扩展应带write/edit/shell/tree/read_imageextensionOverrides里应出现aider
因此:
- 如果对话里 Agent 说“没有
aider/ 没有write_file/ 没有edit_file”,优先怀疑是 旧会话扩展未同步、Goose 实例漂移 或 子会话能力不一致,不是先怀疑 DB 权限没开。 - 注意:
aidergrant 为 true 只表示会话可挂aider平台扩展;Goose 的aider执行器仍会按任务/目录做运行时保护。MindSpace 用户工作区默认禁止aider直接执行,除非显式打开GOOSE_AIDER_ALLOW_MINDSPACE=1。
2026-06-22 实际修复记录
用户贴出的异常话术对应 session:
agent_session_id:20260620_45title:西湖跑步喝茶计划goosed_node:0- 实例:Studio 主 Goose
18006
异常表现:
- Goose session 里有
aider扩展 developer.available_tools只有read_image- 缺少
sandbox-fs - 因此 Agent 看不到
write_file/edit_file
已执行修复:
- 用生产代码的
getAgentSessionPolicy(userId)重新计算该用户应有能力 - 对
20260620_45执行reconcileAgentSession(...) - 修复后
20260620_45已挂上sandbox-fs sandbox-fs.available_tools已包含:read_filewrite_fileedit_filecreate_dirlist_dirprivate_data_infoprivate_data_schemaprivate_data_queryprivate_data_execute
这说明原问题已经定位并修复到具体旧会话:不是用户 DB 权限缺失,而是旧 Goose session 的扩展集没有包含当前策略要求的 sandbox-fs。
同时确认:
aider__code对 MindSpace 页面类任务返回Selected engine: blocked是运行时保护策略,不是这个用户没有aidergrant。- 相关保护在两处存在:
/Users/john/Project/tkmind_go/crates/goose/src/agents/platform_extensions/aider.rs/Users/john/Project/tkmind_go/deploy/coding_router.sh
- 对 MindSpace 页面/HTML/游戏生成任务,应使用
sandbox-fs的write_file/edit_file或发布/App 工具,不应强制走aider。
2026-06-22 深入排查:其他用户影响面
排查口径:
- 不要只看 Goose SQLite 的
sessions.extension_data,它是历史快照/扩展记忆结构,实时状态以 Goose API 为准。 - 正确实时检查方式:按
h5_user_sessions.goosed_node路由到18006或18007,调用/sessions/{id}/extensions。
最近 120 条 active 用户 session 的实时抽样结果:
- 可访问 session:120/120
- 缺
sandbox-fs:24/120 - node 0:89 条,缺 21 条
- node 1:31 条,缺 3 条
- 缺失集中在 2026-06-18 到 2026-06-20 的旧会话;2026-06-21 到 2026-06-22 新会话抽样未见缺失。
受影响样本用户:
wx_ul610et8 / 唐:3 条旧会话仍缺sandbox-fswx_mk4zzaps / Mark 刘名章:1 条旧会话缺sandbox-fswx_5dljrg1a / 123:1 条旧会话缺sandbox-fsjohn / John:若干旧测试/管理会话缺sandbox-fsadmin / 管理员:若干管理会话缺sandbox-fs
结论:
- 这不是“当前双 Goose 负载随机丢能力”的现象。
- 更像是
sandbox-fs上线/策略迁移前后创建的旧 session 没有自动补齐扩展。 - 双 Goose 会放大排查复杂度,因为每个 session 固定在自己的
goosed_node,必须去对应节点查实时 extensions;但只要start/resume/reply都走策略同步,就不会因为双 Goose 本身产生跨节点同步问题。
代码兜底:
/agent/start已同步策略。/agent/resume已同步策略。- 2026-06-22 新增本地修复:
/sessions/:id/reply前也调用reconcileSessionPolicyForUser(...)。 - 这样旧会话即使没有显式经过前端
resumeSession(),在下一次发消息前也会先补齐sandbox-fs等当前策略要求的扩展。
验证:
node --check server.mjs通过node --check tkmind-proxy.mjs通过node --test session-reconcile.test.mjs通过,6/6node --test policies.test.mjs capabilities.test.mjs user-memory-profile.test.mjs通过,38/38- 全量
npm test -- session-reconcile.test.mjs会被项目脚本展开成全量测试,目前失败在既有llm-providers.test.mjs断言:实际返回Aider 未启用模型绑定,不是本次 session policy sync 改动引入。
代码链路
关键代码:
- user-auth.mjs
resolveUserCapabilities()resolveUserPolicies()getAgentSessionPolicy()
- policies.mjs
applyPoliciesToCapabilities()
- capabilities.mjs
buildAgentExtensionPolicy()
- tkmind-proxy.mjs
POST /agent/startPOST /agent/resumeresolveTarget(sessionId)
关键判断:
workspace_access=readonly会强制关闭:shellfilesystemstatic_publishaider
POST /agent/start会实时计算getAgentSessionPolicy()并下发extension_overridesPOST /agent/resume恢复旧会话时,必须结合 session 所在 Goose 实例继续看,不要只看数据库
架构事实
Studio H5 侧:
- 运行中 Portal 的
TKMIND_API_TARGET指向主 Goose:https://127.0.0.1:18006 - 运行中 Portal 的
TKMIND_API_TARGET_1指向第二 Goose:https://127.0.0.1:18007 server.mjs会构造:API_TARGETS = [TKMIND_API_TARGET, TKMIND_API_TARGET_1]
- 105 或外部机器可以通过 Tailscale/内网目标代理到 Studio,但排查 Studio 时优先直接 SSH 到 Studio。
Session 路由:
h5_user_sessions.goosed_node0表示主 Goose1表示第二 Goose
tkmind-proxy.resolveTarget(sessionId)会按goosed_node把 session 路由回对应 Goose
标准排查顺序
1. 先查用户真实权限,不要先猜
查 h5_capability_grants:
- 用户级覆盖
role:user默认值
查 h5_user_policies:
- 用户级覆盖
role:user默认值
重点看:
aiderfilesystemshellcode_browsestatic_publishworkspace_access
2. 再让线上代码实时算一次 session policy
不要只看表。
必须直接调用:
getAgentSessionPolicy(userId)
重点看结果里的:
capabilitiespoliciesextensionOverrides- 是否真的有
aider developer.available_tools是否真的包含write/edit
3. 再看 session 落到哪个 Goose 实例
查:
h5_user_sessions.agent_session_idh5_user_sessions.goosed_node
如果同一用户的不同 session 分布在 0 和 1 两个节点:
- 说明必须分别检查
18006和18007 - 不能只看一个 Goose 的状态就下结论
4. 如果用户说“这条对话里没有能力”
优先排查:
- 这条对话是否是旧 session
- 这条对话是否落在第二 Goose
- 两个 Goose 的扩展配置是否漂移
- 是否进入了能力被收窄的子会话
这次已确认的反常点
- 线上 DB 与
getAgentSessionPolicy()都显示:wx_ul610et8应有aider和write/edit - 但用户贴出的某条对话话术显示:Agent 自述没有
write_file / edit_file - 因为 Studio 是双 Goose,不是单 Goose,所以这类现象优先按“实例间配置漂移”处理
下次直接复用的命令清单
查 105 H5 当前指向的 Goose / RDS
优先查 Studio:
ssh -o BatchMode=yes -o ConnectTimeout=8 john@58.38.22.103 \
"cd /Users/john/Project/Memind && ps eww -p \$(pgrep -f 'node .*server.mjs' | head -1) | tr ' ' '\n' | grep '^TKMIND_API_TARGET'"
如果确实要查 105:
ssh -i ~/.ssh/id_ed25519 root@120.26.184.105 \
"sed -n '1,80p' /root/tkmind_go/ui/h5/.env"
查生产用户权限
ssh john@58.38.22.103
cd /Users/john/Project/Memind
node -e 'process.loadEnvFile(".env"); const mysql=require("mysql2/promise"); (async()=>{ const pool=mysql.createPool(process.env.DATABASE_URL); const userId="a70ff537-8908-486e-9b6c-042e07cc25db"; const [userCaps]=await pool.query("SELECT capability_key, allowed FROM h5_capability_grants WHERE subject_type=\"user\" AND subject_id=? ORDER BY capability_key",[userId]); const [roleCaps]=await pool.query("SELECT capability_key, allowed FROM h5_capability_grants WHERE subject_type=\"role\" AND subject_id=\"user\" ORDER BY capability_key"); const [userPolicies]=await pool.query("SELECT policy_key, policy_value FROM h5_user_policies WHERE subject_type=\"user\" AND subject_id=? ORDER BY policy_key",[userId]); const [rolePolicies]=await pool.query("SELECT policy_key, policy_value FROM h5_user_policies WHERE subject_type=\"role\" AND subject_id=\"user\" ORDER BY policy_key"); console.log(JSON.stringify({userCaps,roleCaps,userPolicies,rolePolicies},null,2)); await pool.end(); })();'
让线上代码直接算最终 session policy
ssh john@58.38.22.103
cd /Users/john/Project/Memind
node --input-type=module -e 'import process from "node:process"; process.loadEnvFile(".env"); const { createDbPool } = await import("./db.mjs"); const { createUserAuth } = await import("./user-auth.mjs"); const pool = createDbPool(); const userAuth = createUserAuth(pool, { h5Root: process.cwd() }); const policy = await userAuth.getAgentSessionPolicy("a70ff537-8908-486e-9b6c-042e07cc25db"); console.log(JSON.stringify(policy, null, 2)); await pool.end();'
查用户最近 session 及节点分布
ssh john@58.38.22.103
cd /Users/john/Project/Memind
node -e 'process.loadEnvFile(".env"); const mysql=require("mysql2/promise"); (async()=>{ const pool=mysql.createPool(process.env.DATABASE_URL); const [rows]=await pool.query("SELECT agent_session_id, user_id, goosed_node, created_at FROM h5_user_sessions WHERE user_id = ? ORDER BY created_at DESC LIMIT 20", ["a70ff537-8908-486e-9b6c-042e07cc25db"]); console.log(JSON.stringify(rows,null,2)); await pool.end(); })();'
查某条 Goose session 实际扩展
先用 h5_user_sessions.goosed_node 判断端口:
0->180061->18007
然后查扩展:
ssh john@58.38.22.103 \
'curl -sk -H "X-Secret-Key: local-dev-secret" https://127.0.0.1:18006/sessions/20260620_45/extensions | python3 -m json.tool'
必记事项
- 以后再看到“Agent 说自己没有
aider/write_file/edit_file”,不要直接重新从数据库查起。 - 先看:
- 这条 session 在哪个
goosed_node - 那个 Goose 实例是不是
18006还是18007 - 那个实例实际拿到的
extension_overrides
- 这条 session 在哪个
- 如果文档还没补全第二 Goose 的运行环境注入方式,优先继续补这里,不要让同样的排查再来一遍。