mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-20 06:02:18 +02:00
Repair Copilot packaging and isolate current instructions in profile 35
Keep Pi source identity intact, expose the metadata probe, synchronize current admission contracts, and restore exact instruction ownership assertions. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
56802d8e3e
commit
1bd7e87482
18 files changed
+166
-78
No files matched your search
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 34,
|
||||
"agentProfileVersion": 35,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed",
|
||||
"commandDigest": "sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"paperclip-runner-eval-session": "./dist/cli/eval-session.js",
|
||||
"paperclip-runner-codex-proxy": "./dist/cli/codex-app-server-unix-proxy.js",
|
||||
"paperclip-runner-opencode-proxy": "./dist/cli/opencode-app-server-proxy.js",
|
||||
"paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js"
|
||||
"paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js",
|
||||
"paperclip-runner-copilot-metadata-probe": "./dist/cli/copilot-metadata-probe.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -4083,7 +4083,7 @@ mod tests {
|
||||
descriptor_value["agentRuntimePackage"] = Value::Null;
|
||||
descriptor_value["agentRuntimeVersion"] = Value::Null;
|
||||
descriptor_value["commandDigest"] = serde_json::from_str::<Value>(include_str!(
|
||||
"../../../../test/fixtures/copilot-profile-v34-identity.json"
|
||||
"../../../../test/fixtures/copilot-profile-v35-identity.json"
|
||||
))
|
||||
.unwrap()["commandDigest"]
|
||||
.clone();
|
||||
@@ -4127,6 +4127,7 @@ mod tests {
|
||||
effective_model: original_descriptor.model.clone(),
|
||||
permission_mode: Some(original_descriptor.permission_mode),
|
||||
mode: original_descriptor.mode.clone(),
|
||||
pi_thinking_level: None,
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
};
|
||||
let operations = Vec::new();
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed",
|
||||
"sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
+1
-1
@@ -1030,7 +1030,7 @@ fn provider_death_expires_input_and_commits_failure_without_querying_the_dead_pr
|
||||
let mut config = acpx_config(&directory, mode);
|
||||
let mut payload = prepare_payload_with_mode(&directory, "codex", mode);
|
||||
let identity: Value = serde_json::from_str(include_str!(
|
||||
"../../../../test/fixtures/copilot-profile-v34-identity.json"
|
||||
"../../../../test/fixtures/copilot-profile-v35-identity.json"
|
||||
))
|
||||
.unwrap();
|
||||
let digest = identity["commandDigest"].as_str().unwrap();
|
||||
|
||||
@@ -206,6 +206,7 @@ try {
|
||||
writePortableExecutableShim(temporaryRoot, "opencode", "opencode-ai/bin/opencode.exe");
|
||||
writePortableCopilotShims(temporaryRoot);
|
||||
writePortableNodeShim("acpx", "acpx/dist/cli.js");
|
||||
writePortableNodeShim("paperclip-runner-copilot-metadata-probe", "../dist/cli/copilot-metadata-probe.js");
|
||||
writePortableNodeShim(
|
||||
"claude-agent-acp",
|
||||
"@agentclientprotocol/claude-agent-acp/dist/index.js",
|
||||
|
||||
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { resolve, dirname } from "node:path";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v34-identity.json");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v35-identity.json");
|
||||
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 34 };
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 35 };
|
||||
delete declaration.permissionIdentitySourceSha256;
|
||||
const sources = {
|
||||
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
|
||||
|
||||
@@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract;
|
||||
assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`);
|
||||
assert.equal(contract.acpxPatchSha256, createHash("sha256")
|
||||
.update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex"));
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v34-identity.json"]]) {
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v35-identity.json"]]) {
|
||||
const identity = await readJson(path);
|
||||
assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion);
|
||||
assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest);
|
||||
|
||||
@@ -13,10 +13,15 @@ import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js";
|
||||
const PERMISSION_POLICY_SOURCES = [
|
||||
["copilot-permission-context.ts", "permissionContextSourceSha256"],
|
||||
["acp-permission-adapter.ts", "permissionAdapterSourceSha256"],
|
||||
["cursor-plan-tool-identity.ts", "permissionToolIdentitySourceSha256"],
|
||||
["safe-locations.ts", "permissionLocationsSourceSha256"],
|
||||
["../../semantic-tools/redaction.ts", "permissionRedactionSourceSha256"],
|
||||
["generated-sidecar-contract.ts", "permissionClassifierSourceSha256"],
|
||||
["cursor-plan-tool-identity.ts", "permissionIdentitySourceSha256"],
|
||||
] as const;
|
||||
|
||||
const COPILOT_DISPLAY_SOURCES = [
|
||||
["copilot-events.ts", "displayEventSourceSha256"],
|
||||
["copilot-extension-adapter.ts", "displayProjectionSourceSha256"],
|
||||
] as const;
|
||||
|
||||
const SEMANTIC_RECEIPT_SOURCES = [
|
||||
@@ -29,6 +34,11 @@ const SEMANTIC_RECEIPT_SOURCES = [
|
||||
["../../protocol/replay-contract.ts", "semanticValidationSourceSha256"],
|
||||
["../../protocol/result-normalization.ts", "semanticNormalizationSourceSha256"],
|
||||
["../../contracts/completion-result.ts", "semanticCompletionContractSourceSha256"],
|
||||
["../../contracts/native-execution.ts", "nativeExecutionSourceSha256"],
|
||||
["installation-integrity.ts", "guardedInstallationSourceSha256"],
|
||||
["../../live/runnerd-codex-transport.ts", "runtimeRequestBridgeSourceSha256"],
|
||||
["../codex/codex-session-notifications.ts", "runtimeRequestFacadeSourceSha256"],
|
||||
["../codex/codex-thread-normalization.ts", "runtimeRequestNotificationSourceSha256"],
|
||||
["../../protocol/generated/standalone-validators.ts", "semanticValidatorsSourceSha256"],
|
||||
["../../protocol/generated/schema-bundle.ts", "semanticSchemaBundleSourceSha256"],
|
||||
] as const;
|
||||
@@ -59,36 +69,8 @@ async function permissionPolicyClosure(extraClassifierImport = false): Promise<S
|
||||
vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi.fn() }));
|
||||
|
||||
describe("Copilot build-owned installation", () => {
|
||||
it("preserves v12 history and changes only the version and four shared source bindings", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8"));
|
||||
const changed = Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort();
|
||||
expect(changed).toEqual(["agentProfileVersion", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256", "semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(prior.declaration.agentProfileVersion).toBe(12);
|
||||
expect(current.declaration.agentProfileVersion).toBe(13);
|
||||
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("preserves v13 history while binding the shared live request snapshot", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["agentProfileVersion", "semanticSidecarSourceSha256"]);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("preserves v14 history while binding the corrected outbound ACPX delivery", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v15-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["acpxPatchSha256", "agentProfileVersion"]);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v17 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
it("admits the current declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v35-identity.json", import.meta.url), "utf8"));
|
||||
expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY);
|
||||
expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT);
|
||||
@@ -104,10 +86,12 @@ 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 [...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES, ["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_DISPLAY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES, ["copilot-policy.ts", "policySha256"], ["profile-compatibility.ts", "profileCompatibilitySourceSha256"], ["provider-assets-root.ts", "assetOwnershipSourceSha256"], ["../../../scripts/install-copilot-assets.mjs", "assetInstallerSourceSha256"], ["../../../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("../../../acpx-profiles.json", import.meta.url), "utf8")).toContain(identity.commandDigest);
|
||||
const manifest = JSON.parse(readFileSync(new URL("../../../acpx-profiles.json", import.meta.url), "utf8"));
|
||||
expect(manifest.profiles.copilot.commandDigest).toBe(identity.commandDigest);
|
||||
expect(readFileSync(new URL("../../../scripts/build-copilot-distribution.mjs", import.meta.url), "utf8")).toContain("profiles.profiles.copilot.commandDigest");
|
||||
expect(readFileSync(new URL("../../../runner/crates/runner-core/src/generated_acpx_profiles.rs", import.meta.url), "utf8")).toContain(identity.commandDigest);
|
||||
});
|
||||
it("binds the complete executable permission-policy import closure", async () => {
|
||||
@@ -118,8 +102,8 @@ describe("Copilot build-owned installation", () => {
|
||||
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, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
it.each([...PERMISSION_POLICY_SOURCES, ...COPILOT_DISPLAY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v35-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);
|
||||
@@ -165,16 +149,4 @@ describe("Copilot build-owned installation", () => {
|
||||
}
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
it("rejects the historical v12 digest even when the caller supplies the current version", async () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8"));
|
||||
vi.mocked(verifyNativeAcpxInstallation).mockClear();
|
||||
for (const override of [
|
||||
{ commandDigest: prior.commandDigest },
|
||||
{ agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
{ commandDigest: prior.commandDigest, agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
]) {
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classifyCopilotFailure, copilotConfiguration, copilotSandboxEnvironment, COPILOT_LAUNCH_ARGUMENTS } from "./copilot-profile.js";
|
||||
import { classifyCopilotFailure, composeCopilotSystemInstructions, COPILOT_TASK_ORIENTATION_INSTRUCTIONS, copilotConfiguration, copilotSandboxEnvironment, COPILOT_LAUNCH_ARGUMENTS } from "./copilot-profile.js";
|
||||
|
||||
describe("Copilot admission policy", () => {
|
||||
it("bounds the complete native instruction file without retaining prior instructions", () => {
|
||||
const suffix = `\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}`;
|
||||
expect(composeCopilotSystemInstructions("Current instructions.")).toBe(`Current instructions.${suffix}`);
|
||||
expect(composeCopilotSystemInstructions("")).toBe(suffix);
|
||||
const remaining = 32 * 1024 - Buffer.byteLength(`${suffix}\n`);
|
||||
expect(Buffer.byteLength(`${composeCopilotSystemInstructions("x".repeat(remaining))}\n`)).toBe(32 * 1024);
|
||||
expect(() => composeCopilotSystemInstructions("x".repeat(remaining + 1))).toThrow("bounded size");
|
||||
expect(() => composeCopilotSystemInstructions("é".repeat(Math.floor(remaining / 2) + 1))).toThrow("bounded size");
|
||||
expect(() => composeCopilotSystemInstructions("embedded\0instruction")).toThrow("bounded size");
|
||||
});
|
||||
it("uses ACP directly without disabling questions or bypassing permissions", () => {
|
||||
expect(COPILOT_LAUNCH_ARGUMENTS.slice(0, 2)).toEqual(["--acp", "--stdio"]);
|
||||
expect(COPILOT_LAUNCH_ARGUMENTS).toContain("--no-auto-update");
|
||||
|
||||
@@ -7,6 +7,15 @@ export const COPILOT_SYSTEM_INSTRUCTION_DELIVERY = "COPILOT_HOME/copilot-instruc
|
||||
export const COPILOT_CREDENTIAL_ENVIRONMENT_NAME = "COPILOT_GITHUB_TOKEN" as const;
|
||||
export const COPILOT_TASK_ORIENTATION_INSTRUCTIONS = "At the start of every task turn, use the paperclip MCP server's dedicated get_task_context tool with {} and wait for its successful result before task work, report_progress, or any other task-dependent mutation. This is an advertised MCP tool, not an HTTP API route: do not use search_api or call_api to discover or substitute for get_task_context. Loaded instructions, seed state, task descriptions, and prior-turn context do not replace this read. Decide the next action from the returned context. Do not issue the context read and a dependent action in parallel. If this tool is unavailable or its read fails, report the failure and do not perform dependent actions." as const;
|
||||
|
||||
/** Compose Copilot-only guidance without changing other providers' sandbox identity. */
|
||||
export function composeCopilotSystemInstructions(instructions: string): string {
|
||||
const composed = `${instructions}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}`;
|
||||
if (composed.includes("\0") || Buffer.byteLength(`${composed}\n`) > 32 * 1024) {
|
||||
throw new Error("Provider runtime instructions exceed their bounded size");
|
||||
}
|
||||
return composed;
|
||||
}
|
||||
|
||||
// Keep permission callbacks enabled even when Paperclip's policy is approve-all.
|
||||
// In particular COPILOT_ALLOW_ALL=true also trusts workspace hooks/plugins/MCP.
|
||||
export const COPILOT_LAUNCH_ARGUMENTS = Object.freeze([
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js";
|
||||
import { AcpxRuntimeHost, type AcpxRuntimeHostDependencies, type AcpxRuntimePortOpenOptions } from "./runtime-host.js";
|
||||
|
||||
it("reports missing Copilot authentication before spawn and releases admission ownership for retry", async () => {
|
||||
@@ -66,24 +67,25 @@ it("does not refresh Copilot instructions for a contender rejected by the lifeti
|
||||
normalizedSessionId: "same-native-session", runtimeDirectory, workingDirectory,
|
||||
providerPolicy: { readOnly: false }, environment: { COPILOT_GITHUB_TOKEN: "synthetic-fixture" },
|
||||
};
|
||||
const composed = (value: string) => `${value}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`;
|
||||
const winner = AcpxRuntimeHost.open({ ...options, systemInstructions: "Winner instructions." }, dependencies);
|
||||
// Attach the rejection handler before deliberately releasing the failed launch.
|
||||
const winnerResult = expect(winner).rejects.toThrow("fixture stops before native process launch");
|
||||
try {
|
||||
await winnerEntered;
|
||||
expect(await readFile(instructionPath, "utf8")).toBe("Winner instructions.\n");
|
||||
expect(await readFile(instructionPath, "utf8")).toBe(composed("Winner instructions."));
|
||||
await expect(AcpxRuntimeHost.open({ ...options, systemInstructions: "Losing contender." }, dependencies))
|
||||
.rejects.toThrow("already has an active lease");
|
||||
expect(openRuntime).toHaveBeenCalledOnce();
|
||||
expect(await readFile(instructionPath, "utf8")).toBe("Winner instructions.\n");
|
||||
expect(await readFile(instructionPath, "utf8")).toBe(composed("Winner instructions."));
|
||||
releaseWinner();
|
||||
await winnerResult;
|
||||
await expect(AcpxRuntimeHost.open({ ...options, systemInstructions: "New owner instructions." }, dependencies))
|
||||
.rejects.toThrow("fixture stops before native process launch");
|
||||
expect(await readFile(instructionPath, "utf8")).toBe("New owner instructions.\n");
|
||||
expect(await readFile(instructionPath, "utf8")).toBe(composed("New owner instructions."));
|
||||
await expect(AcpxRuntimeHost.open({ ...options, environment: {}, systemInstructions: "Unauthenticated admission." }, dependencies))
|
||||
.rejects.toMatchObject({ code: "COPILOT_AUTH_REQUIRED" });
|
||||
expect(await readFile(instructionPath, "utf8")).toBe("New owner instructions.\n");
|
||||
expect(await readFile(instructionPath, "utf8")).toBe(composed("New owner instructions."));
|
||||
} finally {
|
||||
releaseWinner();
|
||||
await winnerResult;
|
||||
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 34,
|
||||
"agentProfileVersion": 35,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed",
|
||||
"commandDigest": "sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { composeCopilotSystemInstructions } from "./copilot-profile.js";
|
||||
import { piProviderConfiguration } from "./pi-provider-config.js";
|
||||
import { withAcpxTurnCancellation } from "./turn-cancellation.js";
|
||||
import { resolveAcpxProviderMode } from "./provider-mode.js";
|
||||
@@ -497,7 +498,10 @@ export class AcpxRuntimeHost {
|
||||
// Do not race this write against cancellation: retain the lifetime lease
|
||||
// until the atomic refresh settles, then honor an intervening abort.
|
||||
options.signal?.throwIfAborted();
|
||||
await refreshCopilotSystemInstructions(sandbox, boundedInstructions(options.systemInstructions));
|
||||
await refreshCopilotSystemInstructions(
|
||||
sandbox,
|
||||
composeCopilotSystemInstructions(boundedInstructions(options.systemInstructions)),
|
||||
);
|
||||
options.signal?.throwIfAborted();
|
||||
}
|
||||
if (options.agent === "pi") {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { composeCopilotSystemInstructions, COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js";
|
||||
import { configuredEnvironmentProjection } from "../../configured-environment.js";
|
||||
import {
|
||||
chmod,
|
||||
@@ -429,15 +430,16 @@ it("refreshes Copilot native instructions privately on every admission, includin
|
||||
const sandbox = await prepareAcpxRuntimeSandbox({
|
||||
binding: fixture.binding, agent: "copilot", providerPolicy: { readOnly: false, systemInstructions },
|
||||
});
|
||||
await refreshCopilotSystemInstructions(sandbox, systemInstructions);
|
||||
await refreshCopilotSystemInstructions(sandbox, composeCopilotSystemInstructions(systemInstructions));
|
||||
return sandbox;
|
||||
};
|
||||
const composed = (value: string) => `${value}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`;
|
||||
const first = await prepare("Original registered instructions.");
|
||||
const path = join(first.agentHomeDirectory, "copilot-instructions.md");
|
||||
expect(await readFile(path, "utf8")).toBe("Original registered instructions.\n");
|
||||
expect(await readFile(path, "utf8")).toBe(composed("Original registered instructions."));
|
||||
await prepareAcpxRuntimeSandbox({ binding: fixture.binding, agent: "copilot",
|
||||
providerPolicy: { readOnly: false, systemInstructions: "Contender must not write during preparation." } });
|
||||
expect(await readFile(path, "utf8")).toBe("Original registered instructions.\n");
|
||||
expect(await readFile(path, "utf8")).toBe(composed("Original registered instructions."));
|
||||
expect((await stat(path)).mode & 0o777).toBe(0o600);
|
||||
expect(first.protectedPaths).toContain(dirname(dirname(first.root)));
|
||||
expect(path.startsWith(`${first.protectedPaths[0]}/`)).toBe(true);
|
||||
@@ -448,11 +450,11 @@ it("refreshes Copilot native instructions privately on every admission, includin
|
||||
await prepare("Current registered instructions.");
|
||||
expect((await lstat(path)).isSymbolicLink()).toBe(false);
|
||||
expect(await readFile(target, "utf8")).toBe("Do not overwrite.");
|
||||
expect(await readFile(path, "utf8")).toBe("Current registered instructions.\n");
|
||||
expect(await readFile(path, "utf8")).toBe(composed("Current registered instructions."));
|
||||
await prepare("");
|
||||
expect(await readFile(path, "utf8")).toBe("\n");
|
||||
expect(await readFile(path, "utf8")).toBe(composed(""));
|
||||
await expect(prepare("x".repeat(32 * 1024 + 1))).rejects.toThrow("bounded size");
|
||||
expect(await readFile(path, "utf8")).toBe("\n");
|
||||
expect(await readFile(path, "utf8")).toBe(composed(""));
|
||||
});
|
||||
|
||||
async function sandboxFixture(agent: "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { piProviderConfiguration } from "./pi-provider-config.js";
|
||||
import { configuredEnvironmentKeys } from "../../configured-environment.js";
|
||||
import { COPILOT_SYSTEM_INSTRUCTIONS_FILE, COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js";
|
||||
import { COPILOT_SYSTEM_INSTRUCTIONS_FILE } from "./copilot-profile.js";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import {
|
||||
constants,
|
||||
@@ -646,15 +646,13 @@ export async function refreshCopilotSystemInstructions(
|
||||
sandbox: Pick<AcpxRuntimeSandbox, "agentHomeDirectory">,
|
||||
instructions: string,
|
||||
): Promise<void> {
|
||||
const current = `${instructions}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`;
|
||||
if (current.includes("\0") || Buffer.byteLength(current) > 32 * 1024) {
|
||||
if (instructions.includes("\0") || Buffer.byteLength(instructions) > 32 * 1024) {
|
||||
throw new Error("Provider runtime instructions exceed their bounded size");
|
||||
}
|
||||
// Native Copilot reloads this file on session/load. Empty caller text clears
|
||||
// old caller instructions while retaining the current task-orientation rule.
|
||||
// Native Copilot reloads this file on session/load. Empty text clears old text.
|
||||
await writePrivateFile(
|
||||
join(sandbox.agentHomeDirectory, COPILOT_SYSTEM_INSTRUCTIONS_FILE),
|
||||
current,
|
||||
`${instructions}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"commandDigest": "sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 35,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
|
||||
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "7fd06053db98cc763c7205424069a5286c9d4cfefb3d197a4a765c693dccb7e6",
|
||||
"semanticDirectDriverSourceSha256": "77882e21ad45b66a688fd1933dbd4fc89fad38e8c1f092805cb9485bdd5223c5",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "fe82ac2cbf194d31ac801de6a3cacfb165cb34146c5c26557f283b8c87a5f01e",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
|
||||
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
|
||||
"configuredEnvironmentSourceSha256": "a03575b3a737dea60da94cf0fa1feed80719a9aaa7f09456eb57a845d9e8e087",
|
||||
"configuredEnvironmentRustSourceSha256": "55d44e48e217385b5c64b1b7d8df16dda62319ca3fb524e452fd78c9ed804d3b",
|
||||
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
|
||||
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
|
||||
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
|
||||
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
|
||||
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
|
||||
"nativeSessionRuntimeSourceSha256": "aac818c1cf50a25899ae95cafc70f093c8cd8af37d19f88cbb7847b4d202d2b6",
|
||||
"nativeRuntimeContextSourceSha256": "0b350f5f4044f342b974254738bab45a470c24db9352b04c92d785d26e7853c8",
|
||||
"nativeSessionContractSourceSha256": "93251b1eadc686e9d8339d4dfc50b995a3d0a9e6ad8aedc6f2bdf7e30c16138d",
|
||||
"harnessSessionContractSourceSha256": "9e3d62b712cae91168c96d11ff850b97a144809b5cddfe08ec9a91229e9bfc40",
|
||||
"harnessBackendSourceSha256": "46fd6523f986d5406eca3b420fe920f1d51a6435fb7afc2acf4d21be13af447f",
|
||||
"harnessSessionSourceSha256": "4dff9bb3370c5560cfc910a7ee814a27725b469c3b1b5dc021e3c0b158ebe746",
|
||||
"transportContractSourceSha256": "5cd208fe11c3bedd476494b0609fb32efbabe4a1170e2c5854d0aa5e429bd6f4",
|
||||
"nativeBackendSourceSha256": "294b9d830bc58c05985087875523db133d32cf999703140fd927e843399424db",
|
||||
"nativeBackendFactorySourceSha256": "ce22825a53669f8c2db5aaa890bae94af19ede22295bda4ed7987a31d804cd8a",
|
||||
"nativeInstructionsSourceSha256": "ea85be2aca8dfbd6b4042dcd7b1ed9db9a57baf8c379649a3f5d7d80ca54a6a2",
|
||||
"runtimeRequestBridgeSourceSha256": "a66ee14f858ccf75c69dfbce132900a30724fe1e6baffc26b02d58a6bc5924ac",
|
||||
"runtimeRequestFacadeSourceSha256": "e1993906ac9f47eb4ea04ae815db869ecde72f582af460cc0080308670612c8f",
|
||||
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
|
||||
"guardedInstallationSourceSha256": "2881b655112f165320a73753385cd54286139a0273a5fa30c1b43fc3b5981a25",
|
||||
"nativeExecutionSourceSha256": "9c702e518862c396ccfa12d84d11cb8dec1fc064c595359e31cc3e299a7378cc",
|
||||
"profileCompatibilitySourceSha256": "20f8aaa363496a4cbede0f33aeacf869fb50171eaee2d63a892c65b3bda4c6a1",
|
||||
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
|
||||
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
|
||||
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
|
||||
"credentialBindingSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc",
|
||||
"sidecarErrorSourceSha256": "23f3dbdc1ce488c690d218a854c820f4b4f3edd822bad574d67707314f8c512a",
|
||||
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
|
||||
"metadataProbeSourceSha256": "826e74747a89b83a6f546ee1355de64ae5265eb920b50ae498a41dd561a493ee",
|
||||
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"runtimeHostSourceSha256": "fd62912cdbaf76e73664024fcec835e899e967a4889fdfdccde7eb215c545f8a",
|
||||
"runtimeAdapterSourceSha256": "3aea6448dc6474e465e5267b61798ad5449bdaec3f4a41bc4be831426893a9bb",
|
||||
"runtimeSandboxSourceSha256": "1c1ddb0b24c3417250ce161cda897ba59b1adce7c7609ed553be298464e6c1c4",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
|
||||
}
|
||||
}
|
||||
@@ -107,16 +107,23 @@ describe("resolveNativeRuntimeMode", () => {
|
||||
kind: "native",
|
||||
profile: { backend: "acpx_runtime" },
|
||||
});
|
||||
expect(resolveNativeRuntimeMode({
|
||||
...eligible,
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" },
|
||||
})).toMatchObject({
|
||||
kind: "native",
|
||||
profile: { backend: "acpx_runtime" },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed OpenCode and unqualified ACPX profiles", () => {
|
||||
it("rejects malformed OpenCode and unregistered ACPX profiles", () => {
|
||||
expect(() => resolveNativeRuntimeMode({
|
||||
...eligible,
|
||||
adapterConfig: { provider: "opencode", model: "gpt-5.6-sol" },
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: "paperclip_runner_opencode_model_invalid",
|
||||
}));
|
||||
for (const acpxAgent of ["copilot"]) {
|
||||
for (const acpxAgent of ["unregistered-acpx-agent"]) {
|
||||
expect(() => resolveNativeRuntimeMode({
|
||||
...eligible,
|
||||
adapterConfig: { provider: "acpx", acpxAgent, model: "explicit-provider-model" },
|
||||
|
||||
Reference in new issue
Block a user