Skip to content

feat: discover and select Pi extensions in provider settings - #853

Merged
wibus-wee merged 6 commits into
LodyAI:mainfrom
Astro-Han:feat/780-pi-extension-discovery
Sep 23, 2026
Merged

wibus-wee merged 6 commits into
LodyAI:mainfrom
Astro-Han:feat/780-pi-extension-discovery

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Related issue

Refs #780

Problem / pressure

The builtin Pi provider launches the managed adapter with --no-extensions, so globally installed Pi extensions (provider registration, packaged tools) cannot be used under Lody at all. #780 agreed on explicit opt-in loading: a read-only scan of installed extensions, candidates default-off, and selected paths forwarded with -e to both the main Pi process and native subagents.

The adapter half landed in LodyAI/acp-extension-pi#2. This PR is the host side: surface the discovered candidates in provider settings and carry selections through launch, gated on a runtime that declares the extension protocol.

Summary

  • machine/pi-extensions Machine RPC runs the managed runtime's read-only --list-extensions under a frozen default or saved-profile environment (persisted configId only, allowlisted env, bounded response schema, 180s client timeout covering runtime install plus scan).
  • The Pi provider dialog gains an extensions field: scanned candidates stay unchecked by default, existing selections stay checked, manual absolute/~ paths are accepted, and unsupported-runtime and scan-failure states render in place. The field appears only when the runtime manifest declares piExtensionsProtocolVersion.
  • Selections ride runtimeOverrides.piExtensions into launch args; the value is digested into the capability source version so changing selections invalidates cached capabilities. In the adapter, native subagents inherit the same explicit list via LODY_PI_EXTENSIONS.
  • Configurations carrying piExtensions take the real create/probe path instead of deferred provider setup, and non-Pi provider rows drop the field.
  • Bumps the adapter gitlink to the merged scan implementation, emits piExtensionsProtocolVersion in the packaged runtime manifest, and updates the builtin-Pi spec and feature note.

Visual explanation

Scanned candidates are default-off while an existing selection stays checked (fixture data — a real scan reads the target machine's Pi profile):

Pi extensions field after scanning

Narrow layout keeps the same controls usable:

Pi extensions field on mobile width

A runtime without the extension protocol disables scanning with an upgrade notice:

Unsupported managed runtime state

A failed scan reports the error in place and keeps manual entry available:

Scan failure state

Before / after

Before After
Builtin Pi always launches with all external discovery disabled; no way to load installed extensions Provider settings can scan globally installed extensions read-only; explicitly selected paths reach the main Pi process and native subagents via -e, while everything else stays disabled

Test plan

  • pnpm check (typecheck, lint, all package tests, i18n and boundary guards) — green.
  • pnpm run docs check — green.
  • Targeted suites covering this change: agent-config-dialog.test.tsx (36, including create mode submitting piExtensions without deferred setup), pi-extensions.test.ts, session-execution-service.test.ts, machine-rpc-server.test.ts, workspace-machine-rpc-facade.test.ts, local-machine-rpc.test.ts, machine-flock.test.ts, ai-capability-cache.test.ts, message-schemas.test.ts — all green.
  • Adapter evidence: acp-extension-pi#2 ran 52 unit tests plus source and installed-tarball smoke on Ubuntu, Windows x64, and Windows ARM64; CI green on the merged commit this gitlink points at.
  • Storybook stories added for scanned, unsupported, and scan-error states; screenshots above are from those stories.
  • Not covered here: end-to-end launch on a real Pi profile requires the extension-aware managed runtime artifact to be published and the manifest updated, which is a follow-up step; native subagent inheritance is exercised by the adapter's smoke suite.

Context handoff

Original user prompt

Show original prompt
那我们要不先做之前另外一个 feat 吧,就是 Pi 支持自定义插件和支持扫描的

Shared conversation

Status: shared
Link: https://share.lody.ai/s/r17b9dptdnarnzx6zvf2qt6qrh8ey346#access=v1.815782ca235dd9f8d0528894ec516bb8d27a50c26384e26e4fc94b62eee068d1

中文说明

关联:Refs #780(适配器半边已合并:LodyAI/acp-extension-pi#2

问题:内置 Pi 以 --no-extensions 启动,用户全局安装的扩展(provider 注册、工具包)完全无法使用。

改动

  • machine/pi-extensions RPC 在冻结的默认或已保存 profile 环境下调用托管运行时的只读 --list-extensions(仅接受已持久化 configId、env 白名单、有界 schema、180s 超时)。
  • Pi provider 对话框新增扩展区:扫描候选默认不勾选,已有选择保持勾选,支持手动输入路径,旧运行时与扫描失败有对应状态。仅当 manifest 声明 piExtensionsProtocolVersion 时渲染。
  • 选择经 runtimeOverrides.piExtensions 进入启动参数并计入能力缓存失效;适配器内子代理通过 LODY_PI_EXTENSIONS 继承同一列表。
  • 携带 piExtensions 的配置走真实 create/probe 而非延迟 providerSetup;非 Pi 行丢弃该字段。
  • 子模块 gitlink 指向已合并实现;打包脚本向 manifest 写入协议版本;spec 与 note 已更新。

验证pnpm checkdocs check 全绿;相关测试套件全过;适配器侧 smoke 三平台全绿。端到端验证待扩展感知运行时产物发布及 manifest 更新后进行。

Surface globally installed Pi extensions in the builtin Pi provider dialog
through a read-only scan of the pinned runtime. Candidates stay disabled
until explicitly selected, and selections reach both the main Pi process
and native subagents through the versioned piExtensions contract. The scan
runs under a frozen default or saved-profile environment so no
caller-supplied launch fields reach it, and the field renders only when
the managed runtime declares piExtensionsProtocolVersion.

Refs LodyAI#780

Model: devin-swe-2-max
The 150s default matched the daemon's worst case (120s ensure + 30s
scan), so a future caller relying on it would race the daemon's own
limits. The only caller already passes 180s.

