Happy 项目权限模式解析机制详解:App 端与 Claude CLI 的 State-Based 权限解析全流程
2026/9/20 9:33:52 网站建设 项目流程

Happy 项目权限模式解析机制详解:App 端与 Claude CLI 的 State-Based 权限解析全流程

【免费下载链接】happyMobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured项目地址: https://gitcode.com/gh_mirrors/happy20/happy

本篇技术指南围绕 happy 仓库中docs/permission-resolution.md的权限解析规范展开,系统讲解permissionMode(权限模式)在移动/Web 客户端(packages/happy-app)与 Claude CLI(packages/happy-cli)之间如何被解析、传递、映射与强制约束,包括会话状态合并、出站消息元数据、CLI 启动解析、逐消息更新与沙箱策略。读完本文,你将掌握 Happy 项目中从"用户选择权限模式"到"最终传给 Claude SDK 的权限模式"的完整状态机链路,以及沙箱会话为何始终强制bypassPermissions的底层原理。

一、范围与整体脉络

权限模式的解析发生在三个层面,原文档(docs/permission-resolution.md)将其明确划分为:

  • App 端状态解析:会话默认值、持久化值、出站消息元数据的生成;
  • Claude CLI 解析:启动模式、逐消息更新、沙箱策略;
  • 最终发送给 Claude SDK 的模式:由 CLI 在 SDK 边界完成映射。

这三层共同保证了"用户在 App 上选择的权限模式"与"Claude 进程实际执行的权限行为"始终一致,并且任何一端都无法通过消息元数据绕过沙箱约束。

二、权限模式全集与 Claude 映射规则

Happy 项目定义了一套共享的权限模式类型(共享于 App 与 CLI 之间):

default | acceptEdits | bypassPermissions | plan | read-only | safe-yolo | yolo

而从源码看,实际完整的PermissionMode联合类型还包含auto,共 8 种模式。这一全集定义在 permissionMode.ts 的VALID_PERMISSION_MODES常量中:

const VALID_PERMISSION_MODES: readonly PermissionMode[] = [ 'auto', 'default', 'acceptEdits', 'bypassPermissions', 'plan', 'read-only', 'safe-yolo', 'yolo', ] as const;

其中auto是 Agent SDK 自身PermissionMode联合类型中的一等公民模式,因此直接透传而非映射到default

Claude SDK 仅支持 4 种模式

Claude SDK 实际支持的权限模式为:default | acceptEdits | bypassPermissions | plan。因此 Happy 需要在 SDK 边界把其他模式映射到 Claude 兼容模式。这一映射只存在于一个地方:permissionMode.ts 的mapToClaudeMode

const codexToClaudeMap: Record<string, ClaudeSdkPermissionMode> = { 'yolo': 'bypassPermissions', 'safe-yolo': 'default', 'read-only': 'default', }; return codexToClaudeMap[mode] ?? (mode as ClaudeSdkPermissionMode);

映射规则:

Happy 模式Claude SDK 模式语义
yolobypassPermissions两者都是跳过全部权限询问
safe-yolodefaultClaude 无对应模式,退化为询问权限
read-onlydefaultClaude 不支持只读模式,退化为询问权限
auto/default/acceptEdits/bypassPermissions/plan原样透传Claude 原生支持的五个模式

一个值得注意的实现细节:mapToClaudeModeundefined也做了特判并返回undefined——注释明确说明 "Undefined is a meaningful value, not a missing one",即undefined表示"无覆盖",SDK 会读取 Claude 自身的配置,而不是被强制为询问模式。这避免了把所有未设置的会话都钉死在提示模式上(见 runClaude.ts 中对currentEnhancedMode的注释)。

未知模式的防护

normalizeRemotePermissionMode(permissionMode.ts)负责收窄从网络到达的模式:消息 schema 允许任意字符串(较新的 App 可能命名了当前 CLI 不认识的模式),未知模式会被丢弃并打一条警告日志,保证消息本身仍可投递、会话保持当前模式,而不是让整条消息失败。

三、App 端状态解析:四层优先级

1) 会话状态加载/合并时的解析

在 storage.ts 的applySessions中,App 端对session.permissionMode的解析遵循以下顺序:

  1. 内存中已存在的会话模式(非default);
  2. 本地存储中持久化的每会话模式(非default);
  3. 服务端会话 payload 中的模式(非default);
  4. 沙箱兜底:
    • session.metadata.sandbox.enabled === truebypassPermissions
    • 否则:default

源码中的核心是resolveModePick辅助函数(storage.ts),它不仅处理 permissionMode,还统一处理modelModeeffortLevel

