fix: verify packaged PI ownership boundaries

This commit is contained in:
2026-08-24 13:58:08 +08:00
parent 13efe84d42
commit 0d03d00ebf
4 changed files with 68 additions and 11 deletions

View File

@@ -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

View File

@@ -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 <final-product-executable> --samples 5 --rep
pnpm run perf:pi:release -- --app-exe <final-product-executable> --samples 5 --report <performance-report.json>
```
产物验证必须确认版本与 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 验证的准确含义

View File

@@ -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,
});

View File

@@ -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', () => {