Files
memind/docs/studio-goose-permission-runbook.md
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

305 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 的实时抽样结果:
- 可访问 session120/120
-`sandbox-fs`24/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 / 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 的运行环境注入方式,优先继续补这里,不要让同样的排查再来一遍。