const resolveModePick = (field: 'permissionMode' | 'modelMode' | 'effortLevel'): string | null => { const existing = state.sessions[session.id]?.[field] ?? null; if (isAgentModePushPending(session.id, field)) { return existing; // 乐观推送在途时,保留本地新值 } return session.metadata && session.metadata[field] !== undefined ? session.metadata[field] ?? null // 同步元数据优先(含显式 null = 重置) : existing; };

这段注释(对应 issue #1492)说明了设计动机:权限/模型/努力等级的选择通过会话元数据同步。元数据值(包括显式null表示重置)优先于本地镜像,唯一例外是当该字段的乐观推送仍在途时——此时入站事件携带的仍是旧元数据,若直接应用会把刚做的本地选择弹回去。

2) 新会话草稿兜底

在 persistence.ts 中,NewSessionDraft结构包含permissionMode: PermissionModeKey | null。如果草稿缺少权限模式,默认值为default

3) 新会话 UI 默认值

新会话向导(NewSessionWizard)的默认选择为default。若当前所选模式对当前选中的 Agent 无效,UI 会重置回上述 Agent 默认值。此外,modelModeOptions.ts中的permissionModeSupportedByCli(modelModeOptions.ts)会根据会话 CLI 的版本过滤掉该 CLI 无法解析的模式:例如auto模式从CLI_VERSION_WITH_AUTO版本才开始支持,旧 CLI 的会话选择器会隐藏auto,而保存的旧模式键(如旧会话里的auto、或应用于旧 CLI 的全局默认auto)绝不允许上线传输——因为旧 CLI 的 schema 会拒绝它并丢弃整条消息。测试 modelModeOptions.test.ts 验证了这一门控逻辑:

permissionModeSupportedByCli('auto', '1.2.1-beta.1') // false permissionModeSupportedByCli('auto', '1.2.1-beta.2') // true permissionModeSupportedByCli('plan', '1.2.0') // true

4) 出站消息模式解析

App 发送消息时,调用链为sync.tsresolveMessageModeMeta(messageMeta.ts)。关键逻辑:

  • session.permissionModedefault:原样发送;
  • 否则:
    • session.metadata.sandbox.enabled === true:发送bypassPermissions
    • 否则发送default

resolveMessageModeMeta对三种 Agent 风味做了差异化处理:

  • Rig(v1 元数据):从session.permissionModemetadata.currentOperatingModeCodemetadata.permissionModemetadata.session.permissionMode依次取第一个可用值;
  • Codex / Agy:总是以具体值发送——因为 Codex 会在 abort 时重置到启动模式,而 Agy 在 provider 边界独立映射 model + effort,若省略兜底值,执行结果可能与 UI 显示不一致;
  • Claude(及其他):会话有值用会话值,否则回落到agentDefaultOverrides.permissionMode

这里还有一个重要的拒绝而非替换设计:当会话或其保存的默认值携带了接收方 CLI 无法解析的模式时,resolveMessageModeMeta会抛出UnsupportedPermissionModeError(messageMeta.ts),调用方 sync.ts 会弹出错误提示并拒绝发送。注释的说明很直白:静默换成一个默认模式会悄悄改变 Agent 的权限边界——对 Claude 而言,这可能把用户选择的"reviewed Auto"升级成 yolo。测试 messageMeta.test.ts 覆盖了旧 CLI 上保存auto的各种拒绝场景,并断言错误消息中携带了模式名与 CLI 版本号。

出站消息的传输载体

最终解析出的模式随消息发送到两个位置(见 sync.ts):

  • 加密消息的meta.permissionMode
  • socket envelope 的permissionMode
meta: { sentFrom, ...(rigSendsMessageReceipts(session.metadata) ? { expectsAcceptance: true } : {}), appendSystemPrompt: systemPrompt, ...(modeMeta.permissionMode !== undefined ? { permissionMode: modeMeta.permissionMode } : {}), ...(modeMeta.model !== undefined ? { model: modeMeta.model } : {}), ... }

注意出站时使用了条件展开:只有permissionMode !== undefined才写入,避免无意义的空值污染消息。

四、Claude CLI 端解析:启动、逐消息与本地进程

1) 启动解析优先级

CLI 启动时的初始模式解析在 runClaude.ts 与 permissionMode.ts 中实现,优先级从高到低:

  1. --dangerously-skip-permissions(最高优先级)→bypassPermissions
  2. --permission-mode VALUE--permission-mode=VALUE
  3. 传入的options.permissionMode

extractPermissionModeFromClaudeArgs(permissionMode.ts)同时支持两种命令行写法:

--permission-mode plan // 空格分隔 --permission-mode=plan // 等号连接

随后应用沙箱策略applySandboxPermissionPolicy(permissionMode.ts):

  • 沙箱启用:强制bypassPermissions
  • 沙箱禁用:保留解析出的模式

runClaude中还会基于初始模式推导出dangerouslySkipPermissions标志,并写入会话元数据(runClaude.ts):

const dangerouslySkipPermissions = initialPermissionMode === 'bypassPermissions' || initialPermissionMode === 'yolo' || sandboxEnabled || Boolean(options.claudeArgs?.includes('--dangerously-skip-permissions'));

2) 远程流程中的逐消息更新

当用户消息携带meta.permissionMode时,CLI 调用resolveRemoteClaudePermissionMode(permissionMode.ts):

  • 沙箱启用:强制bypassPermissions
  • 沙箱禁用:使用入站模式

