Validate retained packages against the active provider contract

This commit is contained in:
Dotta committed 2026-10-10 21:02:56 -05:00
1 parent 38610a4f23
commit 193c2baeee
2 files changed
+71 -11

No files matched your search

@@ -1476,6 +1476,40 @@ describe("remote provider pack manifest", () => {
const changedPins = structuredClone(retainedManifest);
changedPins.payload.pins = { ...changedPins.payload.pins, nodeMinimum: "0.0.0" as typeof changedPins.payload.pins.nodeMinimum };
expect(() => validateRemoteProviderPackIdentity(changedPins)).toThrow("pins or source revision");
const rehash = (manifest: typeof retainedManifest) => {
manifest.digest = digest(canonical(manifest.payload));
return manifest;
};
const oldPack = structuredClone(retainedManifest);
Object.assign(oldPack.payload.pins, { opencode: "1.18.33", nodeMinimum: "24.10.0", codex: "0.159.0" });
Object.assign(oldPack.payload.acpxProfileDigests, { codex: digest("old-codex-profile") });
rehash(oldPack);
const retainedAuthority = { digest: oldPack.digest, agent: "claude" };
// A controller-bound Claude session survives unrelated bundled-provider
// drift, while fresh admissions continue to require the current release.
expect(() => validateRemoteProviderPackIdentity(oldPack)).toThrow("pins or source revision");
expect(validateRemoteProviderPackIdentity(oldPack, root, true, retainedAuthority).digest).toBe(oldPack.digest);
expect(() => validateRemoteProviderPackIdentity(oldPack, root, true,
{ ...retainedAuthority, digest: retainedManifest.digest })).toThrow("manifest digest mismatch");
for (const key of ["acpx", "claudeAcp"] as const) {
const incompatible = structuredClone(oldPack);
Object.assign(incompatible.payload.pins, { [key]: "0.0.1" });
rehash(incompatible);
expect(() => validateRemoteProviderPackIdentity(incompatible, root, true,
{ ...retainedAuthority, digest: incompatible.digest })).toThrow("pins or source revision");
}
const incompatibleProfile = structuredClone(oldPack);
Object.assign(incompatibleProfile.payload.acpxProfileDigests, { claude: digest("old-claude-profile") });
rehash(incompatibleProfile);
expect(() => validateRemoteProviderPackIdentity(incompatibleProfile, root, true,
{ ...retainedAuthority, digest: incompatibleProfile.digest })).toThrow("pins or source revision");
const tamperedManifest = structuredClone(oldPack);
tamperedManifest.payload.artifacts.nodeCommand.sha256 = digest("substitute-node");
rehash(tamperedManifest);
expect(() => validateRemoteProviderPackIdentity(tamperedManifest, root, true, retainedAuthority)).toThrow("manifest digest mismatch");
await writeFile(join(root, "node_modules", "node", "bin", "node"), "substitute-node");
expect(() => validateRemoteProviderPackIdentity(oldPack, root, true, retainedAuthority)).toThrow("provider Node digest mismatch");
await writeFile(join(root, "node_modules", "node", "bin", "node"), node);
expect(readRemoteProviderPackManifest(root).payload.pins.opencode).toBe(
"1.18.34",
);
@@ -1499,6 +1533,11 @@ describe("remote provider pack manifest", () => {
await writeManifest(); await cp(join(root, "provider-pack.json"), manifestPath);
expect(() => readRemoteProviderPackManifest(root)).toThrow("Cursor profile or closure");
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("Cursor profile or closure");
const oldCursorPack = JSON.parse(await readFile(manifestPath, "utf8")) as typeof retainedManifest;
expect(validateRemoteProviderPackIdentity(oldCursorPack, root, true,
{ digest: oldCursorPack.digest, agent: "claude" }).digest).toBe(oldCursorPack.digest);
expect(() => validateRemoteProviderPackIdentity(oldCursorPack, root, true,
{ digest: oldCursorPack.digest, agent: "cursor" })).toThrow("Cursor profile or closure");
Object.assign(cursor, { [field]: previous });
}
await writeManifest(); await cp(join(root, "provider-pack.json"), manifestPath);
@@ -10233,14 +10233,34 @@ function readRemoteProviderPackIdentity(packRoot: string, verifyControllerFiles:
return validateRemoteProviderPackIdentity(manifest, packRoot, verifyControllerFiles);
}
export function validateRemoteProviderPackIdentity(manifest: RemoteProviderPackManifest, packRoot = "", verifyControllerFiles = false): RemoteProviderPackManifest {
export function validateRemoteProviderPackIdentity(
manifest: RemoteProviderPackManifest,
packRoot = "",
verifyControllerFiles = false,
retainedAuthority?: { digest: string; agent: string },
): RemoteProviderPackManifest {
const payload = manifest?.payload;
// A retained session may outlive unrelated bundled-provider upgrades. Its
// active provider must still satisfy this release's qualified ACPX contract.
const retainedAgent = retainedAuthority?.agent;
const retainedProfile = retainedAgent === "claude" || retainedAgent === "codex" || retainedAgent === "grok"
? retainedAgent : null;
const activePins = retainedProfile === "claude" ? ["acpx", "claudeAcp"] as const
: retainedProfile === "codex" ? ["acpx", "codex", "codexAcp"] as const
: ["acpx", "grok"] as const;
const pinsMatch = retainedProfile
? canonicalJson(Object.keys(payload?.pins ?? {}).sort()) === canonicalJson(Object.keys(REMOTE_PROVIDER_PACK_PINS).sort())
&& Object.values(payload?.pins ?? {}).every(value => typeof value === "string" && value.length > 0 && value.length <= 120)
&& activePins.every(key => payload?.pins?.[key] === REMOTE_PROVIDER_PACK_PINS[key])
&& canonicalJson(Object.keys(payload?.acpxProfileDigests ?? {}).sort()) === canonicalJson(Object.keys(REMOTE_PROVIDER_PACK_PROFILE_DIGESTS).sort())
&& Object.values(payload?.acpxProfileDigests ?? {}).every(value => /^sha256:[0-9a-f]{64}$/.test(value))
&& payload?.acpxProfileDigests?.[retainedProfile] === REMOTE_PROVIDER_PACK_PROFILE_DIGESTS[retainedProfile]
: canonicalJson(payload?.pins) === canonicalJson(REMOTE_PROVIDER_PACK_PINS)
&& canonicalJson(payload?.acpxProfileDigests) === canonicalJson(REMOTE_PROVIDER_PACK_PROFILE_DIGESTS);
if (
manifest?.schema !== REMOTE_PROVIDER_PACK_SCHEMA ||
!payload ||
canonicalJson(payload.pins) !== canonicalJson(REMOTE_PROVIDER_PACK_PINS) ||
canonicalJson(payload.acpxProfileDigests) !==
canonicalJson(REMOTE_PROVIDER_PACK_PROFILE_DIGESTS) ||
!pinsMatch ||
typeof payload.target?.platform !== "string" ||
typeof payload.target?.architecture !== "string" ||
!/^[0-9a-f]{40}(?:-dirty)?$/.test(payload.runnerSourceRevision) ||
@@ -10253,7 +10273,7 @@ export function validateRemoteProviderPackIdentity(manifest: RemoteProviderPackM
const digest = `sha256:${createHash("sha256")
.update(canonicalJson(payload))
.digest("hex")}`;
if (manifest.digest !== digest) {
if (manifest.digest !== digest || (retainedAuthority && digest !== retainedAuthority.digest)) {
throw new Error(
"runner_remote_provider_artifact_incompatible: provider pack manifest digest mismatch",
);
@@ -10333,7 +10353,7 @@ export function validateRemoteProviderPackIdentity(manifest: RemoteProviderPackM
throw new Error("runner_remote_provider_artifact_incompatible: invalid candidate identity");
}
const candidatePath = providerPackRelativePath(candidate.path, "candidate assets");
if (provider === "cursor") {
if (provider === "cursor" && !retainedProfile) {
const target = `${payload.target.platform}-${payload.target.architecture}`;
const distribution = Object.hasOwn(CURSOR_DISTRIBUTION_PINS, target)
? CURSOR_DISTRIBUTION_PINS[target as keyof typeof CURSOR_DISTRIBUTION_PINS] : undefined;
@@ -12044,12 +12064,13 @@ async function createRunnerdBackendWithinSessionClaim(
control: JSON.parse(readBoundedNativeFile(resolve(root, "control-plane", "control-plane-state.json"),
NATIVE_CONTROL_PLANE_STATE_MAX_BYTES, "runner_durable_identity_too_large").toString("utf8")),
}) : null;
if (retainedComputerPack && remoteCommandRunner && remoteStorage) {
if (retainedComputerPack && remoteCommandRunner && remoteStorage && input.execution.provider.kind === "acpx") {
// The local attachment template chooses the digest. Remote content cannot
// authorize a newer pack or rewrite the existing provider's launch identity.
const [manifest, state] = await readPinnedComputerProviderMetadata({ runner: remoteCommandRunner,
packRoot: retainedComputerPack.root, stateDirectory: posix.join(remoteStorage.sessionRoot, "runner") });
expectedProviderPackManifest = validateRemoteProviderPackIdentity(manifest as RemoteProviderPackManifest);
expectedProviderPackManifest = validateRemoteProviderPackIdentity(manifest as RemoteProviderPackManifest, "", false,
{ digest: retainedComputerPack.digest, agent: input.execution.provider.agent });
if (expectedProviderPackManifest.digest !== retainedComputerPack.digest ||
expectedProviderPackManifest.payload.target.platform !== "linux" ||
expectedProviderPackManifest.payload.target.architecture !== "x64")
@@ -12282,8 +12303,8 @@ async function createRunnerdBackendWithinSessionClaim(
"if(tree(path.join(root,'dist'))!==manifest.payload.distDigest)throw new Error('dist tree digest mismatch')",
"for(const candidate of Object.values({...manifest.payload.providers,...manifest.payload.candidateProviders})){if(tree(path.join(root,candidate.path))!==candidate.sha256)throw new Error('candidate asset tree digest mismatch')}",
"const version=process.versions.node.split('.').map(Number)",
"const minimum=manifest.payload.pins.nodeMinimum.split('.').map(Number)",
"if(version[0]<minimum[0]||(version[0]===minimum[0]&&(version[1]<minimum[1]||(version[1]===minimum[1]&&version[2]<minimum[2]))))throw new Error('Node version incompatible')",
`const minimums=[manifest.payload.pins.nodeMinimum,${JSON.stringify(REMOTE_PROVIDER_PACK_PINS.nodeMinimum)}]`,
"for(const value of minimums){const minimum=value.split('.').map(Number);if(version[0]<minimum[0]||(version[0]===minimum[0]&&(version[1]<minimum[1]||(version[1]===minimum[1]&&version[2]<minimum[2]))))throw new Error('Node version incompatible')}",
"if(process.platform!==manifest.payload.target.platform||process.arch!==manifest.payload.target.architecture)throw new Error('provider pack target mismatch')",
"const packageVersion=(pkg)=>JSON.parse(fs.readFileSync(path.join(root,'node_modules',...pkg.split('/'),'package.json'),'utf8')).version",
"const expectedPackages={acpx:manifest.payload.pins.acpx,'@agentclientprotocol/claude-agent-acp':manifest.payload.pins.claudeAcp,'@agentclientprotocol/codex-acp':manifest.payload.pins.codexAcp,'opencode-ai':manifest.payload.pins.opencode}",
@@ -12315,7 +12336,7 @@ async function createRunnerdBackendWithinSessionClaim(
if (
opencodeVersion.exitCode !== 0 ||
opencodeVersion.timedOut ||
opencodeVersion.stdout.trim() !== REMOTE_PROVIDER_PACK_PINS.opencode
opencodeVersion.stdout.trim() !== expectedProviderPackManifest.payload.pins.opencode
) {
throw new Error(
"runner_remote_provider_artifact_incompatible: OpenCode version mismatch",