Skip to content

Commit c09508f

Browse files
committed
fix(ci): pin stock graduation artifact
1 parent 8589bc6 commit c09508f

8 files changed

Lines changed: 333 additions & 95 deletions

‎.github/workflows/prime-artifact-graduation.yml‎

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ on:
1212
required: false
1313
default: ""
1414
type: string
15-
stock_version:
16-
description: Exact stock Prime Agent version
17-
required: true
18-
default: 0.8.1
19-
type: string
2015

2116
permissions:
2217
contents: read
@@ -36,17 +31,14 @@ jobs:
3631
env:
3732
PREVIEW_TAG: ${{ inputs.preview_tag }}
3833
SECOND_PREVIEW_TAG: ${{ inputs.second_preview_tag }}
39-
STOCK_VERSION: ${{ inputs.stock_version }}
4034
run: |
4135
set -euo pipefail
4236
preview_pattern='^pylon-build-g[0-9a-f]{12}-r[1-9][0-9]*$'
43-
version_pattern='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'
4437
[[ "$PREVIEW_TAG" =~ $preview_pattern ]]
4538
if test -n "$SECOND_PREVIEW_TAG"; then
4639
[[ "$SECOND_PREVIEW_TAG" =~ $preview_pattern ]]
4740
test "$SECOND_PREVIEW_TAG" != "$PREVIEW_TAG"
4841
fi
49-
[[ "$STOCK_VERSION" =~ $version_pattern ]]
5042
install -d -m 700 "$RUNNER_TEMP/prime-graduation-results" "$RUNNER_TEMP/prime-sigstore-tuf"
5143
{
5244
echo "PYLON_PRIME_GRADUATION_REQUIRED=1"
@@ -58,7 +50,7 @@ jobs:
5850
else
5951
echo "PYLON_PRIME_SECOND_ARTIFACT_DIR="
6052
fi
61-
echo "PYLON_PRIME_STOCK_TARBALL=$RUNNER_TEMP/prime-stock/prime-agent-$STOCK_VERSION.tgz"
53+
echo "PYLON_PRIME_STOCK_TARBALL=$RUNNER_TEMP/prime-stock/prime-agent-0.8.1.tgz"
6254
echo "PYLON_PRIME_AGENT_STOCK_ARTIFACT_BIN=$RUNNER_TEMP/prime-stock-install/node_modules/.bin/prime-agent"
6355
echo "PYLON_PRIME_GRADUATION_RESULT=$RUNNER_TEMP/prime-graduation-results/cases.json"
6456
echo "PYLON_REAL_PRIME_AGENT_MULTI_PROOF=1"
@@ -81,17 +73,15 @@ jobs:
8173
cache: true
8274
run-install: true
8375

84-
- name: Download exact public preview, attestations, and stock package
76+
- name: Download exact public preview, attestations, and frozen stock package
8577
env:
8678
PREVIEW_TAG: ${{ inputs.preview_tag }}
87-
STOCK_VERSION: ${{ inputs.stock_version }}
8879
run: |
8980
set -euo pipefail
9081
node apps/server/scripts/prime-artifact-graduation.ts download-preview \
9182
--tag "$PREVIEW_TAG" \
9283
--artifact-directory "$RUNNER_TEMP/prime-preview"
9384
node apps/server/scripts/prime-artifact-graduation.ts download-stock \
94-
--version "$STOCK_VERSION" \
9585
--stock-directory "$RUNNER_TEMP/prime-stock"
9686
9787
- name: Download optional exact second preview
@@ -127,9 +117,7 @@ jobs:
127117
--tuf-cache "$RUNNER_TEMP/prime-sigstore-tuf" \
128118
--output "$RUNNER_TEMP/prime-graduation-results/second-verification.json"
129119
130-
- name: Install exact stock fixture without lifecycle scripts
131-
env:
132-
STOCK_VERSION: ${{ inputs.stock_version }}
120+
- name: Install frozen stock fixture without lifecycle scripts
133121
run: |
134122
set -euo pipefail
135123
npm install \
@@ -138,7 +126,7 @@ jobs:
138126
--no-audit \
139127
--no-fund \
140128
--package-lock=false \
141-
"$RUNNER_TEMP/prime-stock/prime-agent-$STOCK_VERSION.tgz"
129+
"$PYLON_PRIME_STOCK_TARBALL"
142130
test -x "$PYLON_PRIME_AGENT_STOCK_ARTIFACT_BIN"
143131
144132
- name: Run real bridge, managed store, restart, crash-receipt, and native multi evidence

‎apps/server/scripts/prime-artifact-graduation.ts‎

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ import {
2121
PRIME_RELEASE_MANIFEST,
2222
verifyPrimePublicationArtifactDirectory,
2323
} from "../src/provider/prime/PrimeAgentDistributionVerifier.ts";
24+
import {
25+
PRIME_STOCK_ARTIFACT,
26+
verifyPrimeStockArtifactBytes,
27+
verifyPrimeStockReleaseMetadata,
28+
} from "../src/provider/prime/PrimeAgentStockArtifact.ts";
2429

2530
const MAX_JSON_BYTES = 4 * 1024 * 1024;
2631
const MAX_ASSET_BYTES = 256 * 1024 * 1024;
2732
const FETCH_TIMEOUT_MS = 30_000;
2833
const PREVIEW_TAG = /^pylon-build-g[0-9a-f]{12}-r[1-9][0-9]*$/u;
29-
const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
3034
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
3135
const SHA256 = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/u));
3236
const GIT_SHA = Schema.String.check(Schema.isPattern(/^[0-9a-f]{40}$/u));
@@ -257,40 +261,28 @@ async function downloadPreview(): Promise<void> {
257261
}
258262