该函数还有一个防止"环境性降级"的保护逻辑:Happy App 的各个版本可能每条消息都发送permissionMode: "default",即使 CLI 进程是以 yolo/bypass 模式启动的。由于 Claude 在 SDK 边界把yolobypassPermissions都映射为 bypass,不能让这种环境性的default把当前模式降级,但仍允许plan等显式模式生效:

export function resolveRemoteClaudePermissionMode( currentMode: PermissionMode | undefined, incomingMode: PermissionMode | undefined, sandboxEnabled: boolean, ): PermissionMode | undefined { if (!incomingMode) { return currentMode; } const nextMode = applySandboxPermissionPolicy(incomingMode, sandboxEnabled); if (isClaudeBypassEquivalent(currentMode) && nextMode === 'default') { return currentMode; // 当前是 yolo/bypass,入站 default 不降级 } return nextMode; }

isClaudeBypassEquivalent定义于 permissionMode.ts:mode === 'bypassPermissions' || mode === 'yolo'。在 runClaude.ts 的消息循环中,这一函数与normalizeRemotePermissionMode组合使用,并记录了ignoredDefaultDowngrade以跟踪被忽略的降级。

3) 本地 Claude 进程

在 claudeLocal.ts 中,若沙箱启用,launcher 会在 spawn 前把--dangerously-skip-permissions追加进启动参数:

if (opts.sandboxConfig?.enabled) { ... if (!spawnArgs.includes('--dangerously-skip-permissions')) { spawnArgs = [...spawnArgs, '--dangerously-skip-permissions']; } const fullCommand = ['node', ...spawnArgs.map((arg) => quoteShellArg(arg))].join(' '); spawnCommand = await wrapCommand(fullCommand); spawnWithShell = true; logger.info(`[ClaudeLocal] Sandbox enabled: workspace=..., network=${opts.sandboxConfig.networkMode}`); }

同时注意沙箱初始化失败时的降级路径:捕获异常后cleanupSandbox = null; spawnCommand = null; spawnWithShell = false;并以不带--dangerously-skip-permissions的原始参数继续启动(claudeLocal.ts)。另外,Windows 平台不支持沙箱,会直接警告并继续(claudeLocal.ts)。

五、有效结果矩阵

综合 App 端与 CLI 端的两侧解析,最终生效模式可用矩阵概括:

沙箱启用

场景生效模式
会话模式为default或缺失App 兜底为bypassPermissions
用户消息携带任意模式CLI 沙箱策略强制为bypassPermissions
本地 Claude 进程launcher 追加--dangerously-skip-permissions

沙箱会话在全链路上都无法通过消息元数据重新启用权限询问——这正是权限模型稳定性的关键。

沙箱禁用

场景生效模式
App/会话模式非default使用该模式(如planacceptEdits
App/会话模式为default或缺失App 发送default,CLI 走正常模式解析(无沙箱强制)

六、稳定性设计:为什么这个方案现在很稳定

原文档在结尾点明了该设计的两个核心稳定性保证,结合源码可以进一步展开:

  1. 客户端兜底只在沙箱会话强制跳过权限:App 端的沙箱兜底逻辑(metadata.sandbox.enabled === truebypassPermissions)被收敛在 storage 合并与出站消息解析两处,非沙箱会话不会受到任何强制,用户选择的模式(包括planread-onlyacceptEdits)原样透传。

  2. CLI 沙箱策略保证沙箱化 Claude 会话无法通过消息元数据重新启用权限询问:三处实现形成闭环——启动解析时applySandboxPermissionPolicy强制 bypass、逐消息更新时resolveRemoteClaudePermissionMode再次强制、本地进程 spawn 时追加--dangerously-skip-permissions。即使 App 发送了default或恶意构造的元数据,CLI 也会在三个入口统一拦截。

  3. 版本兼容防护:App 端的permissionModeSupportedByCli(modelModeOptions.ts)与 CLI 端的normalizeRemotePermissionMode(permissionMode.ts)分别在发送端和接收端做了双重校验:发送端对旧 CLI 无法解析的模式拒绝发送而非静默替换UnsupportedPermissionModeError),接收端对未知模式丢弃字段而非丢弃消息。两个方向都遵循"宁可拒绝也不悄悄改变权限"的原则。

七、延伸阅读

  • 权限解析规范原文:docs/permission-resolution.md
  • CLI 端模式映射与解析工具:permissionMode.ts
  • CLI 启动与远程消息循环:runClaude.ts
  • 本地进程沙箱启动器:claudeLocal.ts
  • App 端会话状态合并:storage.ts
  • App 端出站消息模式解析:messageMeta.ts 及测试 messageMeta.test.ts
  • 模式选择器与 CLI 版本兼容门控:modelModeOptions.ts 及测试 modelModeOptions.test.ts
  • 消息发送调用链:sync.ts
  • 新会话草稿持久化:persistence.ts

【免费下载链接】happyMobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured项目地址: https://gitcode.com/gh_mirrors/happy20/happy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询