Connect Copilot candidate installation, events, diagnostics and pack registries

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 15:17:09 -05:00
1 parent 2b19bd22be
commit 4fc5dbd4ec
8 files changed
+122 -14

No files matched your search

@@ -174,7 +174,8 @@ Reasons and priorities are explicit:
| Native external-tool/sampling/limits callbacks | No qualified ACP responder. P0 prove no unresolved request on admitted model/tools; otherwise keep release unqualified. |
| Native capability/model/session lifecycle/config notices | Initial handshake and normalized session/config are admission authority. P1 detect capability/model drift and fail closed rather than treat a notice as authorization. |
Subagent subscribed fields are preserved in full. Empty native
Subagent subscribed fields are preserved within the declared text bounds;
truncated display strings carry an explicit truncation marker. Empty native
`pending_messages.modified` and `session.background_tasks_changed` events have no
queue/task list to preserve; their projection explicitly says refresh unavailable.
Native context repository/git-root strings, completion receipt finalTool,
@@ -196,7 +197,7 @@ python3 packages/paperclip-runner/scripts/probe-copilot-acp.py --package-root /p
python3 packages/paperclip-runner/scripts/probe-copilot-acp.py --package-root /path/to/copilot-darwin-arm64/package --scenario attached-shell
node --test packages/paperclip-runner/scripts/materialize-copilot-binary.test.mjs packages/paperclip-runner/scripts/build-copilot-distribution.test.mjs
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/drivers/acpx/copilot-events.test.ts src/drivers/acpx/copilot-profile.test.ts src/drivers/acpx/copilot-evidence.test.ts
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/drivers/acpx/copilot-extension-adapter.test.ts
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/drivers/acpx/copilot-extension-adapter.test.ts src/drivers/acpx/copilot-registry.test.ts
```
Retained real-binary evidence:
@@ -239,3 +240,13 @@ builder, returning the profile digest above and closure
The temporary output was removed after verification. This packaging proof used
no model credentials, executed no provider turn, and incurred $0 model spend;
it does not qualify either local product behavior or Daytona execution.
The Copilot branch connects all three closed registries: profile installation
selects the pinned native verifier, profile extensions advertise only the 22
selected native event types and create the Copilot adapter, and candidate packs
select the verified archive builder. Registry conformance checks the complete
subagent field projection through the shared turn binder, attribution, canonical
schema validation, meaningful display details, and stale/cross-session rejection.
Admission error classification distinguishes missing authentication, account or
organization denial, and unavailable explicit models using fixed safe messages;
unrelated runner integrity errors keep their original classification.
@@ -18,5 +18,9 @@ export function parseProviderPackArguments(args) {
/** Closed source-owned builder registry; provider branches add their exact pins. */
export async function materializeCandidateProviderPack({ provider, outputRoot }) {
if (!CANDIDATES.has(provider)) throw new Error("Unknown candidate provider");
if (provider === "copilot") {
const { buildPinnedCopilotDistribution } = await import("./build-copilot-distribution.mjs");
return buildPinnedCopilotDistribution({ outputRoot });
}
throw new Error(`The ${provider} candidate distribution builder is not included in this source revision`);
}
@@ -12,3 +12,16 @@ test("candidate builder cannot admit unknown providers, options or duplicate ass
}
await assert.rejects(materializeCandidateProviderPack({ provider: "arbitrary", outputRoot: "/tmp/unused" }), /Unknown candidate/);
});
test("Copilot pack selection reaches only the pinned native archive builder", async t => {
const requests = [];
t.mock.method(globalThis, "fetch", async (url, options) => {
requests.push({ url, redirect: options.redirect, credentials: options.credentials });
return new Response("corrupt archive");
});
await assert.rejects(materializeCandidateProviderPack({ provider: "copilot", outputRoot: "/unused-copilot-build" }), /integrity mismatch/);
assert.equal(requests.length, 1);
assert.match(requests[0].url, /^https:\/\/registry\.npmjs\.org\/@github\/copilot-(darwin-(arm64|x64)|linux-x64)\/-\/copilot-.*-1\.0\.88\.tgz$/);
assert.deepEqual({ redirect: requests[0].redirect, credentials: requests[0].credentials }, { redirect: "error", credentials: "omit" });
await assert.rejects(materializeCandidateProviderPack({ provider: "pi", outputRoot: "/unused" }), /not included/);
});
@@ -3,13 +3,11 @@ import type { CanonicalProviderEvent } from "../../provider-events.js";
import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js";
import { COPILOT_ACP_EVENT_METHOD, normalizeCopilotSessionEvent, type CopilotSessionEvent } from "./copilot-events.js";
import type { AcpxProfileExtensionAdapter, AcpxProfileExtensionContext } from "./profile-extensions.js";
import { classifyCopilotFailure } from "./copilot-profile.js";
/** Display projection only. Native notices cannot settle or charge a runner turn. */
export function createCopilotProfileExtensionAdapter(context: AcpxProfileExtensionContext): AcpxProfileExtensionAdapter & { classifyError: typeof classifyCopilotFailure } {
export function createCopilotProfileExtensionAdapter(context: AcpxProfileExtensionContext): AcpxProfileExtensionAdapter {
let sequence = 0;
return {
classifyError: classifyCopilotFailure,
async request() {
throw new Error("Copilot 1.0.88 has no qualified inbound ACP extension request responder");
},
@@ -65,13 +65,13 @@ export function classifyCopilotFailure(error: unknown): { code: CopilotFailureCo
: typeof error === "string" ? error
: isRecord(error) && typeof error.message === "string" ? error.message : "";
const message = source.slice(0, 8192).toLowerCase();
if (/authentication required|unauthorized|invalid (?:github )?token|not (?:logged|signed) in/.test(message)) {
if (/authentication required|unauthorized|invalid (?:github )?token|not (?:logged|signed) in|copilot_github_token.*(?:missing|required|not configured)|(?:missing|no) (?:copilot |github )?(?:credential|token|authentication)/.test(message)) {
return { code: "COPILOT_AUTH_REQUIRED", message: "Bind a valid COPILOT_GITHUB_TOKEN credential for this runner." };
}
if (/entitlement|subscription|organization policy|organisation policy|policy.*(?:denied|disabled)|access denied|forbidden|not entitled/.test(message)) {
return { code: "COPILOT_ENTITLEMENT_DENIED", message: "Copilot access is denied by the account entitlement or organization policy." };
}
if (/(?:model.*(?:not found|unavailable|not available|unsupported|invalid)|unknown model|invalid model)/.test(message)) {
if (/(?:model.*(?:not found|unavailable|not available|not supported|unsupported|invalid)|unknown model|invalid model)/.test(message)) {
return { code: "COPILOT_MODEL_UNAVAILABLE", message: "The explicitly selected Copilot model is unavailable for this account." };
}
return { code: "COPILOT_REQUEST_FAILED", message: "The Copilot ACP request failed." };
@@ -0,0 +1,73 @@
import { describe, expect, it, vi } from "vitest";
import type { CanonicalProviderEvent } from "../../provider-events.js";
import { COPILOT_ACP_CLIENT_CAPABILITIES, COPILOT_ACP_EVENT_METHOD } from "./copilot-events.js";
import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, createAcpxProfileExtensionAdapter } from "./profile-extensions.js";
import { classifyAcpxProfileError, verifyAcpxProfileInstallation } from "./profile-installation.js";
import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
import { verifyCopilotInstallation } from "./copilot-installation.js";
vi.mock("./copilot-installation.js", () => ({ verifyCopilotInstallation: vi.fn(async () => ({ commandDigest: "verified-by-native-factory" })) }));
const context = { workspacePath: "/workspace", sessionId: "backend-1", turnId: "turn-1" };
describe("Copilot provider registry conformance", () => {
it("selects only the Copilot native installer and preserves the exact caller-selected profile", async () => {
const profile = resolveQualifiedAcpxProfile("copilot", "explicit-exact-model");
expect(await verifyAcpxProfileInstallation(profile)).toMatchObject({ commandDigest: "verified-by-native-factory" });
expect(verifyCopilotInstallation).toHaveBeenCalledExactlyOnceWith(profile);
await expect(verifyAcpxProfileInstallation(resolveQualifiedAcpxProfile("cursor", "exact-model"))).rejects.toThrow(/not installed/);
});
it("negotiates an isolated native event subscription and preserves every safe subagent field through the shared turn binder", async () => {
const capabilities = acpxProfileClientCapabilities("copilot");
expect(capabilities).toEqual(COPILOT_ACP_CLIENT_CAPABILITIES);
(capabilities._meta as Record<string, unknown>)["github.com/copilot"] = {};
expect(acpxProfileClientCapabilities("copilot")).toEqual(COPILOT_ACP_CLIENT_CAPABILITIES);
expect(acpxProfileClientCapabilities("codex")).toEqual({});
const emitted: CanonicalProviderEvent[] = [];
const data = {
toolCallId: "tool-1", agentName: "review", agentDisplayName: "Reviewer", agentDescription: "Review the change",
model: "exact-model", parentId: "parent-1", agentType: "task", executionMode: "background", reasoningEffort: "high",
contextTier: "large", firstDispatchedModel: "model-initial", configuredModelPreference: "model-preferred",
explicitModelOverride: "model-explicit", modelOverrideReason: "task request", modelSelectionSource: "agent",
taskModelSource: "task", factoryRunId: "factory-1", totalToolCalls: 3, totalTokens: 40, durationMs: 12.5,
cancelled: false, resumable: true, multiTurn: true, explicitModelMatchesPreference: false, configuredModelMatchesActual: false,
};
const binder = bindAcpxExtensionTurn({ adapter: createAcpxProfileExtensionAdapter("copilot", context),
active: () => true, sessionId: context.sessionId, waitForInput: async () => { throw new Error("No input allowed"); }, emit: event => emitted.push(event) });
binder.onExtensionNotification(COPILOT_ACP_EVENT_METHOD, { sessionId: context.sessionId, type: "subagent.completed", data,
agentId: "agent-2", timestamp: "2026-09-28T00:00:00Z" });
await binder.drain();
expect(emitted.map(event => event.eventType)).toEqual(["delegation.completed", "provider.notice.recorded"]);
expect(emitted[0]!.payload).toMatchObject({ schema: "paperclip.delegation.v1", action: "spawn", status: "completed",
children: [{ role: "Reviewer", model: "exact-model", status: "completed", summary: "Review the change" }] });
expect(emitted[1]!.payload).toMatchObject({ summary: "Copilot subagent completed.", provenance: {
method: COPILOT_ACP_EVENT_METHOD, eventType: "subagent.completed", agentId: "agent-2",
sessionId: "backend-1", turnId: "turn-1", timestamp: "2026-09-28T00:00:00Z",
} });
expect(emitted[1]!.payload.details).toEqual(Object.entries(data).map(([name, value]) => ({ name, value: String(value) })));
});
it.each(["wrong_session", "stale_turn"])("rejects %s events at the shared boundary before presentation", async scenario => {
const emitted: CanonicalProviderEvent[] = [];
const binder = bindAcpxExtensionTurn({ adapter: createAcpxProfileExtensionAdapter("copilot", context),
active: () => scenario !== "stale_turn", sessionId: context.sessionId, waitForInput: async () => ({}), emit: event => emitted.push(event) });
binder.onExtensionNotification(COPILOT_ACP_EVENT_METHOD, { sessionId: scenario === "wrong_session" ? "agent-1" : "backend-1", type: "session.idle", data: {} });
await expect(binder.drain()).rejects.toThrow();
expect(emitted).toEqual([]);
});
it("classifies admission failures without copying credentials or masking unrelated runner errors", () => {
for (const [message, code] of [
["COPILOT_GITHUB_TOKEN is required", "COPILOT_AUTH_REQUIRED"],
["Unauthorized token ghp_abcdefghijklmnopqrstuvwxyz", "COPILOT_AUTH_REQUIRED"],
["Organization policy denied with Bearer secretsecretsecret", "COPILOT_ENTITLEMENT_DENIED"],
["Model custom-model is not supported", "COPILOT_MODEL_UNAVAILABLE"],
]) {
const error = classifyAcpxProfileError("copilot", new Error(message));
expect(error).toMatchObject({ code, retryable: false });
expect(String(error)).not.toMatch(/ghp_|secretsecretsecret|custom-model/);
}
expect(classifyAcpxProfileError("codex", new Error("Unauthorized"))).toBeNull();
expect(classifyAcpxProfileError("copilot", new Error("native distribution digest mismatch"))).toBeNull();
});
});
@@ -1,3 +1,5 @@
import { createCopilotProfileExtensionAdapter } from "./copilot-extension-adapter.js";
import { COPILOT_ACP_CLIENT_CAPABILITIES } from "./copilot-events.js";
import type { HarnessRuntimeRequestResolution } from "../../contracts/harness-driver.js";
import { parsePaperclipQuestionSet, type PaperclipQuestionSet } from "../../contracts/question-set.js";
import { isCanonicalProviderEventType, type CanonicalProviderEvent } from "../../provider-events.js";
@@ -33,13 +35,14 @@ export interface AcpxProfileExtensionContext {
/** Provider branches install their closed, pinned adapters here after qualification research. */
export function createAcpxProfileExtensionAdapter(
_agent: QualifiedAcpxAgent,
_context: AcpxProfileExtensionContext,
agent: QualifiedAcpxAgent,
context: AcpxProfileExtensionContext,
): AcpxProfileExtensionAdapter | null {
if (agent === "copilot") return createCopilotProfileExtensionAdapter(context);
return null;
}
export function acpxProfileClientCapabilities(_agent: QualifiedAcpxAgent): Record<string, unknown> {
return {};
export function acpxProfileClientCapabilities(agent: QualifiedAcpxAgent): Record<string, unknown> {
return agent === "copilot" ? structuredClone(COPILOT_ACP_CLIENT_CAPABILITIES) : {};
}
/** Reject an oversized approval document; never silently approve a truncated revision. */
@@ -1,8 +1,11 @@
import { classifyCopilotFailure } from "./copilot-profile.js";
import { verifyCopilotInstallation } from "./copilot-installation.js";
import type { QualifiedAcpxAgent, QualifiedAcpxProfile } from "./qualified-profiles.js";
import { verifyQualifiedAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js";
/** Closed build-owned registry. Provider branches add their pinned installations here. */
export async function verifyAcpxProfileInstallation(profile: QualifiedAcpxProfile): Promise<VerifiedAcpxInstallation> {
if (profile.agent === "copilot") return verifyCopilotInstallation(profile);
if (profile.agent !== "claude" && profile.agent !== "codex" && profile.agent !== "grok") {
throw new Error(`ACPX ${profile.agent} verified candidate distribution is not installed in this build`);
}
@@ -15,7 +18,10 @@ export async function assertAcpxProfileWorkspace(_agent: QualifiedAcpxAgent, _wo
/** Candidate branches validate only explicitly bound, sanitized launch credentials. */
export function assertAcpxProfileEnvironment(_agent: QualifiedAcpxAgent, _environment: Readonly<NodeJS.ProcessEnv>): void {}
/** Optional provider-specific classification; never changes whether admission succeeded. */
export function classifyAcpxProfileError(_agent: QualifiedAcpxAgent, _error: unknown): Error | null {
return null;
/** Provider admission diagnostics expose no raw provider strings or credentials. */
export function classifyAcpxProfileError(agent: QualifiedAcpxAgent, error: unknown): Error | null {
if (agent !== "copilot") return null;
const failure = classifyCopilotFailure(error);
if (failure.code === "COPILOT_REQUEST_FAILED") return null;
return Object.assign(new Error(failure.message), { code: failure.code, retryable: false });
}