Skip to content

fix(shared): tolerate missing git-info extension instead of failing child-session load #338

Description

@liwen-aaa

问题(Problem)

extensions/shared/child-session.ts:177-179模块顶层裸调realpathSync.native 计算常量:

const CHILD_DISABLED_OPENPI_EXTENSION =
  "-extensions/git-info/index.ts" as const;
const OPENPI_GIT_INFO_EXTENSION_PATH = realpathSync.native(   // 无 try/catch
  fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
);

realpathSync.native 对不存在的路径抛 ENOENT。因为这是在模块顶层执行(不在任何函数内),
一旦 extensions/git-info/index.ts 缺失,整个模块加载失败。

该模块导出的是子会话核心能力:CHILD_SAFE_PACKAGE_TOOL_NAMES、CHILD_EXCLUDED_TOOL_NAMES、
createChildResources、bindChildSessionExtensions、childToolPolicy、
shutdownAndDisposeChildSession。它被 7 个文件 import(subagents/index.ts、
subagents/src/agent-types.ts、subagents/src/backends/pi.ts、subagents/src/prompt.ts、
workflows/index.ts、workflows/replay-safety.ts、workflows/runner.ts)。

所以一个文件的缺失,会把一个纯展示性的小扩展,放大成所有 Direct Subagent 和 Workflow 子会话的
完全瘫痪,且没有任何降级路径。

复现(已实测)

  mv extensions/git-info/index.ts /tmp/   # 模拟 git-info 缺失
  node --experimental-strip-types -e "await import('./extensions/shared/child-session.ts')"
  # ❌ ENOENT: no such file or directory, realpath '.../extensions/git-info/index.ts'

为什么值得修(尽管触发条件边缘)

正常 npm 安装是原子的,生产环境很少触发。但两条现实路径会踩到:

  1. Fork 裁剪——仓库 README 明确把 OpenPI 定位为可裁剪的东西("don't recommend hard
    copying it")。删掉 git-info 这种纯展示扩展是合理的裁剪;它应该让你失去 git 信息,
    而不是失去整个编排层。
  2. 打包漂移——package.json 的 files 决定发布内容。将来某次重构移动/改名 git-info
    而没有同步这行,发出去的包在用户机器上就是"git-info 缺失"——升级后才发现所有子会话
    加载即死。

讽刺的是,同一文件往下 4 行就是正确写法(canonicalExistingPath 用 try/catch 包住
realpathSync.native 并返回 undefined);顶层常量只是没用它。

建议修复

把路径改为惰性 + 容错——在 excludeOpenPiGitInfoExtension 内部用已有的
canonicalExistingPath 计算,文件缺失时降级为"不排除 git-info"(fail-open 到多带一个
无害扩展),而不是让模块加载失败:

  function excludeOpenPiGitInfoExtension(
    resources: LoadExtensionsResult,
  ): LoadExtensionsResult {
    const gitInfoPath = canonicalExistingPath(
      fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
    );
    if (gitInfoPath === undefined) return resources; // git-info 缺失;无需排除
    return {
      ...resources,
      extensions: resources.extensions.filter(
        (extension) =>
          canonicalExistingPath(extension.resolvedPath) !== gitInfoPath,
      ),
    };
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions