docs: add detailed answers for 21 learning roadmap questions

This commit is contained in:
2026-05-13 03:40:13 +00:00
parent a8d90d2980
commit c4753da8f5
10 changed files with 1308 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
# Q7-Q9: 权限与安全
---
## Q7: 用户身份怎么确定owner / admin / member 三级权限检查在哪里完成?
### 答案
**用户身份确定(两步):**
`src/modules/permissions/index.ts:67-103``extractAndUpsertUser` 函数:
1. **解析原始 handle** 从入站消息 JSON payload 的三处位置按优先级查找:`senderId``sender``author.userId`chat-sdk-bridge 的嵌套格式)。三处都没有返回 `null`
2. **命名空间标准化:** 如果原始 handle 已含 `:` 前缀(如 `slack:U0ABC`),直接使用;否则前缀 `channelType:` → 全局唯一 `userId`
3. **Upsert** `users` 表中不存在则创建(`db/users.ts:13-22``kind` 记录 channel 类型,`display_name` 从消息中提取
这个 resolver 通过 `setSenderResolver(extractAndUpsertUser)` 注册到 router`index.ts:171`),在 agent 解析**之前**执行(`router.ts:252`),确保即使后续被拒绝访问,`users` 行也已存在。
**三级权限表:**
| 表 | 用途 | 关键字段 |
|-----|------|---------|
| `users` | 用户身份 | `id`命名空间化的userId`kind``display_name` |
| `user_roles` | 特权角色 | `user_id``role`owner/admin`agent_group_id`NULL=全局) |
| `agent_group_members` | 非特权成员 | `user_id``agent_group_id` |
**`canAccessAgentGroup` 完整逻辑:**
`src/modules/permissions/access.ts:21-28` — 五步短路求值:
```typescript
function canAccessAgentGroup(userId: string, agentGroupId: string): AccessDecision {
if (!getUser(userId)) return { allowed: false, reason: 'unknown_user' }; // Gate 0
if (isOwner(userId)) return { allowed: true, reason: 'owner' }; // Gate 1
if (isGlobalAdmin(userId)) return { allowed: true, reason: 'global_admin' };// Gate 2
if (isAdminOfAgentGroup(userId, agentGroupId)) return { allowed: true, reason: 'admin_of_group' };// Gate 3
if (isMember(userId, agentGroupId)) return { allowed: true, reason: 'member' };// Gate 4
return { allowed: false, reason: 'not_member' };
}
```
**查库函数**`db/user-roles.ts`
- `isOwner``role='owner' AND agent_group_id IS NULL`line 36-41—— owner 必须全局
- `isGlobalAdmin``role='admin' AND agent_group_id IS NULL`line 43-48
- `isAdminOfAgentGroup``role='admin' AND agent_group_id=<id>`line 50-55
**`isMember` 的隐式 admin 语义**`db/agent-group-members.ts:28-36`
先检查 `isOwner || isGlobalAdmin || isAdminOfAgentGroup` 再查 `agent_group_members` 表——owner 和 admin **自动成为 member**,不需要额外插入行。
---
## Q8: 陌生人在群里 @bot系统怎么决定忽略、审批、还是直接响应
### 答案
**`unknown_sender_policy` 三态**`src/types.ts:31`,默认 `'strict'`
| 值 | 行为 |
|-----|------|
| `public` | 完全跳过访问检查。`router.ts` access gate 在 `index.ts:175` 直接返回 `{ allowed: true }` |
| `strict` | 非 member 的消息静默丢弃,记录到 `unregistered_senders`,不发送通知 |
| `request_approval` | 非 member 的消息被丢弃 + 发起一轮审批流程DM 一个 admin/owner展示 Approve/Deny 卡片 |
注意:即使 `unknown_sender_policy='public'``sender_scope='known'` 仍可作为更严格的叠加层(`router.ts:284` `senderScopeGate`)。
**完整决策链路**`src/router.ts` `routeInbound`
1. Message interceptor 先检查拦截器 hook 是否消费了该消息
2. Messaging group 解析 → sender resolver → agent fan-out
3. **Access gate**line 283就是 `canAccessAgentGroup` —— 此处 `unknown_sender_policy` 生效
4. **Sender scope gate**line 284per-wiring 的 `sender_scope='known'` 额外检查
5. 如果 access gate 拒绝 → `handleUnknownSender`
### sender-approval 完整流程
**Phase 1: 触发与去重**`permissions/index.ts:113-169`
- 记录 `dropped_message`reason=`unknown_sender_request_approval`
- 调用 `requestSenderApproval`
- **去重门控**`sender-approval.ts:59-65`):检查 `pending_sender_approvals` 表中的 `UNIQUE(messaging_group_id, sender_identity)` —— 同一个人已有 pending 卡片时,后续消息静默丢弃
**Phase 2: 选择审批者**`sender-approval.ts:67-87`
- `pickApprover(agentGroupId)` → scoped admins → global admins → owners`primitive.ts:76-93`
- `pickApprovalDelivery``primitive.ts:103-119`):遍历审批者,调用 `ensureUserDm` 找到可 DM 的人,优先同 channel 类型
**Phase 3: 创建 pending + 发送卡片**
- 标题和内容:`"New sender / <name> wants to talk to your agent. Allow?"`
- `ask_question` 卡片,通过 `deliveryAdapter.deliver` 发到审批者 DM
**Phase 4: 审批响应**`index.ts:225-283`
**APPROVE**
1. **授权验证**line 234-244检查点击者是不是 `approver_user_id` **或**有 admin 权限——防止随机用户通过转发卡片自我授权
2. `addMember` 添加到 `agent_group_members`line 249-254
3. **先删除 pending 行**line 264**再**重新调用 `routeInbound(event)`line 268——此时重试经过 access gateuser 已是 member
**DENY**
- 仅删除 `pending_sender_approvals`
- **不创建拒收列表**——同一发送者的新消息会重新触发新审批卡片
### 失败模式
- 没有 owner/admin → 消息永久丢弃
- 没有任何审批者可达(无 DM channel→ 同上
- 没有 delivery adapter → pending 行仍创建(可手动查看),但卡片不发送
- 卡片发送异常 → error log消息已丢弃
---
## Q9: Agent 在容器里能用 `ncl` 命令吗?能查其他 agent group 的数据吗?`cli_scope` 的三个值在哪里被检查?
### 答案
**`cli_scope` 的三个值**`container_configs` 表,迁移 015 添加,默认 `'group'`
| 值 | 含义 |
|-----|------|
| `disabled` | Agent 完全不知道 ncl 存在host 拒绝所有 CLI 请求 |
| `group`(默认) | Agent 只能操作自己的 group4个资源args 被自动绑定到自己 group无法查看其他 group 数据 |
| `global` | 无限制。仅通过 `init-first-agent` 脚本为 owner 的 agent group 设置 |
### 四层防御
**防御层 1CLAUDE.md 层面**`src/claude-md-compose.ts:82-90`
`cli_scope === 'disabled'` 时,`cli.instructions.md` 从 CLAUDE.md 的 fragment 列表中排除。Agent 学不到 ncl 命令的存在。
**防御层 2Host dispatch 核心强制**`src/cli/dispatch.ts`
- **disabled 检查**line 46-48所有 CLI 请求立即拒绝
- **资源白名单**line 51-55`group` 只允许 `groups``sessions``destinations``members` 四种资源
- **参数强制绑定**line 60-72检查 `agent_group_id``group``--id` 参数,不匹配调用者的 group 则拒绝
- **特权升级阻止**line 74-77拒绝任何包含 `cli_scope``cli-scope` 参数的命令
- **自动填充**line 81-90主动填充 `agent_group_id``group``--id` 为调用者的 group ID——agent 不需要也不能指定
- **sessions-get 防 existence oracle**line 95-100如果传入 session UUID 属于其他 group返回 "not found" 而非 "forbidden"
- **Post-handler 过滤**line 150-173`list` 返回的 rows 按 `scopeField` 再次过滤,丢弃不属于调用者 group 的行
各资源的 `scopeField` 映射(`src/cli/resources/`
- `groups``'id'``groups.ts:41`
- `sessions``'agent_group_id'``sessions.ts:10`
- `destinations``'agent_group_id'``destinations.ts:11`
- `members``'agent_group_id'``members.ts:11`
**防御层 3Container 端**`container/agent-runner/src/cli/ncl.ts`
Container 内的 ncl **不做任何权限检查**——它是纯粹的 DB transport 客户端。将 CLI 请求写入 `outbound.db`,从 `inbound.db` poll 响应。权限检查全在 host 端。Container 无法绕过,因为它只能访问 session DB不能直接连中央库或 socket server。
**防御层 4Command Gate斜杠命令**`src/command-gate.ts:23-63`
独立于 `cli_scope`,在消息写入 `messages_in` 之前运行(`router.ts:430-448`
- **Filtered commands**`/help`, `/login`, `/logout`, `/doctor`, `/config`, `/remote-control`)→ 静默丢弃永不进容器
- **Admin commands**`/clear`, `/compact`, `/context`, `/cost`, `/files`)→ 仅 owner / admin 可通过。否则直接写 `messages_out` 返回权限拒绝
- **降级安全**line 51`user_roles` 表不存在 → 所有 admin 命令放行
### 跨层防御矩阵总结
| 防御层 | disabled | group | global |
|--------|----------|-------|--------|
| CLAUDE.md 隐藏 ncl | ✅ 排除 | ❌ 包含 | ❌ 包含 |
| Host dispatch 拒绝所有 | ✅ 拒绝 | ❌ | ❌ |
| 资源白名单 | N/A | ✅ 4种 | ❌ 无限制 |
| 参数强制绑定 | N/A | ✅ 自动填+拒绝跨界 | ❌ |
| `cli_scope` 自我修改阻止 | N/A | ✅ 拒绝 | ❌ |
| Post-handler rows 过滤 | N/A | ✅ scopeField | ❌ |
| Sessions oracle 防护 | N/A | ✅ fail-closed | ❌ |
| Command gate 斜杠命令 | ✅ | ✅ | ✅ |
| Approval gating非host | N/A | ✅ | ✅ |
**关键 gotcha** group-scoped agent 可以通过 `ncl groups config get` 读到自己的完整 config包括 `cli_scope` 字段),所以知道自己在 `group` 范围内。但由于 `cli_scope` 参数被第 74-77 行硬阻止,不能修改自己的 scope。