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>
305 lines
12 KiB
Markdown
305 lines
12 KiB
Markdown
# 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 的运行环境注入方式,优先继续补这里,不要让同样的排查再来一遍。
|