diff --git a/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts index c4eaa1f99e..24c204ac1d 100644 --- a/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts @@ -63,7 +63,9 @@ const VERSION = "1.0.0"; function syntheticPublication( channel: "preview" | "stable" = "preview", policies = { preview: 1, build: 1, promotion: 1 }, + recipeRevision = 1, ): PrimePublicationFixture & { readonly verified: VerifiedPrimePublication } { + const buildId = `pylon-build-g${SOURCE_COMMIT.slice(0, 12)}-r${recipeRevision}`; const artifactBytes = new Map([ [`pylon-prime-agent-${VERSION}.tgz`, Buffer.from("synthetic-root-tarball-v1")], [`pylon-prime-agent-ai-${VERSION}.tgz`, Buffer.from("synthetic-ai-tarball-v1")], @@ -93,12 +95,12 @@ function syntheticPublication( tree: SOURCE_TREE, }, build: { - id: BUILD_ID, - recipeRevision: 1, + id: buildId, + recipeRevision, node: "22.23.2", npm: "11.10.1", lockfile: { file: "package-lock.json", sha256: "e".repeat(64) }, - assetBaseUrl: `${PRIME_DISTRIBUTION_REPOSITORY_URL}/releases/download/${BUILD_ID}`, + assetBaseUrl: `${PRIME_DISTRIBUTION_REPOSITORY_URL}/releases/download/${buildId}`, }, package: { name: "prime-agent", @@ -123,9 +125,9 @@ function syntheticPublication( sequence: 7, workflowRunId: "9001", build: { - tag: BUILD_ID, - id: BUILD_ID, - recipeRevision: 1, + tag: buildId, + id: buildId, + recipeRevision, source: releaseManifest.source, releaseManifest: { file: PRIME_RELEASE_MANIFEST, @@ -140,7 +142,7 @@ function syntheticPublication( channel: "stable", repository: PRIME_DISTRIBUTION_REPOSITORY_URL, sequence: 3, - tag: `pylon-stable-000003-g${SOURCE_COMMIT.slice(0, 12)}-r1`, + tag: `pylon-stable-000003-g${SOURCE_COMMIT.slice(0, 12)}-r${recipeRevision}`, history: { highWater: 2, previous: { @@ -154,9 +156,9 @@ function syntheticPublication( sequence: 7, workflowRunId: "9001", }, - previewTag: BUILD_ID, - id: BUILD_ID, - recipeRevision: 1, + previewTag: buildId, + id: buildId, + recipeRevision, publicationPolicyRevision: policies.build, source: releaseManifest.source, releaseManifest: { @@ -199,10 +201,10 @@ function syntheticPublication( channel, sequenceEpoch: 1, sequence: channel === "preview" ? 7 : 3, - buildId: BUILD_ID, + buildId: buildId, sourceCommit: SOURCE_COMMIT, sourceTree: SOURCE_TREE, - recipeRevision: 1, + recipeRevision, rootAsset: rootAsset.file, rootSha256: rootAsset.sha256, packageVersion: VERSION, @@ -480,6 +482,25 @@ describe("Pylon Prime publication verification", () => { ); }); + it.each(["preview", "stable"] as const)( + "verifies both frozen recipes for %s and rejects unknown recipes", + async (channel) => { + const policies = { preview: 4, build: 4, promotion: 4 }; + for (const revision of [1, 2]) { + const fixture = syntheticPublication(channel, policies, revision); + await expect(verifyPrimePublicationFixture(fixture, validVerification)).resolves.toEqual( + fixture.verified, + ); + } + await expect( + verifyPrimePublicationFixture( + syntheticPublication(channel, policies, 3), + validVerification, + ), + ).rejects.toThrow(/recipe/u); + }, + ); + it("retains immutable historical and current workflow policies", () => { expect(Object.isFrozen(PRIME_PUBLICATION_POLICIES)).toBe(true); expect(PRIME_PUBLICATION_POLICIES.every(Object.isFrozen)).toBe(true); @@ -505,6 +526,13 @@ describe("Pylon Prime publication verification", () => { stableWorkflowPath: PRIME_STABLE_WORKFLOW, stableWorkflowSha256: "96e4f3ccd889a17a391f00b4398d132a4234b5bc337045fedd3605f4eb74c331", }, + { + publicationPolicyRevision: 4, + previewWorkflowPath: PRIME_PREVIEW_WORKFLOW, + previewWorkflowSha256: "8ad08abd8004b7205b04dc3cd9f7cc0e720649c9c6814431622f5b4dfb6e4c32", + stableWorkflowPath: PRIME_STABLE_WORKFLOW, + stableWorkflowSha256: "52c72e078b08c0a2774ab3dc512283c46ab31c4a87738d87007f55f955662703", + }, ]); }); @@ -515,6 +543,9 @@ describe("Pylon Prime publication verification", () => { { preview: 3, build: 3, promotion: 3 }, { preview: 1, build: 1, promotion: 3 }, { preview: 2, build: 2, promotion: 3 }, + { preview: 4, build: 4, promotion: 4 }, + { preview: 1, build: 1, promotion: 4 }, + { preview: 3, build: 3, promotion: 4 }, ])("verifies independent build and promotion policies: %j", async (policies) => { const fixture = syntheticPublication("stable", policies); const verifySourcePolicy = vi.fn(validVerification.verifySourcePolicy); @@ -546,9 +577,9 @@ describe("Pylon Prime publication verification", () => { }); it.each([ - { preview: 4, build: 3, promotion: 3 }, - { preview: 3, build: 4, promotion: 3 }, - { preview: 3, build: 3, promotion: 4 }, + { preview: 5, build: 3, promotion: 3 }, + { preview: 3, build: 5, promotion: 3 }, + { preview: 3, build: 3, promotion: 5 }, ])("rejects an unknown policy revision in any manifest position: %j", async (policies) => { await expect( verifyPrimePublicationFixture(syntheticPublication("stable", policies), validVerification), @@ -566,36 +597,41 @@ describe("Pylon Prime publication verification", () => { ).rejects.toThrow(/Stable manifest does not bind the exact verified preview/u); }); - it.each([1, 2, 3])("checks actual fetched workflow bytes under policy %i", async (revision) => { - const fixture = syntheticPublication("preview", { - preview: revision, - build: revision, - promotion: revision, - }); - const harness = makeNetworkHarness({ - channel: "preview", - candidates: 1, - key: `policy-bytes-${revision}`, - fixture, - }); - const { verifySourcePolicy: _injectedSourcePolicy, ...dependencies } = - harness.dependencyShape(); - const workflowUrl = `https://raw.githubusercontent.com/${PRIME_DISTRIBUTION_REPOSITORY}/${SOURCE_COMMIT}/${PRIME_PREVIEW_WORKFLOW}`; - const fetchBytes = vi.fn(async (url: string, maximumBytes: number) => { - if (url === workflowUrl) return Buffer.from("altered publication workflow"); - return dependencies.fetchBytes(url, maximumBytes); - }); - const load = makeLatestPrimePublicationLoader({ - ...dependencies, - fetchJson: async (url, maximumBytes) => - url.endsWith(`/git/commits/${SOURCE_COMMIT}`) - ? { sha: SOURCE_COMMIT, tree: { sha: SOURCE_TREE } } - : dependencies.fetchJson(url, maximumBytes), - fetchBytes, - }); - await expect(load("preview")).rejects.toThrow(/No exact signed preview publication verified/u); - expect(fetchBytes.mock.calls.some(([url]) => url === workflowUrl)).toBe(true); - }); + it.each([1, 2, 3, 4])( + "checks actual fetched workflow bytes under policy %i", + async (revision) => { + const fixture = syntheticPublication("preview", { + preview: revision, + build: revision, + promotion: revision, + }); + const harness = makeNetworkHarness({ + channel: "preview", + candidates: 1, + key: `policy-bytes-${revision}`, + fixture, + }); + const { verifySourcePolicy: _injectedSourcePolicy, ...dependencies } = + harness.dependencyShape(); + const workflowUrl = `https://raw.githubusercontent.com/${PRIME_DISTRIBUTION_REPOSITORY}/${SOURCE_COMMIT}/${PRIME_PREVIEW_WORKFLOW}`; + const fetchBytes = vi.fn(async (url: string, maximumBytes: number) => { + if (url === workflowUrl) return Buffer.from("altered publication workflow"); + return dependencies.fetchBytes(url, maximumBytes); + }); + const load = makeLatestPrimePublicationLoader({ + ...dependencies, + fetchJson: async (url, maximumBytes) => + url.endsWith(`/git/commits/${SOURCE_COMMIT}`) + ? { sha: SOURCE_COMMIT, tree: { sha: SOURCE_TREE } } + : dependencies.fetchJson(url, maximumBytes), + fetchBytes, + }); + await expect(load("preview")).rejects.toThrow( + /No exact signed preview publication verified/u, + ); + expect(fetchBytes.mock.calls.some(([url]) => url === workflowUrl)).toBe(true); + }, + ); it("fails closed for tarball, manifest, attestation, source, recipe, and stable history tampering", async () => { const fixture = syntheticPublication(); diff --git a/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts index c8650bbba2..ff8a41eacc 100644 --- a/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts +++ b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts @@ -374,6 +374,10 @@ export const PRIME_RELEASE_RECIPE = Object.freeze({ npmVersion: "11.10.1", minimumNodeVersion: "22.8.0", }); +export const PRIME_RELEASE_RECIPES = Object.freeze([ + PRIME_RELEASE_RECIPE, + Object.freeze({ ...PRIME_RELEASE_RECIPE, recipeRevision: 2 }), +]); export const PRIME_PUBLICATION_SCHEMA_SOURCE = Object.freeze({ commit: "f4d9ef03b529faf2e07031c8b7cd703363316ae5", tree: "b9a14b389aa64f54527008fb4d6119a7c57c2b58", @@ -400,6 +404,13 @@ export const PRIME_PUBLICATION_POLICIES = Object.freeze([ stableWorkflowPath: PRIME_STABLE_WORKFLOW, stableWorkflowSha256: "96e4f3ccd889a17a391f00b4398d132a4234b5bc337045fedd3605f4eb74c331", }), + Object.freeze({ + publicationPolicyRevision: 4, + previewWorkflowPath: PRIME_PREVIEW_WORKFLOW, + previewWorkflowSha256: "8ad08abd8004b7205b04dc3cd9f7cc0e720649c9c6814431622f5b4dfb6e4c32", + stableWorkflowPath: PRIME_STABLE_WORKFLOW, + stableWorkflowSha256: "52c72e078b08c0a2774ab3dc512283c46ab31c4a87738d87007f55f955662703", + }), ]); function publicationPolicyFor(revision: number) { @@ -472,14 +483,17 @@ function parseReleaseManifest(bytes: Buffer): ReleaseManifest { "Pylon Prime release manifest", MAX_MANIFEST_BYTES, decodeReleaseManifest, - // Recipe 1 preserves object field order; its exact original bytes remain signed subjects. + // Release recipes preserve field order; the original bytes remain signed subjects. "preserved", ); + const recipe = PRIME_RELEASE_RECIPES.find( + (entry) => entry.recipeRevision === manifest.build.recipeRevision, + ); if ( - manifest.build.recipeRevision !== PRIME_RELEASE_RECIPE.recipeRevision || - manifest.build.node !== PRIME_RELEASE_RECIPE.nodeVersion || - manifest.build.npm !== PRIME_RELEASE_RECIPE.npmVersion || - manifest.package.minimumNode !== PRIME_RELEASE_RECIPE.minimumNodeVersion || + !recipe || + manifest.build.node !== recipe.nodeVersion || + manifest.build.npm !== recipe.npmVersion || + manifest.package.minimumNode !== recipe.minimumNodeVersion || manifest.build.id !== `pylon-build-g${manifest.source.commit.slice(0, 12)}-r${manifest.build.recipeRevision}` || manifest.build.assetBaseUrl !== @@ -576,7 +590,7 @@ function parseStableManifest(bytes: Buffer): StableManifest { Number(/^pylon-stable-([0-9]{6})-/.exec(manifest.history.previous.tag)?.[1]) !== manifest.sequence - 1) || manifest.build.previewTag !== manifest.build.id || - manifest.build.recipeRevision !== PRIME_RELEASE_RECIPE.recipeRevision + !PRIME_RELEASE_RECIPES.some((entry) => entry.recipeRevision === manifest.build.recipeRevision) ) { throw new Error("Stable manifest is not an exact closed Pylon build receipt."); } diff --git a/apps/server/src/provider/prime/PrimeAgentManagedToolStore.test.ts b/apps/server/src/provider/prime/PrimeAgentManagedToolStore.test.ts index 67db74e649..3d572c3dca 100644 --- a/apps/server/src/provider/prime/PrimeAgentManagedToolStore.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentManagedToolStore.test.ts @@ -47,7 +47,7 @@ function tarOctal(value: number, length: number): Buffer { function tarHeader(input: { readonly path: string; - readonly type?: "file" | "directory" | "symlink" | "hardlink"; + readonly type?: "file" | "directory" | "symlink" | "hardlink" | "pax" | "global-pax"; readonly bytes?: Buffer; readonly mode?: number; readonly link?: string; @@ -62,7 +62,17 @@ function tarHeader(input: { header.fill(0x20, 148, 156); const type = input.type ?? "file"; header[156] = - type === "file" ? 0x30 : type === "directory" ? 0x35 : type === "symlink" ? 0x32 : 0x31; + type === "pax" + ? 0x78 + : type === "global-pax" + ? 0x67 + : type === "file" + ? 0x30 + : type === "directory" + ? 0x35 + : type === "symlink" + ? 0x32 + : 0x31; if (input.link) header.write(input.link, 157, 100, "utf8"); header.write("ustar\0", 257, 6, "ascii"); header.write("00", 263, 2, "ascii"); @@ -75,7 +85,7 @@ function tarHeader(input: { function makeTarGz( entries: ReadonlyArray<{ readonly path: string; - readonly type?: "file" | "directory" | "symlink" | "hardlink"; + readonly type?: "file" | "directory" | "symlink" | "hardlink" | "pax" | "global-pax"; readonly bytes?: Buffer; readonly mode?: number; readonly link?: string; @@ -94,6 +104,24 @@ function makeTarGz( return NodeZlib.gzipSync(Buffer.concat(parts), { level: 9 }); } +function paxField(key: string, value: string): Buffer { + const body = ` ${key}=${value}\n`; + let length = Buffer.byteLength(body) + 1; + while (String(length).length + Buffer.byteLength(body) !== length) { + length = String(length).length + Buffer.byteLength(body); + } + return Buffer.from(`${length}${body}`); +} + +function rootWithEntries(entries: Parameters[0]): Buffer { + return NodeZlib.gzipSync( + Buffer.concat([ + NodeZlib.gunzipSync(safeRootTarball("a".repeat(40), "f".repeat(40))).subarray(0, -1024), + NodeZlib.gunzipSync(makeTarGz(entries)), + ]), + ); +} + function distributionMetadata(sourceCommit: string, sourceTree: string) { return { schemaVersion: 1, @@ -963,6 +991,124 @@ describe("Pylon-managed Prime tool store", () => { expect(io).toBe(0); }); + it("installs npm per-file extended names without leaking metadata to the following file", async () => { + const longPath = `package/node_modules/example/${"x".repeat(105)}.d.ts`; + const bytes = Buffer.from("export {};\n"); + const bundle = await publicationBundle({ + rootBytes: rootWithEntries([ + { + path: "PaxHeader/long", + type: "pax", + bytes: Buffer.concat([ + paxField("path", longPath), + paxField("size", String(bytes.length)), + paxField("mtime", "0"), + ]), + }, + { path: "package/short", bytes }, + { path: "package/following", bytes: Buffer.from("following") }, + ]), + }); + const harness = await makeHarness({ bundle, installMode: "production" }); + const result = await harness.store.command({ + commandId: "pax-install", + instanceId: "primeAgent", + action: "install", + }); + expect(result.status).toBe("succeeded"); + const packageRoot = NodePath.resolve(harness.binding.binaryPath, "../../prime-agent"); + expect( + await NodeFSP.readFile(NodePath.join(packageRoot, longPath.slice("package/".length))), + ).toEqual(bytes); + expect(await NodeFSP.readFile(NodePath.join(packageRoot, "following"), "utf8")).toBe( + "following", + ); + await expect(NodeFSP.stat(NodePath.join(packageRoot, "short"))).rejects.toThrow(); + }); + + it.each([ + ["traversal", paxField("path", "package/../escape")], + ["absolute", paxField("path", "/tmp/escape")], + ["backslash", paxField("path", "package/..\\escape")], + ["oversized path", paxField("path", `package/${"x".repeat(512)}`)], + [ + "duplicate field", + Buffer.concat([paxField("path", "package/x"), paxField("path", "package/y")]), + ], + [ + "link field", + Buffer.concat([paxField("path", "package/x"), paxField("linkpath", "package/y")]), + ], + ["missing path", paxField("size", "1")], + ["size mismatch", Buffer.concat([paxField("path", "package/x"), paxField("size", "2")])], + ["mtime mismatch", Buffer.concat([paxField("path", "package/x"), paxField("mtime", "1")])], + ["negative size", Buffer.concat([paxField("path", "package/x"), paxField("size", "-1")])], + ["fractional time", Buffer.concat([paxField("path", "package/x"), paxField("mtime", "0.5")])], + ["malformed length", Buffer.from("999 path=package/x\n")], + ["noncanonical length", Buffer.from("019 path=package/x\n")], + [ + "invalid UTF-8", + Buffer.concat([Buffer.from("20 path=package/"), Buffer.from([0xff]), Buffer.from("abc\n")]), + ], + ["metadata bound", Buffer.alloc(1025, 0x61)], + ["existing path collision", paxField("path", "package/package.json")], + ["case collision", paxField("path", "package/PACKAGE.JSON")], + ])( + "rejects per-file extended metadata with %s before selecting a build", + async (_name, bytes) => { + const bundle = await publicationBundle({ + rootBytes: rootWithEntries([ + { path: "PaxHeader/entry", type: "pax", bytes }, + { path: "package/short", bytes: Buffer.from("x") }, + ]), + }); + const harness = await makeHarness({ bundle, installMode: "production" }); + const result = await harness.store.command({ + commandId: "pax-rejection", + instanceId: "primeAgent", + action: "install", + }); + expect(result.status).toBe("failed"); + expect(harness.binding.binaryPath).toBe(harness.stock); + expect((await harness.store.status("primeAgent")).availableBuilds).toEqual([]); + }, + ); + + it.each(["orphan", "nested", "directory", "symlink", "global"] as const)( + "rejects %s extended header sequencing", + async (kind) => { + const header = { + path: "PaxHeader/entry", + type: "pax" as const, + bytes: paxField("path", "package/x"), + }; + const entries: Parameters[0] = + kind === "orphan" + ? [header] + : kind === "nested" + ? [header, header] + : kind === "global" + ? [{ ...header, path: "package/global", type: "global-pax" }] + : [ + header, + { + path: "package/short", + type: kind, + ...(kind === "symlink" ? { link: "outside" } : {}), + }, + ]; + const bundle = await publicationBundle({ rootBytes: rootWithEntries(entries) }); + const harness = await makeHarness({ bundle, installMode: "production" }); + const result = await harness.store.command({ + commandId: "pax-sequence", + instanceId: "primeAgent", + action: "install", + }); + expect(result.status).toBe("failed"); + expect(harness.binding.binaryPath).toBe(harness.stock); + }, + ); + it.each([ { name: "path traversal", diff --git a/apps/server/src/provider/prime/PrimeAgentManagedToolStore.ts b/apps/server/src/provider/prime/PrimeAgentManagedToolStore.ts index 905c5febfc..bcafd69203 100644 --- a/apps/server/src/provider/prime/PrimeAgentManagedToolStore.ts +++ b/apps/server/src/provider/prime/PrimeAgentManagedToolStore.ts @@ -480,6 +480,48 @@ function safeArchivePath(raw: string): string { return parts.join("/"); } +function parsePrimeFilePax(bytes: Buffer) { + const fields = new Map(); + let offset = 0; + while (offset < bytes.byteLength) { + const space = bytes.indexOf(0x20, offset); + const lengthText = bytes.subarray(offset, space).toString("utf8"); + if (space < offset || !/^[1-9][0-9]{0,3}$/u.test(lengthText)) { + throw new Error("Prime tarball has malformed per-file extended metadata."); + } + const end = offset + Number(lengthText); + if (end > bytes.byteLength || end <= space + 2 || bytes[end - 1] !== 0x0a) { + throw new Error("Prime tarball has an invalid extended record length."); + } + const valueBytes = bytes.subarray(space + 1, end - 1); + const record = valueBytes.toString("utf8"); + const equals = record.indexOf("="); + const key = record.slice(0, equals); + const value = record.slice(equals + 1); + if ( + !Buffer.from(record, "utf8").equals(valueBytes) || + /[\0\r\n]/u.test(record) || + !["path", "size", "mtime"].includes(key) || + fields.has(key) + ) { + throw new Error("Prime tarball has unsupported or repeated extended fields."); + } + fields.set(key, value); + offset = end; + } + const path = fields.get("path"); + if (!path) throw new Error("Prime tarball extended metadata omits its file path."); + const numeric = (key: string): number | undefined => { + const value = fields.get(key); + if (value === undefined) return undefined; + if (!/^(?:0|[1-9][0-9]*)$/u.test(value) || !Number.isSafeInteger(Number(value))) { + throw new Error("Prime tarball extended metadata has an invalid numeric field."); + } + return Number(value); + }; + return { path: safeArchivePath(path), size: numeric("size"), mtime: numeric("mtime") }; +} + async function parsePrimeTarball(bytes: Buffer): Promise> { const tar = await gunzipBounded(bytes); const entries: TarEntry[] = []; @@ -487,6 +529,7 @@ async function parsePrimeTarball(bytes: Buffer): Promise let offset = 0; let zeroBlocks = 0; let expandedFiles = 0; + let pendingPax: ReturnType | undefined; while (offset + 512 <= tar.byteLength) { const block = tar.subarray(offset, offset + 512); offset += 512; @@ -499,8 +542,26 @@ async function parsePrimeTarball(bytes: Buffer): Promise verifyTarChecksum(block); const name = tarString(block, 0, 100); const prefix = tarString(block, 345, 155); - const path = safeArchivePath(prefix ? `${prefix}/${name}` : name); const type = block[156] ?? 0; + const size = tarNumber(block, 124, 12, "entry size"); + if (type === 0x78) { + if ( + pendingPax || + prefix || + !name.startsWith("PaxHeader/") || + size < 1 || + size > 1024 || + offset + size > tar.byteLength + ) { + throw new Error("Prime tarball has unbounded or nested per-file extended metadata."); + } + safeArchivePath(`package/${name}`); + pendingPax = parsePrimeFilePax(tar.subarray(offset, offset + size)); + offset += Math.ceil(size / 512) * 512; + continue; + } + const headerPath = safeArchivePath(prefix ? `${prefix}/${name}` : name); + const path = pendingPax?.path ?? headerPath; const kind = type === 0 || type === 0x30 ? "file" : type === 0x35 ? "directory" : undefined; if (!kind) { throw new Error( @@ -509,7 +570,15 @@ async function parsePrimeTarball(bytes: Buffer): Promise } const mode = tarNumber(block, 100, 8, "mode"); if ((mode & ~0o777) !== 0) throw new Error("Prime tarball contains privileged mode bits."); - const size = tarNumber(block, 124, 12, "entry size"); + if ( + pendingPax && + (kind !== "file" || + (pendingPax.size !== undefined && pendingPax.size !== size) || + (pendingPax.mtime !== undefined && pendingPax.mtime !== tarNumber(block, 136, 12, "mtime"))) + ) { + throw new Error("Prime tarball extended metadata conflicts with its next regular file."); + } + pendingPax = undefined; if (kind === "directory" && size !== 0) { throw new Error("Prime tarball directory contains bytes."); } @@ -534,7 +603,7 @@ async function parsePrimeTarball(bytes: Buffer): Promise }); offset += Math.ceil(size / 512) * 512; } - if (zeroBlocks < 2 || tar.subarray(offset).some((byte) => byte !== 0)) { + if (pendingPax || zeroBlocks < 2 || tar.subarray(offset).some((byte) => byte !== 0)) { throw new Error("Prime tarball has no exact zero-padded end marker."); } return entries; diff --git a/docs/internals/prime-agent-distribution-verification.md b/docs/internals/prime-agent-distribution-verification.md index cbb7c896e7..a741231244 100644 --- a/docs/internals/prime-agent-distribution-verification.md +++ b/docs/internals/prime-agent-distribution-verification.md @@ -13,7 +13,7 @@ publication head `f4d9ef03b529faf2e07031c8b7cd703363316ae5` (tree `b9a14b389aa64f54527008fb4d6119a7c57c2b58`): - `pylon-prime-agent-release-v1.json` binds the four deterministic package assets, full source commit - and tree, recipe revision 1, Node 22.23.2, npm 11.10.1, lockfile digest, and root package identity. + and tree, recipe revisions 1 and 2, Node 22.23.2, npm 11.10.1, lockfile digest, and root package identity. - `pylon-preview-channel-v1.json` binds the build manifest, preview workflow policy revision, workflow run, sequence epoch 1, and monotonic run-number sequence. - `pylon-stable-channel-v1.json` binds an exact preview, stable sequence/history link, protected policy @@ -154,3 +154,9 @@ The verifier itself fetches only bounded public manifests, attestation bundles, optional exact root artifact. It has no extraction, selection, cleanup, or removal authority. The managed tool store consumes its verified publication bundle through that narrow seam. Neither module generalizes Prime feed semantics to Codex, Claude, Cursor, Grok, OpenCode, or Comet. + +## Self-contained recipe 2 + +Recipe 2 includes the locked production dependency tree in the signed root archive, including nested dependencies, installed peers and every optional platform package. Managed installation remains offline: it extracts verified files and creates the contained launcher without npm, lifecycle scripts or dependency downloads. Historical recipe 1 receipts and publication policies 1–3 remain verifiable; policy 4 binds the new recipe's protected build workflow. + +For SDK declaration filenames longer than the basic tar name field, the archive parser accepts one bounded per-file PAX record containing only path, size and mtime. It requires an immediate regular file, exact numeric agreement, and the same package-root, path, collision and size validation as ordinary entries. Global, repeated, nested, orphaned, link-bearing and unknown metadata fail closed. It never creates links from archive entries.