Files
nanoclaw/docs/answers/03-permissions-and-security.md

167 lines
9.3 KiB
Markdown
Raw 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.

# 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。