Refs LodyAI#780

Model: devin-swe-2-max
Move the dialog's private collapsible section into form-primitives as
CollapsibleSection and adopt it for the extensions group: the selected
count stays visible while collapsed, Scan sits in the header action,
and the unsupported-runtime state uses the section's disabled hint.
Errors and scan warnings now use FormMessage; candidates render as the
standard divide-y list with a stale marker for selections missing from
the latest scan.

Refs LodyAI#780

Model: devin-swe-2-max
@Astro-Han
Astro-Han marked this pull request as draft September 22, 2026 10:13
A dispatch-turn inputConfig is caller-authored on the workspace RPC
transport, which cannot authorize a caller to choose a command for the
target daemon. customAcp and runtimeOverrides are launch inputs: they
must come only from the daemon-resolved persisted session launch config,
like env and worktreeSetup already do. Preferring the history entry's
copy let a dispatch caller inject e.g. runtimeOverrides.piExtensions and
bypass the explicit-selection boundary for Pi extensions.

Refs LodyAI#780

Model: devin-swe-2-max
Validate manually entered paths (absolute or ~/, bounded length) so a
rejected value cannot silently drop the whole provider row during daemon
normalization. Route the selection handler through
invalidateBuiltinVerification and clear the probe flag in the probe
effect cleanup so a mid-probe selection change cannot leave a stale
"Probing..." state.

Also remove review-flagged dead weight: the single-mount scan generation
counter (the key remount already discards stale scans), the single-use
options alias, and the facade's same-process re-parse of already
validated RPC results. The 32/4096 selection bounds now come from shared
constants in ai.ts instead of three separate literals.

Refs LodyAI#780

Model: devin-swe-2-max
@Astro-Han
Astro-Han force-pushed the feat/780-pi-extension-discovery branch from b5541c0 to 9cf4124 Compare September 22, 2026 10:34
@Astro-Han
Astro-Han marked this pull request as ready for review September 22, 2026 10:35
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Ready for another look @wibus-wee, whenever you have a moment. Rebased onto latest main, and a self-review pass turned up one real fix: a dispatched turn's inputConfig could carry caller-supplied customAcp/runtimeOverrides into launches, so launch fields now only come from the persisted provider config. Also tightened the extension path input validation and cleaned up a few dead spots the review flagged. Would love your eyes on it. Thanks!

@wibus-wee wibus-wee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is also an implementation gap discovered by my agent after communicating with me:

Missing paths and load failures must not silently succeed.

In the code, the "missing path" is achieved, but the "path exists, extension fails to load itself" is not.

Therefore, there are typical situations:

extension.ts exists

-e extension.ts

import / init throws exceptions

Pi extension_error

adapter stderr

ACP capability probe continues

The base model/configOptions are still normal

Provider Test = success

The result users see might be:

I clearly checked extension, and Test was successful, but extension didn't load at all.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may still be some issues. Regarding the dispatch injection you mentioned, I specifically went to check it out, and it seems there are still trust-boundary issues

Here, the same untrusted fields are still accepted.

Even if a custom ACP session is already in place, the attack input doesn't even need to change the agent identity

I think we can simplify this and stop keeping consumers constantly "remembering to ignore" one by one.

SessionTurnInputConfig derived from Partial<ACPSessionConfig>, which
conflated per-turn dispatch config with provider launch fields. The
normalizer is an explicit field allowlist, but the type and history
schema still admitted customAcp and runtimeOverrides, so a crafted
dispatch-turn inputConfig could inject them into the launch path.

Split ACPTurnConfig out of ACPSessionConfig at both the type and
schema layers: launch fields now extend the durable session config
only, per-turn input is closed by construction, and stored history
strips the keys while staying readable. The dispatch-watcher and
delivery-continuation reads they fed become structurally impossible;
no legitimate producer ever emitted them.

