fix(runner): bind transitive Copilot permission policy sources

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 20:09:09 -05:00
1 parent 03090216b7
commit eeb48425ee
6 files changed
+66 -10

No files matched your search

@@ -223,7 +223,7 @@ impl AcpxProviderDescriptor {
"1.0.88",
None,
None,
"sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6",
"sha256:98936d763497bd6f0e5605f52831a344f456357de58c457e440e69a1a468a2f4",
),
"grok" => (
"grok-4.7",
@@ -2812,7 +2812,7 @@ mod tests {
"copilot",
"@github/copilot",
"1.0.88",
"sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6",
"sha256:98936d763497bd6f0e5605f52831a344f456357de58c457e440e69a1a468a2f4",
None,
None,
"explicit-model",
@@ -7,7 +7,7 @@ import { COPILOT_VERSION, materializePinnedCopilotBinary, resolveCopilotDistribu
const MAX_ARCHIVE_BYTES = 128 * 1024 * 1024;
const MAX_EXPANDED_BYTES = 384 * 1024 * 1024;
const PROFILE_DIGEST = "sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6";
const PROFILE_DIGEST = "sha256:98936d763497bd6f0e5605f52831a344f456357de58c457e440e69a1a468a2f4";
/** Build-time only; outputRoot is the exact runner-owned platform asset directory. */
export async function buildPinnedCopilotDistribution({ outputRoot, platform = process.platform, architecture = process.arch }, { fetchImpl = fetch } = {}) {
@@ -1,13 +1,46 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { resolve } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { build } from "esbuild";
import { verifyNativeAcpxInstallation } from "./installation-integrity.js";
import { COPILOT_CLOSURE_SHA256, verifyCopilotInstallation } from "./copilot-installation.js";
import { COPILOT_LAUNCH_ARGUMENTS, COPILOT_SYSTEM_INSTRUCTION_DELIVERY } from "./copilot-profile.js";
import { COPILOT_PERMISSION_CONTEXT_CONTRACT } from "./copilot-permission-context.js";
import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js";
const PERMISSION_POLICY_SOURCES = [
["copilot-permission-context.ts", "permissionContextSourceSha256"],
["acp-permission-adapter.ts", "permissionAdapterSourceSha256"],
["safe-locations.ts", "permissionLocationsSourceSha256"],
["../../semantic-tools/redaction.ts", "permissionRedactionSourceSha256"],
["generated-sidecar-contract.ts", "permissionClassifierSourceSha256"],
] as const;
// Walk executable imports, not just the projector's immediate dependencies.
async function permissionPolicyClosure(extraClassifierImport = false): Promise<Set<string>> {
const root = fileURLToPath(new URL(".", import.meta.url));
const result = await build({
absWorkingDir: root,
entryPoints: ["acp-permission-adapter.ts"],
bundle: true, write: false, metafile: true, platform: "node", treeShaking: false,
plugins: extraClassifierImport ? [{
name: "unbound-transitive-policy-negative-control",
setup(builder) {
builder.onResolve({ filter: /^unbound-policy$/ }, () => ({ path: "unbound-policy", namespace: "negative-control" }));
builder.onLoad({ filter: /.*/, namespace: "negative-control" }, () => ({ contents: "export const policy = true;", loader: "ts" }));
builder.onLoad({ filter: /generated-sidecar-contract\.ts$/ }, args => ({
contents: readFileSync(args.path, "utf8") + '\nimport "unbound-policy";',
loader: "ts", resolveDir: root,
}));
},
}] : [],
});
return new Set(Object.keys(result.metafile!.inputs).map(path => path.startsWith("negative-control:")
? path : pathToFileURL(resolve(root, path)).href));
}
vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi.fn() }));
describe("Copilot build-owned installation", () => {
@@ -25,10 +58,25 @@ describe("Copilot build-owned installation", () => {
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe(identity.commandDigest);
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(identity.commandDigest);
for (const [relative, field] of [["copilot-permission-context.ts", "permissionContextSourceSha256"], ["acp-permission-adapter.ts", "permissionAdapterSourceSha256"], ["copilot-policy.ts", "policySha256"], ["../../../scripts/materialize-copilot-binary.mjs", "distributionSourceSha256"], ["../../../scripts/copilot-inner-distribution.mjs", "innerDistributionSourceSha256"], ["../../../../../patches/acpx@0.13.1.patch", "acpxPatchSha256"]]) {
for (const [relative, field] of [...PERMISSION_POLICY_SOURCES, ["copilot-policy.ts", "policySha256"], ["../../../scripts/materialize-copilot-binary.mjs", "distributionSourceSha256"], ["../../../scripts/copilot-inner-distribution.mjs", "innerDistributionSourceSha256"], ["../../../../../patches/acpx@0.13.1.patch", "acpxPatchSha256"]]) {
expect(createHash("sha256").update(readFileSync(new URL(relative!, import.meta.url))).digest("hex")).toBe(identity.declaration[field!]);
}
expect(readFileSync(new URL("../../../scripts/build-copilot-distribution.mjs", import.meta.url), "utf8")).toContain(identity.commandDigest);
expect(readFileSync(new URL("../../../runner/crates/runner-core/src/acpx_provider_backend.rs", import.meta.url), "utf8")).toContain(identity.commandDigest);
});
it("binds the complete executable permission-policy import closure", async () => {
expect(await permissionPolicyClosure()).toEqual(new Set(PERMISSION_POLICY_SOURCES.map(([path]) => new URL(path, import.meta.url).href)));
});
it("detects an added transitive dependency even when the projector is unchanged", async () => {
const closure = await permissionPolicyClosure(true);
const bound = new Set(PERMISSION_POLICY_SOURCES.map(([path]) => new URL(path, import.meta.url).href));
expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]);
});
it.each(PERMISSION_POLICY_SOURCES)("changing %s changes the candidate identity", (_path, field) => {
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v9-identity.json", import.meta.url), "utf8"));
identity.declaration[field] = "0".repeat(64);
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest);
});
it("pins the unchanged executable plus complete patched distribution on every target platform", () => {
const fixture = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-message-identity-distributions-1.0.88.json", import.meta.url), "utf8"));
@@ -66,7 +114,7 @@ describe("Copilot build-owned installation", () => {
});
it("rejects changed profile identities before native file access", async () => {
vi.mocked(verifyNativeAcpxInstallation).mockClear();
for (const override of [{ agentServerVersion: "latest" }, { commandDigest: "sha256:untrusted" }, { agent: "cursor" }, { agentProfileVersion: 1 }, { agentProfileVersion: 2 }, { agentProfileVersion: 3 }, { agentProfileVersion: 4 }, { agentProfileVersion: 5 }, { agentProfileVersion: 6 }, { agentProfileVersion: 7 }, { agentProfileVersion: 8 }]) {
for (const override of [{ agentServerVersion: "latest" }, { commandDigest: "sha256:untrusted" }, { commandDigest: "sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6" }, { agent: "cursor" }, { agentProfileVersion: 1 }, { agentProfileVersion: 2 }, { agentProfileVersion: 3 }, { agentProfileVersion: 4 }, { agentProfileVersion: 5 }, { agentProfileVersion: 6 }, { agentProfileVersion: 7 }, { agentProfileVersion: 8 }]) {
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override } as never)).rejects.toThrow("exact pinned");
}
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
@@ -75,7 +75,7 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 9,
agentServerPackage: "@github/copilot", agentServerVersion: "1.0.88",
agentRuntimePackage: null, agentRuntimeVersion: null,
commandDigest: "sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6",
commandDigest: "sha256:98936d763497bd6f0e5605f52831a344f456357de58c457e440e69a1a468a2f4",
// Authenticated discovery has not established a qualification model. Never
// turn this empty declaration into a default; callers must select an ID.
qualificationModel: "", reportedModelId: "", permissionPolicy: "interactive",
@@ -1,5 +1,5 @@
{
"commandDigest": "sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6",
"commandDigest": "sha256:98936d763497bd6f0e5605f52831a344f456357de58c457e440e69a1a468a2f4",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
@@ -20,6 +20,9 @@
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionAdapterSourceSha256": "2bc34ac7899ac2729909459321e5a8a3668342cc61921e8c95e672719c6d91f2"
"permissionAdapterSourceSha256": "2bc34ac7899ac2729909459321e5a8a3668342cc61921e8c95e672719c6d91f2",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "687270d7bbe3dccce6c2fed8664e76cda724b43e545cb153b9f54b018de52927",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4"
}
}