259263
async function downloadStock(): Promise<void> {
260-
const version = flag("version")!;
261264
const destination = await makeEmptyDirectory(flag("stock-directory")!);
262-
if (!VERSION.test(version)) throw new Error("Stock Prime version is not exact.");
263-
const repository = "PrimeIntellect-ai/prime-agent";
264-
const tag = `v${version}`;
265-
const assetName = `prime-agent-${version}.tgz`;
266-
const expectedTarball = `https://github.com/${repository}/releases/download/${tag}/${assetName}`;
265+
const repositoryBytes = await fetchBounded(
266+
`https://api.github.com/repos/${PRIME_STOCK_ARTIFACT.repository}`,
267+
MAX_JSON_BYTES,
268+
"application/vnd.github+json",
269+
);
267270
const releaseBytes = await fetchBounded(
268-
`https://api.github.com/repos/${repository}/releases/tags/${tag}`,
271+
`https://api.github.com/repos/${PRIME_STOCK_ARTIFACT.repository}/releases/${PRIME_STOCK_ARTIFACT.releaseId}`,
269272
MAX_JSON_BYTES,
270273
"application/vnd.github+json",
271274
);
272-
const release = decodeGitHubRelease(parseJson(releaseBytes));
273-
if (release.tag_name !== tag || release.draft || release.prerelease) {
274-
throw new Error("Stock Prime release metadata is not the exact requested version.");
275-
}
276-
const matches = release.assets.filter((asset) => asset.name === assetName);
277-
const asset = matches[0];
278-
if (
279-
matches.length !== 1 ||
280-
!asset ||
281-
asset.browser_download_url !== expectedTarball ||
282-
typeof asset.digest !== "string" ||
283-
!/^sha256:[0-9a-f]{64}$/u.test(asset.digest)
284-
) {
285-
throw new Error("Stock Prime release has no exact digest-bearing root package.");
286-
}
287-
const tarball = await fetchBounded(expectedTarball, MAX_ASSET_BYTES, "application/octet-stream");
288-
if (asset.size !== tarball.byteLength || asset.digest !== `sha256:${sha256(tarball)}`) {
289-
throw new Error("Stock Prime tarball does not match its exact GitHub asset digest.");
290-
}
275+
verifyPrimeStockReleaseMetadata(parseJson(repositoryBytes), parseJson(releaseBytes));
276+
const tarball = await fetchBounded(
277+
PRIME_STOCK_ARTIFACT.url,
278+
PRIME_STOCK_ARTIFACT.size,
279+
"application/octet-stream",
280+
);
281+
verifyPrimeStockArtifactBytes(tarball);
282+
await writeExclusive(NodePath.join(destination, "github-repository.json"), repositoryBytes);
291283
await writeExclusive(NodePath.join(destination, "github-release.json"), releaseBytes);
292-
await writeExclusive(NodePath.join(destination, assetName), tarball);
293-
console.log(`Downloaded exact stock Prime ${version}.`);
284+
await writeExclusive(NodePath.join(destination, PRIME_STOCK_ARTIFACT.assetName), tarball);
285+
console.log(`Downloaded frozen stock Prime ${PRIME_STOCK_ARTIFACT.version}.`);
294286
}
295287

