# 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_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-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-fs` - `wx_mk4zzaps / Mark 刘名章`:1 条旧会话缺 `sandbox-fs` - `wx_5dljrg1a / 123`:1 条旧会话缺 `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 改动引入。 ## 代码链路 关键代码: - [user-auth.mjs](../user-auth.mjs) - `resolveUserCapabilities()` - `resolveUserPolicies()` - `getAgentSessionPolicy()` - [policies.mjs](../policies.mjs) - `applyPoliciesToCapabilities()` - [capabilities.mjs](../capabilities.mjs) - `buildAgentExtensionPolicy()` - [tkmind-proxy.mjs](../tkmind-proxy.mjs) - `POST /agent/start` - `POST /agent/resume` - `resolveTarget(sessionId)` 关键判断: - `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` 指向主 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_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 分布在 `0` 和 `1` 两个节点: - 说明必须分别检查 `18006` 和 `18007` - 不能只看一个 Goose 的状态就下结论 ### 4. 如果用户说“这条对话里没有能力” 优先排查: 1. 这条对话是否是旧 session 2. 这条对话是否落在第二 Goose 3. 两个 Goose 的扩展配置是否漂移 4. 是否进入了能力被收窄的子会话 ## 这次已确认的反常点 - 线上 DB 与 `getAgentSessionPolicy()` 都显示:`wx_ul610et8` 应有 `aider` 和 `write/edit` - 但用户贴出的某条对话话术显示:Agent 自述没有 `write_file / edit_file` - 因为 Studio 是双 Goose,不是单 Goose,所以这类现象优先按“实例间配置漂移”处理 ## 下次直接复用的命令清单 ### 查 105 H5 当前指向的 Goose / RDS 优先查 Studio: ```bash 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: ```bash ssh -i ~/.ssh/id_ed25519 root@120.26.184.105 \ "sed -n '1,80p' /root/tkmind_go/ui/h5/.env" ``` ### 查生产用户权限 ```bash 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 ```bash 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 及节点分布 ```bash 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` 然后查扩展: ```bash 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 的运行环境注入方式,优先继续补这里,不要让同样的排查再来一遍。