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

9.3 KiB
Raw Blame History

Q7-Q9: 权限与安全


Q7: 用户身份怎么确定owner / admin / member 三级权限检查在哪里完成?

答案

用户身份确定(两步):

src/modules/permissions/index.ts:67-103extractAndUpsertUser 函数:

  1. 解析原始 handle 从入站消息 JSON payload 的三处位置按优先级查找:senderIdsenderauthor.userIdchat-sdk-bridge 的嵌套格式)。三处都没有返回 null
  2. 命名空间标准化: 如果原始 handle 已含 : 前缀(如 slack:U0ABC),直接使用;否则前缀 channelType: → 全局唯一 userId
  3. Upsert users 表中不存在则创建(db/users.ts:13-22kind 记录 channel 类型,display_name 从消息中提取

这个 resolver 通过 setSenderResolver(extractAndUpsertUser) 注册到 routerindex.ts:171),在 agent 解析之前执行(router.ts:252),确保即使后续被拒绝访问,users 行也已存在。

三级权限表:

用途 关键字段
users 用户身份 id命名空间化的userIdkinddisplay_name
user_roles 特权角色 user_idroleowner/adminagent_group_idNULL=全局)
agent_group_members 非特权成员 user_idagent_group_id

canAccessAgentGroup 完整逻辑:

src/modules/permissions/access.ts:21-28 — 五步短路求值:

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

  • isOwnerrole='owner' AND agent_group_id IS NULLline 36-41—— owner 必须全局
  • isGlobalAdminrole='admin' AND agent_group_id IS NULLline 43-48
  • isAdminOfAgentGrouprole='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 gateline 283就是 canAccessAgentGroup —— 此处 unknown_sender_policy 生效
  4. Sender scope gateline 284per-wiring 的 sender_scope='known' 额外检查
  5. 如果 access gate 拒绝 → handleUnknownSender

sender-approval 完整流程

Phase 1: 触发与去重permissions/index.ts:113-169

  • 记录 dropped_messagereason=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 → ownersprimitive.ts:76-93
  • pickApprovalDeliveryprimitive.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_membersline 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-55group 只允许 groupssessionsdestinationsmembers 四种资源
  • 参数强制绑定line 60-72检查 agent_group_idgroup--id 参数,不匹配调用者的 group 则拒绝
  • 特权升级阻止line 74-77拒绝任何包含 cli_scopecli-scope 参数的命令
  • 自动填充line 81-90主动填充 agent_group_idgroup--id 为调用者的 group ID——agent 不需要也不能指定
  • sessions-get 防 existence oracleline 95-100如果传入 session UUID 属于其他 group返回 "not found" 而非 "forbidden"
  • Post-handler 过滤line 150-173list 返回的 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 51user_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。