Files
memind/docs/studio-goose-permission-runbook.md
T
john 9b4a25799f Add smart ACK provider for WeChat MP replies
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>
2026-06-26 15:19:03 +08:00

12 KiB
Raw Blame History

Studio Goose 权限与 Session 排障 Runbook

目的:以后再遇到“库里有权限,但 Agent 在对话里说自己没有某能力”的问题,直接按这份 runbook 查,不要从头摸。

2026-06-22 已确认的稳定事实:

  • Studio 上 Goose 是双实例:18006 主、18007 备用/第二实例。
  • 105 不跑 Goose;105 只是无状态 H5 前端,代理回 Studio。
  • Studio 可直接 SSHssh john@58.38.22.103。不要为了查 Studio 再绕 105。
  • H5 会话权限不是只看数据库,还要看会话启动时实际下发给 Goose 的 extension_overrides

这次案例结论

用户:

  • username: wx_ul610et8
  • display_name:
  • user_id: a70ff537-8908-486e-9b6c-042e07cc25db

线上生产结论:

  • h5_capability_grants 里该用户有 aider=1
  • 角色默认 role:user 也有 aider=1
  • h5_user_policies 里该用户没有单独禁用项
  • 角色策略是:
    • workspace_access=readwrite
    • network_egress=allow
    • goose_mode=auto
    • api_lockdown=true

按当前代码实时计算 getAgentSessionPolicy(userId) 的结果:

  • capabilities.aider = true
  • developer 扩展应带 write/edit/shell/tree/read_image
  • extensionOverrides 里应出现 aider

因此:

  • 如果对话里 Agent 说“没有 aider / 没有 write_file / 没有 edit_file”,优先怀疑是 旧会话扩展未同步Goose 实例漂移子会话能力不一致,不是先怀疑 DB 权限没开。
  • 注意:aider grant 为 true 只表示会话可挂 aider 平台扩展;Goose 的 aider 执行器仍会按任务/目录做运行时保护。MindSpace 用户工作区默认禁止 aider 直接执行,除非显式打开 GOOSE_AIDER_ALLOW_MINDSPACE=1

2026-06-22 实际修复记录

用户贴出的异常话术对应 session:

  • agent_session_id: 20260620_45
  • title: 西湖跑步喝茶计划
  • 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_file
    • write_file
    • edit_file
    • create_dir
    • list_dir
    • private_data_info
    • private_data_schema
    • private_data_query
    • private_data_execute

这说明原问题已经定位并修复到具体旧会话:不是用户 DB 权限缺失,而是旧 Goose session 的扩展集没有包含当前策略要求的 sandbox-fs

同时确认:

  • aider__code 对 MindSpace 页面类任务返回 Selected engine: blocked 是运行时保护策略,不是这个用户没有 aider grant。
  • 相关保护在两处存在:
    • /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-fswrite_file/edit_file 或发布/App 工具,不应强制走 aider

2026-06-22 深入排查:其他用户影响面

排查口径:

  • 不要只看 Goose SQLite 的 sessions.extension_data,它是历史快照/扩展记忆结构,实时状态以 Goose API 为准。
  • 正确实时检查方式:按 h5_user_sessions.goosed_node 路由到 1800618007,调用 /sessions/{id}/extensions

最近 120 条 active 用户 session 的实时抽样结果:

  • 可访问 session120/120
  • sandbox-fs24/120
  • node 089 条,缺 21 条
  • node 131 条,缺 3 条
  • 缺失集中在 2026-06-18 到 2026-06-20 的旧会话;2026-06-21 到 2026-06-22 新会话抽样未见缺失。

受影响样本用户:

  • wx_ul610et8 / 唐3 条旧会话仍缺 sandbox-fs
  • wx_mk4zzaps / Mark 刘名章1 条旧会话缺 sandbox-fs
  • wx_5dljrg1a / 1231 条旧会话缺 sandbox-fs
  • john / John:若干旧测试/管理会话缺 sandbox-fs
  • admin / 管理员:若干管理会话缺 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/6
  • node --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 改动引入。

代码链路

关键代码:

关键判断:

  • workspace_access=readonly 会强制关闭:
    • shell
    • filesystem
    • static_publish
    • aider
  • POST /agent/start 会实时计算 getAgentSessionPolicy() 并下发 extension_overrides
  • POST /agent/resume 恢复旧会话时,必须结合 session 所在 Goose 实例继续看,不要只看数据库

架构事实

Studio H5 侧:

  • 运行中 Portal 的 TKMIND_API_TARGET 指向主 Goosehttps://127.0.0.1:18006
  • 运行中 Portal 的 TKMIND_API_TARGET_1 指向第二 Goosehttps://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_node
    • 0 表示主 Goose
    • 1 表示第二 Goose
  • tkmind-proxy.resolveTarget(sessionId) 会按 goosed_node 把 session 路由回对应 Goose

标准排查顺序

1. 先查用户真实权限,不要先猜

h5_capability_grants

  • 用户级覆盖
  • role:user 默认值

h5_user_policies

  • 用户级覆盖
  • role:user 默认值

重点看:

  • aider
  • filesystem
  • shell
  • code_browse
  • static_publish
  • workspace_access

2. 再让线上代码实时算一次 session policy

不要只看表。

必须直接调用:

  • getAgentSessionPolicy(userId)

重点看结果里的:

  • capabilities
  • policies
  • extensionOverrides
  • 是否真的有 aider
  • developer.available_tools 是否真的包含 write/edit

3. 再看 session 落到哪个 Goose 实例

查:

  • h5_user_sessions.agent_session_id
  • h5_user_sessions.goosed_node

如果同一用户的不同 session 分布在 01 两个节点:

  • 说明必须分别检查 1800618007
  • 不能只看一个 Goose 的状态就下结论

4. 如果用户说“这条对话里没有能力”

优先排查:

  1. 这条对话是否是旧 session
  2. 这条对话是否落在第二 Goose
  3. 两个 Goose 的扩展配置是否漂移
  4. 是否进入了能力被收窄的子会话

这次已确认的反常点

  • 线上 DB 与 getAgentSessionPolicy() 都显示:wx_ul610et8 应有 aiderwrite/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 -> 18006
  • 1 -> 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
  • 如果文档还没补全第二 Goose 的运行环境注入方式,优先继续补这里,不要让同样的排查再来一遍。