# 19. 权限设计与权限穿透
# 19.1 四层安全概念不能混用
| 层次 | 问题 | 示例 |
|---|---|---|
| Authentication | 你是谁 | 用户、服务账号、Agent 身份 |
| Authorization | 你原则上能做什么 | 可读仓库、可调用某 MCP Tool |
| Approval | 本次高风险动作是否获准 | 允许这一次部署/命令 |
| Sandbox | 即使程序恶意,OS 实际允许什么 | 只能写 workspace、不能联网 |
权限判断是控制面,沙箱是执行面的强制边界。审批通过不代表应该关闭所有沙箱;某条命令可写一个目录,也不代表它能读取密钥目录。
# 19.2 策略模型与优先级
一个可解释的决策至少输入:
principal + agent lineage + tool + normalized arguments // 第一组决策输入描述调用者身份、Agent 父子来源关系、目标工具和明确化后的参数
+ resource + action + environment + policy version + risk signals // 第二组输入补充资源动作、执行环境、策略版本和实时风险信号
→ ALLOW | DENY | ASK // 策略引擎只输出允许、拒绝或需要审批,并附带原因与作用域
推荐优先级:强制组织 Deny > 安全 Hook/Guardrail Deny > 资源级 Deny > 显式 Ask > 精确 Allow > 模式默认值。Deny 规则不能被低优先级的 Session Allow 覆盖。每次决策返回结构化 reason、匹配规则来源和有效期,便于 UI 展示与审计。
# 19.3 “权限穿透”的正确含义
在 Agent 系统中,权限穿透通常指主 Agent 的身份或能力如何传到子 Agent、MCP、远程沙箱和下游 API。安全目标不是“无损透传全部权限”,而是可验证的最小权限委托:
childEffective = intersection( // 子 Agent 最终权限由多重约束求交集,而不是复制父权限
parentDelegationCeiling, // 父 Agent 的可委托上限决定子 Agent 绝不能超过的边界
taskRequiredCapabilities, // 当前任务声明的最小能力集合限制与任务无关的权限
childPolicy, // 子 Agent 自身角色策略可进一步拒绝特定工具或资源
orgPolicy, // 组织级强制规则提供不可由会话审批绕过的全局 Deny
environmentEnforcement // 当前沙箱或远程环境实际能够强制实施的能力上限
) // 交集结果即本次委托的有效能力集合,任何一层拒绝都保留
委托凭证应绑定 issuer/subject/audience/scope/resource/turn_id/task_id/expiry/nonce。子 Agent 不能把凭证转给任意第三方;MCP Server 只能收到属于该 Server 和 Tool 的 audience/scope。一次性审批不可自动升级为整个 Session、所有子 Agent或所有相似命令的永久 Allow。
典型风险:
- Confused deputy:低权限用户诱导高权限 Agent 代做操作。
- 权限放大:子 Agent 请求的权限超过父 Agent 委托上限。
- 作用域泄漏:项目 A 的 Memory、Token 或 MCP 凭证进入项目 B。
- 检查与执行之间发生变化(TOCTOU):审批时展示的命令或路径与真正执行时的对象不同。
- Symlink/路径逃逸:字符串前缀看似在 workspace,解析后指向外部。
因此审批必须绑定规范化后的参数哈希,执行前再校验;路径授权使用 canonical path、目录句柄或 OS sandbox,不只做字符串 startsWith。
# 19.4 Claude Code 源码映射
Claude Code 把权限结果建模为 allow/ask/deny,Rule 同时保留来源和可选内容范围;ToolPermissionContext 保存模式、额外工作目录、Allow/Deny/Ask 分层规则以及危险规则剥离状态:permissions.ts:44、permissions.ts:54、permissions.ts:427。Permission Decision 还携带 rule/mode/hook/subcommand/sandbox override 等原因:permissions.ts:171、permissions.ts:268。
工具执行严格经过 schema 校验、工具语义校验、PreToolUse Hook、权限决策,再执行;内部字段 _simulatedSedEdit 即使应被 strict schema 拒绝,仍额外剥离做纵深防御:toolExecution.ts:614、toolExecution.ts:682、toolExecution.ts:754、toolExecution.ts:916。
子 Agent 的权限并非简单复制:Agent 自带 permission mode 只有在父线程不是 bypassPermissions/acceptEdits/auto 时才覆盖;异步 Agent 默认避免弹窗;指定 allowedTools 时清除父 Session 级 Allow,仅保留 SDK CLI 级显式规则并加入当前 Agent 的 Session 规则:runAgent.ts:412、runAgent.ts:436、runAgent.ts:465。Fork 子 Agent 使用 bubble 模式把审批请求上浮到父终端:forkSubagent.ts:44。
# 19.5 Codex 源码映射
Codex 将每条命令的沙箱意图区分为默认、请求完全提升、在沙箱内请求附加权限;附加权限可分别描述网络和文件系统:models.rs:40、models.rs:216。PermissionProfile 则明确区分 Codex 管理、完全关闭和外部沙箱三种执行责任:models.rs:312。
审批运行时对 key 做 Session 缓存:只有所有 key 都已经 ApprovedForSession 才跳过询问;一次 Apply Patch 涉及多个文件时按每个 key 分别缓存:sandboxing.rs:41、sandboxing.rs:66。
更关键的是,Codex 对 requested 与 granted 的附加权限提供显式交集运算,同时保留约束性 Deny;只有审批所得范围落在请求范围内才接受:policy_transforms.rs:126。获准的附加项再合并到当前命令的有效 Profile,而不是永久改写全局基础 Profile:policy_transforms.rs:433、policy_transforms.rs:507。
对 denied-read 还有专门保护:若退出沙箱会丢失读拒绝规则,则即使请求 RequireEscalated 也不允许绕过沙箱:sandboxing.rs:250、sandboxing.rs:281。
# 19.6 授权与委托伪代码
function authorize(call, runtime): // 定义工具调用从参数消歧到最小权限沙箱执行的授权流程
normalized = normalizeAndResolve(call.arguments) // 规范化路径、域名和命令,解析符号链接以防规则匹配歧义
decision = policy.evaluate( // 使用显式上下文调用策略引擎,得到可解释授权结论
runtime.principal, // 传入真实用户或服务主体,明确权限最终责任归属
runtime.agentLineage, // 传入父子 Agent 的来源关系,检查委托层级是否超过上限
call.tool, // 传入目标工具身份,匹配工具级允许、询问或拒绝规则
normalized, // 传入规范化参数,确保策略判断与实际执行使用同一资源视图
runtime.environment, // 传入本地、远程或容器环境,应用环境特定约束
runtime.policyVersion) // 固定本次判断的策略版本,便于审计和一致性重放
if decision == DENY: return denied(reason) // 命中拒绝规则时立即终止,并返回可展示但不泄密的拒绝原因
if decision == ASK: // 只有策略明确要求询问时才暂停执行并向审批方请求授权
approval = requestApproval({ // 创建绑定具体调用内容、作用域和时效的审批请求
callHash: hash(call.tool, normalized), // 用工具与规范化参数哈希防止批准后偷偷替换命令或资源
scope: decision.requestedScope, // 展示本次额外需要的精确文件、网络或动作范围
expiresAt: now + shortTTL // 使用短有效期缩小审批被长期复用或泄露后的风险
}) // 完成审批请求对象并等待用户或上级策略返回结果
if !approval.accepted: return denied("user_rejected") // 用户拒绝时停止执行,不能通过重试绕过该选择
granted = intersect(decision.requestedScope, approval.grantedScope) // 只取请求范围与批准范围交集,禁止超批执行
else: // 策略直接允许时无需交互式审批
granted = decision.scope // 使用策略给出的有效作用域,仍受组织 Deny 和环境约束
capability = signCapability( // 签发不可篡改、短期且可验证的最小权限能力凭证
subject=runtime.agentId, // 将凭证绑定当前 Agent,防止被其他 Agent 横向复用
audience=call.toolServer, // 将凭证绑定目标工具服务,防止跨服务重放
scope=granted, // 凭证只携带经过策略与审批共同限制后的最小权限范围
taskId=runtime.taskId, // 绑定当前任务,阻止把一次授权挪给无关任务使用
expiry=shortTTL) // 设置短过期时间,降低凭证泄露和陈旧授权的影响
return sandbox.execute(call, effectiveProfile(runtime.baseProfile, capability)) // 把能力合并进基础 Profile 后在沙箱中执行
# 19.7 高频面试题
问:父 Agent 已获用户批准,子 Agent 能否直接复用?
只有当批准明确包含该子 Agent、同一规范化资源与动作、仍在有效期内,而且没有越过父委托上限时才可以。更稳妥的做法是给子 Agent 签发权限范围更小、有效期更短的 capability(能力凭证),而不是传递父 Agent 的完整 Token 或“已批准”布尔值。
问:Allow rule 与 Sandbox 的关系?
Allow rule 解决是否批准调用;Sandbox 决定调用后进程真正能访问什么。两者应同时存在,且 Sandbox 不应因普通 Allow 自动关闭。
# 19.8 原理深化:授权必须绑定规范化动作、规则来源与委托链
权限系统首先要解决“究竟在判断哪个动作”。模型参数中的相对路径、符号链接、Shell 复合命令、MCP 别名和重定向,必须先转换成稳定且无歧义的动作描述;审批界面、策略匹配器、沙箱配置生成器和审计系统都使用同一描述及哈希。若审批时展示原始字符串、执行阶段却重新解析,攻击者就可能利用工作目录、符号链接或 Shell 展开,使真正执行的对象与已检查对象不同(TOCTOU)。
Claude Code 的 Permission Decision 保留 rule、mode、Hook 和 subcommand 等来源,Codex 对 requested/granted additional permissions 做显式交集并保留约束 Deny,这说明 ALLOW/DENY/ASK 不能只是枚举值,还要携带“谁决定、适用哪些资源、持续多久、能否缓存”。Session approval cache 的 key 必须覆盖规范化资源;涉及多个文件时只有全部 key 获批才能复用,不能用一个全局 approved=true。
跨 Agent/MCP 委托时,权限只能逐级缩小:子主体有效权限等于父委托上限、任务需求、子策略、组织策略和环境可实施范围的交集。凭证绑定 audience、subject、task、resource、expiry 与 nonce;下游只能进一步缩小,不能转授完整父 Token。权限 Profile 由外部环境负责 enforcement 时,Harness 仍要记录责任归属,不能把“外部沙箱”误写成“没有权限限制”。