296288
async function verifyPreview(): Promise<void> {

‎apps/server/src/provider/prime/PrimeAgentArtifactGraduation.integration.test.ts‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as Effect from "effect/Effect";
1212
import { makePrimeArtifactGraduationHarness } from "./PrimeAgentArtifactGraduation.test-fixture.ts";
1313
import { loadPrimeAgentDaemonBridge } from "./PrimeAgentDaemonBridge.ts";
1414
import { PRIME_MANAGED_TOOL_DIRECTORY } from "./PrimeAgentManagedToolStore.ts";
15+
import { PRIME_STOCK_ARTIFACT, verifyPrimeStockArtifactBytes } from "./PrimeAgentStockArtifact.ts";
1516

1617
const execFile = NodeUtil.promisify(NodeChildProcess.execFile);
1718
const required = process.env.PYLON_PRIME_GRADUATION_REQUIRED === "1";
@@ -74,6 +75,7 @@ it.skipIf(!configured)(
7475
try {
7576
const stockRoot = NodePath.resolve(stockBinaryPath!, "../../..");
7677
const stockTarballBefore = await NodeFSP.readFile(stockTarballPath!);
78+
verifyPrimeStockArtifactBytes(stockTarballBefore);
7779
const stockTreeBefore = await treeDigest(stockRoot);
7880
const harness = await makePrimeArtifactGraduationHarness({
7981
stateDir,
@@ -88,7 +90,7 @@ it.skipIf(!configured)(
8890

8991
// eslint-disable-next-line t3code/no-manual-effect-runtime-in-tests -- This one opt-in proof combines the Promise-owned store lifecycle with the bridge Effect.
9092
const stockBridge = await Effect.runPromise(loadPrimeAgentDaemonBridge(stockBinaryPath!));
91-
expect(stockBridge.version).toBe("0.8.1");
93+
expect(stockBridge.version).toBe(PRIME_STOCK_ARTIFACT.version);
9294
expect(stockBridge.negotiatedDaemonSessionCapabilitiesAvailable).toBe(false);
9395

9496
const install = await harness.command({
@@ -123,6 +125,7 @@ it.skipIf(!configured)(
123125
let rollbackBuildId = harness.artifacts[0]!.publication.buildId;
124126
if (harness.artifacts.length === 2) {
125127
harness.useArtifact(1);
128+
const secondArtifact = harness.artifacts[1]!;
126129
const update = await harness.command({
127130
commandId: "graduation-update-second-preview",
128131
action: "update",
@@ -132,8 +135,27 @@ it.skipIf(!configured)(
132135
});
133136
expect(update).toMatchObject({
134137
status: "succeeded",
135-
buildId: harness.artifacts[1]!.publication.buildId,
138+
buildId: secondArtifact.publication.buildId,
136139
});
140+
const updatedStatus = await harness.status();
141+
const selectedSecondBuild = updatedStatus.availableBuilds.find(
142+
(build) => build.buildId === secondArtifact.publication.buildId,
143+
);
144+
expect(selectedSecondBuild).toBeDefined();
145+
expect(harness.binding().binaryPath).toBe(selectedSecondBuild!.binaryPath);
146+
// eslint-disable-next-line t3code/no-manual-effect-runtime-in-tests -- This opt-in proof loads the exact launcher selected by the production managed store.
147+
const secondBridge = await Effect.runPromise(
148+
loadPrimeAgentDaemonBridge(selectedSecondBuild!.binaryPath),
149+
);
150+
expect(secondBridge.version).toBe(secondArtifact.publication.packageVersion);
151+
expect(secondBridge.negotiatedDaemonSessionCapabilitiesAvailable).toBe(true);
152+
const secondVersion = await execFile(selectedSecondBuild!.binaryPath, ["--version"], {
153+
timeout: 30_000,
154+
maxBuffer: 256 * 1024,
155+
windowsHide: true,
156+
});
157+
expect(secondVersion.stdout).toContain(secondArtifact.publication.packageVersion);
158+
expect(harness.binding().binaryPath).toBe(selectedSecondBuild!.binaryPath);
137159
} else {
138160
const update = await harness.command({
139161
commandId: "graduation-update-exact-no-op",
@@ -189,8 +211,9 @@ it.skipIf(!configured)(
189211
const result = {
190212
schemaVersion: 1,
191213
status: "passed",
192-
stockVersion: "0.8.1",
193-
stockSha256: digest(stockTarballBefore),
214+
stockVersion: PRIME_STOCK_ARTIFACT.version,
215+
stockSha256: PRIME_STOCK_ARTIFACT.sha256,
216+
stockSha512: PRIME_STOCK_ARTIFACT.sha512,
194217
preview: harness.artifacts.map((artifact) => ({
195218
tag: artifact.publication.buildId,
196219
sequence: artifact.publication.sequence,
@@ -204,7 +227,7 @@ it.skipIf(!configured)(
204227
"signed-preview-capability",
205228
"side-by-side-install",
206229
"preview-start",
207-
harness.artifacts.length === 2 ? "second-build-update" : "exact-update-no-op",
230+
harness.artifacts.length === 2 ? "second-build-update-executed" : "exact-update-no-op",
208231
"rollback",
209232
"use-stock",
210233
"stock-bytes-unchanged",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import {
4+
PRIME_STOCK_ARTIFACT,
5+
verifyPrimeStockArtifactIdentity,
6+
verifyPrimeStockReleaseMetadata,
7+
} from "./PrimeAgentStockArtifact.ts";
8+
9+
const repository = {
10+
id: PRIME_STOCK_ARTIFACT.repositoryId,
11+
full_name: PRIME_STOCK_ARTIFACT.repository,
12+
};
13+
const asset = {
14+
id: PRIME_STOCK_ARTIFACT.assetId,
15+
name: PRIME_STOCK_ARTIFACT.assetName,
16+
size: PRIME_STOCK_ARTIFACT.size,
17+
browser_download_url: PRIME_STOCK_ARTIFACT.url,
18+
digest: `sha256:${PRIME_STOCK_ARTIFACT.sha256}`,
19+
};
20+
const release = {
21+
id: PRIME_STOCK_ARTIFACT.releaseId,
22+
tag_name: PRIME_STOCK_ARTIFACT.tag,
23+
draft: false,
24+
prerelease: false,
25+
immutable: false,
26+
assets: [asset],
27+
};
28+
29+
describe("frozen stock Prime artifact", () => {
30+
it("accepts only the reviewed repository, release, and asset metadata", () => {
31+
expect(() => verifyPrimeStockReleaseMetadata(repository, release)).not.toThrow();
32+
for (const mutation of [
33+
{ repository: { ...repository, id: repository.id + 1 }, release },
34+
{ repository: { ...repository, full_name: "other/prime-agent" }, release },
35+
{ repository, release: { ...release, id: release.id + 1 } },
36+
{ repository, release: { ...release, tag_name: "v0.8.2" } },
37+
{
38+
repository,
39+
release: { ...release, assets: [{ ...asset, id: asset.id + 1 }] },
40+
},
41+
{
42+
repository,
43+
release: { ...release, assets: [{ ...asset, name: "prime-agent-0.8.2.tgz" }] },
44+
},
45+
{
46+
repository,
47+
release: { ...release, assets: [{ ...asset, size: asset.size + 1 }] },
48+
},
49+
{
50+
repository,
51+
release: {
52+
...release,
53+
assets: [{ ...asset, browser_download_url: `${asset.browser_download_url}.mutated` }],
54+
},
55+
},
56+
{
57+
repository,
58+
release: { ...release, assets: [{ ...asset, digest: `sha256:${"0".repeat(64)}` }] },
59+
},
60+
]) {
61+
expect(() =>
62+
verifyPrimeStockReleaseMetadata(mutation.repository, mutation.release),
63+
).toThrow();
64+
}
65+
});
66+
67+
it("uses the frozen byte size, SHA-256, and SHA-512 as the trust root", () => {
68+
const identity = {
69+
size: PRIME_STOCK_ARTIFACT.size,
70+
sha256: PRIME_STOCK_ARTIFACT.sha256,
71+
sha512: PRIME_STOCK_ARTIFACT.sha512,
72+
};
73+
expect(() => verifyPrimeStockArtifactIdentity(identity)).not.toThrow();
74+
for (const mutation of [
75+
{ ...identity, size: identity.size + 1 },
76+
{ ...identity, sha256: "0".repeat(64) },
77+
{ ...identity, sha512: "0".repeat(128) },
78+
]) {
79+
expect(() => verifyPrimeStockArtifactIdentity(mutation)).toThrow();
80+
}
81+
});
82+
83+
it("does not depend on GitHub supplying a live asset digest", () => {
84+
expect(() =>
85+
verifyPrimeStockReleaseMetadata(repository, {
86+
...release,
87+
assets: [{ ...asset, digest: null }],
88+
}),
89+
).not.toThrow();
90+
});
91+
});

0 commit comments

Comments
 (0)