扩展代码从哪里加载,信任边界在哪里
从 package/resource resolution、untrusted bootstrap、project_trust 决策到 jiti import,说明 Pi 的扩展来源、加载时执行和项目 trust 的准确保护范围。
Markdown 资源最终影响 prompt,extension 则在加载阶段直接执行代码。loadExtensionModule() 用 jiti import TypeScript/JavaScript,确认 default export 是 function,然后创建 ExtensionAPI 并 await factory(api)。factory 可以注册 tool、event、provider 和 renderer,也可以执行普通 Node 代码。Pi 必须在 import 项目 extension 之前做 trust 决定,否则“先加载再询问是否信任”已经太晚。
Bootstrap 集合为什么也包含 extension
启动层先判断 cwd 是否存在需要 trust 的项目资源。范围包括 .pi 下指定项目配置/资源,以及 cwd 或祖先的 .agents/skills;用户 home 下的 ~/.agents/skills 明确按 user resource 排除。没有这些资源时,Pi 可以把项目视为无需额外询问,而不是所有目录都弹 trust 对话框。
需要决策时,DefaultResourceLoader.loadProjectTrustExtensions() 主动把 SettingsManager 设为 untrusted 并 reload。项目 settings 因此读成空对象,项目 package/resource 不进入 bootstrap;用户级资源、临时 CLI -e 路径和 inline factories 仍可加载。这样可信的全局 extension 可以订阅 project_trust,例如企业策略 extension 可以先给出决定。
这条顺序可以画成两次不同范围的加载,不要画成“加载所有 extension 后过滤”:
flowchart TD
accTitle: Project trust 前后的扩展集合
accDescr: bootstrap pass 在 untrusted settings 下只加载用户、CLI 与 inline extension;trust 决策后重载 settings,再把获准的项目 extension 补入同一 runtime。
DETECT["project resources detected"] --> UNTRUST["SettingsManager projectTrusted=false"]
UNTRUST --> PRE["pre-trust extensions: user + CLI + inline"]
PRE --> DECIDE["project_trust handler / store / default / UI"]
DECIDE -->|"trusted"| FINAL["reload + include project resources"]
DECIDE -->|"untrusted"| LIMITED["reload without project resources"]
FINAL --> RUNNER["final ExtensionRunner"]
LIMITED --> RUNNER
loadFinalExtensionSet() 会复用 pre-trust 已成功加载的 extension,失败过的路径也不重复 import,只加载 final paths 中剩余的项。它保持 extensionPaths 的最终顺序,再把 inline extension 放到尾部,并沿用同一个 ExtensionRuntime。这样 trust handler 所在的 extension 不会无故执行两次 factory,注册的 runtime state 也能延续到最终 runner。
Trust 决定有明确的优先顺序
resolveProjectTrusted() 先看显式 override;若 cwd 没有 trust-requiring resource,直接允许。然后才询问 preloaded extension 的 project_trust handler。第一个返回 yes 或 no 的 handler 结束遍历,undecided 才继续;handler 抛错被记录,但不会让整个 trust 流程自动允许。
extension 没给结论时,Pi 读取 trust.json 的已保存决定,再看全局 defaultProjectTrust 的 always/never/ask。走到 ask 但当前 mode 没有 UI 时返回 false。print、JSON、RPC 的无人值守启动因此不会在 stdin 上假装弹框,也不会因为无法询问就默认加载项目代码。
下面的静态实验能核对非交互 fallback 和 factory 执行点,不会实际 import 任何项目 extension:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" show v0.83.0:packages/coding-agent/src/core/project-trust.ts \
| sed -n '46,95p'
git -C "$repo" grep -n 'await factory(api)' v0.83.0 -- \
packages/coding-agent/src/core/extensions/loader.ts
若要做真实 trust 实验,必须使用一次性 HOME、agentDir 与 cwd,且 extension 内容由你控制。直接在工作项目运行会读取并可能执行现有用户 extension,这超出“验证发现顺序”的必要范围。
--no-extensions 也保留显式路径
ResourceLoader 计算 final extensionPaths 时,noExtensions 为 true 仍保留 CLI enabled extensions,只排除 settings/auto discovered 集合。语义是“关闭 discovery”,不是“拒绝所有 extension 代码”。如果调用命令同时给了 --no-extensions -e ./probe.ts,显式 probe 仍会加载。安全审计不能看到前一个 flag 就停止检查 argv。
加载冲突也不会卸载后来的 extension。ResourceLoader 检测重复 tool 与 flag,将其追加为 diagnostics,并保留所有 extension;真正 precedence 由后续 registry 的 load order 决定。这种做法便于展示冲突来源,但“启动出现 error diagnostic”不必然表示冲突 extension 没有执行 factory。
Trust 不是沙箱
项目 trust 的承诺很具体:未信任时不读取项目 settings,不访问项目 package storage,不把项目 extension 加入 final set。它没有给用户级、CLI 或已信任项目 extension 创建权限隔离。factory 和 handler 仍在 Pi 进程里运行,继承启动用户能访问的文件、网络与环境;ExtensionAPI.exec() 也会委托普通命令执行。
另外,factory 执行时 runtime action 尚未绑定到 AgentSession。createExtensionRuntime() 对 sendMessage、session mutation、active tools 等 action 安装 throwing stub;provider registration 是特例,会先排进 pending queue。扩展作者若在顶层 factory 调运行期 action,会得到“runtime not initialized”,应把它放进 session_start、command 或 tool handler。
到这里,extension 已经被发现、执行 factory,并留下 handlers、tools、renderers 与 pending providers。它们还没有自己驱动 Agent loop。第 36 章继续看 ExtensionRunner:谁把 agent event 投影给 handler,哪些返回值按顺序变换,哪些事件会短路,handler 失败又会不会终止本轮。