From 0d03d00ebfd9bba79fc62e7b5939ee9d3106e119 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 24 Aug 2026 13:58:08 +0800 Subject: [PATCH] fix: verify packaged PI ownership boundaries --- .../20260824-pi-release-proof-3e725ac7.md | 17 +++++++ docs/pi-runtime-release-runbook.md | 4 +- scripts/lib/pi-product-artifact.mjs | 48 +++++++++++++++---- tests/unit/pi-product-artifact.test.ts | 10 ++++ 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md b/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md index a59346c..f995b4c 100644 --- a/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md +++ b/.project-docs/30-worklog/tasks/20260824-pi-release-proof-3e725ac7.md @@ -101,6 +101,23 @@ exposed that pnpm `10.33.4` forwards the conventional `--` separator to the script; the parser is being corrected and the artifact will be rebuilt from the corrected commit before evidence is accepted. +- The rebuilt `13efe84` Windows artifact passed the existing verifier. The new + verifier then found OpenCode-named files only inside the exact pinned + `@earendil-works/pi-ai/dist/providers` production package. Pi statically + imports those provider modules from `providers/all.js` and + `models.generated.js`; deleting them would corrupt the pinned closure. The + verifier is being narrowed to reject product-owned/legacy OpenCode resource + paths while reporting this upstream Pi package exception explicitly. +- A one-sample verifier probe then showed the source filename + `makelore-runtime-v3.mjs` is not retained literally by the production bundle. + The final verifier now checks four stable extension contract markers inside + `app.asar`; executable extension/subagent behavior remains owned by + `smoke:pi:real`. With both corrections applied, the final-product verifier + probe passed against the existing unpacked Windows artifact, including exact + Pi `0.84.2`, Node `24.18.1`, 130 expected production packages, six assets, + four Skills, empty product-owned OpenCode paths, the explicit upstream Pi + exception list, empty development-path residue, and actual `get_state`/ + lifecycle execution. ## Follow-ups diff --git a/docs/pi-runtime-release-runbook.md b/docs/pi-runtime-release-runbook.md index 12ff06a..8dfbd9f 100644 --- a/docs/pi-runtime-release-runbook.md +++ b/docs/pi-runtime-release-runbook.md @@ -4,7 +4,7 @@ ## 发布边界 -- 正式包固定使用仓库锁定的 Pi 版本和生产依赖闭包。Electron Main 从安装目录中的 `resources/pi-runtime` 解析运行时、扩展、Manifest 与编码 Skills;缺失、版本漂移、开发路径泄漏或 OpenCode 命名资源残留均阻断发布。 +- 正式包固定使用仓库锁定的 Pi 版本和生产依赖闭包。Electron Main 从安装目录中的 `resources/pi-runtime` 解析运行时、扩展、Manifest 与编码 Skills;缺失、版本漂移、开发路径泄漏或产品自有的 OpenCode runtime/资源残留均阻断发布。锁定的 `@earendil-works/pi-ai` 生产包会静态导入其内置 `providers/opencode*` 模块;这些上游文件必须在报告中单独列出,不能冒充旧 Makelore OpenCode runtime,也不能为制造“零命中”而破坏 Pi 闭包。 - Renderer 只使用 Main-owned Host API 和产品 Conversation 契约,不读取 Pi wire 类型,也不直接启动 worker、访问本地运行时地址或接触 Provider 凭证。 - 产品保留既有 `niancode` 应用 id、协议、数据目录和项目配置兼容标识;这不代表旧 OpenCode 运行时、路由或资源仍被携带。 - 正式产品界面不提供 share/unshare、revert/unrevert、todos 或全局运行时控制。项目分支只创建新的 Conversation 历史,不表示文件回滚。 @@ -36,7 +36,7 @@ pnpm run smoke:pi:real -- --app-exe --samples 5 --rep pnpm run perf:pi:release -- --app-exe --samples 5 --report ``` -产物验证必须确认版本与 Node engine、生产依赖闭包、扩展、Manifest、编码 Skills、`resolve/get_state`、无 OpenCode 资源以及无构建工作区绝对路径。Smoke 必须从最终产品可执行文件启动最终 `resources/pi-runtime`,覆盖 session、prompt、tool、abort、settle、reopen、双 worker 重叠与隔离、子 Agent 和 shutdown。性能报告必须记录 p50/p95/max/样本数组、RSS、Main→Renderer IPC/提交延迟和 Git commit,并覆盖 Spec 17.3 的十个场景。 +产物验证必须确认版本与 Node engine、生产依赖闭包、扩展、Manifest、编码 Skills、`resolve/get_state`、无产品自有 OpenCode runtime/资源、上游 Pi provider 例外清单,以及无构建工作区绝对路径。Smoke 必须从最终产品可执行文件启动最终 `resources/pi-runtime`,覆盖 session、prompt、tool、abort、settle、reopen、双 worker 重叠与隔离、子 Agent 和 shutdown。性能报告必须记录 p50/p95/max/样本数组、RSS、Main→Renderer IPC/提交延迟和 Git commit,并覆盖 Spec 17.3 的十个场景。 ## Provider 验证的准确含义 diff --git a/scripts/lib/pi-product-artifact.mjs b/scripts/lib/pi-product-artifact.mjs index dc2e469..fc3b1c5 100644 --- a/scripts/lib/pi-product-artifact.mjs +++ b/scripts/lib/pi-product-artifact.mjs @@ -23,7 +23,13 @@ import { } from '../probe-pi-packaged-runtime.mjs'; const PRODUCT_NAME = 'Makelore'; -const EXTENSION_MARKER = 'makelore-runtime-v3.mjs'; +const EXTENSION_CONTRACT_MARKERS = Object.freeze([ + 'Makelore runtime bridge rejected the request', + 'subagent.dispatch', + 'makelore.write-lease', + 'MAKELORE_PI_BRIDGE_URL', +]); +const PI_AI_PROVIDER_PREFIX = 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/'; async function pathExists(path) { try { @@ -171,6 +177,12 @@ export async function collectForbiddenResourcePaths(root, pattern = /opencode/i) return matches.sort(); } +export function classifyOpenCodeResourcePaths(paths) { + const upstreamPiProvider = paths.filter((path) => path.startsWith(PI_AI_PROVIDER_PREFIX)); + const productOwned = paths.filter((path) => !path.startsWith(PI_AI_PROVIDER_PREFIX)); + return { productOwned, upstreamPiProvider }; +} + async function filesContainingNeedles(root, needles) { const matches = []; const visit = async (path) => { @@ -270,9 +282,13 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) { if (absoluteManifestValues.length > 0) { throw new Error(`Pi runtime manifest contains absolute paths: ${JSON.stringify(absoluteManifestValues)}`); } - const forbiddenResourcePaths = await collectForbiddenResourcePaths(resourcesDirectory); - if (forbiddenResourcePaths.length > 0) { - throw new Error(`Product resources contain OpenCode paths: ${forbiddenResourcePaths.join(', ')}`); + const openCodeResourcePaths = classifyOpenCodeResourcePaths( + await collectForbiddenResourcePaths(resourcesDirectory), + ); + if (openCodeResourcePaths.productOwned.length > 0) { + throw new Error( + `Product-owned resources contain OpenCode paths: ${openCodeResourcePaths.productOwned.join(', ')}`, + ); } const expectedSkills = await sourceSkillIds(root); @@ -281,8 +297,13 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) { throw new Error(`Packaged coding skills differ: expected ${expectedSkills}, got ${actualSkills}`); } const appAsarContents = await readFile(appAsar); - if (!appAsarContents.includes(Buffer.from(EXTENSION_MARKER))) { - throw new Error(`Packaged app.asar does not contain ${EXTENSION_MARKER}`); + const missingExtensionMarkers = EXTENSION_CONTRACT_MARKERS.filter( + (marker) => !appAsarContents.includes(Buffer.from(marker)), + ); + if (missingExtensionMarkers.length > 0) { + throw new Error( + `Packaged app.asar does not contain Pi extension contract markers: ${missingExtensionMarkers.join(', ')}`, + ); } const sourceNeedles = [ @@ -325,9 +346,18 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) { nodeEngine: manifest.runtime.nodeEngine, }, packagedClosure, - extension: { marker: EXTENSION_MARKER, packaged: true }, + extension: { + contractMarkers: EXTENSION_CONTRACT_MARKERS, + packaged: true, + executionProof: 'smoke:pi:real final-product extension/subagent run', + }, skills: actualSkills, - openCodeResourcePaths: [], + openCodeResourcePaths: { + ...openCodeResourcePaths, + upstreamDecision: openCodeResourcePaths.upstreamPiProvider.length > 0 + ? 'retained-required-files-from-exact-pinned-pi-production-package' + : 'none', + }, developmentPathResidue: [], result: 'pass', }; @@ -336,5 +366,5 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) { export const PI_PRODUCT_ARTIFACT_DEFAULTS = Object.freeze({ platform: hostPlatform(), arch: hostArch(), - extensionMarker: EXTENSION_MARKER, + extensionContractMarkers: EXTENSION_CONTRACT_MARKERS, }); diff --git a/tests/unit/pi-product-artifact.test.ts b/tests/unit/pi-product-artifact.test.ts index 5e6897d..fa4f70a 100644 --- a/tests/unit/pi-product-artifact.test.ts +++ b/tests/unit/pi-product-artifact.test.ts @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { assertNodeEngineCompatible, + classifyOpenCodeResourcePaths, collectAbsoluteManifestValues, collectForbiddenResourcePaths, defaultProductExecutable, @@ -81,6 +82,15 @@ describe('final Pi product artifact verification', () => { expect(await collectForbiddenResourcePaths(root)).toEqual([ 'pi-runtime/node_modules/opencode-ai', ]); + expect(classifyOpenCodeResourcePaths([ + 'app.asar.unpacked/resources/opencode-runtime', + 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js', + ])).toEqual({ + productOwned: ['app.asar.unpacked/resources/opencode-runtime'], + upstreamPiProvider: [ + 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js', + ], + }); }); it('uses final unpacked-product paths and strictly parses verifier options', () => {