Model: swe-2-max
@Astro-Han

Astro-Han commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @wibus-wee for the close look at this. Both comments deserved the digging.

The inputConfig boundary is fixed at the normalization layer in c7d6002b. SessionTurnInputConfig was Partial<ACPSessionConfig>, which folded launch fields into per-turn input. I split ACPTurnConfig out at the type and schema layers, so launch fields now exist only on the durable session config, stored history strips them on read (legacy entries stay parseable), and the normalizer stops retaining them. The type change immediately caught a second consumer I had missed: the delivery-continuation path in operation-coordinator.ts was re-applying frozen.customAcp/frozen.runtimeOverrides from turn-derived input into the continuation launch, the same injectable channel on another path. Those reads are gone too. New test coverage asserts both the normalizer and the history schema drop the fields.

On silent extension load failures, I couldn't reproduce the silent-success path on the pinned runtime. On 0.85.1, load failures land in LoadExtensionsResult.errors, get merged into runtime.diagnostics as error-level entries in main.js, and the CLI prints them to stderr and exits(1) before the RPC loop starts, --mode rpc included. extension_error only fires for runtime hook errors after binding; load failures never emit it. So a bad -e gives: Pi writes Failed to load extension "<path>": <err> to stderr and exits, the adapter pipes that through and exits too, session/new fails, and the daemon's stderr-tail capture puts the named extension into the startup error. I ran it end-to-end with a broken -e: session/new rejects and stderr names the exact path. The adapter smoke suite already asserts this contract: a failing -e must make newSession reject and the adapter exit.

The gap that does exist is narrower: on new_session/switch_session/fork Pi recollects diagnostics but rpc-mode never reads runtime.diagnostics, so an extension that breaks mid-session (file deleted or corrupted after startup) could drop silently. That needs the file to change under a running session, which I'd call marginal but worth an upstream report. If you've seen a silent path I've missed, like a launch mode where the diagnostics gate doesn't run, I'm happy to look again. Otherwise I think the remaining work is the boundary fix above plus filing that upstream issue.

中文说明

感谢细致的评审,两条意见都值得认真挖。

inputConfig 边界已在归一化层修复(c7d6002b)。SessionTurnInputConfig 之前是 Partial<ACPSessionConfig>,把 launch 字段混进了 per-turn 输入。我在类型和 schema 两层把 ACPTurnConfig 拆了出来:launch 字段只留在持久化会话配置上,存储的历史记录在读取时剥离这两个字段(旧条目仍可解析),normalizer 不再保留它们。类型收窄还立刻抓到一个我此前漏掉的消费方:operation-coordinator.ts 的 delivery 续跑路径会把 frozen.customAcp/frozen.runtimeOverrides 从 turn 输入重新注入续跑启动参数——同一条可注入通道的第二条路径,现已一并删除。新增了回归测试,断言 normalizer 和历史 schema 都会丢弃这两个字段。

关于扩展加载失败静默成功:在锁定版本上我没能复现这条链路。0.85.1 里加载失败会进入 LoadExtensionsResult.errors,在 main.js 中被并入 error 级 runtime.diagnostics,CLI 会打印到 stderr 并在 RPC 命令循环启动前 process.exit(1)--mode rpc 也一样。extension_error 事件只在绑定后的运行时 hook 错误时触发,加载失败从不发这个事件。所以坏 -e 的实际行为是:Pi 往 stderr 写 Failed to load extension "<path>": <err> 然后退出,adapter 透传 stderr 后自己也退出,session/new 失败,daemon 的 stderr-tail 捕获会把带扩展名的那行放进启动错误里。我端到端跑过:用一个会抛错的 -e 拉起 adapter,session/new 拒绝且 stderr 点名了具体路径。adapter 的 smoke 套件已经覆盖了这个契约:失败的 -e 必须让 newSession 拒绝并让 adapter 退出。

真正存在的缺口更窄:在 new_session/switch_session/fork 这些会话替换路径上,Pi 会重新收集 diagnostics 但 rpc-mode 从不读 runtime.diagnostics,所以一个会话中途才坏掉的扩展(文件被删或损坏)可能静默丢失。这需要文件在运行中的会话下发生变化,我认为可达性很低,但值得给上游报个 issue。如果你见过我没覆盖到的静默路径(比如某个不走 diagnostics 闸门的启动模式),我很愿意再看一遍;否则我认为这里剩余的工作就是上面的边界修复,外加一个可选的上游 issue。

@wibus-wee wibus-wee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@wibus-wee wibus-wee added the status:pr-policy-bypass Maintainer exempted this PR from contribution policy label Sep 23, 2026
@wibus-wee
wibus-wee merged commit 7577290 into LodyAI:main Sep 23, 2026
5 of 7 checks passed
@github-actions github-actions Bot removed the status:needs-pr-attention External PR needs contributor attention before review label Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants