diff --git a/apps/server/package.json b/apps/server/package.json index 3d93c9fe5b..cb2ae4a318 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -30,6 +30,10 @@ "@ff-labs/fff-node": "0.9.4", "@opencode-ai/sdk": "^1.3.15", "@pierre/diffs": "catalog:", + "@sigstore/bundle": "4.0.0", + "@sigstore/core": "3.2.1", + "@sigstore/tuf": "4.0.0", + "@sigstore/verify": "3.0.0", "effect": "catalog:", "msgpackr-extract": "3.0.4", "node-pty": "^1.1.0", diff --git a/apps/server/src/provider/Drivers/PrimeAgentDriver.ts b/apps/server/src/provider/Drivers/PrimeAgentDriver.ts index d7a0ec10b3..b2537dca3e 100644 --- a/apps/server/src/provider/Drivers/PrimeAgentDriver.ts +++ b/apps/server/src/provider/Drivers/PrimeAgentDriver.ts @@ -1,4 +1,9 @@ -import { PrimeAgentSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; +import { + PrimeAgentSettings, + ProviderDriverKind, + type ServerProvider, + type ServerProviderDistribution, +} from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { resolveCommandPath } from "@t3tools/shared/shell"; import * as Crypto from "effect/Crypto"; @@ -32,6 +37,12 @@ import { readPrimeAgentBackends, readPrimeAgentCapacity } from "../primeAgentBac const PRIME_AGENT_TURN_END_CAPACITY_FRESH_MS = 60_000; import { makePrimeAgentDaemonAdapter } from "../prime/PrimeAgentDaemonAdapter.ts"; import { negotiatePrimeAgentBackend } from "../prime/PrimeAgentBackendSelection.ts"; +import { locatePrimeAgentPublicPackage } from "../prime/PrimeAgentDaemonBridge.ts"; +import { + inspectPrimeAgentDistribution, + makeLatestPrimePublicationLoader, + makePrimeDistributionNetworkDependencies, +} from "../prime/PrimeAgentDistributionVerifier.ts"; import { makePrimeAgentDaemonManager } from "../prime/PrimeAgentDaemonManager.ts"; import { defaultProviderContinuationIdentity, @@ -137,6 +148,67 @@ export const PrimeAgentDriver: ProviderDriver => { + if (!snapshot.enabled || !snapshot.installed) { + return Effect.succeed({ + classification: "stock-or-custom" as const, + channel: null, + buildId: null, + sequence: null, + latestBuildId: null, + latestSequence: null, + updateAvailable: false, + checkedAt: snapshot.checkedAt, + message: "This Prime installation is maintained manually.", + }); + } + return Effect.gen(function* () { + const executablePath = path.resolve( + yield* resolveCommandPath(effectiveConfig.binaryPath || "prime-agent", { + env: processEnv, + }), + ); + const publicPackage = yield* locatePrimeAgentPublicPackage(executablePath); + return yield* Effect.promise(() => + inspectPrimeAgentDistribution( + { + stateDir: serverConfig.stateDir, + instanceId, + packageRoot: publicPackage.packageRoot, + platform: hostPlatform, + checkedAt: snapshot.checkedAt, + ...(enableUpdateChecks === undefined ? {} : { enableUpdateChecks }), + }, + { loadLatestVerifiedPublication }, + ), + ); + }).pipe( + Effect.catchCause(() => + Effect.succeed({ + classification: "stock-or-custom" as const, + channel: null, + buildId: null, + sequence: null, + latestBuildId: null, + latestSequence: null, + updateAvailable: false, + checkedAt: snapshot.checkedAt, + message: + "Pylon could not inspect the selected Prime distribution; Prime remains ready and manually maintained.", + }), + ), + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), + ); + }; const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, env: processEnv, @@ -286,6 +358,7 @@ export const PrimeAgentDriver: ProviderDriver { + it("keeps runtime readiness independent and maps only signed build identity to advisory", () => { + const snapshot: ServerProvider = { + instanceId: ProviderInstanceId.make("primeAgent"), + driver: ProviderDriverKind.make("primeAgent"), + enabled: true, + installed: true, + version: "0.8.1", + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-08-20T12:00:00.000Z", + models: [], + slashCommands: [], + skills: [], + }; + const result = applyPrimeAgentDistribution(snapshot, { + classification: "pylon-managed", + channel: "preview", + buildId: "pylon-build-g0123456789ab-r1", + sequence: 9, + latestBuildId: "pylon-build-gabcdef012345-r1", + latestSequence: 10, + updateAvailable: true, + checkedAt: "2026-08-20T12:00:00.000Z", + message: "A signed preview build is available.", + }); + + expect(result.status).toBe("ready"); + expect(result.version).toBe("0.8.1"); + expect(result.versionAdvisory).toMatchObject({ + status: "behind_latest", + currentVersion: "pylon-build-g0123456789ab-r1", + latestVersion: "pylon-build-gabcdef012345-r1", + canUpdate: false, + updateCommand: null, + }); + }); +}); + describe("PrimeAgentProvider models", () => { it("always publishes the synthetic default model before unique custom models", () => { expect( diff --git a/apps/server/src/provider/Layers/PrimeAgentProvider.ts b/apps/server/src/provider/Layers/PrimeAgentProvider.ts index f503778752..71c5d5b6e9 100644 --- a/apps/server/src/provider/Layers/PrimeAgentProvider.ts +++ b/apps/server/src/provider/Layers/PrimeAgentProvider.ts @@ -2,6 +2,7 @@ import { type ModelCapabilities, type PrimeAgentSettings, type ServerProvider, + type ServerProviderDistribution, type ServerProviderModel, type ServerProviderBackend, } from "@t3tools/contracts"; @@ -534,20 +535,48 @@ ${versionOutput.stderr}`); }); }); +export function applyPrimeAgentDistribution( + snapshot: ServerProvider, + distribution: ServerProviderDistribution, +): ServerProvider { + const versionAdvisory = + distribution.classification === "pylon-managed" + ? { + status: distribution.updateAvailable ? ("behind_latest" as const) : ("current" as const), + currentVersion: distribution.buildId, + latestVersion: distribution.latestBuildId, + updateCommand: null, + canUpdate: false, + checkedAt: distribution.checkedAt, + message: distribution.updateAvailable ? distribution.message : null, + } + : snapshot.versionAdvisory; + return { + ...snapshot, + distribution, + ...(versionAdvisory ? { versionAdvisory } : {}), + }; +} + export const enrichPrimeAgentSnapshot = (input: { readonly snapshot: ServerProvider; readonly maintenanceCapabilities: ProviderMaintenanceCapabilities; readonly enableProviderUpdateChecks?: boolean; + readonly distribution: Effect.Effect; readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect; readonly httpClient: HttpClient.HttpClient; }): Effect.Effect => - enrichProviderSnapshotWithVersionAdvisory(input.snapshot, input.maintenanceCapabilities, { - enableProviderUpdateChecks: input.enableProviderUpdateChecks, + Effect.gen(function* () { + const snapshot = yield* enrichProviderSnapshotWithVersionAdvisory( + input.snapshot, + input.maintenanceCapabilities, + { enableProviderUpdateChecks: input.enableProviderUpdateChecks }, + ).pipe(Effect.provideService(HttpClient.HttpClient, input.httpClient)); + const distribution = yield* input.distribution; + yield* input.publishSnapshot(applyPrimeAgentDistribution(snapshot, distribution)); }).pipe( - Effect.provideService(HttpClient.HttpClient, input.httpClient), - Effect.flatMap(input.publishSnapshot), Effect.catchCause((cause) => - Effect.logWarning("Prime Agent version advisory enrichment failed", { cause }), + Effect.logWarning("Prime Agent distribution advisory enrichment failed", { cause }), ), Effect.asVoid, ); diff --git a/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts new file mode 100644 index 0000000000..467adee91c --- /dev/null +++ b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts @@ -0,0 +1,753 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeCrypto from "node:crypto"; +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; + +import { + assertPrimeAttestationBinding, + assertPrimeCertificateClaims, + authenticatePrimeManagedState, + canonicalPrimeDistributionJson, + inspectPrimeAgentDistribution, + PRIME_DISTRIBUTION_REF, + PRIME_DISTRIBUTION_REPOSITORY, + PRIME_DISTRIBUTION_REPOSITORY_URL, + PRIME_HIGH_WATER_FILE, + PRIME_PREVIEW_MANIFEST, + PRIME_PREVIEW_WORKFLOW, + PRIME_PUBLICATION_POLICY, + PRIME_PUBLICATION_SCHEMA_SOURCE, + PRIME_RECEIPT_FILE, + PRIME_RECEIPT_KEY_FILE, + PRIME_RELEASE_MANIFEST, + PRIME_STABLE_WORKFLOW, + persistPrimeManagedReceipt, + primeDistributionStateDirectory, + requireRealPrimePublicationFixture, + type ExpectedPrimeAttestation, + type PrimeManagedStatePayload, + type PrimePublicationFixture, + type PrimeSlsaStatement, + type VerifiedPrimePublication, + verifyPrimePublicationFixture, +} from "./PrimeAgentDistributionVerifier.ts"; + +const temporaryDirectories: string[] = []; +afterEach(async () => { + vi.restoreAllMocks(); + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => NodeFSP.rm(directory, { recursive: true, force: true })), + ); +}); + +const digest = (algorithm: "sha256" | "sha512", value: Buffer): string => + NodeCrypto.createHash(algorithm).update(value).digest("hex"); + +const SOURCE_COMMIT = "a".repeat(40); +const SOURCE_TREE = "b".repeat(40); +const POLICY_COMMIT = "c".repeat(40); +const POLICY_TREE = "d".repeat(40); +const BUILD_ID = `pylon-build-g${SOURCE_COMMIT.slice(0, 12)}-r1`; +const VERSION = "1.0.0"; + +function syntheticPublication( + channel: "preview" | "stable" = "preview", +): PrimePublicationFixture & { readonly verified: VerifiedPrimePublication } { + 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")], + [`pylon-prime-agent-core-${VERSION}.tgz`, Buffer.from("synthetic-core-tarball-v1")], + [`pylon-prime-agent-tui-${VERSION}.tgz`, Buffer.from("synthetic-tui-tarball-v1")], + ]); + const packages = new Map([ + [`pylon-prime-agent-${VERSION}.tgz`, "prime-agent"], + [`pylon-prime-agent-ai-${VERSION}.tgz`, "@earendil-works/pi-ai"], + [`pylon-prime-agent-core-${VERSION}.tgz`, "@earendil-works/pi-agent-core"], + [`pylon-prime-agent-tui-${VERSION}.tgz`, "@earendil-works/pi-tui"], + ]); + const assets = [...artifactBytes] + .map(([file, bytes]) => ({ + package: packages.get(file)!, + file, + size: bytes.byteLength, + sha256: digest("sha256", bytes), + sha512: digest("sha512", bytes), + })) + .toSorted((left, right) => (left.file < right.file ? -1 : left.file > right.file ? 1 : 0)); + const releaseManifest = { + schemaVersion: 1, + source: { + repository: PRIME_DISTRIBUTION_REPOSITORY_URL, + commit: SOURCE_COMMIT, + tree: SOURCE_TREE, + }, + build: { + id: BUILD_ID, + recipeRevision: 1, + 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}`, + }, + package: { + name: "prime-agent", + command: "prime-agent", + version: VERSION, + minimumNode: "22.8.0", + }, + assets, + attestationSubjects: assets.map((asset) => ({ + name: asset.file, + digest: { sha256: asset.sha256, sha512: asset.sha512 }, + })), + }; + const releaseManifestBytes = Buffer.from(canonicalPrimeDistributionJson(releaseManifest)); + const previewManifest = { + schemaVersion: 1, + channel: "preview", + repository: PRIME_DISTRIBUTION_REPOSITORY_URL, + publicationPolicyRevision: 1, + sequenceEpoch: 1, + sequence: 7, + workflowRunId: "9001", + build: { + tag: BUILD_ID, + id: BUILD_ID, + recipeRevision: 1, + source: releaseManifest.source, + releaseManifest: { + file: PRIME_RELEASE_MANIFEST, + sha256: digest("sha256", releaseManifestBytes), + }, + }, + assets: assets.map(({ file, size, sha256, sha512 }) => ({ file, size, sha256, sha512 })), + }; + const previewManifestBytes = Buffer.from(canonicalPrimeDistributionJson(previewManifest)); + const stableManifest = { + schemaVersion: 1, + channel: "stable", + repository: PRIME_DISTRIBUTION_REPOSITORY_URL, + sequence: 3, + tag: `pylon-stable-000003-g${SOURCE_COMMIT.slice(0, 12)}-r1`, + history: { + highWater: 2, + previous: { + tag: `pylon-stable-000002-g${"f".repeat(12)}-r1`, + sha256: "1".repeat(64), + }, + }, + build: { + previewSequence: { + sequenceEpoch: 1, + sequence: 7, + workflowRunId: "9001", + }, + previewTag: BUILD_ID, + id: BUILD_ID, + recipeRevision: 1, + publicationPolicyRevision: 1, + source: releaseManifest.source, + releaseManifest: { + file: PRIME_RELEASE_MANIFEST, + sha256: digest("sha256", releaseManifestBytes), + }, + previewManifest: { + file: PRIME_PREVIEW_MANIFEST, + sha256: digest("sha256", previewManifestBytes), + }, + assets: previewManifest.assets, + }, + promotion: { + kind: "promote", + policyCommit: POLICY_COMMIT, + policyTree: POLICY_TREE, + publicationPolicyRevision: 1, + }, + revocations: [], + }; + const stableManifestBytes = Buffer.from(canonicalPrimeDistributionJson(stableManifest)); + const attestationBundlesBySubjectSha256 = new Map>(); + for (const sha256 of [ + ...assets.map((asset) => asset.sha256), + digest("sha256", releaseManifestBytes), + digest("sha256", previewManifestBytes), + digest("sha256", stableManifestBytes), + ]) { + attestationBundlesBySubjectSha256.set(sha256, [{ synthetic: sha256 }]); + } + const rootAsset = assets.find((asset) => asset.package === "prime-agent")!; + return { + channel, + releaseManifestBytes, + previewManifestBytes, + ...(channel === "stable" ? { stableManifestBytes } : {}), + rootArtifactBytes: artifactBytes.get(rootAsset.file)!, + attestationBundlesBySubjectSha256, + verified: { + channel, + sequenceEpoch: 1, + sequence: channel === "preview" ? 7 : 3, + buildId: BUILD_ID, + sourceCommit: SOURCE_COMMIT, + sourceTree: SOURCE_TREE, + recipeRevision: 1, + rootAsset: rootAsset.file, + rootSha256: rootAsset.sha256, + packageVersion: VERSION, + }, + }; +} + +function statementFor(expected: ExpectedPrimeAttestation): PrimeSlsaStatement { + return { + _type: "https://in-toto.io/Statement/v1", + subject: expected.subjects.map((subject) => ({ + name: subject.name, + digest: { sha256: subject.sha256 }, + })), + predicateType: "https://slsa.dev/provenance/v1", + predicate: { + buildDefinition: { + buildType: "https://actions.github.io/buildtypes/workflow/v1", + externalParameters: { + workflow: { + repository: PRIME_DISTRIBUTION_REPOSITORY_URL, + path: expected.workflow, + ref: PRIME_DISTRIBUTION_REF, + }, + }, + internalParameters: { + github: { + event_name: expected.event, + repository_id: "1349002285", + repository_owner_id: "11325514", + runner_environment: "github-hosted", + }, + }, + resolvedDependencies: [ + { + uri: `git+${PRIME_DISTRIBUTION_REPOSITORY_URL}@${PRIME_DISTRIBUTION_REF}`, + digest: { gitCommit: expected.sourceCommit }, + }, + ], + }, + runDetails: { + builder: { + id: `${PRIME_DISTRIBUTION_REPOSITORY_URL}/${expected.workflow}@${PRIME_DISTRIBUTION_REF}`, + }, + metadata: { + invocationId: `https://github.com/${PRIME_DISTRIBUTION_REPOSITORY}/actions/runs/${expected.workflowRunId ?? "9100"}/attempts/1`, + }, + }, + }, + }; +} + +const validVerification = { + verifyBundle: async (_bundle: unknown, expected: ExpectedPrimeAttestation) => { + const statement = statementFor(expected); + assertPrimeAttestationBinding(statement, expected); + return statement; + }, + verifySourcePolicy: async () => {}, +}; + +async function makePackage(input?: { + readonly version?: string; + readonly metadata?: unknown; +}): Promise<{ readonly root: string; readonly stateDir: string }> { + const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pylon-prime-dist-")); + temporaryDirectories.push(directory); + const root = NodePath.join(directory, "prime-agent"); + const stateDir = NodePath.join(directory, "state"); + await NodeFSP.mkdir(root, { recursive: true }); + await NodeFSP.mkdir(stateDir, { recursive: true }); + await NodeFSP.writeFile( + NodePath.join(root, "package.json"), + JSON.stringify({ + name: "prime-agent", + version: input?.version ?? "0.8.1", + ...(input && "metadata" in input ? { pylonDistribution: input.metadata } : {}), + }), + ); + return { root: await NodeFSP.realpath(root), stateDir }; +} + +function distributionMetadata(sourceCommit = SOURCE_COMMIT) { + return { + schemaVersion: 1, + repository: PRIME_DISTRIBUTION_REPOSITORY_URL, + sourceCommit, + sourceTree: SOURCE_TREE, + buildId: `pylon-build-g${sourceCommit.slice(0, 12)}-r1`, + recipeRevision: 1, + node: "22.23.2", + npm: "11.10.1", + packageLockSha256: "e".repeat(64), + }; +} + +async function writeManagedReceipt(input: { + readonly stateDir: string; + readonly instanceId?: string; + readonly packageRoot: string; + readonly publication?: VerifiedPrimePublication; + readonly corruptHmac?: boolean; + readonly receiptPackageRoot?: string; +}): Promise { + const publication = input.publication ?? syntheticPublication().verified; + const directory = primeDistributionStateDirectory( + input.stateDir, + input.instanceId ?? "primeAgent", + ); + await NodeFSP.mkdir(directory, { recursive: true, mode: 0o700 }); + await NodeFSP.chmod(directory, 0o700); + const key = Buffer.alloc(32, 0x5a); + const payload: PrimeManagedStatePayload = { + schemaVersion: 1, + kind: "managed-receipt", + buildId: publication.buildId, + channel: publication.channel, + sequenceEpoch: 1, + sequence: publication.sequence, + sourceCommit: publication.sourceCommit, + sourceTree: publication.sourceTree, + recipeRevision: publication.recipeRevision, + rootAsset: publication.rootAsset, + rootSha256: publication.rootSha256, + packageRoot: input.receiptPackageRoot ?? input.packageRoot, + }; + const receipt = authenticatePrimeManagedState(payload, key); + const source = canonicalPrimeDistributionJson( + input.corruptHmac ? { ...receipt, hmacSha256: "0".repeat(64) } : receipt, + ); + await NodeFSP.writeFile(NodePath.join(directory, PRIME_RECEIPT_KEY_FILE), key, { mode: 0o600 }); + await NodeFSP.writeFile(NodePath.join(directory, PRIME_RECEIPT_FILE), source, { mode: 0o600 }); + await Promise.all([ + NodeFSP.chmod(NodePath.join(directory, PRIME_RECEIPT_KEY_FILE), 0o600), + NodeFSP.chmod(NodePath.join(directory, PRIME_RECEIPT_FILE), 0o600), + ]); +} + +function inspectionInput(root: string, stateDir: string, platform: NodeJS.Platform = "linux") { + return { + stateDir, + instanceId: "primeAgent", + packageRoot: root, + platform, + checkedAt: "2026-08-20T12:00:00.000Z", + } as const; +} + +describe("Pylon Prime publication verification", () => { + it("verifies preview and stable PR #42 manifests through an injected cryptographic verifier", async () => { + expect(PRIME_PUBLICATION_SCHEMA_SOURCE).toEqual({ + commit: "f4d9ef03b529faf2e07031c8b7cd703363316ae5", + tree: "b9a14b389aa64f54527008fb4d6119a7c57c2b58", + }); + expect(PRIME_PUBLICATION_POLICY).toMatchObject({ + publicationPolicyRevision: 1, + previewWorkflowSha256: "e790a5da7063bd40fbd886e84945c3200291194fdbd5b002079349e45356a41d", + stableWorkflowSha256: "dfcecdf6b58f143f9b7a543eadd124c190350ae29ac9eadccb907f1398b0958a", + }); + const preview = syntheticPublication("preview"); + const stable = syntheticPublication("stable"); + await expect(verifyPrimePublicationFixture(preview, validVerification)).resolves.toEqual( + preview.verified, + ); + await expect(verifyPrimePublicationFixture(stable, validVerification)).resolves.toEqual( + stable.verified, + ); + }); + + it("fails closed for tarball, manifest, attestation, source, recipe, and stable history tampering", async () => { + const fixture = syntheticPublication(); + await expect( + verifyPrimePublicationFixture( + { ...fixture, rootArtifactBytes: Buffer.from("synthetic-root-tarball-v2") }, + validVerification, + ), + ).rejects.toThrow(/root tarball digest/u); + + const release = JSON.parse(fixture.releaseManifestBytes.toString("utf8")) as Record< + string, + unknown + >; + const source = release.source as Record; + source.tree = "f".repeat(40); + await expect( + verifyPrimePublicationFixture( + { + ...fixture, + releaseManifestBytes: Buffer.from(canonicalPrimeDistributionJson(release)), + }, + validVerification, + ), + ).rejects.toThrow(/preview manifest|release manifest/iu); + + const recipe = JSON.parse(fixture.releaseManifestBytes.toString("utf8")) as { + build: { recipeRevision: number }; + }; + recipe.build.recipeRevision = 2; + await expect( + verifyPrimePublicationFixture( + { + ...fixture, + releaseManifestBytes: Buffer.from(canonicalPrimeDistributionJson(recipe)), + }, + validVerification, + ), + ).rejects.toThrow(/recipe/u); + + await expect( + verifyPrimePublicationFixture(fixture, { + verifyBundle: async () => { + throw new Error("synthetic signature failure"); + }, + verifySourcePolicy: async () => {}, + }), + ).rejects.toThrow(/No valid Pylon Sigstore attestation/u); + + await expect( + verifyPrimePublicationFixture(fixture, { + ...validVerification, + verifySourcePolicy: async () => { + throw new Error("workflow digest or source tree mismatch"); + }, + }), + ).rejects.toThrow(/workflow digest|source tree/u); + + const stable = syntheticPublication("stable"); + const stableManifest = JSON.parse(stable.stableManifestBytes!.toString("utf8")) as { + history: { highWater: number }; + }; + stableManifest.history.highWater = 0; + await expect( + verifyPrimePublicationFixture( + { + ...stable, + stableManifestBytes: Buffer.from(canonicalPrimeDistributionJson(stableManifest)), + }, + validVerification, + ), + ).rejects.toThrow(/stable manifest/iu); + }); + + it("binds exact GitHub issuer, signer digest, source, workflow/ref, invocation, and subject digest", () => { + const expected: ExpectedPrimeAttestation = { + workflow: PRIME_PREVIEW_WORKFLOW, + event: "push", + signerDigest: SOURCE_COMMIT, + sourceCommit: SOURCE_COMMIT, + workflowRunId: "9001", + subjects: [{ name: PRIME_PREVIEW_MANIFEST, sha256: "1".repeat(64) }], + }; + const statement = statementFor(expected); + expect(() => assertPrimeAttestationBinding(statement, expected)).not.toThrow(); + expect(() => + assertPrimeCertificateClaims( + { + issuer: "https://token.actions.githubusercontent.com", + event: "push", + signerDigest: SOURCE_COMMIT, + repository: PRIME_DISTRIBUTION_REPOSITORY, + ref: PRIME_DISTRIBUTION_REF, + }, + expected, + ), + ).not.toThrow(); + + for (const claims of [ + { issuer: "https://issuer.invalid" }, + { signerDigest: "f".repeat(40) }, + { repository: "attacker/prime-agent" }, + { ref: "refs/heads/main" }, + { event: "workflow_dispatch" }, + ]) { + expect(() => + assertPrimeCertificateClaims( + { + issuer: "https://token.actions.githubusercontent.com", + event: "push", + signerDigest: SOURCE_COMMIT, + repository: PRIME_DISTRIBUTION_REPOSITORY, + ref: PRIME_DISTRIBUTION_REF, + ...claims, + }, + expected, + ), + ).toThrow(/certificate/u); + } + + const mutations: PrimeSlsaStatement[] = [ + { + ...statement, + subject: [{ name: PRIME_PREVIEW_MANIFEST, digest: { sha256: "2".repeat(64) } }], + }, + { + ...statement, + predicate: { + ...statement.predicate, + buildDefinition: { + ...statement.predicate.buildDefinition, + externalParameters: { + workflow: { + ...statement.predicate.buildDefinition.externalParameters.workflow, + path: PRIME_STABLE_WORKFLOW, + }, + }, + }, + }, + }, + { + ...statement, + predicate: { + ...statement.predicate, + buildDefinition: { + ...statement.predicate.buildDefinition, + resolvedDependencies: [ + { + uri: `git+${PRIME_DISTRIBUTION_REPOSITORY_URL}@${PRIME_DISTRIBUTION_REF}`, + digest: { gitCommit: "f".repeat(40) }, + }, + ], + }, + }, + }, + { + ...statement, + predicate: { + ...statement.predicate, + runDetails: { + ...statement.predicate.runDetails, + metadata: { + invocationId: `https://github.com/${PRIME_DISTRIBUTION_REPOSITORY}/actions/runs/9002/attempts/1`, + }, + }, + }, + }, + ]; + for (const mutation of mutations) { + expect(() => assertPrimeAttestationBinding(mutation, expected)).toThrow(/SLSA provenance/u); + } + }); + + it("keeps the real immutable fixture gate fail-closed until all exact inputs exist", () => { + expect(() => requireRealPrimePublicationFixture({})).toThrow(/immutable preview\/stable/u); + expect(() => + requireRealPrimePublicationFixture({ tag: BUILD_ID, artifactDirectory: "relative" }), + ).toThrow(/absolute fixture/u); + expect( + requireRealPrimePublicationFixture({ tag: BUILD_ID, artifactDirectory: "/tmp/prime-proof" }), + ).toEqual({ tag: BUILD_ID, artifactDirectory: "/tmp/prime-proof" }); + }); +}); + +describe("Pylon Prime distribution classification and advisory", () => { + it("keeps stock 0.8.1 ready for manual maintenance without a fork update warning", async () => { + const { root, stateDir } = await makePackage({ version: "0.8.1" }); + const loadLatestVerifiedPublication = vi.fn(); + const result = await inspectPrimeAgentDistribution(inspectionInput(root, stateDir), { + loadLatestVerifiedPublication, + }); + expect(result).toMatchObject({ + classification: "stock-or-custom", + buildId: null, + updateAvailable: false, + }); + expect(result.message).toMatch(/maintained manually/u); + expect(loadLatestVerifiedPublication).not.toHaveBeenCalled(); + }); + + it("classifies a manual fork and forged distribution metadata as unmanaged", async () => { + const manual = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + const manualResult = await inspectPrimeAgentDistribution( + inspectionInput(manual.root, manual.stateDir), + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(manualResult.classification).toBe("pylon-unmanaged"); + expect(manualResult.updateAvailable).toBe(false); + + const forged = await makePackage({ + version: VERSION, + metadata: { ...distributionMetadata(), repository: "https://attacker.invalid/prime-agent" }, + }); + const forgedResult = await inspectPrimeAgentDistribution( + inspectionInput(forged.root, forged.stateDir), + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(forgedResult.classification).toBe("pylon-unmanaged"); + }); + + it("requires the exact authenticated receipt and package root for pylon-managed", async () => { + const selected = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + const verifiedPublication = await verifyPrimePublicationFixture( + syntheticPublication(), + validVerification, + ); + await persistPrimeManagedReceipt({ + stateDir: selected.stateDir, + instanceId: "primeAgent", + packageRoot: selected.root, + platform: "linux", + publication: verifiedPublication, + }); + await expect( + persistPrimeManagedReceipt({ + stateDir: selected.stateDir, + instanceId: "primeAgent", + packageRoot: selected.root, + platform: "linux", + publication: verifiedPublication, + }), + ).rejects.toThrow(/will not be replaced/u); + const managed = await inspectPrimeAgentDistribution( + { ...inspectionInput(selected.root, selected.stateDir), enableUpdateChecks: false }, + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(managed).toMatchObject({ + classification: "pylon-managed", + channel: "preview", + buildId: BUILD_ID, + sequence: 7, + latestBuildId: BUILD_ID, + latestSequence: 7, + updateAvailable: false, + }); + + const mismatch = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + await writeManagedReceipt({ + stateDir: mismatch.stateDir, + packageRoot: mismatch.root, + receiptPackageRoot: selected.root, + }); + const invalid = await inspectPrimeAgentDistribution( + inspectionInput(mismatch.root, mismatch.stateDir), + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(invalid.classification).toBe("invalid-receipt"); + expect(invalid.message).toMatch(/package root/u); + }); + + it("treats malformed, forged, or symlinked receipt state as invalid without disabling Prime", async () => { + const corrupt = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + await writeManagedReceipt({ + stateDir: corrupt.stateDir, + packageRoot: corrupt.root, + corruptHmac: true, + }); + const corruptResult = await inspectPrimeAgentDistribution( + inspectionInput(corrupt.root, corrupt.stateDir), + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(corruptResult.classification).toBe("invalid-receipt"); + expect(corruptResult.message).toMatch(/Prime remains usable/u); + + const symlinked = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + const directory = primeDistributionStateDirectory(symlinked.stateDir, "primeAgent"); + await NodeFSP.mkdir(directory, { recursive: true, mode: 0o700 }); + await NodeFSP.chmod(directory, 0o700); + const external = NodePath.join(symlinked.stateDir, "external-receipt"); + await NodeFSP.writeFile(external, "{}", { mode: 0o600 }); + await NodeFSP.symlink(external, NodePath.join(directory, PRIME_RECEIPT_FILE)); + await NodeFSP.writeFile(NodePath.join(directory, PRIME_RECEIPT_KEY_FILE), Buffer.alloc(32), { + mode: 0o600, + }); + const symlinkResult = await inspectPrimeAgentDistribution( + inspectionInput(symlinked.root, symlinked.stateDir), + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(symlinkResult.classification).toBe("invalid-receipt"); + }); + + it("orders updates only by signed sequence/build id, persists high-water, and rejects replay", async () => { + const selected = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + await writeManagedReceipt({ stateDir: selected.stateDir, packageRoot: selected.root }); + const nextCommit = "9".repeat(40); + const next: VerifiedPrimePublication = { + ...syntheticPublication().verified, + sequence: 8, + buildId: `pylon-build-g${nextCommit.slice(0, 12)}-r1`, + sourceCommit: nextCommit, + sourceTree: "8".repeat(40), + // Deliberately the same package version: SemVer must not order builds. + packageVersion: VERSION, + rootSha256: "7".repeat(64), + }; + const first = await inspectPrimeAgentDistribution( + inspectionInput(selected.root, selected.stateDir), + { loadLatestVerifiedPublication: async () => next }, + ); + expect(first).toMatchObject({ + classification: "pylon-managed", + sequence: 7, + latestSequence: 8, + latestBuildId: next.buildId, + updateAvailable: true, + }); + const highWaterPath = NodePath.join( + primeDistributionStateDirectory(selected.stateDir, "primeAgent"), + PRIME_HIGH_WATER_FILE, + ); + expect((await NodeFSP.lstat(highWaterPath)).isFile()).toBe(true); + + const replay = await inspectPrimeAgentDistribution( + inspectionInput(selected.root, selected.stateDir), + { loadLatestVerifiedPublication: async () => syntheticPublication().verified }, + ); + expect(replay).toMatchObject({ + classification: "pylon-managed", + latestSequence: 8, + latestBuildId: next.buildId, + updateAvailable: true, + }); + expect(replay.message).toMatch(/replay|unavailable or invalid/u); + }); + + it("fails soft offline and retains the authenticated installed build and prior advisory", async () => { + const selected = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + await writeManagedReceipt({ stateDir: selected.stateDir, packageRoot: selected.root }); + const offline = await inspectPrimeAgentDistribution( + inspectionInput(selected.root, selected.stateDir), + { + loadLatestVerifiedPublication: async () => { + throw new Error("rate limited"); + }, + }, + ); + expect(offline).toMatchObject({ + classification: "pylon-managed", + buildId: BUILD_ID, + latestBuildId: BUILD_ID, + updateAvailable: false, + }); + expect(offline.message).toMatch(/installed build remains ready/u); + }); + + it("supports Linux, macOS, and WSL2 receipts but explicitly leaves native Windows unmanaged", async () => { + for (const platform of ["linux", "darwin"] as const) { + const selected = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + await writeManagedReceipt({ stateDir: selected.stateDir, packageRoot: selected.root }); + const result = await inspectPrimeAgentDistribution( + { + ...inspectionInput(selected.root, selected.stateDir, platform), + enableUpdateChecks: false, + }, + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(result.classification).toBe("pylon-managed"); + } + + const windows = await makePackage({ version: VERSION, metadata: distributionMetadata() }); + const windowsResult = await inspectPrimeAgentDistribution( + inspectionInput(windows.root, windows.stateDir, "win32"), + { loadLatestVerifiedPublication: vi.fn() }, + ); + expect(windowsResult.classification).toBe("pylon-unmanaged"); + expect(windowsResult.message).toMatch(/WSL2/u); + }); +}); diff --git a/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts new file mode 100644 index 0000000000..9cd05b7beb --- /dev/null +++ b/apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts @@ -0,0 +1,1705 @@ +// @effect-diagnostics globalFetch:off +// @effect-diagnostics nodeBuiltinImport:off +import { bundleFromJSON, assertBundleLatest, isBundleWithDsseEnvelope } from "@sigstore/bundle"; +import { X509Certificate } from "@sigstore/core"; +import { getTrustedRoot } from "@sigstore/tuf"; +import { toSignedEntity, toTrustMaterial, Verifier } from "@sigstore/verify"; +import type { + ServerProviderDistribution, + ServerProviderDistributionChannel, +} from "@t3tools/contracts"; +import * as NodeCrypto from "node:crypto"; +import * as NodeFS from "node:fs"; +import * as NodeFSP from "node:fs/promises"; +import * as NodePath from "node:path"; +import * as Schema from "effect/Schema"; + +export const PRIME_DISTRIBUTION_REPOSITORY = "pylon-code/prime-agent"; +export const PRIME_DISTRIBUTION_REPOSITORY_URL = "https://github.com/pylon-code/prime-agent"; +export const PRIME_DISTRIBUTION_REF = "refs/heads/pylon"; +export const PRIME_PREVIEW_WORKFLOW = ".github/workflows/pylon-preview-release.yml"; +export const PRIME_STABLE_WORKFLOW = ".github/workflows/pylon-stable-release.yml"; +export const PRIME_RELEASE_MANIFEST = "pylon-prime-agent-release-v1.json"; +export const PRIME_PREVIEW_MANIFEST = "pylon-preview-channel-v1.json"; +export const PRIME_STABLE_MANIFEST = "pylon-stable-channel-v1.json"; +export const PRIME_RECEIPT_FILE = "managed-receipt-v1.json"; +export const PRIME_HIGH_WATER_FILE = "channel-high-water-v1.json"; +export const PRIME_RECEIPT_KEY_FILE = "receipt-auth-v1.key"; + +const GITHUB_REPOSITORY_ID = "1349002285"; +const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; +const SLSA_PROVENANCE_V1 = "https://slsa.dev/provenance/v1"; +const WORKFLOW_BUILD_TYPE = "https://actions.github.io/buildtypes/workflow/v1"; +const IN_TOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json"; +const MAX_MANIFEST_BYTES = 64 * 1024; +const MAX_PACKAGE_MANIFEST_BYTES = 256 * 1024; +const MAX_RECEIPT_BYTES = 64 * 1024; +const MAX_ATTESTATION_RESPONSE_BYTES = 4 * 1024 * 1024; +const MAX_RELEASE_RESPONSE_BYTES = 4 * 1024 * 1024; +const MAX_ROOT_ARTIFACT_BYTES = 256 * 1024 * 1024; +const MAX_FEED_CANDIDATES = 12; +const FETCH_TIMEOUT_MS = 12_000; + +const SHA256 = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/)); +const SHA512 = Schema.String.check(Schema.isPattern(/^[0-9a-f]{128}$/)); +const GIT_SHA = Schema.String.check(Schema.isPattern(/^[0-9a-f]{40}$/)); +const POSITIVE_DECIMAL = Schema.String.check(Schema.isPattern(/^[1-9][0-9]*$/)); +const POSITIVE_INT = Schema.Int.check(Schema.isGreaterThan(0)); +const PACKAGE_VERSION = Schema.String.check( + Schema.isPattern(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/), +); +const PREVIEW_TAG = Schema.String.check( + Schema.isPattern(/^pylon-build-g[0-9a-f]{12}-r[1-9][0-9]*$/), +); +const STABLE_TAG = Schema.String.check( + Schema.isPattern(/^pylon-stable-[0-9]{6}-g[0-9a-f]{12}-r[1-9][0-9]*$/), +); +const SAFE_ASSET = Schema.String.check( + Schema.isPattern( + /^pylon-prime-agent(?:-(?:ai|core|tui))?-\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\.tgz$/, + ), +); + +const strict = (schema: S) => + schema.annotate({ parseOptions: { onExcessProperty: "error" } }); + +const SourceSchema = strict( + Schema.Struct({ + repository: Schema.Literal(PRIME_DISTRIBUTION_REPOSITORY_URL), + commit: GIT_SHA, + tree: GIT_SHA, + }), +); + +const ReleaseAssetSchema = strict( + Schema.Struct({ + package: Schema.String, + file: SAFE_ASSET, + size: POSITIVE_INT, + sha256: SHA256, + sha512: SHA512, + }), +); + +const AttestationSubjectSchema = strict( + Schema.Struct({ + name: Schema.String, + digest: strict(Schema.Struct({ sha256: SHA256, sha512: SHA512 })), + }), +); + +const ReleaseManifestSchema = strict( + Schema.Struct({ + schemaVersion: Schema.Literal(1), + source: SourceSchema, + build: strict( + Schema.Struct({ + id: PREVIEW_TAG, + recipeRevision: POSITIVE_INT, + node: Schema.String, + npm: Schema.String, + lockfile: strict( + Schema.Struct({ file: Schema.Literal("package-lock.json"), sha256: SHA256 }), + ), + assetBaseUrl: Schema.String, + }), + ), + package: strict( + Schema.Struct({ + name: Schema.Literal("prime-agent"), + command: Schema.Literal("prime-agent"), + version: PACKAGE_VERSION, + minimumNode: Schema.String, + }), + ), + assets: Schema.Array(ReleaseAssetSchema), + attestationSubjects: Schema.Array(AttestationSubjectSchema), + }), +); + +const PublicationAssetSchema = strict( + Schema.Struct({ file: SAFE_ASSET, size: POSITIVE_INT, sha256: SHA256, sha512: SHA512 }), +); + +const PreviewSequenceSchema = strict( + Schema.Struct({ + sequenceEpoch: Schema.Literal(1), + sequence: POSITIVE_INT, + workflowRunId: POSITIVE_DECIMAL, + }), +); + +const PreviewManifestSchema = strict( + Schema.Struct({ + schemaVersion: Schema.Literal(1), + channel: Schema.Literal("preview"), + repository: Schema.Literal(PRIME_DISTRIBUTION_REPOSITORY_URL), + publicationPolicyRevision: POSITIVE_INT, + sequenceEpoch: Schema.Literal(1), + sequence: POSITIVE_INT, + workflowRunId: POSITIVE_DECIMAL, + build: strict( + Schema.Struct({ + tag: PREVIEW_TAG, + id: PREVIEW_TAG, + recipeRevision: POSITIVE_INT, + source: SourceSchema, + releaseManifest: strict( + Schema.Struct({ file: Schema.Literal(PRIME_RELEASE_MANIFEST), sha256: SHA256 }), + ), + }), + ), + assets: Schema.Array(PublicationAssetSchema), + }), +); + +const StablePreviousSchema = strict(Schema.Struct({ tag: STABLE_TAG, sha256: SHA256 })); +const RevocationSchema = strict( + Schema.Struct({ + stableTag: STABLE_TAG, + buildTag: PREVIEW_TAG, + reason: Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9-]{2,63}$/)), + revokedBySequence: POSITIVE_INT, + }), +); +const PromotionSchema = Schema.Union([ + strict( + Schema.Struct({ + kind: Schema.Literal("promote"), + policyCommit: GIT_SHA, + policyTree: GIT_SHA, + publicationPolicyRevision: POSITIVE_INT, + }), + ), + strict( + Schema.Struct({ + kind: Schema.Literal("withdraw"), + policyCommit: GIT_SHA, + policyTree: GIT_SHA, + publicationPolicyRevision: POSITIVE_INT, + revocation: RevocationSchema, + }), + ), +]); + +const StableManifestSchema = strict( + Schema.Struct({ + schemaVersion: Schema.Literal(1), + channel: Schema.Literal("stable"), + repository: Schema.Literal(PRIME_DISTRIBUTION_REPOSITORY_URL), + sequence: POSITIVE_INT, + tag: STABLE_TAG, + history: strict( + Schema.Struct({ + highWater: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), + previous: Schema.NullOr(StablePreviousSchema), + }), + ), + build: strict( + Schema.Struct({ + previewSequence: PreviewSequenceSchema, + previewTag: PREVIEW_TAG, + id: PREVIEW_TAG, + recipeRevision: POSITIVE_INT, + publicationPolicyRevision: POSITIVE_INT, + source: SourceSchema, + releaseManifest: strict( + Schema.Struct({ file: Schema.Literal(PRIME_RELEASE_MANIFEST), sha256: SHA256 }), + ), + previewManifest: strict( + Schema.Struct({ file: Schema.Literal(PRIME_PREVIEW_MANIFEST), sha256: SHA256 }), + ), + assets: Schema.Array(PublicationAssetSchema), + }), + ), + promotion: PromotionSchema, + revocations: Schema.Array(RevocationSchema), + }), +); + +const DistributionMetadataSchema = strict( + Schema.Struct({ + schemaVersion: Schema.Literal(1), + repository: Schema.Literal(PRIME_DISTRIBUTION_REPOSITORY_URL), + sourceCommit: GIT_SHA, + sourceTree: GIT_SHA, + buildId: PREVIEW_TAG, + recipeRevision: POSITIVE_INT, + node: Schema.String, + npm: Schema.String, + packageLockSha256: SHA256, + }), +); + +const PackageManifestSchema = Schema.Struct({ + name: Schema.Literal("prime-agent"), + version: PACKAGE_VERSION, + pylonDistribution: Schema.optional(Schema.Unknown), +}); + +const ManagedStatePayloadSchema = strict( + Schema.Struct({ + schemaVersion: Schema.Literal(1), + kind: Schema.Literals(["managed-receipt", "channel-high-water"]), + buildId: PREVIEW_TAG, + channel: Schema.Literals(["preview", "stable"]), + sequenceEpoch: Schema.Literal(1), + sequence: POSITIVE_INT, + sourceCommit: GIT_SHA, + sourceTree: GIT_SHA, + recipeRevision: POSITIVE_INT, + rootAsset: SAFE_ASSET, + rootSha256: SHA256, + packageRoot: Schema.String, + }), +); +const ManagedStateSchema = strict( + Schema.Struct({ + schemaVersion: Schema.Literal(1), + kind: Schema.Literals(["managed-receipt", "channel-high-water"]), + buildId: PREVIEW_TAG, + channel: Schema.Literals(["preview", "stable"]), + sequenceEpoch: Schema.Literal(1), + sequence: POSITIVE_INT, + sourceCommit: GIT_SHA, + sourceTree: GIT_SHA, + recipeRevision: POSITIVE_INT, + rootAsset: SAFE_ASSET, + rootSha256: SHA256, + packageRoot: Schema.String, + hmacSha256: SHA256, + }), +); + +const StatementSubjectSchema = strict( + Schema.Struct({ name: Schema.String, digest: strict(Schema.Struct({ sha256: SHA256 })) }), +); +const SlsaStatementSchema = Schema.Struct({ + _type: Schema.String, + subject: Schema.Array(StatementSubjectSchema), + predicateType: Schema.Literal(SLSA_PROVENANCE_V1), + predicate: Schema.Struct({ + buildDefinition: Schema.Struct({ + buildType: Schema.Literal(WORKFLOW_BUILD_TYPE), + externalParameters: Schema.Struct({ + workflow: Schema.Struct({ + repository: Schema.String, + path: Schema.String, + ref: Schema.String, + }), + }), + internalParameters: Schema.Struct({ + github: Schema.Struct({ + event_name: Schema.String, + repository_id: Schema.Union([Schema.String, Schema.Number]), + repository_owner_id: Schema.Union([Schema.String, Schema.Number]), + runner_environment: Schema.String, + }), + }), + resolvedDependencies: Schema.Array( + Schema.Struct({ uri: Schema.String, digest: Schema.Struct({ gitCommit: GIT_SHA }) }), + ), + }), + runDetails: Schema.Struct({ + builder: Schema.Struct({ id: Schema.String }), + metadata: Schema.Struct({ invocationId: Schema.String }), + }), + }), +}); + +const GitHubReleaseAssetSchema = Schema.Struct({ + id: POSITIVE_INT, + name: Schema.String, + size: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), + browser_download_url: Schema.String, +}); +const GitHubReleaseSchema = Schema.Struct({ + id: POSITIVE_INT, + tag_name: Schema.String, + draft: Schema.Boolean, + prerelease: Schema.Boolean, + immutable: Schema.Boolean, + assets: Schema.Array(GitHubReleaseAssetSchema), +}); +const GitHubAttestationsSchema = Schema.Struct({ + attestations: Schema.Array(Schema.Struct({ bundle: Schema.Unknown })), +}); +const GitHubCommitSchema = Schema.Struct({ + sha: GIT_SHA, + tree: Schema.Struct({ sha: GIT_SHA }), +}); + +const decodeReleaseManifest = Schema.decodeUnknownSync(ReleaseManifestSchema); +const decodePreviewManifest = Schema.decodeUnknownSync(PreviewManifestSchema); +const decodeStableManifest = Schema.decodeUnknownSync(StableManifestSchema); +const decodePackageManifest = Schema.decodeUnknownSync(PackageManifestSchema); +const decodeDistributionMetadata = Schema.decodeUnknownSync(DistributionMetadataSchema); +const decodeManagedStatePayload = Schema.decodeUnknownSync(ManagedStatePayloadSchema); +const decodeManagedState = Schema.decodeUnknownSync(ManagedStateSchema); +const decodeSlsaStatement = Schema.decodeUnknownSync(SlsaStatementSchema); +const decodeGitHubRelease = Schema.decodeUnknownSync(GitHubReleaseSchema); +const decodeGitHubReleases = Schema.decodeUnknownSync(Schema.Array(GitHubReleaseSchema)); +const decodeGitHubAttestations = Schema.decodeUnknownSync(GitHubAttestationsSchema); +const decodeGitHubCommit = Schema.decodeUnknownSync(GitHubCommitSchema); + +type ReleaseManifest = typeof ReleaseManifestSchema.Type; +type PreviewManifest = typeof PreviewManifestSchema.Type; +type StableManifest = typeof StableManifestSchema.Type; +type DistributionMetadata = typeof DistributionMetadataSchema.Type; +export type PrimeManagedStatePayload = typeof ManagedStatePayloadSchema.Type; +type PrimeManagedState = typeof ManagedStateSchema.Type; +export type PrimeSlsaStatement = typeof SlsaStatementSchema.Type; +type GitHubRelease = typeof GitHubReleaseSchema.Type; + +type TrustedRoot = Awaited>; + +export const PRIME_RELEASE_RECIPE = Object.freeze({ + recipeRevision: 1, + manifestSchemaVersion: 1, + nodeVersion: "22.23.2", + npmVersion: "11.10.1", + minimumNodeVersion: "22.8.0", +}); +export const PRIME_PUBLICATION_SCHEMA_SOURCE = Object.freeze({ + commit: "f4d9ef03b529faf2e07031c8b7cd703363316ae5", + tree: "b9a14b389aa64f54527008fb4d6119a7c57c2b58", +}); +export const PRIME_PUBLICATION_POLICY = Object.freeze({ + publicationPolicyRevision: 1, + previewWorkflowPath: PRIME_PREVIEW_WORKFLOW, + previewWorkflowSha256: "e790a5da7063bd40fbd886e84945c3200291194fdbd5b002079349e45356a41d", + stableWorkflowPath: PRIME_STABLE_WORKFLOW, + stableWorkflowSha256: "dfcecdf6b58f143f9b7a543eadd124c190350ae29ac9eadccb907f1398b0958a", +}); + +function compareText(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} + +function canonicalValue(value: unknown): unknown { + if (value === null || typeof value === "string" || typeof value === "boolean") return value; + if (typeof value === "number" && Number.isFinite(value)) return value; + if (Array.isArray(value)) return value.map(canonicalValue); + if (typeof value !== "object") throw new Error("Publication JSON contains an unsupported value."); + const record = value as Readonly>; + return Object.fromEntries( + Object.keys(record) + .sort(compareText) + .map((key) => [key, canonicalValue(record[key])]), + ); +} + +export function canonicalPrimeDistributionJson(value: unknown): string { + return `${JSON.stringify(canonicalValue(value), null, 2)}\n`; +} + +function sha256(value: NodeJS.ArrayBufferView | string): string { + return NodeCrypto.createHash("sha256").update(value).digest("hex"); +} + +function sha512(value: NodeJS.ArrayBufferView | string): string { + return NodeCrypto.createHash("sha512").update(value).digest("hex"); +} + +function parseCanonicalJson( + bytes: Buffer, + name: string, + maxBytes: number, + decode: (input: unknown) => T, +): T { + if (bytes.byteLength < 1 || bytes.byteLength > maxBytes) { + throw new Error(`${name} exceeds its bounded size.`); + } + let parsed: unknown; + try { + parsed = JSON.parse(bytes.toString("utf8")) as unknown; + } catch (cause) { + throw new Error(`${name} is not JSON.`, { cause }); + } + const decoded = decode(parsed); + if (canonicalPrimeDistributionJson(decoded) !== bytes.toString("utf8")) { + throw new Error(`${name} is not canonical publication JSON.`); + } + return decoded; +} + +function parseReleaseManifest(bytes: Buffer): ReleaseManifest { + const manifest = parseCanonicalJson( + bytes, + "Pylon Prime release manifest", + MAX_MANIFEST_BYTES, + decodeReleaseManifest, + ); + 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 || + manifest.build.id !== + `pylon-build-g${manifest.source.commit.slice(0, 12)}-r${manifest.build.recipeRevision}` || + manifest.build.assetBaseUrl !== + `${PRIME_DISTRIBUTION_REPOSITORY_URL}/releases/download/${manifest.build.id}` + ) { + throw new Error("Release manifest does not match the frozen recipe and source identity."); + } + const version = manifest.package.version; + const expectedPackages = new Map([ + [`pylon-prime-agent-ai-${version}.tgz`, "@earendil-works/pi-ai"], + [`pylon-prime-agent-core-${version}.tgz`, "@earendil-works/pi-agent-core"], + [`pylon-prime-agent-${version}.tgz`, "prime-agent"], + [`pylon-prime-agent-tui-${version}.tgz`, "@earendil-works/pi-tui"], + ]); + const files = manifest.assets.map((asset) => asset.file); + if ( + manifest.assets.length !== 4 || + manifest.attestationSubjects.length !== 4 || + files.join("\n") !== files.toSorted(compareText).join("\n") || + new Set(files).size !== files.length + ) { + throw new Error("Release manifest asset set is not exact, unique, and sorted."); + } + for (let index = 0; index < manifest.assets.length; index += 1) { + const asset = manifest.assets[index]!; + const subject = manifest.attestationSubjects[index]!; + if ( + expectedPackages.get(asset.file) !== asset.package || + subject.name !== asset.file || + subject.digest.sha256 !== asset.sha256 || + subject.digest.sha512 !== asset.sha512 + ) { + throw new Error(`Release subject does not bind its exact artifact: ${asset.file}`); + } + expectedPackages.delete(asset.file); + } + if (expectedPackages.size !== 0) throw new Error("Release manifest omits a required package."); + return manifest; +} + +function parsePreviewManifest( + bytes: Buffer, + release: ReleaseManifest, + releaseBytes: Buffer, +): PreviewManifest { + const manifest = parseCanonicalJson( + bytes, + "Pylon Prime preview manifest", + MAX_MANIFEST_BYTES, + decodePreviewManifest, + ); + if ( + manifest.publicationPolicyRevision !== PRIME_PUBLICATION_POLICY.publicationPolicyRevision || + manifest.build.tag !== release.build.id || + manifest.build.id !== release.build.id || + manifest.build.recipeRevision !== release.build.recipeRevision || + canonicalPrimeDistributionJson(manifest.build.source) !== + canonicalPrimeDistributionJson(release.source) || + manifest.build.releaseManifest.sha256 !== sha256(releaseBytes) || + canonicalPrimeDistributionJson(manifest.assets) !== + canonicalPrimeDistributionJson( + release.assets.map(({ file, size, sha256: sha256Digest, sha512: sha512Digest }) => ({ + file, + size, + sha256: sha256Digest, + sha512: sha512Digest, + })), + ) + ) { + throw new Error("Preview manifest does not bind the exact deterministic build manifest."); + } + return manifest; +} + +function parseStableManifest(bytes: Buffer): StableManifest { + const manifest = parseCanonicalJson( + bytes, + "Pylon Prime stable manifest", + MAX_MANIFEST_BYTES, + decodeStableManifest, + ); + const stableMatch = /^pylon-stable-([0-9]{6})-g([0-9a-f]{12})-r([1-9][0-9]*)$/.exec(manifest.tag); + if ( + !stableMatch || + Number(stableMatch[1]) !== manifest.sequence || + stableMatch[2] !== manifest.build.source.commit.slice(0, 12) || + Number(stableMatch[3]) !== manifest.build.recipeRevision || + manifest.history.highWater !== manifest.sequence - 1 || + (manifest.sequence === 1 + ? manifest.history.previous !== null + : manifest.history.previous === null || + 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 || + manifest.build.publicationPolicyRevision !== + PRIME_PUBLICATION_POLICY.publicationPolicyRevision || + manifest.promotion.publicationPolicyRevision !== + PRIME_PUBLICATION_POLICY.publicationPolicyRevision + ) { + throw new Error("Stable manifest is not an exact closed Pylon build receipt."); + } + const promotion = manifest.promotion; + const sortedRevocations = manifest.revocations.toSorted((left, right) => + compareText(left.stableTag, right.stableTag), + ); + if ( + canonicalPrimeDistributionJson(sortedRevocations) !== + canonicalPrimeDistributionJson(manifest.revocations) || + new Set(manifest.revocations.map((entry) => entry.stableTag)).size !== + manifest.revocations.length || + manifest.revocations.some((entry) => { + const revokedSequence = Number(/^pylon-stable-([0-9]{6})-/u.exec(entry.stableTag)?.[1]); + return ( + revokedSequence >= entry.revokedBySequence || entry.revokedBySequence > manifest.sequence + ); + }) || + (promotion.kind === "withdraw" && + (promotion.revocation.revokedBySequence !== manifest.sequence || + !manifest.revocations.some( + (entry) => + canonicalPrimeDistributionJson(entry) === + canonicalPrimeDistributionJson(promotion.revocation), + ))) + ) { + throw new Error("Stable revocation history is not an exact sorted append-only receipt."); + } + return manifest; +} + +export interface ExpectedPrimeAttestation { + readonly workflow: typeof PRIME_PREVIEW_WORKFLOW | typeof PRIME_STABLE_WORKFLOW; + readonly event: "push" | "workflow_dispatch"; + readonly signerDigest: string; + readonly sourceCommit: string; + readonly workflowRunId?: string; + readonly subjects: ReadonlyArray<{ readonly name: string; readonly sha256: string }>; +} + +function exactRegex(value: string): string { + return `^${value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}$`; +} + +function certificateExtension(cert: X509Certificate, oid: string): string | undefined { + const extension = cert.extension(oid); + const value = extension?.valueObj.subs[0]?.value; + return value?.toString("utf8"); +} + +function certificateFromBundle(bundle: ReturnType): X509Certificate { + const material = bundle.verificationMaterial.content; + if (material.$case === "certificate") { + return X509Certificate.parse( + `-----BEGIN CERTIFICATE-----\n${Buffer.from(material.certificate.rawBytes).toString("base64")}\n-----END CERTIFICATE-----`, + ); + } + if (material.$case === "x509CertificateChain") { + const leaf = material.x509CertificateChain.certificates[0]; + if (!leaf) throw new Error("Sigstore bundle certificate chain is empty."); + return X509Certificate.parse( + `-----BEGIN CERTIFICATE-----\n${Buffer.from(leaf.rawBytes).toString("base64")}\n-----END CERTIFICATE-----`, + ); + } + throw new Error("Pylon publication requires a Fulcio certificate bundle."); +} + +export interface PrimeCertificateClaims { + readonly issuer: string | undefined; + readonly event: string | undefined; + readonly signerDigest: string | undefined; + readonly repository: string | undefined; + readonly ref: string | undefined; +} + +export function assertPrimeCertificateClaims( + claims: PrimeCertificateClaims, + expected: ExpectedPrimeAttestation, +): void { + if ( + claims.issuer !== GITHUB_OIDC_ISSUER || + claims.event !== expected.event || + claims.signerDigest !== expected.signerDigest || + claims.repository !== PRIME_DISTRIBUTION_REPOSITORY || + claims.ref !== PRIME_DISTRIBUTION_REF + ) { + throw new Error( + "Sigstore GitHub certificate does not match the exact issuer, signer, repository, and ref.", + ); + } +} + +function assertGitHubCertificatePolicy( + certificate: X509Certificate, + expected: ExpectedPrimeAttestation, +): void { + assertPrimeCertificateClaims( + { + issuer: certificateExtension(certificate, "1.3.6.1.4.1.57264.1.1"), + event: certificateExtension(certificate, "1.3.6.1.4.1.57264.1.2"), + signerDigest: certificateExtension(certificate, "1.3.6.1.4.1.57264.1.3"), + repository: certificateExtension(certificate, "1.3.6.1.4.1.57264.1.5"), + ref: certificateExtension(certificate, "1.3.6.1.4.1.57264.1.6"), + }, + expected, + ); +} + +function parseBundleStatement(bundle: ReturnType): PrimeSlsaStatement { + if (!isBundleWithDsseEnvelope(bundle)) { + throw new Error("Pylon publication attestation is not a DSSE envelope."); + } + const envelope = bundle.content.dsseEnvelope; + if (envelope.payloadType !== IN_TOTO_PAYLOAD_TYPE || envelope.signatures.length !== 1) { + throw new Error("Pylon publication attestation has an unsupported DSSE envelope."); + } + let parsed: unknown; + try { + parsed = JSON.parse(envelope.payload.toString("utf8")) as unknown; + } catch (cause) { + throw new Error("Sigstore DSSE payload is not JSON.", { cause }); + } + return decodeSlsaStatement(parsed); +} + +export function assertPrimeAttestationBinding( + statement: PrimeSlsaStatement, + expected: ExpectedPrimeAttestation, +): void { + const expectedSubjects = expected.subjects + .map((subject) => ({ name: subject.name, digest: { sha256: subject.sha256 } })) + .toSorted((left, right) => compareText(left.name, right.name)); + const actualSubjects = statement.subject.toSorted((left, right) => + compareText(left.name, right.name), + ); + const definition = statement.predicate.buildDefinition; + const workflow = definition.externalParameters.workflow; + const github = definition.internalParameters.github; + const dependencies = definition.resolvedDependencies; + const invocation = new RegExp( + `^https://github\\.com/${PRIME_DISTRIBUTION_REPOSITORY}/actions/runs/([1-9][0-9]*)/attempts/([1-9][0-9]*)$`, + "u", + ).exec(statement.predicate.runDetails.metadata.invocationId); + if ( + statement._type !== "https://in-toto.io/Statement/v1" || + canonicalPrimeDistributionJson(actualSubjects) !== + canonicalPrimeDistributionJson(expectedSubjects) || + new Set(actualSubjects.map((subject) => subject.name)).size !== actualSubjects.length || + workflow.repository !== PRIME_DISTRIBUTION_REPOSITORY_URL || + workflow.path !== expected.workflow || + workflow.ref !== PRIME_DISTRIBUTION_REF || + github.event_name !== expected.event || + String(github.repository_id) !== GITHUB_REPOSITORY_ID || + github.runner_environment !== "github-hosted" || + !/^[1-9][0-9]*$/u.test(String(github.repository_owner_id)) || + dependencies.length !== 1 || + dependencies[0]?.uri !== `git+${PRIME_DISTRIBUTION_REPOSITORY_URL}@${PRIME_DISTRIBUTION_REF}` || + dependencies[0]?.digest.gitCommit !== expected.sourceCommit || + statement.predicate.runDetails.builder.id !== + `${PRIME_DISTRIBUTION_REPOSITORY_URL}/${expected.workflow}@${PRIME_DISTRIBUTION_REF}` || + !invocation || + (expected.workflowRunId !== undefined && invocation[1] !== expected.workflowRunId) + ) { + throw new Error( + "SLSA provenance does not bind the exact source, workflow/ref, run, and subjects.", + ); + } +} + +export function verifyPrimeSigstoreBundle( + bundleJson: unknown, + trustedRoot: TrustedRoot, + expected: ExpectedPrimeAttestation, +): PrimeSlsaStatement { + const bundle = bundleFromJSON(bundleJson); + assertBundleLatest(bundle); + const verifier = new Verifier(toTrustMaterial(trustedRoot), { + tlogThreshold: 1, + ctlogThreshold: 1, + tsaThreshold: 0, + }); + verifier.verify(toSignedEntity(bundle), { + subjectAlternativeName: exactRegex( + `${PRIME_DISTRIBUTION_REPOSITORY_URL}/${expected.workflow}@${PRIME_DISTRIBUTION_REF}`, + ), + extensions: { issuer: GITHUB_OIDC_ISSUER }, + }); + assertGitHubCertificatePolicy(certificateFromBundle(bundle), expected); + const statement = parseBundleStatement(bundle); + assertPrimeAttestationBinding(statement, expected); + return statement; +} + +export interface PrimeSourcePolicyExpectation { + readonly commit: string; + readonly tree: string; + readonly workflow: typeof PRIME_PREVIEW_WORKFLOW | typeof PRIME_STABLE_WORKFLOW; + readonly publicationPolicyRevision: number; +} + +export interface PrimePublicationVerificationDependencies { + readonly verifyBundle: ( + bundle: unknown, + expected: ExpectedPrimeAttestation, + ) => Promise; + readonly verifySourcePolicy: (expected: PrimeSourcePolicyExpectation) => Promise; +} + +export interface PrimePublicationFixture { + readonly channel: ServerProviderDistributionChannel; + readonly releaseManifestBytes: Buffer; + readonly previewManifestBytes: Buffer; + readonly stableManifestBytes?: Buffer; + readonly rootArtifactBytes?: Buffer; + readonly attestationBundlesBySubjectSha256: ReadonlyMap>; +} + +export interface VerifiedPrimePublication { + readonly channel: ServerProviderDistributionChannel; + readonly sequenceEpoch: 1; + readonly sequence: number; + readonly buildId: string; + readonly sourceCommit: string; + readonly sourceTree: string; + readonly recipeRevision: number; + readonly rootAsset: string; + readonly rootSha256: string; + readonly packageVersion: string; +} + +async function verifyExpectedSubjectBundles( + expected: ExpectedPrimeAttestation, + bundlesByDigest: PrimePublicationFixture["attestationBundlesBySubjectSha256"], + dependencies: PrimePublicationVerificationDependencies, +): Promise { + for (const subject of expected.subjects) { + const bundles = bundlesByDigest.get(subject.sha256); + if (!bundles || bundles.length < 1 || bundles.length > 20) { + throw new Error(`No bounded Sigstore bundle set exists for ${subject.name}.`); + } + let verified = false; + for (const bundle of bundles) { + try { + await dependencies.verifyBundle(bundle, expected); + verified = true; + } catch { + // Public repositories can accumulate unrelated attestations. Only one exact, fully verified + // Pylon signer/source/subject binding is required for this digest. + } + } + if (!verified) + throw new Error(`No valid Pylon Sigstore attestation exists for ${subject.name}.`); + } +} + +export async function verifyPrimePublicationFixture( + fixture: PrimePublicationFixture, + dependencies: PrimePublicationVerificationDependencies, +): Promise { + const release = parseReleaseManifest(fixture.releaseManifestBytes); + const preview = parsePreviewManifest( + fixture.previewManifestBytes, + release, + fixture.releaseManifestBytes, + ); + await dependencies.verifySourcePolicy({ + commit: preview.build.source.commit, + tree: preview.build.source.tree, + workflow: PRIME_PREVIEW_WORKFLOW, + publicationPolicyRevision: preview.publicationPolicyRevision, + }); + let channelSequence: number; + if (fixture.channel === "stable") { + if (!fixture.stableManifestBytes) throw new Error("Stable verification requires its manifest."); + const stable = parseStableManifest(fixture.stableManifestBytes); + if ( + stable.build.previewManifest.sha256 !== sha256(fixture.previewManifestBytes) || + stable.build.releaseManifest.sha256 !== sha256(fixture.releaseManifestBytes) || + stable.build.previewTag !== preview.build.tag || + stable.build.id !== preview.build.id || + stable.build.recipeRevision !== preview.build.recipeRevision || + canonicalPrimeDistributionJson(stable.build.source) !== + canonicalPrimeDistributionJson(preview.build.source) || + canonicalPrimeDistributionJson(stable.build.assets) !== + canonicalPrimeDistributionJson(preview.assets) + ) { + throw new Error("Stable manifest does not bind the exact verified preview and recipe."); + } + await dependencies.verifySourcePolicy({ + commit: stable.promotion.policyCommit, + tree: stable.promotion.policyTree, + workflow: PRIME_STABLE_WORKFLOW, + publicationPolicyRevision: stable.promotion.publicationPolicyRevision, + }); + const stableExpected: ExpectedPrimeAttestation = { + workflow: PRIME_STABLE_WORKFLOW, + event: "workflow_dispatch", + signerDigest: stable.promotion.policyCommit, + sourceCommit: stable.promotion.policyCommit, + subjects: [{ name: PRIME_STABLE_MANIFEST, sha256: sha256(fixture.stableManifestBytes) }], + }; + await verifyExpectedSubjectBundles( + stableExpected, + fixture.attestationBundlesBySubjectSha256, + dependencies, + ); + channelSequence = stable.sequence; + } else { + if (fixture.stableManifestBytes !== undefined) { + throw new Error("Preview verification cannot accept a stable manifest."); + } + channelSequence = preview.sequence; + } + + const previewSubjects = [ + ...release.assets.map((asset) => ({ name: asset.file, sha256: asset.sha256 })), + { name: PRIME_RELEASE_MANIFEST, sha256: sha256(fixture.releaseManifestBytes) }, + { name: PRIME_PREVIEW_MANIFEST, sha256: sha256(fixture.previewManifestBytes) }, + ].toSorted((left, right) => compareText(left.name, right.name)); + await verifyExpectedSubjectBundles( + { + workflow: PRIME_PREVIEW_WORKFLOW, + event: "push", + signerDigest: preview.build.source.commit, + sourceCommit: preview.build.source.commit, + workflowRunId: preview.workflowRunId, + subjects: previewSubjects, + }, + fixture.attestationBundlesBySubjectSha256, + dependencies, + ); + + const root = release.assets.find((asset) => asset.package === "prime-agent"); + if (!root) throw new Error("Release manifest has no Prime root artifact."); + if ( + fixture.rootArtifactBytes && + (fixture.rootArtifactBytes.byteLength !== root.size || + fixture.rootArtifactBytes.byteLength > MAX_ROOT_ARTIFACT_BYTES || + sha256(fixture.rootArtifactBytes) !== root.sha256 || + sha512(fixture.rootArtifactBytes) !== root.sha512) + ) { + throw new Error("Selected Prime root tarball digest does not match its signed subject."); + } + return { + channel: fixture.channel, + sequenceEpoch: 1, + sequence: channelSequence, + buildId: preview.build.id, + sourceCommit: preview.build.source.commit, + sourceTree: preview.build.source.tree, + recipeRevision: preview.build.recipeRevision, + rootAsset: root.file, + rootSha256: root.sha256, + packageVersion: release.package.version, + }; +} + +export function primeDistributionStateDirectory(stateDir: string, instanceId: string): string { + const identity = sha256(instanceId).slice(0, 32); + return NodePath.join(stateDir, "provider-state", "prime-agent-distribution", identity); +} + +export function authenticatePrimeManagedState( + payload: PrimeManagedStatePayload, + key: Buffer, +): PrimeManagedState { + if (key.byteLength !== 32) + throw new Error("Managed receipt authentication key must be 32 bytes."); + const decodedPayload = decodeManagedStatePayload(payload); + return { + ...decodedPayload, + hmacSha256: NodeCrypto.createHmac("sha256", key) + .update(canonicalPrimeDistributionJson(decodedPayload)) + .digest("hex"), + }; +} + +function verifyStateHmac(state: PrimeManagedState, key: Buffer): void { + const { hmacSha256, ...payload } = state; + const actual = Buffer.from(hmacSha256, "hex"); + const expected = Buffer.from( + NodeCrypto.createHmac("sha256", key) + .update(canonicalPrimeDistributionJson(payload)) + .digest("hex"), + "hex", + ); + if (actual.byteLength !== expected.byteLength || !NodeCrypto.timingSafeEqual(actual, expected)) { + throw new Error("Managed receipt authentication failed."); + } +} + +interface OpenedPrivateFile { + readonly bytes: Buffer; + readonly dev: bigint; + readonly ino: bigint; +} + +async function readPrivateFile( + path: string, + maxBytes: number, + uid: number, +): Promise { + const handle = await NodeFSP.open( + path, + NodeFS.constants.O_RDONLY | (NodeFS.constants.O_NOFOLLOW ?? 0), + ); + try { + const before = await handle.stat({ bigint: true }); + if (!before.isFile() || Number(before.uid) !== uid || (Number(before.mode) & 0o777) !== 0o600) { + throw new Error(`Private distribution state is not an owner-only regular file: ${path}`); + } + if (before.size < 1n || before.size > BigInt(maxBytes)) { + throw new Error(`Private distribution state exceeds its bounded size: ${path}`); + } + const bytes = Buffer.alloc(Number(before.size)); + let offset = 0; + while (offset < bytes.byteLength) { + const read = await handle.read(bytes, offset, bytes.byteLength - offset, offset); + if (read.bytesRead === 0) + throw new Error(`Private distribution state was truncated: ${path}`); + offset += read.bytesRead; + } + const after = await handle.stat({ bigint: true }); + const pathAfter = await NodeFSP.lstat(path, { bigint: true }); + if ( + before.dev !== after.dev || + before.ino !== after.ino || + before.size !== after.size || + before.mtimeNs !== after.mtimeNs || + after.dev !== pathAfter.dev || + after.ino !== pathAfter.ino || + !pathAfter.isFile() + ) { + throw new Error(`Private distribution state changed while it was read: ${path}`); + } + return { bytes, dev: before.dev, ino: before.ino }; + } finally { + await handle.close(); + } +} + +async function pathKind(path: string): Promise<"missing" | "file" | "other"> { + try { + const info = await NodeFSP.lstat(path); + return info.isFile() ? "file" : "other"; + } catch (cause) { + if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === "ENOENT") { + return "missing"; + } + throw cause; + } +} + +interface ManagedStateRead { + readonly receipt: PrimeManagedState; + readonly highWater: PrimeManagedState; + readonly key: Buffer; + readonly directory: string; +} + +async function readManagedState( + stateDir: string, + instanceId: string, + packageRoot: string, +): Promise { + const uid = process.getuid?.(); + if (uid === undefined) throw new Error("Managed Prime receipts require a numeric current uid."); + const directory = primeDistributionStateDirectory(stateDir, instanceId); + const receiptPath = NodePath.join(directory, PRIME_RECEIPT_FILE); + const keyPath = NodePath.join(directory, PRIME_RECEIPT_KEY_FILE); + const highWaterPath = NodePath.join(directory, PRIME_HIGH_WATER_FILE); + const kinds = await Promise.all([ + pathKind(receiptPath), + pathKind(keyPath), + pathKind(highWaterPath), + ]); + if (kinds.every((kind) => kind === "missing")) return undefined; + if (kinds[0] !== "file" || kinds[1] !== "file" || kinds[2] === "other") { + throw new Error("Managed Prime receipt state is partial or not regular-file state."); + } + const directoryInfo = await NodeFSP.lstat(directory); + if ( + !directoryInfo.isDirectory() || + directoryInfo.isSymbolicLink() || + directoryInfo.uid !== uid || + (directoryInfo.mode & 0o777) !== 0o700 + ) { + throw new Error("Managed Prime receipt directory is not private owner-only state."); + } + const [keyRead, receiptRead] = await Promise.all([ + readPrivateFile(keyPath, 32, uid), + readPrivateFile(receiptPath, MAX_RECEIPT_BYTES, uid), + ]); + if (keyRead.bytes.byteLength !== 32) throw new Error("Managed Prime receipt key is malformed."); + const receipt = parseCanonicalJson( + receiptRead.bytes, + "Managed Prime receipt", + MAX_RECEIPT_BYTES, + decodeManagedState, + ); + verifyStateHmac(receipt, keyRead.bytes); + if (receipt.kind !== "managed-receipt" || receipt.packageRoot !== packageRoot) { + throw new Error("Managed Prime receipt does not bind the exact selected package root."); + } + let highWater = receipt; + if (kinds[2] === "file") { + const highWaterRead = await readPrivateFile(highWaterPath, MAX_RECEIPT_BYTES, uid); + highWater = parseCanonicalJson( + highWaterRead.bytes, + "Managed Prime channel high-water", + MAX_RECEIPT_BYTES, + decodeManagedState, + ); + verifyStateHmac(highWater, keyRead.bytes); + if ( + highWater.kind !== "channel-high-water" || + highWater.channel !== receipt.channel || + highWater.sequenceEpoch !== receipt.sequenceEpoch || + highWater.packageRoot !== receipt.packageRoot || + highWater.sequence < receipt.sequence || + (highWater.sequence === receipt.sequence && highWater.buildId !== receipt.buildId) + ) { + throw new Error("Managed Prime channel high-water conflicts with its install receipt."); + } + } + return { receipt, highWater, key: keyRead.bytes, directory }; +} + +async function writeHighWater( + state: ManagedStateRead, + candidate: VerifiedPrimePublication, +): Promise { + const next = authenticatePrimeManagedState( + { + schemaVersion: 1, + kind: "channel-high-water", + buildId: candidate.buildId, + channel: candidate.channel, + sequenceEpoch: candidate.sequenceEpoch, + sequence: candidate.sequence, + sourceCommit: candidate.sourceCommit, + sourceTree: candidate.sourceTree, + recipeRevision: candidate.recipeRevision, + rootAsset: candidate.rootAsset, + rootSha256: candidate.rootSha256, + packageRoot: state.receipt.packageRoot, + }, + state.key, + ); + const target = NodePath.join(state.directory, PRIME_HIGH_WATER_FILE); + const temporary = NodePath.join( + state.directory, + `.channel-high-water-${process.pid}-${NodeCrypto.randomBytes(12).toString("hex")}.tmp`, + ); + const handle = await NodeFSP.open( + temporary, + NodeFS.constants.O_WRONLY | NodeFS.constants.O_CREAT | NodeFS.constants.O_EXCL, + 0o600, + ); + try { + await handle.writeFile(canonicalPrimeDistributionJson(next), "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + try { + await NodeFSP.rename(temporary, target); + const directoryHandle = await NodeFSP.open(state.directory, NodeFS.constants.O_RDONLY); + try { + await directoryHandle.sync(); + } finally { + await directoryHandle.close(); + } + } catch (cause) { + await NodeFSP.rm(temporary, { force: true }); + throw cause; + } + return next; +} + +async function readPackageDistribution(packageRoot: string): Promise<{ + readonly packageRoot: string; + readonly version: string; + readonly claimed: boolean; + readonly metadata?: DistributionMetadata; +}> { + const canonicalRoot = await NodeFSP.realpath(packageRoot); + if (canonicalRoot !== packageRoot) { + throw new Error("Selected Prime package root is not its canonical path."); + } + const manifestPath = NodePath.join(packageRoot, "package.json"); + const handle = await NodeFSP.open( + manifestPath, + NodeFS.constants.O_RDONLY | (NodeFS.constants.O_NOFOLLOW ?? 0), + ); + let bytes: Buffer; + try { + const before = await handle.stat({ bigint: true }); + if (!before.isFile() || before.size < 1n || before.size > BigInt(MAX_PACKAGE_MANIFEST_BYTES)) { + throw new Error("Selected Prime package manifest is not one bounded regular file."); + } + bytes = Buffer.alloc(Number(before.size)); + let offset = 0; + while (offset < bytes.byteLength) { + const read = await handle.read(bytes, offset, bytes.byteLength - offset, offset); + if (read.bytesRead === 0) throw new Error("Selected Prime package manifest was truncated."); + offset += read.bytesRead; + } + const after = await handle.stat({ bigint: true }); + const pathAfter = await NodeFSP.lstat(manifestPath, { bigint: true }); + if ( + before.dev !== after.dev || + before.ino !== after.ino || + before.size !== after.size || + before.mtimeNs !== after.mtimeNs || + after.dev !== pathAfter.dev || + after.ino !== pathAfter.ino || + !pathAfter.isFile() + ) { + throw new Error("Selected Prime package manifest changed while it was read."); + } + } finally { + await handle.close(); + } + let raw: unknown; + try { + raw = JSON.parse(bytes.toString("utf8")) as unknown; + } catch (cause) { + throw new Error("Selected Prime package manifest is not JSON.", { cause }); + } + const manifest = decodePackageManifest(raw); + const claimed = manifest.pylonDistribution !== undefined; + if (!claimed) return { packageRoot, version: manifest.version, claimed: false }; + try { + return { + packageRoot, + version: manifest.version, + claimed: true, + metadata: decodeDistributionMetadata(manifest.pylonDistribution), + }; + } catch { + return { packageRoot, version: manifest.version, claimed: true }; + } +} + +function assertReceiptMatchesPackage( + receipt: PrimeManagedState, + metadata: DistributionMetadata, + version: string, +): void { + if ( + receipt.buildId !== metadata.buildId || + receipt.sourceCommit !== metadata.sourceCommit || + receipt.sourceTree !== metadata.sourceTree || + receipt.recipeRevision !== metadata.recipeRevision || + receipt.rootAsset !== `pylon-prime-agent-${version}.tgz` + ) { + throw new Error( + "Managed receipt does not bind the selected package metadata and root artifact.", + ); + } +} + +export interface PersistPrimeManagedReceiptInput { + readonly stateDir: string; + readonly instanceId: string; + readonly packageRoot: string; + readonly platform: NodeJS.Platform; + readonly publication: VerifiedPrimePublication; +} + +async function writeExclusivePrivateFile(path: string, bytes: Buffer | string): Promise { + const handle = await NodeFSP.open( + path, + NodeFS.constants.O_WRONLY | NodeFS.constants.O_CREAT | NodeFS.constants.O_EXCL, + 0o600, + ); + try { + await handle.writeFile(bytes); + await handle.sync(); + } finally { + await handle.close(); + } +} + +async function assertPrivateDirectory(path: string, uid: number): Promise { + const info = await NodeFSP.lstat(path); + if ( + !info.isDirectory() || + info.isSymbolicLink() || + info.uid !== uid || + (info.mode & 0o777) !== 0o700 + ) { + throw new Error(`Prime distribution state directory is not private owner-only state: ${path}`); + } +} + +/** + * Persists one immutable managed-install receipt after real publication verification succeeds. + * It never mutates the package and never replaces existing receipt state. + */ +export async function persistPrimeManagedReceipt( + input: PersistPrimeManagedReceiptInput, +): Promise { + if (input.platform === "win32") { + throw new Error("Native Windows Prime managed receipts are unavailable; use WSL2."); + } + const uid = process.getuid?.(); + if (uid === undefined) throw new Error("Managed Prime receipts require a numeric current uid."); + const selected = await readPackageDistribution(input.packageRoot); + if (!selected.metadata) { + throw new Error("A managed receipt requires exact Pylon distribution metadata."); + } + const publication = input.publication; + if ( + publication.buildId !== selected.metadata.buildId || + publication.sourceCommit !== selected.metadata.sourceCommit || + publication.sourceTree !== selected.metadata.sourceTree || + publication.recipeRevision !== selected.metadata.recipeRevision || + publication.rootAsset !== `pylon-prime-agent-${selected.version}.tgz` + ) { + throw new Error("Verified publication does not match the selected Prime package."); + } + + const finalDirectory = primeDistributionStateDirectory(input.stateDir, input.instanceId); + const parent = NodePath.dirname(finalDirectory); + await NodeFSP.mkdir(parent, { recursive: true, mode: 0o700 }); + await assertPrivateDirectory(parent, uid); + if ((await pathKind(finalDirectory)) !== "missing") { + throw new Error("Managed Prime receipt state already exists and will not be replaced."); + } + await NodeFSP.mkdir(finalDirectory, { mode: 0o700 }); + await assertPrivateDirectory(finalDirectory, uid); + const key = NodeCrypto.randomBytes(32); + const receipt = authenticatePrimeManagedState( + { + schemaVersion: 1, + kind: "managed-receipt", + buildId: publication.buildId, + channel: publication.channel, + sequenceEpoch: publication.sequenceEpoch, + sequence: publication.sequence, + sourceCommit: publication.sourceCommit, + sourceTree: publication.sourceTree, + recipeRevision: publication.recipeRevision, + rootAsset: publication.rootAsset, + rootSha256: publication.rootSha256, + packageRoot: selected.packageRoot, + }, + key, + ); + await writeExclusivePrivateFile(NodePath.join(finalDirectory, PRIME_RECEIPT_KEY_FILE), key); + await writeExclusivePrivateFile( + NodePath.join(finalDirectory, PRIME_RECEIPT_FILE), + canonicalPrimeDistributionJson(receipt), + ); + const directoryHandle = await NodeFSP.open(finalDirectory, NodeFS.constants.O_RDONLY); + try { + await directoryHandle.sync(); + } finally { + await directoryHandle.close(); + } + const parentHandle = await NodeFSP.open(parent, NodeFS.constants.O_RDONLY); + try { + await parentHandle.sync(); + } finally { + await parentHandle.close(); + } +} + +export interface PrimeDistributionInspectionInput { + readonly stateDir: string; + readonly instanceId: string; + readonly packageRoot: string; + readonly platform: NodeJS.Platform; + readonly checkedAt: string; + readonly enableUpdateChecks?: boolean; +} + +export interface PrimeDistributionInspectionDependencies { + readonly loadLatestVerifiedPublication: ( + channel: ServerProviderDistributionChannel, + ) => Promise; +} + +function distributionStatus( + input: Partial & Pick, +): ServerProviderDistribution { + return { + classification: input.classification, + channel: input.channel ?? null, + buildId: input.buildId ?? null, + sequence: input.sequence ?? null, + latestBuildId: input.latestBuildId ?? null, + latestSequence: input.latestSequence ?? null, + updateAvailable: input.updateAvailable ?? false, + checkedAt: input.checkedAt ?? null, + message: input.message ?? null, + }; +} + +export async function inspectPrimeAgentDistribution( + input: PrimeDistributionInspectionInput, + dependencies: PrimeDistributionInspectionDependencies, +): Promise { + let selected; + try { + selected = await readPackageDistribution(input.packageRoot); + } catch (cause) { + return distributionStatus({ + classification: "stock-or-custom", + checkedAt: input.checkedAt, + message: `Pylon could not inspect the selected Prime package root: ${cause instanceof Error ? cause.message : String(cause)}`, + }); + } + if (!selected.claimed) { + return distributionStatus({ + classification: "stock-or-custom", + checkedAt: input.checkedAt, + message: "This Prime installation is maintained manually.", + }); + } + if (!selected.metadata) { + return distributionStatus({ + classification: "pylon-unmanaged", + checkedAt: input.checkedAt, + message: "This installation claims Pylon metadata, but it has no valid managed receipt.", + }); + } + if (input.platform === "win32") { + return distributionStatus({ + classification: "pylon-unmanaged", + buildId: selected.metadata.buildId, + checkedAt: input.checkedAt, + message: + "Native Windows Prime distributions are unavailable. Use Prime Agent through WSL2, which follows the Linux receipt path.", + }); + } + + let state: ManagedStateRead | undefined; + try { + state = await readManagedState(input.stateDir, input.instanceId, selected.packageRoot); + } catch (cause) { + return distributionStatus({ + classification: "invalid-receipt", + buildId: selected.metadata.buildId, + checkedAt: input.checkedAt, + message: `The managed receipt is invalid; Prime remains usable but managed updates are disabled. ${cause instanceof Error ? cause.message : String(cause)}`, + }); + } + if (!state) { + return distributionStatus({ + classification: "pylon-unmanaged", + buildId: selected.metadata.buildId, + checkedAt: input.checkedAt, + message: "This Pylon Prime build was installed manually and is maintained manually.", + }); + } + try { + assertReceiptMatchesPackage(state.receipt, selected.metadata, selected.version); + } catch (cause) { + return distributionStatus({ + classification: "invalid-receipt", + buildId: selected.metadata.buildId, + checkedAt: input.checkedAt, + message: `The managed receipt does not match the selected Prime package; managed updates are disabled. ${cause instanceof Error ? cause.message : String(cause)}`, + }); + } + + let highWater = state.highWater; + let advisoryMessage: string | null = null; + if (input.enableUpdateChecks !== false) { + try { + const candidate = await dependencies.loadLatestVerifiedPublication(state.receipt.channel); + if ( + candidate.channel !== state.receipt.channel || + candidate.sequenceEpoch !== state.receipt.sequenceEpoch || + candidate.sequence < highWater.sequence || + (candidate.sequence === highWater.sequence && candidate.buildId !== highWater.buildId) + ) { + throw new Error("Signed channel replay conflicts with the authenticated local high-water."); + } + if (candidate.sequence > highWater.sequence) { + highWater = await writeHighWater(state, candidate); + } + } catch (cause) { + advisoryMessage = `The signed ${state.receipt.channel} feed is unavailable or invalid. The verified installed build remains ready. ${cause instanceof Error ? cause.message : String(cause)}`; + } + } + + const updateAvailable = + highWater.sequence > state.receipt.sequence || highWater.buildId !== state.receipt.buildId; + return distributionStatus({ + classification: "pylon-managed", + channel: state.receipt.channel, + buildId: state.receipt.buildId, + sequence: state.receipt.sequence, + latestBuildId: highWater.buildId, + latestSequence: highWater.sequence, + updateAvailable, + checkedAt: input.checkedAt, + message: + advisoryMessage ?? + (updateAvailable + ? `Verified ${state.receipt.channel} build ${highWater.buildId} (sequence ${highWater.sequence}) is available. Pylon will not download or install it automatically.` + : `Verified Pylon-managed ${state.receipt.channel} build ${state.receipt.buildId}.`), + }); +} + +export interface PrimeDistributionNetworkDependencies { + readonly fetchJson: (url: string, maxBytes: number) => Promise; + readonly fetchBytes: (url: string, maxBytes: number) => Promise; + readonly getTrustedRoot: () => Promise; +} + +async function boundedFetch(url: string, maxBytes: number, accept: string): Promise { + const parsed = new URL(url); + const allowedHosts = new Set([ + "api.github.com", + "github.com", + "objects.githubusercontent.com", + "release-assets.githubusercontent.com", + "raw.githubusercontent.com", + ]); + if (parsed.protocol !== "https:" || !allowedHosts.has(parsed.hostname)) { + throw new Error(`Pylon distribution fetch rejected an untrusted URL: ${url}`); + } + const response = await fetch(url, { + headers: { accept, "user-agent": "pylon-prime-distribution-verifier" }, + redirect: "follow", + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }); + const finalUrl = new URL(response.url); + if (finalUrl.protocol !== "https:" || !allowedHosts.has(finalUrl.hostname)) { + throw new Error("Pylon distribution fetch followed an untrusted redirect."); + } + if (!response.ok || !response.body) { + throw new Error(`Pylon distribution fetch failed with HTTP ${response.status}.`); + } + const length = Number(response.headers.get("content-length")); + if (Number.isFinite(length) && length > maxBytes) { + throw new Error("Pylon distribution response exceeds its bounded size."); + } + const reader = response.body.getReader(); + const chunks: Buffer[] = []; + let total = 0; + while (true) { + const chunk = await reader.read(); + if (chunk.done) break; + total += chunk.value.byteLength; + if (total > maxBytes) { + await reader.cancel(); + throw new Error("Pylon distribution response exceeds its bounded size."); + } + chunks.push(Buffer.from(chunk.value)); + } + return Buffer.concat(chunks, total); +} + +export function makePrimeDistributionNetworkDependencies( + options: { readonly tufCachePath?: string } = {}, +): PrimeDistributionNetworkDependencies { + return { + fetchJson: async (url, maxBytes) => { + const bytes = await boundedFetch(url, maxBytes, "application/vnd.github+json"); + return JSON.parse(bytes.toString("utf8")) as unknown; + }, + fetchBytes: (url, maxBytes) => boundedFetch(url, maxBytes, "application/octet-stream"), + getTrustedRoot: () => + getTrustedRoot({ + ...(options.tufCachePath ? { cachePath: options.tufCachePath } : {}), + timeout: FETCH_TIMEOUT_MS, + retry: { retries: 1 }, + }), + }; +} + +function releaseAsset(release: GitHubRelease, name: string): GitHubRelease["assets"][number] { + const matches = release.assets.filter((asset) => asset.name === name); + if (matches.length !== 1) throw new Error(`Immutable release lacks one exact ${name} asset.`); + return matches[0]!; +} + +async function fetchReleaseByTag( + tag: string, + dependencies: PrimeDistributionNetworkDependencies, +): Promise { + const raw = await dependencies.fetchJson( + `https://api.github.com/repos/${PRIME_DISTRIBUTION_REPOSITORY}/releases/tags/${encodeURIComponent(tag)}`, + MAX_RELEASE_RESPONSE_BYTES, + ); + const release = decodeGitHubRelease(raw); + if (release.tag_name !== tag || release.draft || !release.immutable) { + throw new Error("Pylon publication release is not the exact immutable tag."); + } + return release; +} + +async function fetchAttestationBundles( + digest: string, + dependencies: PrimeDistributionNetworkDependencies, +): Promise> { + const raw = await dependencies.fetchJson( + `https://api.github.com/repos/${PRIME_DISTRIBUTION_REPOSITORY}/attestations/sha256:${digest}?predicate_type=${encodeURIComponent(SLSA_PROVENANCE_V1)}`, + MAX_ATTESTATION_RESPONSE_BYTES, + ); + return decodeGitHubAttestations(raw).attestations.map((entry) => entry.bundle); +} + +async function loadPublicationFixtureForRelease( + channel: ServerProviderDistributionChannel, + release: GitHubRelease, + dependencies: PrimeDistributionNetworkDependencies, +): Promise { + let previewRelease = release; + let stableManifestBytes: Buffer | undefined; + if (channel === "stable") { + stableManifestBytes = await dependencies.fetchBytes( + releaseAsset(release, PRIME_STABLE_MANIFEST).browser_download_url, + MAX_MANIFEST_BYTES, + ); + const stable = parseStableManifest(stableManifestBytes); + if (stable.tag !== release.tag_name) throw new Error("Stable release tag and manifest differ."); + previewRelease = await fetchReleaseByTag(stable.build.previewTag, dependencies); + } + const releaseManifestBytes = await dependencies.fetchBytes( + releaseAsset(previewRelease, PRIME_RELEASE_MANIFEST).browser_download_url, + MAX_MANIFEST_BYTES, + ); + const previewManifestBytes = await dependencies.fetchBytes( + releaseAsset(previewRelease, PRIME_PREVIEW_MANIFEST).browser_download_url, + MAX_MANIFEST_BYTES, + ); + const parsedRelease = parseReleaseManifest(releaseManifestBytes); + const expectedPreviewAssets = new Set([ + PRIME_RELEASE_MANIFEST, + PRIME_PREVIEW_MANIFEST, + ...parsedRelease.assets.map((asset) => asset.file), + ]); + if ( + previewRelease.assets.length !== expectedPreviewAssets.size || + previewRelease.assets.some((asset) => !expectedPreviewAssets.delete(asset.name)) || + expectedPreviewAssets.size !== 0 + ) { + throw new Error("Preview release asset set is not exact."); + } + if ( + channel === "stable" && + (release.assets.length !== 1 || release.assets[0]?.name !== PRIME_STABLE_MANIFEST) + ) { + throw new Error("Stable release must contain only its signed singleton manifest."); + } + const subjectDigests = new Set([ + ...parsedRelease.assets.map((asset) => asset.sha256), + sha256(releaseManifestBytes), + sha256(previewManifestBytes), + ...(stableManifestBytes ? [sha256(stableManifestBytes)] : []), + ]); + const attestationBundlesBySubjectSha256 = new Map>(); + await Promise.all( + [...subjectDigests].map(async (digest) => { + attestationBundlesBySubjectSha256.set( + digest, + await fetchAttestationBundles(digest, dependencies), + ); + }), + ); + return { + channel, + releaseManifestBytes, + previewManifestBytes, + ...(stableManifestBytes ? { stableManifestBytes } : {}), + attestationBundlesBySubjectSha256, + }; +} + +async function verifyRemoteSourcePolicy( + expected: PrimeSourcePolicyExpectation, + dependencies: PrimeDistributionNetworkDependencies, +): Promise { + if (expected.publicationPolicyRevision !== PRIME_PUBLICATION_POLICY.publicationPolicyRevision) { + throw new Error("Unsupported Pylon publication policy revision."); + } + const commit = decodeGitHubCommit( + await dependencies.fetchJson( + `https://api.github.com/repos/${PRIME_DISTRIBUTION_REPOSITORY}/git/commits/${expected.commit}`, + MAX_RELEASE_RESPONSE_BYTES, + ), + ); + if (commit.sha !== expected.commit || commit.tree.sha !== expected.tree) { + throw new Error("Pylon publication source commit does not bind the signed full tree."); + } + const workflowBytes = await dependencies.fetchBytes( + `https://raw.githubusercontent.com/${PRIME_DISTRIBUTION_REPOSITORY}/${expected.commit}/${expected.workflow}`, + MAX_MANIFEST_BYTES, + ); + const expectedDigest = + expected.workflow === PRIME_PREVIEW_WORKFLOW + ? PRIME_PUBLICATION_POLICY.previewWorkflowSha256 + : PRIME_PUBLICATION_POLICY.stableWorkflowSha256; + if (sha256(workflowBytes) !== expectedDigest) { + throw new Error("Signer workflow bytes do not match the frozen publication policy revision."); + } +} + +export function makeLatestPrimePublicationLoader( + dependencies: PrimeDistributionNetworkDependencies = makePrimeDistributionNetworkDependencies(), +): PrimeDistributionInspectionDependencies["loadLatestVerifiedPublication"] { + return async (channel) => { + const raw = await dependencies.fetchJson( + `https://api.github.com/repos/${PRIME_DISTRIBUTION_REPOSITORY}/releases?per_page=100`, + MAX_RELEASE_RESPONSE_BYTES, + ); + const releases = decodeGitHubReleases(raw) + .filter( + (release) => + !release.draft && + release.immutable && + (channel === "preview" + ? /^pylon-build-g[0-9a-f]{12}-r[1-9][0-9]*$/u.test(release.tag_name) + : /^pylon-stable-[0-9]{6}-g[0-9a-f]{12}-r[1-9][0-9]*$/u.test(release.tag_name)), + ) + .slice(0, MAX_FEED_CANDIDATES); + if (releases.length === 0) throw new Error(`No immutable ${channel} publication exists.`); + const trustedRoot = await dependencies.getTrustedRoot(); + const verified: VerifiedPrimePublication[] = []; + for (const release of releases) { + try { + const fixture = await loadPublicationFixtureForRelease(channel, release, dependencies); + verified.push( + await verifyPrimePublicationFixture(fixture, { + verifyBundle: async (bundle, expected) => + verifyPrimeSigstoreBundle(bundle, trustedRoot, expected), + verifySourcePolicy: (expected) => verifyRemoteSourcePolicy(expected, dependencies), + }), + ); + } catch { + // One malformed, draft-like, or unrelated release must not hide a later exact candidate. + } + } + const latest = verified.toSorted((left, right) => right.sequence - left.sequence)[0]; + if (!latest) throw new Error(`No exact signed ${channel} publication verified.`); + return latest; + }; +} + +/** + * A real immutable fixture gate for bridge CI. It is deliberately fail-closed: callers must supply + * every byte and bundle through {@link verifyPrimePublicationFixture}; no marker or package metadata + * can stand in for Sigstore verification. + */ +export function requireRealPrimePublicationFixture(input: { + readonly tag?: string; + readonly artifactDirectory?: string; +}): { readonly tag: string; readonly artifactDirectory: string } { + if ( + !input.tag || + !/^pylon-(?:build-g[0-9a-f]{12}-r[1-9][0-9]*|stable-[0-9]{6}-g[0-9a-f]{12}-r[1-9][0-9]*)$/u.test( + input.tag, + ) || + !input.artifactDirectory || + !NodePath.isAbsolute(input.artifactDirectory) + ) { + throw new Error( + "Real Pylon Prime proof requires an immutable preview/stable tag and an absolute fixture directory.", + ); + } + return { tag: input.tag, artifactDirectory: input.artifactDirectory }; +} diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx index 5a7f3d5f66..b272359120 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.tsx +++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx @@ -53,6 +53,7 @@ import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker"; import { RedactedSensitiveText } from "./RedactedSensitiveText"; import { ProviderUsageRows, ProviderUsageSummary } from "../providerUsage/ProviderUsageRows"; import { + getProviderDistributionLabel, getProviderVersionAdvisoryPresentation, PROVIDER_STATUS_STYLES, getProviderSummary, @@ -512,6 +513,7 @@ export function ProviderInstanceCard({ // headline shows, so a broken provider is actionable from the list. const needsAttention = statusKey === "warning" || statusKey === "error"; const versionLabel = getProviderVersionLabel(liveProvider?.version); + const distributionLabel = getProviderDistributionLabel(liveProvider?.distribution); const versionAdvisory = getProviderVersionAdvisoryPresentation(liveProvider?.versionAdvisory); const updateCommand = versionAdvisory?.updateCommand ?? null; const FallbackIconComponent = driverOption?.icon; @@ -852,6 +854,14 @@ export function ProviderInstanceCard({
{titleHeadNode} {versionCodeNode} + {distributionLabel ? ( + + {distributionLabel} + + ) : null} {/* Only the write actions go inert on read-only sessions — the update popover, the drain-order chevrons, and the delete button. diff --git a/apps/web/src/components/settings/providerStatus.test.ts b/apps/web/src/components/settings/providerStatus.test.ts new file mode 100644 index 0000000000..46b86358fa --- /dev/null +++ b/apps/web/src/components/settings/providerStatus.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { getProviderDistributionLabel } from "./providerStatus"; + +describe("getProviderDistributionLabel", () => { + it("keeps stock providers quiet and labels managed, manual, and invalid proof", () => { + expect(getProviderDistributionLabel(undefined)).toBeNull(); + expect( + getProviderDistributionLabel({ + classification: "stock-or-custom", + channel: null, + buildId: null, + sequence: null, + latestBuildId: null, + latestSequence: null, + updateAvailable: false, + checkedAt: null, + message: null, + }), + ).toBeNull(); + expect( + getProviderDistributionLabel({ + classification: "pylon-managed", + channel: "preview", + buildId: "pylon-build-g0123456789ab-r1", + sequence: 9, + latestBuildId: "pylon-build-g0123456789ab-r1", + latestSequence: 9, + updateAvailable: false, + checkedAt: null, + message: null, + }), + ).toBe("Pylon managed · preview #9"); + expect( + getProviderDistributionLabel({ + classification: "pylon-unmanaged", + channel: null, + buildId: "pylon-build-g0123456789ab-r1", + sequence: null, + latestBuildId: null, + latestSequence: null, + updateAvailable: false, + checkedAt: null, + message: null, + }), + ).toBe("Pylon build · manual"); + expect( + getProviderDistributionLabel({ + classification: "invalid-receipt", + channel: null, + buildId: null, + sequence: null, + latestBuildId: null, + latestSequence: null, + updateAvailable: false, + checkedAt: null, + message: null, + }), + ).toBe("Managed receipt invalid"); + }); +}); diff --git a/apps/web/src/components/settings/providerStatus.ts b/apps/web/src/components/settings/providerStatus.ts index 22ede0cd13..b12fcb6f9e 100644 --- a/apps/web/src/components/settings/providerStatus.ts +++ b/apps/web/src/components/settings/providerStatus.ts @@ -1,4 +1,8 @@ -import type { ServerProvider, ServerProviderVersionAdvisory } from "@t3tools/contracts"; +import type { + ServerProvider, + ServerProviderDistribution, + ServerProviderVersionAdvisory, +} from "@t3tools/contracts"; import { getProviderUnavailablePresentation } from "../../providerInstances"; @@ -94,6 +98,17 @@ export function getProviderVersionLabel(version: string | null | undefined) { return version.startsWith("v") ? version : `v${version}`; } +export function getProviderDistributionLabel( + distribution: ServerProviderDistribution | undefined, +): string | null { + if (!distribution || distribution.classification === "stock-or-custom") return null; + if (distribution.classification === "invalid-receipt") return "Managed receipt invalid"; + if (distribution.classification === "pylon-unmanaged") return "Pylon build · manual"; + const channel = distribution.channel ?? "managed"; + const sequence = distribution.sequence === null ? "" : ` #${distribution.sequence}`; + return `Pylon managed · ${channel}${sequence}`; +} + export function getProviderVersionAdvisoryPresentation( advisory: ServerProviderVersionAdvisory | undefined, ): { diff --git a/docs/README.md b/docs/README.md index c3ec64036e..a9d8533513 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,6 +39,7 @@ policy in [CONTRIBUTING.md](../CONTRIBUTING.md); agent rules in [AGENTS.md](../A - [Pylon Connect](./internals/t3-connect.md) - [CI gates](./internals/ci.md) - [Prime Agent native parity](./internals/prime-agent-native-parity.md) +- [Prime Agent distribution verification](./internals/prime-agent-distribution-verification.md) - [Prime Agent integration investigation](./internals/prime-agent-integration.md) - [Engineering work artifacts](./internals/work-artifacts.md) diff --git a/docs/internals/prime-agent-distribution-verification.md b/docs/internals/prime-agent-distribution-verification.md new file mode 100644 index 0000000000..d0d20fef05 --- /dev/null +++ b/docs/internals/prime-agent-distribution-verification.md @@ -0,0 +1,102 @@ +# Prime Agent distribution verification + +Pylon verifies Prime distribution identity at the provider boundary. It does not use distribution +identity to select the daemon SDK, infer capabilities, or suppress ACP fallback. Runtime negotiation +continues to resolve and import the exact configured package and then negotiates post-attach +capabilities. + +## Publication contract + +The verifier freezes the protected-publication schemas from `pylon-code/prime-agent` PR #42 at +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. +- `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 + commit/tree, promotion or withdrawal, and cumulative revocations. + +Unknown recipes, policy revisions, fields, assets, or tag shapes fail closed. The policy registry pins +both publication workflow byte digests. A publication workflow change therefore needs a new reviewed +policy revision rather than a permissive parser change. + +The server uses `@sigstore/bundle`, `@sigstore/core`, `@sigstore/tuf`, and `@sigstore/verify` directly. +It requires a current Sigstore bundle with an inclusion proof, one verified Rekor timestamp, one +verified certificate-transparency timestamp, the GitHub OIDC issuer, and these exact certificate and +SLSA bindings: + +- repository `pylon-code/prime-agent` and `refs/heads/pylon`; +- preview or stable signer workflow and its exact signer digest; +- GitHub-hosted runner, canonical repository id, event, run and attempt; +- source commit, full signed source tree in the manifests, and the exact six preview subjects; +- one stable-manifest subject for stable promotion. + +The fetch and trusted-root functions are injected. Production keeps Sigstore TUF cache data below the +Pylon runtime state directory rather than writing to an unrelated user cache. Focused tests use +deterministic manifests and a cryptographic-verifier seam, then exercise certificate and SLSA binding separately. Bridge CI can +supply the first immutable artifact set through the fail-closed real-fixture gate. The gate has no +skip or metadata-only success mode. + +## Private managed state + +Distribution state is scoped by a SHA-256 hash of the provider instance id below: + +```text +/provider-state/prime-agent-distribution// + receipt-auth-v1.key + managed-receipt-v1.json + channel-high-water-v1.json # appears after a newer signed build is observed +``` + +The directory is current-user `0700`. Each file is current-user `0600`. Reads use `O_NOFOLLOW`, a +bounded exact read, descriptor/path identity checks, and before/after metadata checks. The 32-byte +private key authenticates canonical receipt and high-water JSON with HMAC-SHA-256. A receipt binds: + +- build id and preview/stable channel; +- sequence epoch and channel sequence; +- source commit and tree; +- recipe revision; +- root package asset name and SHA-256; +- the canonical selected package root. + +The status path never creates a managed install receipt. The module exposes an exclusive receipt +persistence function for a future managed installer, and that function accepts the verified +publication result and refuses to replace existing state. It creates the private directory and files +with no replacement only after the exact package metadata matches the verified build. This slice has +no installer caller. The status path can atomically advance the adjacent high-water after a newer +exact signed sequence verifies. It writes an owner-only temporary, fsyncs it, renames it inside the +private directory, and fsyncs the directory. Neither path mutates the selected package. + +A lower sequence or a different build at the same sequence is a replay. It cannot replace the local +high-water. If the feed is offline, rate-limited, malformed, or replayed, the authenticated installed +receipt and prior advisory remain usable. + +## Classification and clients + +The provider snapshot carries one optional, provider-neutral `distribution` record. Prime emits one +of four classifications: + +- `stock-or-custom`: no Pylon distribution claim; +- `pylon-unmanaged`: Pylon metadata without a matching managed receipt; +- `pylon-managed`: exact package metadata and package root match an authenticated receipt; +- `invalid-receipt`: private state exists but fails authentication, mode, no-follow, binding, or + high-water validation. + +Classification never changes provider readiness. Only `pylon-managed` can receive a signed build +advisory. Advisory order comes from channel sequence and build ID, never SemVer. The existing optional +version-advisory UI receives that already-decided status only to reuse its provider-neutral update +marker; it never compares the values. Settings also shows the distribution label. Web and desktop use +that shared view. Mobile receives the same optional contract and continues to operate the provider, +but currently has no provider-management detail surface. + +Stock Prime Agent 0.8.1 remains ready and manually maintained. Linux and macOS use private receipts; +WSL2 follows Linux. Native Windows and `.cmd` receipt admission are explicitly unavailable until Prime +supports native Windows distributions. + +## Scope boundary + +This verifier fetches only bounded public manifests, attestation bundles, and trust material for proof +and advisory. It has no download-to-install, package install, switch, update, cleanup, or removal path. +It does not generalize Prime feed semantics to Codex, Claude, Cursor, Grok, OpenCode, or Comet. diff --git a/docs/user/providers-prime-agent.md b/docs/user/providers-prime-agent.md index a966ec355d..c8428037f7 100644 --- a/docs/user/providers-prime-agent.md +++ b/docs/user/providers-prime-agent.md @@ -309,6 +309,34 @@ sessions receive Pylon's thread-scoped preview tools. This works in daemon-backe compatibility mode. The scoped connection is removed when the provider session stops. Turning browser access off withholds both the tools and their instructions; it does not affect browser tabs you control. +## Distribution Verification + +Pylon treats Prime runtime support and Prime distribution proof as separate checks. The exact +configured `prime-agent` binary still negotiates its installed public SDK after Pylon attaches. A +missing or invalid distribution receipt does not disable the provider and does not bypass ACP +compatibility fallback. + +In **Settings → Providers → Prime Agent**, a Pylon publication can show one of these labels: + +- **Pylon managed** means a private Pylon receipt matches the exact package root and the build was + admitted from signed Pylon preview or stable publication evidence. +- **Pylon build · manual** means the package claims Pylon build metadata but has no matching managed + receipt. Update it using the same manual method that installed it. +- **Managed receipt invalid** means the private receipt, package-root binding, or local channel + high-water is invalid. Prime remains usable, but managed update advice is disabled. +- Stock Prime Agent and other custom installations stay manually maintained and show no fork update + warning. + +Managed update advice compares the signed channel sequence and immutable build ID. It never orders +builds by the package version. Pylon verifies the GitHub/Sigstore issuer, transparency evidence, +repository, signer workflow and ref, source commit and tree, recipe, manifests, and artifact digest +before advancing its private channel high-water. An offline or rate-limited feed keeps the installed +build ready and retains the last authenticated advice. + +This feature only verifies and reports. It does not download, install, switch, remove, or clean up a +Prime package. Native Windows receipts are not supported. Run Prime Agent in WSL2 to use the Linux +receipt path. + ## Current Limitations - Prime Agent 0.8.1 has no daemon-native or operating-system sandbox policy. Supervised mode gates diff --git a/packages/contracts/src/server.test.ts b/packages/contracts/src/server.test.ts index 5f56969d49..b7e47f21ee 100644 --- a/packages/contracts/src/server.test.ts +++ b/packages/contracts/src/server.test.ts @@ -49,6 +49,7 @@ describe("ServerProvider", () => { expect(parsed.slashCommands).toEqual([]); expect(parsed.skills).toEqual([]); expect(parsed.versionAdvisory).toBeUndefined(); + expect(parsed.distribution).toBeUndefined(); expect(parsed.updateState).toBeUndefined(); expect(getServerProviderSupportedRuntimeModes(parsed)).toEqual( DEFAULT_SERVER_PROVIDER_RUNTIME_MODES, @@ -97,6 +98,37 @@ describe("ServerProvider", () => { expect(parsed.versionAdvisory?.canUpdate).toBe(false); }); + it("decodes optional signed distribution identity independently from runtime status", () => { + const parsed = decodeServerProvider({ + ...baseProviderSnapshot, + driver: "primeAgent", + distribution: { + classification: "pylon-managed", + channel: "preview", + buildId: "pylon-build-g0123456789ab-r1", + sequence: 9, + latestBuildId: "pylon-build-gabcdef012345-r1", + latestSequence: 10, + updateAvailable: true, + checkedAt: "2026-04-10T00:00:00.000Z", + message: "A signed build is available.", + }, + }); + + expect(parsed.status).toBe("ready"); + expect(parsed.distribution).toEqual({ + classification: "pylon-managed", + channel: "preview", + buildId: "pylon-build-g0123456789ab-r1", + sequence: 9, + latestBuildId: "pylon-build-gabcdef012345-r1", + latestSequence: 10, + updateAvailable: true, + checkedAt: "2026-04-10T00:00:00.000Z", + message: "A signed build is available.", + }); + }); + it("decodes continuation group metadata", () => { const parsed = decodeServerProvider({ instanceId: "codex_personal", diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index e685a2ddaa..afad6e3af1 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -198,6 +198,37 @@ export const ServerProviderVersionAdvisory = Schema.Struct({ }); export type ServerProviderVersionAdvisory = typeof ServerProviderVersionAdvisory.Type; +export const ServerProviderDistributionClassification = Schema.Literals([ + "stock-or-custom", + "pylon-unmanaged", + "pylon-managed", + "invalid-receipt", +]); +export type ServerProviderDistributionClassification = + typeof ServerProviderDistributionClassification.Type; + +export const ServerProviderDistributionChannel = Schema.Literals(["preview", "stable"]); +export type ServerProviderDistributionChannel = typeof ServerProviderDistributionChannel.Type; + +/** + * Signed build identity for provider distributions that publish one. + * + * This stays separate from runtime capabilities and package versions. A provider can remain ready + * while distribution proof or its advisory feed is unavailable. + */ +export const ServerProviderDistribution = Schema.Struct({ + classification: ServerProviderDistributionClassification, + channel: Schema.NullOr(ServerProviderDistributionChannel), + buildId: Schema.NullOr(TrimmedNonEmptyString), + sequence: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))), + latestBuildId: Schema.NullOr(TrimmedNonEmptyString), + latestSequence: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))), + updateAvailable: Schema.Boolean, + checkedAt: Schema.NullOr(IsoDateTime), + message: Schema.NullOr(TrimmedNonEmptyString), +}); +export type ServerProviderDistribution = typeof ServerProviderDistribution.Type; + export const ServerProviderUpdateStatus = Schema.Literals([ "idle", "queued", @@ -291,6 +322,7 @@ export const ServerProvider = Schema.Struct({ backends: Schema.optional(Schema.Array(ServerProviderBackend)), rateLimit: Schema.optional(ServerProviderRateLimit), versionAdvisory: Schema.optionalKey(ServerProviderVersionAdvisory), + distribution: Schema.optionalKey(ServerProviderDistribution), updateState: Schema.optionalKey(ServerProviderUpdateState), }); export type ServerProvider = typeof ServerProvider.Type; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a184574ff5..9e15bd6392 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -492,6 +492,18 @@ importers: '@pierre/diffs': specifier: 'catalog:' version: 1.3.0-beta.10(patch_hash=7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@sigstore/bundle': + specifier: 4.0.0 + version: 4.0.0 + '@sigstore/core': + specifier: 3.2.1 + version: 3.2.1 + '@sigstore/tuf': + specifier: 4.0.0 + version: 4.0.0 + '@sigstore/verify': + specifier: 3.0.0 + version: 3.0.0 effect: specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6) @@ -2786,6 +2798,10 @@ packages: '@formkit/auto-animate@0.9.0': resolution: {integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==} + '@gar/promise-retry@1.0.3': + resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} + engines: {node: ^20.17.0 || >=22.9.0} + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -3273,6 +3289,18 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@npmcli/agent@4.0.2': + resolution: {integrity: sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/redact@4.0.0': + resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} + engines: {node: ^20.17.0 || >=22.9.0} + '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} @@ -4421,6 +4449,26 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sigstore/bundle@4.0.0': + resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/core@3.2.1': + resolution: {integrity: sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/protobuf-specs@0.5.2': + resolution: {integrity: sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@sigstore/tuf@4.0.0': + resolution: {integrity: sha512-0QFuWDHOQmz7t66gfpfNO6aEjoFrdhkJaej/AOqb4kqWZVbPWFZifXZzkxyQBB1OwTbkhdT3LNpMFxwkTvf+2w==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/verify@3.0.0': + resolution: {integrity: sha512-moXtHH33AobOhTZF8xcX1MpOFqdvfCk7v6+teJL8zymBiDXwEsQH6XG9HGx2VIxnJZNm4cNSzflTLDnQLmIdmw==} + engines: {node: ^20.17.0 || >=22.9.0} + '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} @@ -4800,6 +4848,14 @@ packages: '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@tufjs/canonical-json@2.0.0': + resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@tufjs/models@4.1.0': + resolution: {integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==} + engines: {node: ^20.17.0 || >=22.9.0} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -5780,6 +5836,10 @@ packages: resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} engines: {node: '>=6.0.0'} + cacache@20.0.4: + resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} + engines: {node: ^20.17.0 || >=22.9.0} + cacheable-lookup@5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -7124,6 +7184,10 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -8033,6 +8097,10 @@ packages: magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + make-fetch-happen@15.0.6: + resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} + engines: {node: ^20.17.0 || >=22.9.0} + makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -8414,6 +8482,30 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@5.0.2: + resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -8768,6 +8860,10 @@ packages: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} + p-map@7.0.7: + resolution: {integrity: sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ==} + engines: {node: '>=18'} + p-queue@9.3.0: resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==} engines: {node: '>=20'} @@ -9772,6 +9868,10 @@ packages: resolution: {integrity: sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==} engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -10063,6 +10163,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tuf-js@4.1.0: + resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==} + engines: {node: ^20.17.0 || >=22.9.0} + type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} @@ -13097,6 +13201,8 @@ snapshots: '@formkit/auto-animate@0.9.0': {} + '@gar/promise-retry@1.0.3': {} + '@hono/node-server@1.19.14(hono@4.12.27)': dependencies: hono: 4.12.27 @@ -13624,6 +13730,22 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@npmcli/agent@4.0.2': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.5.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.8.5 + + '@npmcli/redact@4.0.0': {} + '@octokit/auth-token@6.0.0': {} '@octokit/core@7.0.6': @@ -14611,6 +14733,27 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sigstore/bundle@4.0.0': + dependencies: + '@sigstore/protobuf-specs': 0.5.2 + + '@sigstore/core@3.2.1': {} + + '@sigstore/protobuf-specs@0.5.2': {} + + '@sigstore/tuf@4.0.0': + dependencies: + '@sigstore/protobuf-specs': 0.5.2 + tuf-js: 4.1.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/verify@3.0.0': + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.2 + '@sinclair/typebox@0.27.10': {} '@sindresorhus/is@4.6.0': {} @@ -14962,6 +15105,13 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} + '@tufjs/canonical-json@2.0.0': {} + + '@tufjs/models@4.1.0': + dependencies: + '@tufjs/canonical-json': 2.0.0 + minimatch: 10.2.5 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -16074,6 +16224,19 @@ snapshots: bytestreamjs@2.0.1: {} + cacache@20.0.4: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.6 + lru-cache: 11.5.1 + minipass: 7.1.3 + minipass-collect: 2.0.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + p-map: 7.0.7 + ssri: 13.0.1 + cacheable-lookup@5.0.4: {} cacheable-request@7.0.4: @@ -17570,6 +17733,10 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.3 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -18459,6 +18626,23 @@ snapshots: '@babel/types': 7.29.7 source-map-js: 1.2.1 + make-fetch-happen@15.0.6: + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/agent': 4.0.2 + '@npmcli/redact': 4.0.0 + cacache: 20.0.4 + http-cache-semantics: 4.2.0 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -19273,6 +19457,34 @@ snapshots: minimist@1.2.8: {} + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.3 + + minipass-fetch@5.0.2: + dependencies: + minipass: 7.1.3 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + iconv-lite: 0.7.2 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.3 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + minipass@7.1.3: {} minizlib@3.1.0: @@ -19659,6 +19871,8 @@ snapshots: dependencies: p-limit: 2.3.0 + p-map@7.0.7: {} + p-queue@9.3.0: dependencies: eventemitter3: 5.0.4 @@ -20959,6 +21173,10 @@ snapshots: sql-escaper@1.5.1: {} + ssri@13.0.1: + dependencies: + minipass: 7.1.3 + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -21264,6 +21482,14 @@ snapshots: tslib@2.8.1: {} + tuf-js@4.1.0: + dependencies: + '@tufjs/models': 4.1.0 + debug: 4.4.3 + make-fetch-happen: 15.0.6 + transitivePeerDependencies: + - supports-color + type-fest@0.13.1: optional: true