build(pi): prepare pinned offline Undici rebuild recipe (#713)
This commit is contained in:
1 parent
09a847edaa
commit
e99c6726a1
11 files changed
+1599
No files matched your search
@@ -5816,3 +5816,9 @@ Before: agents can read the library; supporting-path and push-credential depende
|
||||
2026-10-26 | Sage | mos1884/#662 scope after330/331landing | ReadQ222/Q261/Q263 and659/660/662; posted bounded human-only factory settings/triage/defaulttwo-roundmandate/trackedno-exemptionflake scope with10acceptance criteria, task662comment1338 exactwholebody/author13GETverified. No implementation/review/gate launched.660c1335notready(review/merge, noVM); gatefacts1329/1334 retained; target integration/fullrunner confirmation follows readiness, no localfallback. Landedproduct/evidence unchanged;657/658backlog.
|
||||
|
||||
2026-10-26 | Sage | Mos1909/1915/1917 real postlanding reviews | Boyd/Dewey/Filbert/Gladstone real authored candidate52ba verdictfiles independently byte/hash-verified, allAPPROVE scoped; Dewey656broaderPARTIAL retained. Formal330R1/331R4 requests failedpre-send/no comments or approvals; no borrowed/copied/mintedkeys. LegacyhelperGETname jason.woltje is NOT ownJarvis, ownslotabsent; rows tracking-open/#436, files cited via normalqueue. NewFilbertNEW-330-01 filedtask665 exactreadback, notlandingreopen/gate. Repair646-648/652-656 remainOPEN untilactualcomments;1917 conditionalbacklog routing retained. #662scope1338posted;660PR405REVISE/notready. Product1968inputs unchanged; no source/helper/test/gate edits/runs; peerjournal prefixes preserved.
|
||||
|
||||
2026-10-28T22:34:42-05:00 | Sage FIX-713 seam investigation AFTER/BLOCKED | Branch sage/q284-713-pi-undici at exact base 09a847edaa25b609bac7728ff83f224708a29399. Checksum-verified Pi0.85.1 registry artifact (lock SHA512 FGRN+OHb...L/oQ==) maps bin pi to dist/bundle/cli.js; that CLI imports chunk-JVUZSMYM.js, which embeds node_modules/undici/index.js and Undici8.9.0 advisory signatures (orphaned fetch body before content-encoding reject; HTTP1/2 ERR_TLS_CERT_ALTNAME_INVALID assertions). Exact upstream v0.85.1 bundler bundles all non-allowlisted packages and does not externalize Undici; root8.12.2 override would therefore be installed but not consumed. Public registry metadata latest1.4.0 and all published declared Undici values remain below8.12.2. Read-only focused source verification PASS; no product/lock/test change, no full gate/container/native/provider/live/API operation, commit, push or PR. Minimal missing boundary is an upstream exact pinned Pi artifact rebuilt with bundled Undici>=8.12.2, or separate authorization and compatibility admission for a rebuilt/alternate entry artifact. Owned scratch /mnt/storage/scratch/tmp/q284-713.l8x9AG disposed.
|
||||
|
||||
2026-10-29T00:37:47-05:00 | Sage Mos2248/FIX-713 source-only rebuild recipe AFTER/READY-FOR-PARENT-REVIEW | On sage/q284-713-pi-undici at unchanged 09a847edaa25b609bac7728ff83f224708a29399, added only packages/pi-rebuild plus append-only receipts. Exact Pi v0.85.1 commit d981de1 source/tool/package pins; zero-context three-target patch sha256 6f1c3aaf5c6bdf08cfb02932d77251d8048130ba59c63a099b25cae26cb0bf0e moves bundled Undici8.9.0->8.12.2 and full-A5 brace-expansion5.0.9->5.0.12, retaining all unrelated pins and the truthful unchanged public-registry install-lock. Actual published CLI closure=40 files, contains chunk-JVUZSMYM.js, has no external Undici import, and fixed verifier refuses its8.9.0 shrinkwrap. Exact clean/preimage and patched/postimage validation PASS; 3 fixture tests PASS (fail-closed pins/patch, source graph, reachable-bundle signatures plus deterministic manifest/SPDX/provenance logic); bash/node syntax and tracked/untracked whitespace checks PASS. Recipe/build/CI instructions explicitly UNRUN; no fetched-code execution, package install, rebuild/artifact, runtime/package-lock adoption, full gate, container/native/provider/live/credential/API/commit/push/PR/release/publish/spend action. Later consumption still needs Jason choice: immutable internal/local artifact+hash adoption or fork/new identity/version. Owned scratch /mnt/storage/scratch/tmp/q284-713-recipe.U19vvs disposed.
|
||||
|
||||
2026-10-29T00:51:13-05:00 | Sage Mos2248/FIX-713 parent-precheck correction AFTER/READY-FOR-PARENT-REVIEW | CORRECTION to the 00:37 entry: its custom-prefix scratch `/mnt/storage/scratch/tmp/q284-713-recipe.U19vvs` did not satisfy the required exact `mktemp -d -p /mnt/storage/scratch/tmp` command (parent confirmed it absent), and its readiness claim preceded discovery of stale Node22.19.0 and incomplete whitespace handling in the TLS signature guard. Corrected the still-uncommitted recipe to exact official patched Node22.23.4/npm10.9.9 (archive sha256 1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce; PGP unverified; binary unobserved), retained Pi0.85.1/esbuild0.28.1/all unrelated pins and unchanged patch6f1c3aaf. Parent TLS control RED2/1 reproduced, then GREEN3/0 with all three compact/spaced/assert-space vulnerable forms refused while the fixed abort and non-vulnerable equality control remain accepted; the guard is explicitly only a known-direct-signature scan, not universal JS proof. Exact source trace selected upstream `build:offline`; regular AI build has live catalog fetches, offline build only validates committed model data/compiles/copies, and `check:package-install` uses ignore-scripts registry materialization plus PI_OFFLINE/telemetry-off import/`--version` smoke. Driver now enforces owned mode0700 exact-shaped mktemp root, empty user/global npm configs, offline build mode and three build-input hosts. Fresh exact archive clean/preimage and patched/postimage validation PASS; published Pi reference sha/size reverified, actual CLI closure40/chunk-JVUZSMYM/no external Undici/embedded8.9 vulnerable signatures reconfirmed and refused; Undici8.12.2 and brace5.0.12 package sha/size plus four fixed-source hashes reverified; recipe fixtures3/0, validate-recipe, bash/node/JSON syntax and tracked/untracked whitespace PASS. No Node binary download/execution, fetched-code execution, install, rebuild/artifact, Mosaic compatibility/full gate, runtime/lock adoption, provider/live/credential/API/commit/push/PR/release/publish/spend action. Compliant owned scratch `/mnt/storage/scratch/tmp/tmp.lqYHVE1uKt` disposed; parent review remains before commit.
|
||||
@@ -1879,3 +1879,9 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-26 | Sage | mos1884/#662 scope after330/331landing | ReadQ222/Q261/Q263 and659/660/662; posted bounded human-only factory settings/triage/defaulttwo-roundmandate/trackedno-exemptionflake scope with10acceptance criteria, task662comment1338 exactwholebody/author13GETverified. No implementation/review/gate launched.660c1335notready(review/merge, noVM); gatefacts1329/1334 retained; target integration/fullrunner confirmation follows readiness, no localfallback. Landedproduct/evidence unchanged;657/658backlog.
|
||||
|
||||
2026-10-26 | Sage | Mos1909/1915/1917 real postlanding reviews | Boyd/Dewey/Filbert/Gladstone real authored candidate52ba verdictfiles independently byte/hash-verified, allAPPROVE scoped; Dewey656broaderPARTIAL retained. Formal330R1/331R4 requests failedpre-send/no comments or approvals; no borrowed/copied/mintedkeys. LegacyhelperGETname jason.woltje is NOT ownJarvis, ownslotabsent; rows tracking-open/#436, files cited via normalqueue. NewFilbertNEW-330-01 filedtask665 exactreadback, notlandingreopen/gate. Repair646-648/652-656 remainOPEN untilactualcomments;1917 conditionalbacklog routing retained. #662scope1338posted;660PR405REVISE/notready. Product1968inputs unchanged; no source/helper/test/gate edits/runs; peerjournal prefixes preserved.
|
||||
|
||||
2026-10-28T22:34:42-05:00 | Sage | FIX-713 Pi Undici consumer seam on sage/q284-713-pi-undici | Exact verified Pi0.85.1 bundle embeds vulnerable Undici8.9.0; no external override seam or compatible in-scope fix. Focused source assertions PASS; required upstream rebuilt exact artifact or separately reviewed alternate-entry/rebuild compatibility boundary reported. No product/lock/test/full-gate/container/native/provider/live/API/commit/push/PR action; owned scratch disposed.
|
||||
|
||||
2026-10-29T00:37:47-05:00 | Sage | Mos2248/FIX-713 source-only Pi0.85.1 rebuild recipe on sage/q284-713-pi-undici | Prepared review-ready exact-source recipe, three-file upstream patch (Undici8.12.2 plus A5-required brace-expansion5.0.12), fail-closed pre/postimage and actual bundle-closure/signature checks, deterministic manifest/SPDX/provenance writers, and UNRUN focused CI/no-model acceptance. Local source/fixture/static evidence PASS; actual rebuild/artifact/adoption/CI/full gate and all protected operations unrun. Parent review required before commit; later consumption needs Jason artifact-source identity choice. Owned scratch /mnt/storage/scratch/tmp/q284-713-recipe.U19vvs disposed.
|
||||
|
||||
2026-10-29T00:51:13-05:00 | Sage | FIX-713 parent-precheck correction on sage/q284-713-pi-undici at base09a847ed | Supersedes prior readiness defects: records custom-prefix scratch deviation, updates exact unrun toolchain to patched Node22.23.4/npm10.9.9, uses exact upstream build:offline after source-tracing model/network and check:package-install behavior, fixes known direct TLS signature whitespace discrimination with parent RED2/1→GREEN3/0 controls, and narrows driver/README cleanup to exact owned mode0700 mktemp roots. Exact source/patched/bundle/package/static checks PASS; build/install/artifact/Mosaic compatibility/full CI and protected operations remain UNRUN; no commit/push/review dispatch. Compliant scratch /mnt/storage/scratch/tmp/tmp.lqYHVE1uKt disposed; prior custom-prefix path confirmed absent.
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"state": "UNRUN",
|
||||
"ruling": "Mos2248",
|
||||
"pi": {
|
||||
"name": "@earendil-works/pi-coding-agent",
|
||||
"version": "0.85.1",
|
||||
"tag": "v0.85.1",
|
||||
"commit": "d981de1229ef899957bbe968bc8dcda02a21f477",
|
||||
"tree": "346294a615d2d0ad4f6e5fbccb4cee4ccd7b2d6c",
|
||||
"commitTimestamp": "2026-09-05T11:54:46Z",
|
||||
"sourceDateEpoch": 1788609286,
|
||||
"source": {
|
||||
"url": "https://codeload.github.com/earendil-works/pi/tar.gz/d981de1229ef899957bbe968bc8dcda02a21f477",
|
||||
"archiveRoot": "pi-d981de1229ef899957bbe968bc8dcda02a21f477",
|
||||
"bytes": 6978387,
|
||||
"sha256": "d2a04b35e57f59873d67f667f22e681e93c78a46ba8e2cdedf9d4c323b1ae8dc",
|
||||
"sha512": "77acb7819e75da17059cb507064739d6eaf033ad76b9a5d2b090cb607bf97ddff5a94ec0ae57ce5f1c168c95f370b6e97513e667fdb47b53ed55f486286a2ae9"
|
||||
},
|
||||
"registryReference": {
|
||||
"metadataUrl": "https://registry.npmjs.org/@earendil-works%2fpi-coding-agent/0.85.1",
|
||||
"metadataSha256": "3f6f840ec6df9cc731e6c9d1dca4ca31ffa609797fb8b0ee78aaf34e2981c3c0",
|
||||
"tarballUrl": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.85.1.tgz",
|
||||
"tarballBytes": 6986356,
|
||||
"tarballSha256": "1f498729649bdce647d1160993b4d92bf3c614cc819213bee2f91dd34f2a7af4",
|
||||
"tarballSha512": "14644df8e1db59a79f04f19a4e080074b8eb2075beb363f32f2825cffe5d7cbcdbf687fbbae5cc5d80b47c921e653c3eb214b7da8d9cb90f6317b6120ee2ffa1",
|
||||
"integrity": "sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ==",
|
||||
"shasum": "4cd00f653c3dabeb193b46f511044e7fbfe0f947",
|
||||
"gitHead": "d981de1229ef899957bbe968bc8dcda02a21f477",
|
||||
"attestationsUrl": "https://registry.npmjs.org/-/npm/v1/attestations/@earendil-works%[email protected]",
|
||||
"attestationsSha256": "bb337af8436f7596a127d43c067d10103ede1d4aa5ebe143a2dafdc9d24291bd"
|
||||
}
|
||||
},
|
||||
"undici": {
|
||||
"name": "undici",
|
||||
"version": "8.12.2",
|
||||
"gitHead": "bb6cc0ae9b6827ed273ed5ba344148273629638f",
|
||||
"metadata": {
|
||||
"url": "https://registry.npmjs.org/undici/8.12.2",
|
||||
"sha256": "9da5247dcaedb24bde2a1434c418f067f0e19ef272a032e99588076c14ec2487"
|
||||
},
|
||||
"tarball": {
|
||||
"url": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
|
||||
"bytes": 542130,
|
||||
"sha256": "a765b5e63ab5c6b1b91a59072c88f7857b36f47b09c376762241d177279cdefd",
|
||||
"sha512": "5edb8359576a4cbaf7340c66da5cbdfe153a914b093a01a1d51012ccc05bb30e9a58239a4331dd5be4100df7444637c9d95e6f2f9a513dae117ad3bd51f8af0e",
|
||||
"integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
|
||||
"shasum": "e869237430adcc5de6bca798c94edd7db9dac5ac"
|
||||
},
|
||||
"attestations": {
|
||||
"url": "https://registry.npmjs.org/-/npm/v1/attestations/[email protected]",
|
||||
"sha256": "6b1d108f4e81e752548a8b1cda5aedb4246c02749bd3b30162bf356e6f5709ca"
|
||||
},
|
||||
"fixedSourceFiles": {
|
||||
"lib/web/fetch/index.js": "e3cc1063bb3bb8353f918d2b1c54b8d6991a71b06c4278959f0c2ef412a433b1",
|
||||
"lib/dispatcher/client-h1.js": "b4473e4ef9e6a3fb5ca0de69fd705c3797d138e7586e40beb49fa13b58c8a370",
|
||||
"lib/dispatcher/client-h2.js": "9acfef9064b7639a937d5f279e93bafb831ca2e3c018b4e3864b198f97368fb9",
|
||||
"package.json": "16723528eaa06df7f3b29ea52584e9127608fd9e8de066c39339aa806ad9da84"
|
||||
}
|
||||
},
|
||||
"braceExpansion": {
|
||||
"name": "brace-expansion",
|
||||
"version": "5.0.12",
|
||||
"gitHead": "f3410159d768f56c9d9f4511d3e1b46425fc1099",
|
||||
"metadata": {
|
||||
"url": "https://registry.npmjs.org/brace-expansion/5.0.12",
|
||||
"sha256": "5421e68ea58a0286041e3f11998da4fa44055026b11c527cc14ec74895b58929"
|
||||
},
|
||||
"tarball": {
|
||||
"url": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||
"bytes": 12620,
|
||||
"sha256": "ef8448ec78f20b692f04fa6d01f39b5ab34c66404bea3429f5a39c6c9e0be8b4",
|
||||
"sha512": "628bd0debce168b308ac38cd0cc90d4b4d6d79af77aa11211b9c72f1febe47497e770dca8bee6c0a822823de368ae2d94c75a881692d830543854d3d88d12299",
|
||||
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||
"shasum": "995fbb4750a77c4d16a7dc942ff2ca6ef8e675ec"
|
||||
}
|
||||
},
|
||||
"toolchain": {
|
||||
"platform": "linux-x64",
|
||||
"node": {
|
||||
"version": "22.23.4",
|
||||
"url": "https://nodejs.org/dist/v22.23.4/node-v22.23.4-linux-x64.tar.xz",
|
||||
"sha256": "1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce",
|
||||
"releaseUrl": "https://nodejs.org/en/blog/release/v22.23.4",
|
||||
"securityReleaseUrl": "https://nodejs.org/en/blog/vulnerability/october-2026-security-releases",
|
||||
"shasumsUrl": "https://nodejs.org/dist/v22.23.4/SHASUMS256.txt",
|
||||
"shasumsSha256": "c90c3f1478fda8308b22cc1a1b0dc536169196444e67ee8b5c582fd351d02bce",
|
||||
"pgpSignatureVerified": false
|
||||
},
|
||||
"npm": {
|
||||
"version": "10.9.9",
|
||||
"packageJsonUrl": "https://raw.githubusercontent.com/nodejs/node/v22.23.4/deps/npm/package.json",
|
||||
"packageJsonSha256": "7c04bf69824d17a1db64defd3a9a86d538781bf2d6ca2764e3f1143fa5b71166"
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.28.1"
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"path": "patches/pi-0.85.1-undici-8.12.2.patch",
|
||||
"sha256": "6f1c3aaf5c6bdf08cfb02932d77251d8048130ba59c63a099b25cae26cb0bf0e",
|
||||
"allowedTargets": [
|
||||
"package-lock.json",
|
||||
"packages/coding-agent/npm-shrinkwrap.json",
|
||||
"packages/coding-agent/package.json"
|
||||
]
|
||||
},
|
||||
"sourcePreimages": {
|
||||
".npmrc": "d2c8ca514b23b7378b7ceb25e9599954e7c9d83328dc53185e2b69ba1b2dc146",
|
||||
"package.json": "7e305a4e6382517028f3b6fdf2b2350c38fa89a24e2cbec6ac84e8d99f99ba1d",
|
||||
"package-lock.json": "e3569b99e673a0051be908f2bed57651b90a966a5a0d208913c29e6830a6a6c2",
|
||||
"packages/ai/package.json": "b54df5a36d523febdeebfc5682dc4faed101fee10aba913d5f3679582f018da3",
|
||||
"packages/ai/scripts/check-model-data.ts": "818e463211322ab41fe4c82eb8e9543fe6b40c1bfb734999d49d87f2071802aa",
|
||||
"packages/ai/scripts/generate-models.ts": "188383e7107430d2c3321a8435e3295daf0877317cbab94b67a58e6afaec41d6",
|
||||
"packages/ai/scripts/model-data.ts": "9265bb6f494765be06ad42e50b6f5667c88b690670e9dfd673cb55513d183056",
|
||||
"packages/coding-agent/package.json": "f1738e4b42203e5f22bcb513f13fb2fb224f1e98d1f129ff042f87048665a94c",
|
||||
"packages/coding-agent/npm-shrinkwrap.json": "5ff461d87e0a7ccac07e6a9eb3edcd18841d3373d375bed043606b51082f1a26",
|
||||
"packages/coding-agent/install-lock/package.json": "9079bdf9f1b920e228bee48a3f5aae8c2b83361e3a518436616b91b0445ead33",
|
||||
"packages/coding-agent/install-lock/package-lock.json": "48d12884e64315740002a71c9aeed8a07f435163f702f1e3582251607fd11ea3",
|
||||
"scripts/build-coding-agent-bundle.mjs": "ba226d9c812ee1610cf61a0ad3efa8920f672a309fb32d122b2b3a4148071e8a",
|
||||
"scripts/generate-coding-agent-shrinkwrap.mjs": "494af1eda0552f570df6e4173e9db58b16414db91de43256c9abe0d3bd69067b",
|
||||
"scripts/generate-coding-agent-install-lock.mjs": "1ceab3fe345dd1441a098bc8d147e63216f4e0700779ec3ec3d3a9a1917d57fc",
|
||||
"scripts/coding-agent-consumer.mjs": "fe947e1e9140ea33567b32705106ce3c725bd98fb024ace2ed8eafe795ac1580",
|
||||
"scripts/package-workspaces.mjs": "4214d03e1fdcbe245b903ff938da50c3ccf93a0aa762157f6d58f80e0d1ae955",
|
||||
"scripts/release-packages.mjs": "1e8f1e35011df54a790109fe42441523818d41e0f4815fe7011ffb14d8014827"
|
||||
},
|
||||
"sourcePostimages": {
|
||||
".npmrc": "d2c8ca514b23b7378b7ceb25e9599954e7c9d83328dc53185e2b69ba1b2dc146",
|
||||
"package.json": "7e305a4e6382517028f3b6fdf2b2350c38fa89a24e2cbec6ac84e8d99f99ba1d",
|
||||
"package-lock.json": "b54fedeedc2c042caef98e7b2e21c19f99553abaebe269d648d287875ce54657",
|
||||
"packages/ai/package.json": "b54df5a36d523febdeebfc5682dc4faed101fee10aba913d5f3679582f018da3",
|
||||
"packages/ai/scripts/check-model-data.ts": "818e463211322ab41fe4c82eb8e9543fe6b40c1bfb734999d49d87f2071802aa",
|
||||
"packages/ai/scripts/generate-models.ts": "188383e7107430d2c3321a8435e3295daf0877317cbab94b67a58e6afaec41d6",
|
||||
"packages/ai/scripts/model-data.ts": "9265bb6f494765be06ad42e50b6f5667c88b690670e9dfd673cb55513d183056",
|
||||
"packages/coding-agent/package.json": "d63c0319c402cb3f855f46dc724a63b771e6e40ca872b698273717f482c5760d",
|
||||
"packages/coding-agent/npm-shrinkwrap.json": "ac5a5b05a700c98f97d573973c54fd5f875b7e0be63f6f3612f07cc029fb5f65",
|
||||
"packages/coding-agent/install-lock/package.json": "9079bdf9f1b920e228bee48a3f5aae8c2b83361e3a518436616b91b0445ead33",
|
||||
"packages/coding-agent/install-lock/package-lock.json": "48d12884e64315740002a71c9aeed8a07f435163f702f1e3582251607fd11ea3",
|
||||
"scripts/build-coding-agent-bundle.mjs": "ba226d9c812ee1610cf61a0ad3efa8920f672a309fb32d122b2b3a4148071e8a",
|
||||
"scripts/generate-coding-agent-shrinkwrap.mjs": "494af1eda0552f570df6e4173e9db58b16414db91de43256c9abe0d3bd69067b",
|
||||
"scripts/generate-coding-agent-install-lock.mjs": "1ceab3fe345dd1441a098bc8d147e63216f4e0700779ec3ec3d3a9a1917d57fc",
|
||||
"scripts/coding-agent-consumer.mjs": "fe947e1e9140ea33567b32705106ce3c725bd98fb024ace2ed8eafe795ac1580",
|
||||
"scripts/package-workspaces.mjs": "4214d03e1fdcbe245b903ff938da50c3ccf93a0aa762157f6d58f80e0d1ae955",
|
||||
"scripts/release-packages.mjs": "1e8f1e35011df54a790109fe42441523818d41e0f4815fe7011ffb14d8014827"
|
||||
},
|
||||
"securityEvidence": {
|
||||
"briefSha256": "83b9462ba40e93934fcf3cc96617f59dff3950d4321e3351eb49ab83a03c1359",
|
||||
"a5ReportSha256": "71270a41a957921b5a83b09650f817fdaef31ef4e762f03a68c8eed8207fb307",
|
||||
"a5MetadataSha256": "4ae5158491a25d99778fa84cefe65260c0d6aed701b6e710de2c80ba8e39877f"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
# Pi 0.85.1 / Undici 8.12.2 source rebuild recipe
|
||||
|
||||
Status: **UNRUN**. Mos2248 authorizes preparation of this source-only recipe. It does not authorize running fetched code, installing packages, building or adopting an artifact, switching the runtime, publishing, releasing, spending, using credentials, or contacting a model/provider. Nothing in this directory is a claim that Mosaic currently consumes a fixed bundle.
|
||||
|
||||
This directory is intentionally outside Mosaic's root dependency graph. It adds no root override and changes neither `package.json` nor `package-lock.json`. A root override would be cosmetic: Pi 0.85.1's `bin.pi` is `dist/bundle/cli.js`, its esbuild graph has `bundle: true`, and Undici is not allowlisted as external. The published bundle therefore consumes its embedded Undici, not Mosaic's root resolution.
|
||||
|
||||
## Exact inputs
|
||||
|
||||
`PIN.json` is the machine-readable authority. `src/recipe.mjs` independently enforces its security-critical values and refuses drift.
|
||||
|
||||
| Input | Exact identity | SHA-256 |
|
||||
|---|---|---|
|
||||
| Pi source | tag `v0.85.1`, commit `d981de1229ef899957bbe968bc8dcda02a21f477`, tree `346294a615d2d0ad4f6e5fbccb4cee4ccd7b2d6c` | source archive `d2a04b35e57f59873d67f667f22e681e93c78a46ba8e2cdedf9d4c323b1ae8dc` |
|
||||
| Published Pi reference (not a build input) | `@earendil-works/[email protected]`, `gitHead` equal to the commit above | tarball `1f498729649bdce647d1160993b4d92bf3c614cc819213bee2f91dd34f2a7af4` |
|
||||
| Fixed package | `[email protected]`, `gitHead bb6cc0ae9b6827ed273ed5ba344148273629638f` | tarball `a765b5e63ab5c6b1b91a59072c88f7857b36f47b09c376762241d177279cdefd` |
|
||||
| A5 companion pin | `[email protected]`, `gitHead f3410159d768f56c9d9f4511d3e1b46425fc1099` | tarball `ef8448ec78f20b692f04fa6d01f39b5ab34c66404bea3429f5a39c6c9e0be8b4` |
|
||||
| Toolchain | Node `22.23.4` Linux x64 with bundled npm `10.9.9`; esbuild remains `0.28.1` from Pi's lock | Node archive `1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce` |
|
||||
| Patch | `patches/pi-0.85.1-undici-8.12.2.patch` | `6f1c3aaf5c6bdf08cfb02932d77251d8048130ba59c63a099b25cae26cb0bf0e` |
|
||||
|
||||
The Pi npm attestation binds the published 0.85.1 reference tarball to the same upstream tag and commit. The exact GitHub commit is unsigned, so the recipe does not claim a signed source commit. The Undici npm attestation binds 8.12.2 to `bb6cc0ae9b6827ed273ed5ba344148273629638f`. The npm attestation endpoint returned 404 for brace-expansion 5.0.12 during preparation; its exact registry metadata (including registry signatures), tarball integrity, Git head, and hashes are pinned without claiming provenance attestation. Exact metadata and attestation response hashes where present, archive SHA-512 values, source/postimage hashes, and the original FIX-713/A5 evidence hashes are in `PIN.json`.
|
||||
|
||||
Node 22.23.4 is the 2026-10-21 security release on the same major line permitted by Pi's unchanged upstream `node >=22.19.0` engine. The official release page and October 2026 security notice identify the 22.x fixes; the official `SHASUMS256.txt` digest, archive digest, and exact `deps/npm/package.json` digest establishing bundled npm 10.9.9 are pinned. Preparation did not download or execute the Node archive and did not verify the published PGP signature, so no binary-observation or PGP-verification claim is made.
|
||||
|
||||
## Minimal source change
|
||||
|
||||
The patch changes exactly three existing Pi files. It replaces seven Undici `8.9.0` literals and the two `[email protected]` lock resolutions:
|
||||
|
||||
- `packages/coding-agent/package.json`: direct dependency only;
|
||||
- `package-lock.json`: the coding-agent workspace edge plus top-level Undici and brace-expansion resolutions only;
|
||||
- `packages/coding-agent/npm-shrinkwrap.json`: the packed package edge plus those two resolutions only.
|
||||
|
||||
It preserves Pi at 0.85.1, every unrelated pin, and Gondolin's nested `[email protected]`. `[email protected]` is the sole companion change because the full A5 report explicitly requires this remediation's resulting tree to meet that floor; Minimatch's existing `^5.0.5` edge is unchanged. It does not alter the esbuild script: that exact script already bundles non-external dependencies and rejects unexpected external imports.
|
||||
|
||||
The stored patch has zero context so the repository's whitespace gate does not misread tab-indented JSON context lines as indentation errors. The driver uses `git apply --unidiff-zero`; exact full-file preimage hashes and the three-path allowlist are checked first.
|
||||
|
||||
`packages/coding-agent/install-lock/package-lock.json` deliberately stays byte-identical and still describes the public registry's published Pi 0.85.1, including Undici 8.9.0 and brace-expansion 5.0.9. Rewriting that lock while its dependency still points at public `@earendil-works/[email protected]` would be false evidence. Consequently, the broad upstream `npm run check` is not an admitted command: it includes `check:install-lock:coding-agent`. The recipe instead runs the truthful static/build checks individually and the upstream local-package consumer check.
|
||||
|
||||
## Offline build and consumer source trace
|
||||
|
||||
The exact checksum-verified Pi source archive, not fetched-code execution, establishes the future command boundary:
|
||||
|
||||
- root `package.json` defines `build:offline` with the same workspace build sequence as `build`, except the AI workspace receives `npm run build:offline`;
|
||||
- `packages/ai/package.json` defines normal `build` as `generate-models` followed by `build:offline`. `generate-models.ts` contains live catalog fetches to models.dev, NVIDIA, OpenRouter, and Vercel AI Gateway, so the driver does not use it;
|
||||
- AI `build:offline` runs `check:model-data`, TypeScript compilation, and local file copies. `check-model-data.ts` and `model-data.ts` only read, parse, hash, and validate the committed generated catalog; they contain no provider/model call;
|
||||
- every other workspace build in the root sequence is TypeScript compilation or a local copy/bundle operation. The unchanged esbuild script reads compiled local inputs and embeds non-allowlisted dependencies.
|
||||
|
||||
The driver therefore uses upstream's supported `npm run build:offline`, sets npm's offline mode for that build command, and exports `PI_OFFLINE=1` and `PI_TELEMETRY=0`; it does not invent a build hook. The exact upstream `check:package-install` source runs local `npm pack --ignore-scripts`, creates a consumer whose public workspaces are local tarball overrides, and runs `npm install --ignore-scripts --omit=dev --no-audit --no-fund`. Its smoke subprocess imports three SDK symbols and runs the CLI and unbundled CLI with `--version` under an isolated HOME, `PI_OFFLINE=1`, and `PI_TELEMETRY=0`; it neither configures a model nor sends a provider request. The exact root lock and coding-agent shrinkwrap resolve remote packages only from `registry.npmjs.org`. Thus consumer installation remains a future networked CI step limited to the registry host, while its compatibility smoke is explicitly no-model.
|
||||
|
||||
The future driver fetches build inputs only from `codeload.github.com`, `registry.npmjs.org`, and `nodejs.org`. `PIN.json` also records a read-only `raw.githubusercontent.com` npm metadata evidence URL, but the driver never fetches it; a three-host CI egress policy must continue to refuse that fourth host.
|
||||
|
||||
## Evidence emitted by a future admitted run
|
||||
|
||||
The driver validates the unpatched archive against exact preimage hashes, applies the three-file patch, and validates exact postimage hashes before package materialization. After a future build it validates the installed Undici package and four security-relevant source files against the 8.12.2 tarball hashes.
|
||||
|
||||
It then opens the packed package and follows relative static/dynamic imports from the actual `package.json` `bin.pi` entry. The reachable closure must:
|
||||
|
||||
- resolve `[email protected]` and `[email protected]` with exact npm integrities in the packed shrinkwrap;
|
||||
- have no external `undici` import;
|
||||
- contain the fixed `controller.abort(new Error(\`too many content-encodings...` signature;
|
||||
- omit the vulnerable `reject(new Error(\`too many content-encodings...` signature;
|
||||
- omit the known direct vulnerable assertion signatures equivalent to `assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID")`, including source and minified whitespace forms.
|
||||
|
||||
Those are the two A5-002 advisory seams: GHSA-xjj6-f33q-929c (orphaned response stream after excessive content codings) and GHSA-qrxm-fqhp-jwvm (TLS alternate-name assertion termination). These conservative source signatures discriminate the fixed abort from the known vulnerable direct assertions; they are not a parser and do not prove the absence of every semantically equivalent JavaScript program. The recipe makes no broader security claim about Undici or Pi.
|
||||
|
||||
On success, `src/receipts.mjs` writes deterministic, tab-formatted receipts:
|
||||
|
||||
- `artifact-manifest.json`: artifact hashes, exact material/toolchain pins, actual bundle assertion results, and a sorted hash/mode inventory of every packed file;
|
||||
- `sbom.spdx.json`: SPDX 2.3 packages and resolved dependency relationships derived from the packed shrinkwrap;
|
||||
- `provenance.intoto.json`: an in-toto statement with SLSA provenance v1 predicate, source/package/tool/patch materials, and artifact/manifest/SBOM digests;
|
||||
- `SHA256SUMS`: sorted hashes of the artifact and all three receipts.
|
||||
|
||||
Dynamic wall-clock time, scratch paths, hostnames, and CI run IDs are excluded. `SOURCE_DATE_EPOCH` is the pinned upstream commit epoch. CI acceptance still requires two clean builds to prove the upstream build output itself is reproducible.
|
||||
|
||||
## Executable instructions — UNRUN
|
||||
|
||||
These commands are review material. Do not run them under the current authorization.
|
||||
|
||||
```bash
|
||||
# UNRUN — package preparation checks only; no fetched code or package installation.
|
||||
cleanup_owned_scratch() {
|
||||
case "$1" in
|
||||
/mnt/storage/scratch/tmp/tmp.??????????)
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 2
|
||||
[ "$(stat -c %u -- "$1")" = "$(id -u)" ] || return 2
|
||||
[ "$(stat -c %a -- "$1")" = "700" ] || return 2
|
||||
find "$1" -depth -delete
|
||||
;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
}
|
||||
recipe_scratch="$(mktemp -d -p /mnt/storage/scratch/tmp)"
|
||||
MOSAIC_PI_REBUILD_TEST_TMP="$recipe_scratch" \
|
||||
node --test packages/pi-rebuild/tests/recipe.test.mjs
|
||||
node packages/pi-rebuild/src/cli.mjs validate-recipe
|
||||
cleanup_owned_scratch "$recipe_scratch"
|
||||
```
|
||||
|
||||
The full driver downloads only the exact pinned Pi source archive, Undici package, brace-expansion package, and Node toolchain; verifies them before use; clears the environment for every fetched-code command; uses an empty HOME, empty user/global npm configs, and owned cache; runs `npm ci --ignore-scripts`; and never publishes or changes Mosaic's dependency graph.
|
||||
|
||||
```bash
|
||||
# UNRUN — requires a separate parent-approved build/CI action.
|
||||
cleanup_owned_scratch() {
|
||||
case "$1" in
|
||||
/mnt/storage/scratch/tmp/tmp.??????????)
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 2
|
||||
[ "$(stat -c %u -- "$1")" = "$(id -u)" ] || return 2
|
||||
[ "$(stat -c %a -- "$1")" = "700" ] || return 2
|
||||
find "$1" -depth -delete
|
||||
;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
}
|
||||
build_scratch="$(mktemp -d -p /mnt/storage/scratch/tmp)"
|
||||
MOSAIC_PI_REBUILD_EXECUTE=MOS2248_REVIEWED_UNRUN_RECIPE \
|
||||
packages/pi-rebuild/rebuild.sh "$build_scratch"
|
||||
|
||||
# Review/copy results before disposing only this owned directory.
|
||||
find "$build_scratch/result" -maxdepth 1 -type f -print | sort
|
||||
cleanup_owned_scratch "$build_scratch"
|
||||
```
|
||||
|
||||
The driver additionally requires that `WORK_ROOT` resolve to an empty, non-symlink, mode-0700 directory owned by the invoking uid, directly under `/mnt/storage/scratch/tmp`, with the exact `tmp.` plus ten-character basename shape emitted by the required `mktemp` command. The guard value is only a fail-closed acknowledgement that the recipe was admitted; it is not release, adoption, publish, credential, provider, or spend authority.
|
||||
|
||||
## Admitted CI acceptance — UNRUN
|
||||
|
||||
A future job must have `permissions: contents: read`, no secrets, no npm token, no provider credentials, and outbound access restricted to the three build-input HTTPS hosts named above. It must not use `npm publish`, `npm install -g`, a container build, Mosaic release/activation commands, or any model/provider test.
|
||||
|
||||
1. Run the three local recipe fixture tests and `validate-recipe`. This proves recipe/input validation, TLS signature discrimination, and deterministic receipt generation against synthetic fixtures only; it does not prove a rebuilt bundle fixed.
|
||||
2. In two separately created `mktemp -d -p /mnt/storage/scratch/tmp` directories, run `rebuild.sh` with the admitted guard. Compare every same-named file in both `result/` directories byte-for-byte and compare both `SHA256SUMS` files. Any difference refuses.
|
||||
3. In each result, run `sha256sum --check --strict SHA256SUMS`, unpack the package, and rerun `verify-bundle` against that package and the installed build-tree `node_modules/undici`. This is the actual consumed-bundle/fixed-signature gate.
|
||||
4. Require the packed CLI and unbundled CLI to print exactly `0.85.1` under an empty HOME with `PI_OFFLINE=1` and `PI_TELEMETRY=0`, matching upstream `check:package-install`. No request is sent and no provider/model is configured.
|
||||
5. In the Mosaic checkout, run only the no-model seams below. The Pi harness test executes a stand-in binary and fixes the adapter argv contract; the runner test uses its fake adapter/broker and fixes dispatch/publication behavior. Neither consumes credentials or contacts a provider.
|
||||
|
||||
```bash
|
||||
# UNRUN — focused Mosaic compatibility, not the full gate.
|
||||
node --test packages/plugins/tests/harness.test.mjs
|
||||
node --test packages/harness/tests/runner.test.mjs
|
||||
```
|
||||
|
||||
6. Review the artifact manifest, SPDX dependency edges for `[email protected]` and `[email protected]`, provenance materials, and absence of changes outside this recipe directory plus append-only logs. Parent then records the CI URLs/hashes; this recipe does not do API, PR, CI, or release operations.
|
||||
|
||||
## Remaining boundary
|
||||
|
||||
No rebuilt artifact exists yet. Even after admitted CI succeeds, the result has the public npm identity and version `@earendil-works/[email protected]` but different bytes. It cannot be published over that immutable npm version, and this task does not authorize Mosaic to adopt a local tarball or another package source.
|
||||
|
||||
Jason must later choose one separately reviewed consumption boundary:
|
||||
|
||||
1. approve an immutable internal/local artifact URL plus hash and the corresponding Mosaic `package.json`/lock adoption, while retaining the package's internal 0.85.1 identity; or
|
||||
2. approve a fork/new package identity or version and its source/publish/release path.
|
||||
|
||||
Either choice requires separate adoption, CI, release, and runtime-switch authority. No such choice is needed to review this source-ready recipe.
|
||||
|
||||
The generated provenance and SBOM are deterministic but unsigned. A later trusted CI admission must bind their hashes to its authenticated run; this recipe does not claim a trusted-builder signature.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@mosaic/pi-rebuild-recipe",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "UNRUN source-only Pi 0.85.1 / Undici 8.12.2 rebuild recipe and evidence validators",
|
||||
"engines": {
|
||||
"node": "22.23.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test tests/*.test.mjs",
|
||||
"validate": "node src/cli.mjs validate-recipe"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
diff --git a/package-lock.json b/package-lock.json
|
||||
--- a/package-lock.json
|
||||
+++ b/package-lock.json
|
||||
@@ -2523,3 +2523,3 @@
|
||||
- "version": "5.0.9",
|
||||
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
- "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
+ "version": "5.0.12",
|
||||
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||
@@ -4979,3 +4979,3 @@
|
||||
- "version": "8.9.0",
|
||||
- "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||
- "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
|
||||
+ "version": "8.12.2",
|
||||
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
|
||||
+ "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
|
||||
@@ -5441 +5441 @@
|
||||
- "undici": "8.9.0",
|
||||
+ "undici": "8.12.2",
|
||||
diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json
|
||||
--- a/packages/coding-agent/npm-shrinkwrap.json
|
||||
+++ b/packages/coding-agent/npm-shrinkwrap.json
|
||||
@@ -29 +29 @@
|
||||
- "undici": "8.9.0",
|
||||
+ "undici": "8.12.2",
|
||||
@@ -1432,3 +1432,3 @@
|
||||
- "version": "5.0.9",
|
||||
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
- "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
+ "version": "5.0.12",
|
||||
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||
@@ -2144,3 +2144,3 @@
|
||||
- "version": "8.9.0",
|
||||
- "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||
- "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
|
||||
+ "version": "8.12.2",
|
||||
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
|
||||
+ "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
|
||||
diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json
|
||||
--- a/packages/coding-agent/package.json
|
||||
+++ b/packages/coding-agent/package.json
|
||||
@@ -69 +69 @@
|
||||
- "undici": "8.9.0",
|
||||
+ "undici": "8.12.2",
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env bash
|
||||
# UNRUN: this source-only driver is prepared for a separately authorized CI run.
|
||||
# It intentionally refuses unless the caller supplies the Mos2248 review token.
|
||||
set -euo pipefail
|
||||
|
||||
fail() { echo "pi rebuild refused: $*" >&2; exit 2; }
|
||||
|
||||
[ "$#" -eq 1 ] || fail "usage: rebuild.sh WORK_ROOT"
|
||||
[ "${MOSAIC_PI_REBUILD_EXECUTE:-}" = "MOS2248_REVIEWED_UNRUN_RECIPE" ] || fail "set MOSAIC_PI_REBUILD_EXECUTE=MOS2248_REVIEWED_UNRUN_RECIPE only in the admitted rebuild job"
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)"
|
||||
work_root="$1"
|
||||
[ -d "$work_root" ] && [ ! -L "$work_root" ] || fail "WORK_ROOT must be the non-symlink directory returned by mktemp"
|
||||
work_root="$(realpath -- "$work_root")"
|
||||
[ "$(dirname -- "$work_root")" = "/mnt/storage/scratch/tmp" ] || fail "WORK_ROOT must be created by mktemp -d -p /mnt/storage/scratch/tmp"
|
||||
[[ "$(basename -- "$work_root")" =~ ^tmp\.[A-Za-z0-9]{10}$ ]] || fail "WORK_ROOT name must match the exact mktemp -d -p /mnt/storage/scratch/tmp result"
|
||||
[ "$(/usr/bin/stat -c %u -- "$work_root")" = "$(/usr/bin/id -u)" ] || fail "WORK_ROOT must be owned by the invoking user"
|
||||
[ "$(/usr/bin/stat -c %a -- "$work_root")" = "700" ] || fail "WORK_ROOT permissions must be 0700"
|
||||
[ -z "$(find "$work_root" -mindepth 1 -maxdepth 1 -print -quit)" ] || fail "WORK_ROOT must start empty"
|
||||
|
||||
pin_file="$script_dir/PIN.json"
|
||||
patch_file="$script_dir/patches/pi-0.85.1-undici-8.12.2.patch"
|
||||
system_path="/usr/local/bin:/usr/bin:/bin"
|
||||
|
||||
pin() {
|
||||
/usr/bin/env -i PATH="$system_path" /usr/bin/node -e '
|
||||
const value = process.argv[2].split(".").reduce((v, key) => v?.[key], JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")));
|
||||
if (value === undefined || (typeof value === "object" && value !== null)) process.exit(2);
|
||||
process.stdout.write(String(value));
|
||||
' "$pin_file" "$1"
|
||||
}
|
||||
|
||||
mkdir -m 0700 -- "$work_root/inputs" "$work_root/home" "$work_root/npm-cache" "$work_root/source-parent" "$work_root/pack" "$work_root/package-root" "$work_root/tmp"
|
||||
install -m 0600 /dev/null "$work_root/npmrc"
|
||||
install -m 0600 /dev/null "$work_root/npm-globalrc"
|
||||
|
||||
pi_archive="$work_root/inputs/pi-source.tar.gz"
|
||||
undici_archive="$work_root/inputs/undici-8.12.2.tgz"
|
||||
brace_archive="$work_root/inputs/brace-expansion-5.0.12.tgz"
|
||||
node_archive="$work_root/inputs/node-v22.23.4-linux-x64.tar.xz"
|
||||
|
||||
fetch() {
|
||||
url="$1"
|
||||
output="$2"
|
||||
/usr/bin/env -i PATH="$system_path" HOME="$work_root/home" \
|
||||
curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error --output "$output" "$url"
|
||||
}
|
||||
|
||||
fetch "$(pin pi.source.url)" "$pi_archive"
|
||||
fetch "$(pin undici.tarball.url)" "$undici_archive"
|
||||
fetch "$(pin braceExpansion.tarball.url)" "$brace_archive"
|
||||
fetch "$(pin toolchain.node.url)" "$node_archive"
|
||||
|
||||
check_file() {
|
||||
path="$1"
|
||||
expected_bytes="$2"
|
||||
expected_sha256="$3"
|
||||
[ "$(stat -c %s -- "$path")" = "$expected_bytes" ] || fail "byte length mismatch: $path"
|
||||
printf '%s %s\n' "$expected_sha256" "$path" | sha256sum --check --strict --status || fail "sha256 mismatch: $path"
|
||||
}
|
||||
|
||||
check_file "$pi_archive" "$(pin pi.source.bytes)" "$(pin pi.source.sha256)"
|
||||
check_file "$undici_archive" "$(pin undici.tarball.bytes)" "$(pin undici.tarball.sha256)"
|
||||
check_file "$brace_archive" "$(pin braceExpansion.tarball.bytes)" "$(pin braceExpansion.tarball.sha256)"
|
||||
printf '%s %s\n' "$(pin pi.source.sha512)" "$pi_archive" | sha512sum --check --strict --status || fail "Pi source sha512 mismatch"
|
||||
printf '%s %s\n' "$(pin undici.tarball.sha512)" "$undici_archive" | sha512sum --check --strict --status || fail "Undici sha512 mismatch"
|
||||
printf '%s %s\n' "$(pin braceExpansion.tarball.sha512)" "$brace_archive" | sha512sum --check --strict --status || fail "brace-expansion sha512 mismatch"
|
||||
printf '%s %s\n' "$(pin toolchain.node.sha256)" "$node_archive" | sha256sum --check --strict --status || fail "Node archive sha256 mismatch"
|
||||
|
||||
archive_root="$(pin pi.source.archiveRoot)"
|
||||
tar -tzf "$pi_archive" > "$work_root/pi-members"
|
||||
while IFS= read -r member; do
|
||||
case "$member" in
|
||||
"$archive_root"|"$archive_root"/*) ;;
|
||||
*) fail "Pi source archive path escapes its pinned root: $member" ;;
|
||||
esac
|
||||
case "/$member/" in *"/../"*) fail "Pi source archive contains a parent path: $member" ;; esac
|
||||
done < "$work_root/pi-members"
|
||||
tar -tvzf "$pi_archive" | awk '$1 !~ /^[-d]/ { exit 1 }' || fail "Pi source archive contains a link or special entry"
|
||||
tar -xzf "$pi_archive" -C "$work_root/source-parent"
|
||||
source_root="$work_root/source-parent/$archive_root"
|
||||
|
||||
tar -xJf "$node_archive" -C "$work_root"
|
||||
node_root="$work_root/node-v22.23.4-linux-x64"
|
||||
node_bin="$node_root/bin/node"
|
||||
npm_bin="$node_root/bin/npm"
|
||||
|
||||
build_path="$node_root/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
node_env=(
|
||||
/usr/bin/env -i
|
||||
PATH="$build_path"
|
||||
HOME="$work_root/home"
|
||||
XDG_CACHE_HOME="$work_root/home/cache"
|
||||
TMPDIR="$work_root/tmp"
|
||||
LANG=C.UTF-8
|
||||
LC_ALL=C.UTF-8
|
||||
TZ=UTC
|
||||
CI=1
|
||||
SOURCE_DATE_EPOCH="$(pin pi.sourceDateEpoch)"
|
||||
npm_config_cache="$work_root/npm-cache"
|
||||
npm_config_userconfig="$work_root/npmrc"
|
||||
npm_config_globalconfig="$work_root/npm-globalrc"
|
||||
npm_config_registry=https://registry.npmjs.org
|
||||
npm_config_audit=false
|
||||
npm_config_fund=false
|
||||
npm_config_update_notifier=false
|
||||
PI_OFFLINE=1
|
||||
PI_TELEMETRY=0
|
||||
)
|
||||
|
||||
# Even the first fetched-tool version probes use the closed build environment.
|
||||
[ "$("${node_env[@]}" "$node_bin" --version)" = "v$(pin toolchain.node.version)" ] || fail "Node executable version mismatch"
|
||||
[ "$("${node_env[@]}" "$npm_bin" --version)" = "$(pin toolchain.npm.version)" ] || fail "npm executable version mismatch"
|
||||
|
||||
"${node_env[@]}" "$node_bin" "$script_dir/src/cli.mjs" validate-recipe --source-root "$source_root"
|
||||
git -C "$source_root" apply --unidiff-zero --check "$patch_file"
|
||||
git -C "$source_root" apply --unidiff-zero "$patch_file"
|
||||
"${node_env[@]}" "$node_bin" "$script_dir/src/cli.mjs" validate-patched --source-root "$source_root"
|
||||
|
||||
# Network materialization is lock/integrity constrained; package lifecycle scripts stay disabled.
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" ci --ignore-scripts --no-audit --no-fund)
|
||||
|
||||
# These are the source/build checks that remain truthful with the registry install-lock intentionally unchanged.
|
||||
(cd "$source_root" && "${node_env[@]}" npm_config_offline=true "$npm_bin" run build:offline)
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" run check:pinned-deps)
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" run check:runtime-deps)
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" run check:ts-imports)
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" run check:entry-graphs)
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" run check:shrinkwrap)
|
||||
(cd "$source_root" && "${node_env[@]}" "$source_root/node_modules/.bin/tsgo" --noEmit)
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" run check:browser-smoke)
|
||||
(cd "$source_root" && "${node_env[@]}" "$npm_bin" run check:package-install)
|
||||
|
||||
pack_json="$(cd "$source_root/packages/coding-agent" && "${node_env[@]}" "$npm_bin" pack --ignore-scripts --json --pack-destination "$work_root/pack")"
|
||||
artifact_name="$("${node_env[@]}" "$node_bin" -e '
|
||||
const parsed = JSON.parse(process.argv[1]);
|
||||
const entry = Array.isArray(parsed) ? parsed[0] : Object.values(parsed)[0];
|
||||
if (!entry?.filename || /[\\/]/.test(entry.filename)) process.exit(2);
|
||||
process.stdout.write(entry.filename);
|
||||
' "$pack_json")" || fail "npm pack did not return one safe filename"
|
||||
artifact="$work_root/pack/$artifact_name"
|
||||
[ -f "$artifact" ] && [ ! -L "$artifact" ] || fail "packed artifact is absent or unsafe"
|
||||
|
||||
tar -tzf "$artifact" | awk '$0 !~ /^package\// || /(^|\/)\.\.($|\/)/ { exit 1 }' || fail "packed artifact contains an unsafe member"
|
||||
tar -xzf "$artifact" -C "$work_root/package-root"
|
||||
package_root="$work_root/package-root/package"
|
||||
|
||||
"${node_env[@]}" "$node_bin" "$script_dir/src/cli.mjs" verify-bundle \
|
||||
--package-root "$package_root" \
|
||||
--installed-undici-root "$source_root/node_modules/undici"
|
||||
|
||||
mkdir -m 0700 "$work_root/result"
|
||||
"${node_env[@]}" MOSAIC_PI_REBUILD_NPM_VERSION="$(pin toolchain.npm.version)" \
|
||||
"$node_bin" "$script_dir/src/cli.mjs" write-receipts \
|
||||
--artifact "$artifact" \
|
||||
--package-root "$package_root" \
|
||||
--installed-undici-root "$source_root/node_modules/undici" \
|
||||
--output "$work_root/result"
|
||||
install -m 0600 "$artifact" "$work_root/result/$artifact_name"
|
||||
(cd "$work_root/result" && sha256sum --check --strict SHA256SUMS)
|
||||
|
||||
printf 'UNPUBLISHED source rebuild prepared at %s\n' "$work_root/result"
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
assertBundleConsumption,
|
||||
validatePatch,
|
||||
validatePatchedTree,
|
||||
validatePin,
|
||||
validateSourceTree,
|
||||
} from "./recipe.mjs";
|
||||
import { writeReceipts } from "./receipts.mjs";
|
||||
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const pin = JSON.parse(readFileSync(join(packageRoot, "PIN.json"), "utf8"));
|
||||
const patch = readFileSync(join(packageRoot, pin.patch.path), "utf8");
|
||||
|
||||
function usage(message) {
|
||||
if (message) process.stderr.write(`${message}\n`);
|
||||
process.stderr.write("usage: cli.mjs validate-recipe [--source-root DIR] | validate-patched --source-root DIR | verify-bundle --package-root DIR --installed-undici-root DIR | write-receipts --artifact FILE --package-root DIR --installed-undici-root DIR --output DIR\n");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function options(args, allowed) {
|
||||
const parsed = {};
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const name = args[index];
|
||||
const value = args[index + 1];
|
||||
if (!name?.startsWith("--") || value === undefined || value.startsWith("--")) usage(`invalid option sequence at ${name ?? "<end>"}`);
|
||||
const key = name.slice(2);
|
||||
if (!allowed.has(key) || parsed[key] !== undefined) usage(`unknown or repeated option: ${name}`);
|
||||
parsed[key] = resolve(value);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function requireOptions(parsed, names) {
|
||||
for (const name of names) if (!parsed[name]) usage(`--${name} is required`);
|
||||
}
|
||||
|
||||
function print(value) {
|
||||
process.stdout.write(`${JSON.stringify(value, null, "\t")}\n`);
|
||||
}
|
||||
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
try {
|
||||
validatePin(pin);
|
||||
const patchEvidence = validatePatch(patch, pin);
|
||||
switch (command) {
|
||||
case "validate-recipe": {
|
||||
const parsed = options(args, new Set(["source-root"]));
|
||||
const source = parsed["source-root"] ? validateSourceTree(parsed["source-root"], pin) : undefined;
|
||||
print({ state: pin.state, pin: "valid", patch: patchEvidence, ...(source ? { source } : {}) });
|
||||
break;
|
||||
}
|
||||
case "validate-patched": {
|
||||
const parsed = options(args, new Set(["source-root"]));
|
||||
requireOptions(parsed, ["source-root"]);
|
||||
print({ state: pin.state, patchedSource: validatePatchedTree(parsed["source-root"], pin) });
|
||||
break;
|
||||
}
|
||||
case "verify-bundle": {
|
||||
const parsed = options(args, new Set(["package-root", "installed-undici-root"]));
|
||||
requireOptions(parsed, ["package-root", "installed-undici-root"]);
|
||||
print({ state: pin.state, bundle: assertBundleConsumption(parsed["package-root"], pin, { installedUndiciRoot: parsed["installed-undici-root"] }) });
|
||||
break;
|
||||
}
|
||||
case "write-receipts": {
|
||||
const parsed = options(args, new Set(["artifact", "package-root", "installed-undici-root", "output"]));
|
||||
requireOptions(parsed, ["artifact", "package-root", "installed-undici-root", "output"]);
|
||||
const npmVersion = process.env.MOSAIC_PI_REBUILD_NPM_VERSION;
|
||||
if (!npmVersion) throw new Error("MOSAIC_PI_REBUILD_NPM_VERSION is required");
|
||||
print({ state: pin.state, receipts: writeReceipts({
|
||||
artifactPath: parsed.artifact,
|
||||
packageRoot: parsed["package-root"],
|
||||
installedUndiciRoot: parsed["installed-undici-root"],
|
||||
outputDirectory: parsed.output,
|
||||
pin,
|
||||
toolVersions: { node: process.version, npm: npmVersion },
|
||||
}) });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
usage(command ? `unknown command: ${command}` : "command is required");
|
||||
}
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error?.stack ?? error}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, join, posix, relative, resolve, sep } from "node:path";
|
||||
|
||||
import { assertBundleConsumption, constants } from "./recipe.mjs";
|
||||
|
||||
function refuse(condition, message) {
|
||||
if (!condition) throw new Error(`pi rebuild receipts refused: ${message}`);
|
||||
}
|
||||
|
||||
function hashBuffer(contents, algorithm = "sha256") {
|
||||
return createHash(algorithm).update(contents).digest("hex");
|
||||
}
|
||||
|
||||
function fileRecord(path) {
|
||||
const stat = lstatSync(path);
|
||||
refuse(stat.isFile() && !stat.isSymbolicLink(), `not a regular non-symlink file: ${path}`);
|
||||
const contents = readFileSync(path);
|
||||
return { bytes: contents.length, sha256: hashBuffer(contents), sha512: hashBuffer(contents, "sha512") };
|
||||
}
|
||||
|
||||
function jsonFile(path) {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
function writeJson(path, value) {
|
||||
writeFileSync(path, `${JSON.stringify(value, null, "\t")}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
||||
}
|
||||
|
||||
function walk(root, directory = root, records = []) {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const path = join(directory, entry.name);
|
||||
refuse(!entry.isSymbolicLink(), `package inventory refuses symlink: ${path}`);
|
||||
if (entry.isDirectory()) walk(root, path, records);
|
||||
else {
|
||||
refuse(entry.isFile(), `package inventory refuses non-file: ${path}`);
|
||||
const record = fileRecord(path);
|
||||
record.path = relative(root, path).split(sep).join("/");
|
||||
record.mode = (lstatSync(path).mode & 0o777).toString(8).padStart(4, "0");
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
return records.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
function packageName(lockPath, entry, rootManifest) {
|
||||
if (lockPath === "") return rootManifest.name;
|
||||
if (entry.name) return entry.name;
|
||||
const marker = "node_modules/";
|
||||
const index = lockPath.lastIndexOf(marker);
|
||||
refuse(index >= 0, `cannot derive package name from shrinkwrap path ${lockPath}`);
|
||||
const parts = lockPath.slice(index + marker.length).split("/");
|
||||
return parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
|
||||
}
|
||||
|
||||
function spdxId(lockPath) {
|
||||
return `SPDXRef-Package-${hashBuffer(lockPath || "root").slice(0, 24)}`;
|
||||
}
|
||||
|
||||
function integrityChecksum(integrity) {
|
||||
if (typeof integrity !== "string") return undefined;
|
||||
const match = integrity.match(/^sha512-(.+)$/);
|
||||
if (!match) return undefined;
|
||||
return { algorithm: "SHA512", checksumValue: Buffer.from(match[1], "base64").toString("hex") };
|
||||
}
|
||||
|
||||
function dependencyNames(entry) {
|
||||
return Object.keys({ ...(entry.dependencies ?? {}), ...(entry.optionalDependencies ?? {}) }).sort();
|
||||
}
|
||||
|
||||
function resolveDependency(packages, name, from) {
|
||||
let current = from;
|
||||
const candidates = [];
|
||||
while (true) {
|
||||
candidates.push(current ? `${current}/node_modules/${name}` : `node_modules/${name}`);
|
||||
if (!current) break;
|
||||
const parent = posix.dirname(current);
|
||||
current = parent === "." || parent === current ? "" : parent;
|
||||
}
|
||||
return candidates.find((candidate) => packages[candidate] && !packages[candidate].link);
|
||||
}
|
||||
|
||||
function createSpdx(packageRoot, artifact, pin) {
|
||||
const manifest = jsonFile(join(packageRoot, "package.json"));
|
||||
const lock = jsonFile(join(packageRoot, "npm-shrinkwrap.json"));
|
||||
const entries = Object.entries(lock.packages ?? {})
|
||||
.filter(([, entry]) => !entry.link)
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
const packages = entries.map(([lockPath, entry]) => {
|
||||
const name = packageName(lockPath, entry, manifest);
|
||||
const version = entry.version ?? manifest.version;
|
||||
const checksum = integrityChecksum(entry.integrity);
|
||||
return {
|
||||
SPDXID: spdxId(lockPath),
|
||||
name,
|
||||
versionInfo: version,
|
||||
downloadLocation: entry.resolved ?? "NOASSERTION",
|
||||
filesAnalyzed: false,
|
||||
licenseConcluded: "NOASSERTION",
|
||||
licenseDeclared: entry.license ?? "NOASSERTION",
|
||||
copyrightText: "NOASSERTION",
|
||||
...(checksum ? { checksums: [checksum] } : {}),
|
||||
externalRefs: [{
|
||||
referenceCategory: "PACKAGE-MANAGER",
|
||||
referenceType: "purl",
|
||||
referenceLocator: `pkg:npm/${encodeURIComponent(name).replaceAll("%2F", "/")}@${encodeURIComponent(version)}`,
|
||||
}],
|
||||
comment: `npm shrinkwrap path: ${lockPath || "<root>"}`,
|
||||
};
|
||||
});
|
||||
const relationships = [{
|
||||
spdxElementId: "SPDXRef-DOCUMENT",
|
||||
relationshipType: "DESCRIBES",
|
||||
relatedSpdxElement: spdxId(""),
|
||||
}];
|
||||
for (const [lockPath, entry] of entries) {
|
||||
for (const name of dependencyNames(entry)) {
|
||||
const target = resolveDependency(lock.packages, name, lockPath);
|
||||
refuse(target, `cannot resolve ${name} from shrinkwrap path ${lockPath || "<root>"}`);
|
||||
relationships.push({
|
||||
spdxElementId: spdxId(lockPath),
|
||||
relationshipType: "DEPENDS_ON",
|
||||
relatedSpdxElement: spdxId(target),
|
||||
});
|
||||
}
|
||||
}
|
||||
relationships.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
||||
return {
|
||||
spdxVersion: "SPDX-2.3",
|
||||
dataLicense: "CC0-1.0",
|
||||
SPDXID: "SPDXRef-DOCUMENT",
|
||||
name: `${manifest.name}@${manifest.version}-undici-${pin.undici.version}`,
|
||||
documentNamespace: `https://mosaicstack.invalid/spdx/pi-rebuild/${artifact.sha256}`,
|
||||
creationInfo: {
|
||||
created: pin.pi.commitTimestamp,
|
||||
creators: ["Tool: @mosaic/pi-rebuild-recipe"],
|
||||
},
|
||||
documentDescribes: [spdxId("")],
|
||||
packages,
|
||||
relationships,
|
||||
};
|
||||
}
|
||||
|
||||
function createManifest(packageRoot, artifactPath, pin, evidence, toolVersions) {
|
||||
const artifact = fileRecord(artifactPath);
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
identity: {
|
||||
name: constants.piName,
|
||||
version: constants.piVersion,
|
||||
undici: constants.undiciVersion,
|
||||
braceExpansion: constants.braceExpansionVersion,
|
||||
},
|
||||
artifact: { file: basename(artifactPath), ...artifact },
|
||||
inputs: {
|
||||
piSource: { url: pin.pi.source.url, sha256: pin.pi.source.sha256, sha512: pin.pi.source.sha512 },
|
||||
undiciTarball: { url: pin.undici.tarball.url, sha256: pin.undici.tarball.sha256, sha512: pin.undici.tarball.sha512 },
|
||||
braceExpansionTarball: { url: pin.braceExpansion.tarball.url, sha256: pin.braceExpansion.tarball.sha256, sha512: pin.braceExpansion.tarball.sha512 },
|
||||
patch: { path: pin.patch.path, sha256: pin.patch.sha256 },
|
||||
node: { url: pin.toolchain.node.url, sha256: pin.toolchain.node.sha256 },
|
||||
},
|
||||
toolchain: {
|
||||
node: toolVersions.node,
|
||||
npm: toolVersions.npm,
|
||||
esbuild: pin.toolchain.esbuild.version,
|
||||
platform: pin.toolchain.platform,
|
||||
},
|
||||
consumedBundle: evidence,
|
||||
packageFiles: walk(packageRoot),
|
||||
};
|
||||
}
|
||||
|
||||
function createProvenance(artifactPath, artifact, manifestHash, sbomHash, pin, toolVersions) {
|
||||
return {
|
||||
_type: "https://in-toto.io/Statement/v1",
|
||||
subject: [{
|
||||
name: basename(artifactPath),
|
||||
digest: { sha256: artifact.sha256, sha512: artifact.sha512 },
|
||||
}],
|
||||
predicateType: "https://slsa.dev/provenance/v1",
|
||||
predicate: {
|
||||
buildDefinition: {
|
||||
buildType: "https://mosaicstack.invalid/buildtypes/pi-source-rebuild/v1",
|
||||
externalParameters: {
|
||||
pi: { name: constants.piName, version: constants.piVersion, commit: pin.pi.commit },
|
||||
undici: { version: constants.undiciVersion },
|
||||
braceExpansion: { version: constants.braceExpansionVersion },
|
||||
sourceDateEpoch: pin.pi.sourceDateEpoch,
|
||||
},
|
||||
internalParameters: {
|
||||
patchSha256: pin.patch.sha256,
|
||||
manifestSha256: manifestHash,
|
||||
sbomSha256: sbomHash,
|
||||
toolchain: toolVersions,
|
||||
},
|
||||
resolvedDependencies: [
|
||||
{ uri: pin.pi.source.url, digest: { sha256: pin.pi.source.sha256, sha512: pin.pi.source.sha512 } },
|
||||
{ uri: pin.undici.tarball.url, digest: { sha256: pin.undici.tarball.sha256, sha512: pin.undici.tarball.sha512 } },
|
||||
{ uri: pin.braceExpansion.tarball.url, digest: { sha256: pin.braceExpansion.tarball.sha256, sha512: pin.braceExpansion.tarball.sha512 } },
|
||||
{ uri: pin.toolchain.node.url, digest: { sha256: pin.toolchain.node.sha256 } },
|
||||
{ uri: `file:${pin.patch.path}`, digest: { sha256: pin.patch.sha256 } },
|
||||
{ uri: "file:package-lock.json", digest: { sha256: pin.sourcePostimages["package-lock.json"] } },
|
||||
],
|
||||
},
|
||||
runDetails: {
|
||||
builder: { id: "https://mosaicstack.invalid/builders/pi-source-rebuild/v1" },
|
||||
metadata: { reproducibleEpoch: pin.pi.sourceDateEpoch },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function writeReceipts({ artifactPath, packageRoot, installedUndiciRoot, outputDirectory, pin, toolVersions }) {
|
||||
refuse(toolVersions?.node === `v${pin.toolchain.node.version}`, `Node must be v${pin.toolchain.node.version}`);
|
||||
refuse(toolVersions?.npm === pin.toolchain.npm.version, `npm must be ${pin.toolchain.npm.version}`);
|
||||
const artifact = fileRecord(artifactPath);
|
||||
const evidence = assertBundleConsumption(packageRoot, pin, { installedUndiciRoot });
|
||||
const output = resolve(outputDirectory);
|
||||
if (existsSync(output)) refuse(readdirSync(output).length === 0, `receipt directory is not empty: ${output}`);
|
||||
else mkdirSync(output, { recursive: false, mode: 0o700 });
|
||||
|
||||
const manifest = createManifest(packageRoot, artifactPath, pin, evidence, toolVersions);
|
||||
const sbom = createSpdx(packageRoot, artifact, pin);
|
||||
const manifestPath = join(output, "artifact-manifest.json");
|
||||
const sbomPath = join(output, "sbom.spdx.json");
|
||||
writeJson(manifestPath, manifest);
|
||||
writeJson(sbomPath, sbom);
|
||||
const manifestHash = fileRecord(manifestPath).sha256;
|
||||
const sbomHash = fileRecord(sbomPath).sha256;
|
||||
const provenance = createProvenance(artifactPath, artifact, manifestHash, sbomHash, pin, toolVersions);
|
||||
const provenancePath = join(output, "provenance.intoto.json");
|
||||
writeJson(provenancePath, provenance);
|
||||
|
||||
const records = [
|
||||
[artifactPath, artifact.sha256],
|
||||
[manifestPath, manifestHash],
|
||||
[sbomPath, sbomHash],
|
||||
[provenancePath, fileRecord(provenancePath).sha256],
|
||||
].map(([path, sha256]) => ({ file: basename(path), sha256 })).sort((a, b) => a.file.localeCompare(b.file));
|
||||
writeFileSync(join(output, "SHA256SUMS"), `${records.map(({ file, sha256 }) => `${sha256} ${file}`).join("\n")}\n`, { flag: "wx", mode: 0o600 });
|
||||
return { artifact, evidence, files: records };
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
const PI_NAME = "@earendil-works/pi-coding-agent";
|
||||
const PI_VERSION = "0.85.1";
|
||||
const PI_COMMIT = "d981de1229ef899957bbe968bc8dcda02a21f477";
|
||||
const UNDICI_OLD = "8.9.0";
|
||||
const UNDICI_FIXED = "8.12.2";
|
||||
const UNDICI_URL = "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz";
|
||||
const UNDICI_INTEGRITY = "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==";
|
||||
const BRACE_OLD = "5.0.9";
|
||||
const BRACE_FIXED = "5.0.12";
|
||||
const BRACE_URL = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz";
|
||||
const BRACE_INTEGRITY = "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==";
|
||||
const PATCH_TARGETS = [
|
||||
"package-lock.json",
|
||||
"packages/coding-agent/npm-shrinkwrap.json",
|
||||
"packages/coding-agent/package.json",
|
||||
];
|
||||
const ALLOWED_HOSTS = new Set(["codeload.github.com", "nodejs.org", "raw.githubusercontent.com", "registry.npmjs.org"]);
|
||||
|
||||
function refuse(condition, message) {
|
||||
if (!condition) throw new Error(`pi rebuild recipe refused: ${message}`);
|
||||
}
|
||||
|
||||
function at(object, path) {
|
||||
return path.split(".").reduce((value, key) => value?.[key], object);
|
||||
}
|
||||
|
||||
function expect(pin, path, expected) {
|
||||
const actual = at(pin, path);
|
||||
refuse(actual === expected, `${path} must be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||||
}
|
||||
|
||||
function expectHash(value, length, label) {
|
||||
refuse(typeof value === "string" && new RegExp(`^[0-9a-f]{${length}}$`).test(value), `${label} is not a lowercase ${length}-hex digest`);
|
||||
}
|
||||
|
||||
function validateUrl(value, expected, label) {
|
||||
expect({ value }, "value", expected);
|
||||
const url = new URL(value);
|
||||
refuse(url.protocol === "https:", `${label} must use HTTPS`);
|
||||
refuse(!url.username && !url.password && !url.hash, `${label} must not contain userinfo or a fragment`);
|
||||
refuse(ALLOWED_HOSTS.has(url.hostname), `${label} host is not approved: ${url.hostname}`);
|
||||
}
|
||||
|
||||
function json(path) {
|
||||
const stat = lstatSync(path);
|
||||
refuse(stat.isFile() && !stat.isSymbolicLink(), `JSON input is not a regular non-symlink file: ${path}`);
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
function digest(contents, algorithm = "sha256") {
|
||||
return createHash(algorithm).update(contents).digest("hex");
|
||||
}
|
||||
|
||||
function fileDigest(path, algorithm = "sha256") {
|
||||
const stat = lstatSync(path);
|
||||
refuse(stat.isFile() && !stat.isSymbolicLink(), `not a regular non-symlink file: ${path}`);
|
||||
return digest(readFileSync(path), algorithm);
|
||||
}
|
||||
|
||||
function safePath(root, path) {
|
||||
refuse(typeof path === "string" && path.length > 0 && !isAbsolute(path), `unsafe relative path: ${path}`);
|
||||
const target = resolve(root, path);
|
||||
const base = `${resolve(root)}${sep}`;
|
||||
refuse(target.startsWith(base), `path escapes its root: ${path}`);
|
||||
return target;
|
||||
}
|
||||
|
||||
function validateDigestMap(root, map, label) {
|
||||
refuse(map && typeof map === "object" && !Array.isArray(map), `${label} must be an object`);
|
||||
for (const [path, expected] of Object.entries(map)) {
|
||||
expectHash(expected, 64, `${label}.${path}`);
|
||||
const actual = fileDigest(safePath(root, path));
|
||||
refuse(actual === expected, `${label} sha256 preimage mismatch for ${path}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
function directUndici(lock) {
|
||||
return lock?.packages?.["node_modules/undici"];
|
||||
}
|
||||
|
||||
function directBraceExpansion(lock) {
|
||||
return lock?.packages?.["node_modules/brace-expansion"];
|
||||
}
|
||||
|
||||
function codingWorkspace(lock) {
|
||||
return lock?.packages?.["packages/coding-agent"];
|
||||
}
|
||||
|
||||
function shrinkwrapRoot(lock) {
|
||||
return lock?.packages?.[""];
|
||||
}
|
||||
|
||||
function assertUndiciResolution(entry, version, url, integrity, label) {
|
||||
refuse(entry?.version === version, `${label} version must be ${version}`);
|
||||
if (url !== undefined) refuse(entry?.resolved === url, `${label} resolved URL must be ${url}`);
|
||||
if (integrity !== undefined) refuse(entry?.integrity === integrity, `${label} integrity must be ${integrity}`);
|
||||
}
|
||||
|
||||
export function validatePin(pin) {
|
||||
refuse(pin && typeof pin === "object" && !Array.isArray(pin), "PIN must be an object");
|
||||
expect(pin, "schemaVersion", 1);
|
||||
expect(pin, "state", "UNRUN");
|
||||
expect(pin, "ruling", "Mos2248");
|
||||
expect(pin, "pi.name", PI_NAME);
|
||||
expect(pin, "pi.version", PI_VERSION);
|
||||
expect(pin, "pi.tag", "v0.85.1");
|
||||
expect(pin, "pi.commit", PI_COMMIT);
|
||||
expect(pin, "pi.tree", "346294a615d2d0ad4f6e5fbccb4cee4ccd7b2d6c");
|
||||
expect(pin, "pi.commitTimestamp", "2026-09-05T11:54:46Z");
|
||||
expect(pin, "pi.sourceDateEpoch", 1788609286);
|
||||
validateUrl(pin.pi?.source?.url, `https://codeload.github.com/earendil-works/pi/tar.gz/${PI_COMMIT}`, "Pi source URL");
|
||||
expect(pin, "pi.source.archiveRoot", `pi-${PI_COMMIT}`);
|
||||
expect(pin, "pi.source.bytes", 6978387);
|
||||
expect(pin, "pi.source.sha256", "d2a04b35e57f59873d67f667f22e681e93c78a46ba8e2cdedf9d4c323b1ae8dc");
|
||||
expect(pin, "pi.source.sha512", "77acb7819e75da17059cb507064739d6eaf033ad76b9a5d2b090cb607bf97ddff5a94ec0ae57ce5f1c168c95f370b6e97513e667fdb47b53ed55f486286a2ae9");
|
||||
validateUrl(pin.pi?.registryReference?.metadataUrl, "https://registry.npmjs.org/@earendil-works%2fpi-coding-agent/0.85.1", "Pi registry metadata URL");
|
||||
validateUrl(pin.pi?.registryReference?.tarballUrl, "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.85.1.tgz", "Pi registry tarball URL");
|
||||
expect(pin, "pi.registryReference.gitHead", PI_COMMIT);
|
||||
expect(pin, "pi.registryReference.integrity", "sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ==");
|
||||
expect(pin, "pi.registryReference.tarballSha256", "1f498729649bdce647d1160993b4d92bf3c614cc819213bee2f91dd34f2a7af4");
|
||||
expect(pin, "pi.registryReference.tarballSha512", "14644df8e1db59a79f04f19a4e080074b8eb2075beb363f32f2825cffe5d7cbcdbf687fbbae5cc5d80b47c921e653c3eb214b7da8d9cb90f6317b6120ee2ffa1");
|
||||
validateUrl(pin.pi?.registryReference?.attestationsUrl, "https://registry.npmjs.org/-/npm/v1/attestations/@earendil-works%[email protected]", "Pi attestations URL");
|
||||
|
||||
expect(pin, "undici.name", "undici");
|
||||
expect(pin, "undici.version", UNDICI_FIXED);
|
||||
expect(pin, "undici.gitHead", "bb6cc0ae9b6827ed273ed5ba344148273629638f");
|
||||
validateUrl(pin.undici?.metadata?.url, "https://registry.npmjs.org/undici/8.12.2", "Undici metadata URL");
|
||||
validateUrl(pin.undici?.tarball?.url, UNDICI_URL, "Undici tarball URL");
|
||||
expect(pin, "undici.tarball.bytes", 542130);
|
||||
expect(pin, "undici.tarball.sha256", "a765b5e63ab5c6b1b91a59072c88f7857b36f47b09c376762241d177279cdefd");
|
||||
expect(pin, "undici.tarball.sha512", "5edb8359576a4cbaf7340c66da5cbdfe153a914b093a01a1d51012ccc05bb30e9a58239a4331dd5be4100df7444637c9d95e6f2f9a513dae117ad3bd51f8af0e");
|
||||
expect(pin, "undici.tarball.integrity", UNDICI_INTEGRITY);
|
||||
validateUrl(pin.undici?.attestations?.url, "https://registry.npmjs.org/-/npm/v1/attestations/[email protected]", "Undici attestations URL");
|
||||
expect(pin, "braceExpansion.name", "brace-expansion");
|
||||
expect(pin, "braceExpansion.version", BRACE_FIXED);
|
||||
expect(pin, "braceExpansion.gitHead", "f3410159d768f56c9d9f4511d3e1b46425fc1099");
|
||||
validateUrl(pin.braceExpansion?.metadata?.url, "https://registry.npmjs.org/brace-expansion/5.0.12", "brace-expansion metadata URL");
|
||||
expect(pin, "braceExpansion.metadata.sha256", "5421e68ea58a0286041e3f11998da4fa44055026b11c527cc14ec74895b58929");
|
||||
validateUrl(pin.braceExpansion?.tarball?.url, BRACE_URL, "brace-expansion tarball URL");
|
||||
expect(pin, "braceExpansion.tarball.bytes", 12620);
|
||||
expect(pin, "braceExpansion.tarball.sha256", "ef8448ec78f20b692f04fa6d01f39b5ab34c66404bea3429f5a39c6c9e0be8b4");
|
||||
expect(pin, "braceExpansion.tarball.sha512", "628bd0debce168b308ac38cd0cc90d4b4d6d79af77aa11211b9c72f1febe47497e770dca8bee6c0a822823de368ae2d94c75a881692d830543854d3d88d12299");
|
||||
expect(pin, "braceExpansion.tarball.integrity", BRACE_INTEGRITY);
|
||||
|
||||
expect(pin, "toolchain.platform", "linux-x64");
|
||||
expect(pin, "toolchain.node.version", "22.23.4");
|
||||
validateUrl(pin.toolchain?.node?.url, "https://nodejs.org/dist/v22.23.4/node-v22.23.4-linux-x64.tar.xz", "Node archive URL");
|
||||
expect(pin, "toolchain.node.sha256", "1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce");
|
||||
validateUrl(pin.toolchain?.node?.releaseUrl, "https://nodejs.org/en/blog/release/v22.23.4", "Node release URL");
|
||||
validateUrl(pin.toolchain?.node?.securityReleaseUrl, "https://nodejs.org/en/blog/vulnerability/october-2026-security-releases", "Node security release URL");
|
||||
validateUrl(pin.toolchain?.node?.shasumsUrl, "https://nodejs.org/dist/v22.23.4/SHASUMS256.txt", "Node SHASUMS URL");
|
||||
expect(pin, "toolchain.node.shasumsSha256", "c90c3f1478fda8308b22cc1a1b0dc536169196444e67ee8b5c582fd351d02bce");
|
||||
expect(pin, "toolchain.node.pgpSignatureVerified", false);
|
||||
expect(pin, "toolchain.npm.version", "10.9.9");
|
||||
validateUrl(pin.toolchain?.npm?.packageJsonUrl, "https://raw.githubusercontent.com/nodejs/node/v22.23.4/deps/npm/package.json", "bundled npm package metadata URL");
|
||||
expect(pin, "toolchain.npm.packageJsonSha256", "7c04bf69824d17a1db64defd3a9a86d538781bf2d6ca2764e3f1143fa5b71166");
|
||||
expect(pin, "toolchain.esbuild.version", "0.28.1");
|
||||
|
||||
expect(pin, "patch.path", "patches/pi-0.85.1-undici-8.12.2.patch");
|
||||
expect(pin, "patch.sha256", "6f1c3aaf5c6bdf08cfb02932d77251d8048130ba59c63a099b25cae26cb0bf0e");
|
||||
refuse(JSON.stringify(pin.patch?.allowedTargets) === JSON.stringify(PATCH_TARGETS), "patch target allowlist changed");
|
||||
expect(pin, "securityEvidence.briefSha256", "83b9462ba40e93934fcf3cc96617f59dff3950d4321e3351eb49ab83a03c1359");
|
||||
expect(pin, "securityEvidence.a5ReportSha256", "71270a41a957921b5a83b09650f817fdaef31ef4e762f03a68c8eed8207fb307");
|
||||
expect(pin, "securityEvidence.a5MetadataSha256", "4ae5158491a25d99778fa84cefe65260c0d6aed701b6e710de2c80ba8e39877f");
|
||||
|
||||
for (const [label, value] of [
|
||||
["Pi metadata", pin.pi?.registryReference?.metadataSha256],
|
||||
["Pi attestations", pin.pi?.registryReference?.attestationsSha256],
|
||||
["Undici metadata", pin.undici?.metadata?.sha256],
|
||||
["Undici attestations", pin.undici?.attestations?.sha256],
|
||||
["brace-expansion metadata", pin.braceExpansion?.metadata?.sha256],
|
||||
["Node SHASUMS", pin.toolchain?.node?.shasumsSha256],
|
||||
["bundled npm package metadata", pin.toolchain?.npm?.packageJsonSha256],
|
||||
["patch", pin.patch?.sha256],
|
||||
...Object.entries(pin.sourcePreimages ?? {}).map(([path, value]) => [`sourcePreimages.${path}`, value]),
|
||||
...Object.entries(pin.sourcePostimages ?? {}).map(([path, value]) => [`sourcePostimages.${path}`, value]),
|
||||
...Object.entries(pin.undici?.fixedSourceFiles ?? {}).map(([path, value]) => [`fixedSourceFiles.${path}`, value]),
|
||||
]) expectHash(value, 64, label);
|
||||
|
||||
return pin;
|
||||
}
|
||||
|
||||
export function validatePatch(patch, pin) {
|
||||
refuse(typeof patch === "string" && patch.length > 0, "patch is empty");
|
||||
refuse(digest(patch) === pin.patch.sha256, `patch sha256 does not match ${pin.patch.sha256}`);
|
||||
const targets = [...patch.matchAll(/^diff --git a\/(.+) b\/(.+)$/gm)].map((match) => {
|
||||
refuse(match[1] === match[2], `patch renames a target: ${match[1]} -> ${match[2]}`);
|
||||
return match[1];
|
||||
});
|
||||
refuse(JSON.stringify(targets) === JSON.stringify(pin.patch.allowedTargets), `patch targets must be exactly ${pin.patch.allowedTargets.join(", ")}`);
|
||||
refuse(!patch.includes("install-lock"), "patch must not rewrite the registry install-lock");
|
||||
|
||||
const allowedOld = new Set([
|
||||
'\t\t\t"version": "8.9.0",',
|
||||
'\t\t\t"resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",',
|
||||
'\t\t\t"integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",',
|
||||
'\t\t\t\t"undici": "8.9.0",',
|
||||
'\t\t"undici": "8.9.0",',
|
||||
'\t\t\t"version": "5.0.9",',
|
||||
'\t\t\t"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",',
|
||||
'\t\t\t"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",',
|
||||
]);
|
||||
const allowedNew = new Set([
|
||||
'\t\t\t"version": "8.12.2",',
|
||||
`\t\t\t"resolved": "${UNDICI_URL}",`,
|
||||
`\t\t\t"integrity": "${UNDICI_INTEGRITY}",`,
|
||||
'\t\t\t\t"undici": "8.12.2",',
|
||||
'\t\t"undici": "8.12.2",',
|
||||
'\t\t\t"version": "5.0.12",',
|
||||
`\t\t\t"resolved": "${BRACE_URL}",`,
|
||||
`\t\t\t"integrity": "${BRACE_INTEGRITY}",`,
|
||||
]);
|
||||
for (const line of patch.split("\n")) {
|
||||
if (line.startsWith("---") || line.startsWith("+++")) continue;
|
||||
if (line.startsWith("-")) refuse(allowedOld.has(line.slice(1)), `unapproved patch deletion: ${line}`);
|
||||
if (line.startsWith("+")) refuse(allowedNew.has(line.slice(1)), `unapproved patch addition: ${line}`);
|
||||
}
|
||||
refuse((patch.match(/8\.9\.0/g) ?? []).length === 7, "patch must replace exactly seven 8.9.0 literals");
|
||||
refuse((patch.match(/8\.12\.2/g) ?? []).length === 7, "patch must add exactly seven 8.12.2 literals");
|
||||
refuse((patch.match(/5\.0\.9/g) ?? []).length === 4, "patch must replace exactly four 5.0.9 literals");
|
||||
refuse((patch.match(/5\.0\.12/g) ?? []).length === 4, "patch must add exactly four 5.0.12 literals");
|
||||
return { sha256: digest(patch), targets };
|
||||
}
|
||||
|
||||
export function validateSourceTree(root, pin) {
|
||||
validateDigestMap(root, pin.sourcePreimages, "source preimage");
|
||||
const packageLock = json(safePath(root, "package-lock.json"));
|
||||
const codingPackage = json(safePath(root, "packages/coding-agent/package.json"));
|
||||
const shrinkwrap = json(safePath(root, "packages/coding-agent/npm-shrinkwrap.json"));
|
||||
const bundler = readFileSync(safePath(root, "scripts/build-coding-agent-bundle.mjs"), "utf8");
|
||||
|
||||
refuse(codingPackage.name === PI_NAME && codingPackage.version === PI_VERSION, "source coding-agent identity changed");
|
||||
refuse(codingPackage.bin?.pi === "dist/bundle/cli.js", "source Pi bin is not the bundled CLI");
|
||||
refuse(codingPackage.dependencies?.undici === UNDICI_OLD, `source coding-agent must start at Undici ${UNDICI_OLD}`);
|
||||
assertUndiciResolution(directUndici(packageLock), UNDICI_OLD, "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", undefined, "source package-lock Undici");
|
||||
refuse(codingWorkspace(packageLock)?.dependencies?.undici === UNDICI_OLD, "source workspace lock Undici changed");
|
||||
assertUndiciResolution(directUndici(shrinkwrap), UNDICI_OLD, "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", undefined, "source shrinkwrap Undici");
|
||||
refuse(shrinkwrapRoot(shrinkwrap)?.dependencies?.undici === UNDICI_OLD, "source shrinkwrap root Undici changed");
|
||||
assertUndiciResolution(directBraceExpansion(packageLock), BRACE_OLD, "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", undefined, "source package-lock brace-expansion");
|
||||
assertUndiciResolution(directBraceExpansion(shrinkwrap), BRACE_OLD, "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", undefined, "source shrinkwrap brace-expansion");
|
||||
refuse(packageLock.packages?.["node_modules/@earendil-works/gondolin/node_modules/undici"]?.version === "6.28.0", "unrelated Gondolin Undici pin changed");
|
||||
refuse(/bundle:\s*true/.test(bundler), "upstream coding-agent build no longer bundles dependencies");
|
||||
const externalBlock = bundler.match(/allowedExternalPackages\s*=\s*new Set\(\[([\s\S]*?)\]\)/)?.[1] ?? "";
|
||||
refuse(externalBlock.length > 0 && !/["']undici(?:\/[^"']*)?["']/.test(externalBlock), "Undici is externalized or the bundler allowlist is unreadable");
|
||||
|
||||
if (pin.sourcePreimages["packages/coding-agent/install-lock/package-lock.json"]) {
|
||||
const installLock = json(safePath(root, "packages/coding-agent/install-lock/package-lock.json"));
|
||||
assertUndiciResolution(directUndici(installLock), UNDICI_OLD, "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", undefined, "published-registry install-lock Undici");
|
||||
assertUndiciResolution(directBraceExpansion(installLock), BRACE_OLD, "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", undefined, "published-registry install-lock brace-expansion");
|
||||
}
|
||||
return { piVersion: codingPackage.version, undiciVersion: codingPackage.dependencies.undici, braceExpansionVersion: directBraceExpansion(shrinkwrap).version, bin: codingPackage.bin.pi };
|
||||
}
|
||||
|
||||
export function validatePatchedTree(root, pin) {
|
||||
validateDigestMap(root, pin.sourcePostimages, "source postimage");
|
||||
const packageLock = json(safePath(root, "package-lock.json"));
|
||||
const codingPackage = json(safePath(root, "packages/coding-agent/package.json"));
|
||||
const shrinkwrap = json(safePath(root, "packages/coding-agent/npm-shrinkwrap.json"));
|
||||
refuse(codingPackage.name === PI_NAME && codingPackage.version === PI_VERSION, "patched coding-agent identity changed");
|
||||
refuse(codingPackage.bin?.pi === "dist/bundle/cli.js", "patched Pi bin is not the bundled CLI");
|
||||
refuse(codingPackage.dependencies?.undici === UNDICI_FIXED, `patched coding-agent must use Undici ${UNDICI_FIXED}`);
|
||||
assertUndiciResolution(directUndici(packageLock), UNDICI_FIXED, UNDICI_URL, UNDICI_INTEGRITY, "patched package-lock Undici");
|
||||
refuse(codingWorkspace(packageLock)?.dependencies?.undici === UNDICI_FIXED, "patched workspace lock Undici changed");
|
||||
assertUndiciResolution(directUndici(shrinkwrap), UNDICI_FIXED, UNDICI_URL, UNDICI_INTEGRITY, "patched shrinkwrap Undici");
|
||||
refuse(shrinkwrapRoot(shrinkwrap)?.dependencies?.undici === UNDICI_FIXED, "patched shrinkwrap root Undici changed");
|
||||
assertUndiciResolution(directBraceExpansion(packageLock), BRACE_FIXED, BRACE_URL, BRACE_INTEGRITY, "patched package-lock brace-expansion");
|
||||
assertUndiciResolution(directBraceExpansion(shrinkwrap), BRACE_FIXED, BRACE_URL, BRACE_INTEGRITY, "patched shrinkwrap brace-expansion");
|
||||
refuse(packageLock.packages?.["node_modules/@earendil-works/gondolin/node_modules/undici"]?.version === "6.28.0", "unrelated Gondolin Undici pin changed");
|
||||
const installLock = json(safePath(root, "packages/coding-agent/install-lock/package-lock.json"));
|
||||
assertUndiciResolution(directUndici(installLock), UNDICI_OLD, "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", undefined, "unchanged published-registry install-lock Undici");
|
||||
assertUndiciResolution(directBraceExpansion(installLock), BRACE_OLD, "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", undefined, "unchanged published-registry install-lock brace-expansion");
|
||||
return { piVersion: codingPackage.version, undiciVersion: codingPackage.dependencies.undici, braceExpansionVersion: directBraceExpansion(shrinkwrap).version, bin: codingPackage.bin.pi };
|
||||
}
|
||||
|
||||
export function assertInstalledUndici(root, pin) {
|
||||
const manifest = json(safePath(root, "package.json"));
|
||||
refuse(manifest.name === "undici" && manifest.version === UNDICI_FIXED, `installed Undici must be exactly ${UNDICI_FIXED}`);
|
||||
for (const [path, expected] of Object.entries(pin.undici.fixedSourceFiles)) {
|
||||
const actual = fileDigest(safePath(root, path));
|
||||
refuse(actual === expected, `installed Undici fixed-source hash mismatch for ${path}`);
|
||||
}
|
||||
return { version: manifest.version, sourceFiles: pin.undici.fixedSourceFiles };
|
||||
}
|
||||
|
||||
function quoted(contents, start) {
|
||||
const quote = contents[start];
|
||||
let value = "";
|
||||
for (let index = start + 1; index < contents.length; index++) {
|
||||
const character = contents[index];
|
||||
if (character === "\\") {
|
||||
refuse(index + 1 < contents.length, "unterminated escape in bundled JavaScript");
|
||||
value += contents[index + 1];
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (character === quote) return { next: index + 1, value };
|
||||
value += character;
|
||||
}
|
||||
throw new Error(`pi rebuild recipe refused: unterminated string at byte ${start} in bundled JavaScript near ${JSON.stringify(contents.slice(Math.max(0, start - 80), start + 160))}`);
|
||||
}
|
||||
|
||||
function skipTrivia(contents, start) {
|
||||
let index = start;
|
||||
while (index < contents.length) {
|
||||
if (/\s/.test(contents[index])) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (contents.startsWith("//", index)) {
|
||||
const end = contents.indexOf("\n", index + 2);
|
||||
index = end < 0 ? contents.length : end + 1;
|
||||
continue;
|
||||
}
|
||||
if (contents.startsWith("/*", index)) {
|
||||
const end = contents.indexOf("*/", index + 2);
|
||||
refuse(end >= 0, "unterminated block comment in bundled JavaScript");
|
||||
index = end + 2;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function identifierAt(contents, start) {
|
||||
if (!/[A-Za-z_$]/.test(contents[start] ?? "")) return undefined;
|
||||
let end = start + 1;
|
||||
while (/[A-Za-z0-9_$]/.test(contents[end] ?? "")) end++;
|
||||
return { end, value: contents.slice(start, end) };
|
||||
}
|
||||
|
||||
function importClauseSpecifier(contents, start) {
|
||||
let index = start;
|
||||
while (index < contents.length) {
|
||||
index = skipTrivia(contents, index);
|
||||
if (contents[index] === ";") return undefined;
|
||||
if (contents[index] === '"' || contents[index] === "'") {
|
||||
index = quoted(contents, index).next;
|
||||
continue;
|
||||
}
|
||||
const identifier = identifierAt(contents, index);
|
||||
if (identifier) {
|
||||
index = identifier.end;
|
||||
if (identifier.value !== "from") continue;
|
||||
index = skipTrivia(contents, index);
|
||||
refuse(contents[index] === '"' || contents[index] === "'", "module from-clause has no literal specifier");
|
||||
return quoted(contents, index);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function regexLiteralEnd(contents, start) {
|
||||
let characterClass = false;
|
||||
for (let index = start + 1; index < contents.length; index++) {
|
||||
const character = contents[index];
|
||||
if (character === "\\") {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (character === "[") {
|
||||
characterClass = true;
|
||||
continue;
|
||||
}
|
||||
if (character === "]") {
|
||||
characterClass = false;
|
||||
continue;
|
||||
}
|
||||
if (character === "/" && !characterClass) {
|
||||
index++;
|
||||
while (/[A-Za-z]/.test(contents[index] ?? "")) index++;
|
||||
return index;
|
||||
}
|
||||
refuse(character !== "\n" && character !== "\r", `unterminated regex literal at byte ${start} in bundled JavaScript`);
|
||||
}
|
||||
throw new Error(`pi rebuild recipe refused: unterminated regex literal at byte ${start} in bundled JavaScript`);
|
||||
}
|
||||
|
||||
function importSpecifiers(contents) {
|
||||
const found = new Set();
|
||||
const regexPrefixKeywords = new Set([
|
||||
"await", "case", "delete", "do", "else", "in", "instanceof", "new", "of", "return", "throw", "typeof", "void", "yield",
|
||||
]);
|
||||
let index = 0;
|
||||
let canStartRegex = true;
|
||||
while (index < contents.length) {
|
||||
index = skipTrivia(contents, index);
|
||||
const character = contents[index];
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
index = quoted(contents, index).next;
|
||||
canStartRegex = false;
|
||||
continue;
|
||||
}
|
||||
if (character === "/") {
|
||||
if (canStartRegex) {
|
||||
index = regexLiteralEnd(contents, index);
|
||||
canStartRegex = false;
|
||||
} else {
|
||||
index += contents[index + 1] === "=" ? 2 : 1;
|
||||
canStartRegex = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (/[0-9]/.test(character ?? "")) {
|
||||
index++;
|
||||
while (/[A-Za-z0-9_.]/.test(contents[index] ?? "")) index++;
|
||||
canStartRegex = false;
|
||||
continue;
|
||||
}
|
||||
const identifier = identifierAt(contents, index);
|
||||
if (!identifier) {
|
||||
if ((character === "+" || character === "-") && contents[index + 1] === character) {
|
||||
index += 2;
|
||||
canStartRegex = false;
|
||||
continue;
|
||||
}
|
||||
canStartRegex = !")]}".includes(character) && character !== ".";
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
index = identifier.end;
|
||||
if (identifier.value !== "import" && identifier.value !== "export") {
|
||||
canStartRegex = regexPrefixKeywords.has(identifier.value);
|
||||
continue;
|
||||
}
|
||||
index = skipTrivia(contents, index);
|
||||
if (identifier.value === "import" && contents[index] === ".") {
|
||||
canStartRegex = false;
|
||||
continue;
|
||||
}
|
||||
if (identifier.value === "import" && contents[index] === "(") {
|
||||
index = skipTrivia(contents, index + 1);
|
||||
if (contents[index] === '"' || contents[index] === "'") {
|
||||
const specifier = quoted(contents, index);
|
||||
found.add(specifier.value);
|
||||
index = specifier.next;
|
||||
}
|
||||
canStartRegex = false;
|
||||
continue;
|
||||
}
|
||||
if (identifier.value === "import" && (contents[index] === '"' || contents[index] === "'")) {
|
||||
const specifier = quoted(contents, index);
|
||||
found.add(specifier.value);
|
||||
index = specifier.next;
|
||||
canStartRegex = false;
|
||||
continue;
|
||||
}
|
||||
const specifier = importClauseSpecifier(contents, index);
|
||||
if (specifier) {
|
||||
found.add(specifier.value);
|
||||
index = specifier.next;
|
||||
}
|
||||
canStartRegex = false;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function reachableBundle(packageRoot, entry) {
|
||||
const bundleRoot = realpathSync(safePath(packageRoot, "dist/bundle"));
|
||||
const entryPath = safePath(packageRoot, entry);
|
||||
const entryStat = lstatSync(entryPath);
|
||||
refuse(entryStat.isFile() && !entryStat.isSymbolicLink(), `bundle entry is not a regular non-symlink file: ${entry}`);
|
||||
const queue = [realpathSync(entryPath)];
|
||||
const files = new Map();
|
||||
const external = new Set();
|
||||
while (queue.length > 0) {
|
||||
const file = queue.shift();
|
||||
refuse(file === bundleRoot || file.startsWith(`${bundleRoot}${sep}`), `bundle import escapes dist/bundle: ${file}`);
|
||||
if (files.has(file)) continue;
|
||||
const contents = readFileSync(file, "utf8");
|
||||
files.set(file, contents);
|
||||
let specifiers;
|
||||
try {
|
||||
specifiers = importSpecifiers(contents);
|
||||
} catch (error) {
|
||||
throw new Error(`${error.message}; bundle file ${relative(packageRoot, file).split(sep).join("/")}`);
|
||||
}
|
||||
for (const specifier of specifiers) {
|
||||
if (!specifier.startsWith(".")) {
|
||||
external.add(specifier);
|
||||
continue;
|
||||
}
|
||||
const unresolved = resolve(dirname(file), specifier);
|
||||
const targetStat = lstatSync(unresolved);
|
||||
refuse(targetStat.isFile() && !targetStat.isSymbolicLink(), `bundle import is not a regular non-symlink file: ${specifier}`);
|
||||
const target = realpathSync(unresolved);
|
||||
refuse(target.startsWith(`${bundleRoot}${sep}`), `relative bundle import escapes dist/bundle: ${specifier}`);
|
||||
queue.push(target);
|
||||
}
|
||||
}
|
||||
return { files, external };
|
||||
}
|
||||
|
||||
export function inspectBundleClosure(packageRoot, entry = "dist/bundle/cli.js") {
|
||||
const closure = reachableBundle(packageRoot, entry);
|
||||
return {
|
||||
reachableFiles: [...closure.files.keys()]
|
||||
.map((path) => relative(packageRoot, path).split(sep).join("/"))
|
||||
.sort(),
|
||||
externalImports: [...closure.external].sort(),
|
||||
};
|
||||
}
|
||||
|
||||
export function assertBundleConsumption(packageRoot, pin, options = {}) {
|
||||
const manifest = json(safePath(packageRoot, "package.json"));
|
||||
const shrinkwrap = json(safePath(packageRoot, "npm-shrinkwrap.json"));
|
||||
refuse(manifest.name === PI_NAME && manifest.version === PI_VERSION, `artifact identity must remain ${PI_NAME}@${PI_VERSION}`);
|
||||
refuse(manifest.bin?.pi === "dist/bundle/cli.js", "artifact Pi bin does not consume dist/bundle/cli.js");
|
||||
assertUndiciResolution(directUndici(shrinkwrap), UNDICI_FIXED, UNDICI_URL, UNDICI_INTEGRITY, "artifact shrinkwrap Undici");
|
||||
refuse(shrinkwrapRoot(shrinkwrap)?.dependencies?.undici === UNDICI_FIXED, "artifact shrinkwrap root does not declare Undici 8.12.2");
|
||||
assertUndiciResolution(directBraceExpansion(shrinkwrap), BRACE_FIXED, BRACE_URL, BRACE_INTEGRITY, "artifact shrinkwrap brace-expansion");
|
||||
const installedUndici = options.installedUndiciRoot ? assertInstalledUndici(options.installedUndiciRoot, pin) : undefined;
|
||||
|
||||
const closure = reachableBundle(packageRoot, manifest.bin.pi);
|
||||
refuse(![...closure.external].some((specifier) => specifier === "undici" || specifier.startsWith("undici/")), "bundle leaves Undici external");
|
||||
const joined = [...closure.files.values()].join("\n");
|
||||
const fixedContentEncoding = /controller\.abort\(new Error\(`too many content-encodings in response:/.test(joined);
|
||||
const vulnerableContentEncoding = /reject\(new Error\(`too many content-encodings in response:/.test(joined);
|
||||
// This is a conservative signature check for the direct assertion forms in
|
||||
// Undici's affected source and bundles, not a proof of arbitrary JS semantics.
|
||||
const vulnerableTlsAssertion = /\bassert\s*\(\s*[^);\n]*?\.code\s*!==\s*(["'])ERR_TLS_CERT_ALTNAME_INVALID\1/.test(joined);
|
||||
refuse(fixedContentEncoding, "fixed content-encoding abort signature is absent from the reachable CLI bundle");
|
||||
refuse(!vulnerableContentEncoding, "vulnerable content-encoding reject signature remains in the reachable CLI bundle");
|
||||
refuse(!vulnerableTlsAssertion, "vulnerable TLS assertion signature remains in the reachable CLI bundle");
|
||||
|
||||
const reachableFiles = [...closure.files.keys()]
|
||||
.map((path) => relative(packageRoot, path).split(sep).join("/"))
|
||||
.sort();
|
||||
return {
|
||||
piVersion: manifest.version,
|
||||
undiciVersion: directUndici(shrinkwrap).version,
|
||||
braceExpansionVersion: directBraceExpansion(shrinkwrap).version,
|
||||
entry: manifest.bin.pi,
|
||||
reachableFiles,
|
||||
externalImports: [...closure.external].sort(),
|
||||
fixedSignatures: {
|
||||
contentEncodingAbort: true,
|
||||
vulnerableContentEncodingRejectAbsent: true,
|
||||
knownDirectVulnerableTlsAssertionSignaturesAbsent: true,
|
||||
},
|
||||
...(installedUndici ? { installedUndici } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export const constants = Object.freeze({
|
||||
piName: PI_NAME,
|
||||
piVersion: PI_VERSION,
|
||||
piCommit: PI_COMMIT,
|
||||
undiciVersion: UNDICI_FIXED,
|
||||
undiciUrl: UNDICI_URL,
|
||||
undiciIntegrity: UNDICI_INTEGRITY,
|
||||
braceExpansionVersion: BRACE_FIXED,
|
||||
braceExpansionUrl: BRACE_URL,
|
||||
braceExpansionIntegrity: BRACE_INTEGRITY,
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
assertBundleConsumption,
|
||||
validatePatch,
|
||||
validatePin,
|
||||
validateSourceTree,
|
||||
} from "../src/recipe.mjs";
|
||||
import { writeReceipts } from "../src/receipts.mjs";
|
||||
|
||||
const PACKAGE_ROOT = resolve(import.meta.dirname, "..");
|
||||
const PIN = JSON.parse(readFileSync(join(PACKAGE_ROOT, "PIN.json"), "utf8"));
|
||||
const PATCH = readFileSync(join(PACKAGE_ROOT, PIN.patch.path), "utf8");
|
||||
const TEST_SCRATCH = process.env.MOSAIC_PI_REBUILD_TEST_TMP;
|
||||
|
||||
function scratch(t) {
|
||||
assert.ok(TEST_SCRATCH, "MOSAIC_PI_REBUILD_TEST_TMP must name the owned mktemp scratch directory");
|
||||
const directory = mkdtempSync(join(TEST_SCRATCH, "fixture-"));
|
||||
t.after(() => rmSync(directory, { force: true, recursive: true }));
|
||||
return directory;
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
function write(directory, path, contents) {
|
||||
const target = join(directory, path);
|
||||
mkdirSync(resolve(target, ".."), { recursive: true });
|
||||
writeFileSync(target, contents);
|
||||
}
|
||||
|
||||
test("the exact source, toolchain, Undici and three-file patch pins fail closed", () => {
|
||||
assert.doesNotThrow(() => validatePin(PIN));
|
||||
assert.equal(PIN.braceExpansion.version, "5.0.12", "full A5 tree requirement");
|
||||
assert.doesNotThrow(() => validatePatch(PATCH, PIN));
|
||||
|
||||
for (const mutate of [
|
||||
(pin) => { pin.pi.version = "0.85.2"; },
|
||||
(pin) => { pin.pi.commit = "0".repeat(40); },
|
||||
(pin) => { pin.undici.version = "8.12.3"; },
|
||||
(pin) => { pin.undici.tarball.sha256 = "0".repeat(64); },
|
||||
(pin) => { pin.braceExpansion.version = "5.0.13"; },
|
||||
(pin) => { pin.toolchain.node.version = "22.23.3"; },
|
||||
(pin) => { pin.pi.source.url = "https://[email protected]/pi.tar.gz"; },
|
||||
]) {
|
||||
const changed = structuredClone(PIN);
|
||||
mutate(changed);
|
||||
assert.throws(() => validatePin(changed));
|
||||
}
|
||||
|
||||
assert.throws(
|
||||
() => validatePatch(`${PATCH}\ndiff --git a/package.json b/package.json\n`, PIN),
|
||||
/sha256|package\.json|allowed|target/i,
|
||||
);
|
||||
assert.throws(
|
||||
() => validatePatch(PATCH.replace("packages/coding-agent/npm-shrinkwrap.json", "packages/coding-agent/install-lock/package-lock.json"), PIN),
|
||||
/sha256|install-lock|allowed|target/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("source validation binds preimage bytes and the existing bundled consumer graph", (t) => {
|
||||
const root = scratch(t);
|
||||
const files = {
|
||||
"package-lock.json": `${JSON.stringify({ packages: {
|
||||
"node_modules/brace-expansion": {
|
||||
version: "5.0.9",
|
||||
resolved: "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
},
|
||||
"node_modules/undici": {
|
||||
version: "8.9.0",
|
||||
resolved: "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||
integrity: "old-integrity",
|
||||
},
|
||||
"packages/coding-agent": { dependencies: { undici: "8.9.0" } },
|
||||
"node_modules/@earendil-works/gondolin/node_modules/undici": { version: "6.28.0" },
|
||||
} }, null, "\t")}\n`,
|
||||
"packages/coding-agent/package.json": `${JSON.stringify({
|
||||
name: "@earendil-works/pi-coding-agent",
|
||||
version: "0.85.1",
|
||||
bin: { pi: "dist/bundle/cli.js" },
|
||||
dependencies: { undici: "8.9.0" },
|
||||
}, null, "\t")}\n`,
|
||||
"packages/coding-agent/npm-shrinkwrap.json": `${JSON.stringify({ packages: {
|
||||
"": { version: "0.85.1", dependencies: { undici: "8.9.0" } },
|
||||
"node_modules/brace-expansion": { version: "5.0.9", resolved: "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", integrity: "old-brace-integrity" },
|
||||
"node_modules/undici": { version: "8.9.0", resolved: "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", integrity: "old-integrity" },
|
||||
} }, null, "\t")}\n`,
|
||||
"scripts/build-coding-agent-bundle.mjs": "const allowedExternalPackages = new Set(['jiti']); const options = { bundle: true };\n",
|
||||
};
|
||||
for (const [path, contents] of Object.entries(files)) write(root, path, contents);
|
||||
const preimages = Object.fromEntries(Object.entries(files).map(([path, contents]) => [path, sha256(contents)]));
|
||||
|
||||
assert.doesNotThrow(() => validateSourceTree(root, { ...PIN, sourcePreimages: preimages }));
|
||||
write(root, "packages/coding-agent/package.json", files["packages/coding-agent/package.json"].replace("8.9.0", "8.12.2"));
|
||||
assert.throws(() => validateSourceTree(root, { ...PIN, sourcePreimages: preimages }), /preimage|sha256/i);
|
||||
});
|
||||
|
||||
test("bundle assertion fixture requires the fixed signatures in the reachable CLI closure", (t) => {
|
||||
const root = scratch(t);
|
||||
const packageRoot = join(root, "package");
|
||||
write(packageRoot, "package.json", `${JSON.stringify({
|
||||
name: "@earendil-works/pi-coding-agent",
|
||||
version: "0.85.1",
|
||||
bin: { pi: "dist/bundle/cli.js" },
|
||||
}, null, "\t")}\n`);
|
||||
write(packageRoot, "npm-shrinkwrap.json", `${JSON.stringify({ packages: {
|
||||
"": { version: "0.85.1", dependencies: { undici: "8.12.2" } },
|
||||
"node_modules/brace-expansion": {
|
||||
version: "5.0.12",
|
||||
resolved: PIN.braceExpansion.tarball.url,
|
||||
integrity: PIN.braceExpansion.tarball.integrity,
|
||||
},
|
||||
"node_modules/undici": {
|
||||
version: "8.12.2",
|
||||
resolved: PIN.undici.tarball.url,
|
||||
integrity: PIN.undici.tarball.integrity,
|
||||
},
|
||||
} }, null, "\t")}\n`);
|
||||
write(packageRoot, "dist/bundle/cli.js", 'import "./chunks/fixed.js";\n');
|
||||
const fixedBundle = [
|
||||
"const documentation=\"pass the fetch implementation from the same undici package: import { fetch, ProxyAgent } from 'undici';\";",
|
||||
"const syntaxPattern=/from[\"']undici/;",
|
||||
"function fixed(controller,codings,maxContentEncodings){",
|
||||
"if(codings.length>maxContentEncodings){controller.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`));return}",
|
||||
"}",
|
||||
"function handled(err2){return err2.code===\"ERR_TLS_CERT_ALTNAME_INVALID\"}",
|
||||
].join("");
|
||||
write(packageRoot, "dist/bundle/chunks/fixed.js", fixedBundle);
|
||||
|
||||
const evidence = assertBundleConsumption(packageRoot, PIN);
|
||||
assert.deepEqual(evidence.reachableFiles, ["dist/bundle/chunks/fixed.js", "dist/bundle/cli.js"]);
|
||||
assert.ok(!evidence.externalImports.includes("undici"), "documentation text is not an import");
|
||||
assert.equal(evidence.undiciVersion, "8.12.2");
|
||||
const artifact = join(root, "fixture-artifact.tgz");
|
||||
writeFileSync(artifact, "fixture only; not a Pi artifact\n");
|
||||
for (const outputDirectory of [join(root, "receipts-a"), join(root, "receipts-b")]) {
|
||||
writeReceipts({
|
||||
artifactPath: artifact,
|
||||
packageRoot,
|
||||
outputDirectory,
|
||||
pin: PIN,
|
||||
toolVersions: { node: "v22.23.4", npm: "10.9.9" },
|
||||
});
|
||||
}
|
||||
assert.deepEqual(readdirSync(join(root, "receipts-a")).sort(), readdirSync(join(root, "receipts-b")).sort());
|
||||
for (const file of readdirSync(join(root, "receipts-a"))) {
|
||||
assert.deepEqual(readFileSync(join(root, "receipts-a", file)), readFileSync(join(root, "receipts-b", file)), file);
|
||||
}
|
||||
|
||||
// Keep the fixed-abort positive present so only the TLS guard can refuse.
|
||||
for (const assertion of [
|
||||
'assert(err.code!=="ERR_TLS_CERT_ALTNAME_INVALID")',
|
||||
'assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID")',
|
||||
"assert (err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')",
|
||||
]) {
|
||||
write(packageRoot, "dist/bundle/chunks/fixed.js", `${fixedBundle};${assertion};`);
|
||||
assert.throws(() => assertBundleConsumption(packageRoot, PIN), /vulnerable TLS assertion signature/);
|
||||
}
|
||||
|
||||
write(packageRoot, "dist/bundle/chunks/fixed.js", `import"undici";${fixedBundle}`);
|
||||
assert.throws(() => assertBundleConsumption(packageRoot, PIN), /leaves Undici external/i);
|
||||
write(packageRoot, "dist/bundle/chunks/fixed.js", "function vulnerable(reject,err2){reject(new Error(`too many content-encodings in response:`));assert(err2.code!==\"ERR_TLS_CERT_ALTNAME_INVALID\")}\n");
|
||||
assert.throws(() => assertBundleConsumption(packageRoot, PIN), /fixed content-encoding|TLS assertion|signature/i);
|
||||
});
|
||||
Reference in new issue
Block a user