mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-20 15:09:57 +02:00
Carry verified current-master Copilot corrections into qualification
Co-Authored-By: Paperclip <noreply@paperclip.ing> * codex/copilot-setup-v34: Preserve Copilot model access and durable connection catalog source Keep guarded recovery fixture synthetic under installed Codex selection Repair Copilot packaging and isolate current instructions in profile 35
This commit is contained in:
commit
470cdae69c
25 files changed
+321
-40
No files matched your search
@@ -122,13 +122,16 @@ vi.mock("../routes/ai-connections.js", async (importOriginal) => ({
|
||||
validateAiApiKey: mockValidateAiApiKey,
|
||||
}));
|
||||
const mockMarkAuthenticationFailed = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const mockSelectAiConnection = vi.hoisted(() => vi.fn(async (_input: unknown) => ({ connectionId: "copilot-connection", grantId: "copilot-grant" })));
|
||||
const mockAiCredential = vi.hoisted(() => vi.fn(async (_selection: unknown) => "synthetic-copilot-token"));
|
||||
const mockCopilotModels = vi.hoisted(() => vi.fn(async () => ({ models: [{ id: "gpt-5.6-luna", label: "GPT-5.6 Luna" }], promptSent: false })));
|
||||
const mockCopilotProbe = vi.hoisted(() => vi.fn(async () => ({ status: "verified", promptSent: false })));
|
||||
vi.mock("../services/copilot-connection-probe.js", async importOriginal => ({
|
||||
...(await importOriginal<object>()), probeCopilotExecutionTarget: mockCopilotProbe,
|
||||
...(await importOriginal<object>()), probeCopilotExecutionTarget: mockCopilotProbe, probeCopilotConnection: mockCopilotModels,
|
||||
}));
|
||||
vi.mock("../services/ai-connections.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../services/ai-connections.js")>()),
|
||||
aiConnectionService: () => ({ markAuthenticationFailed: mockMarkAuthenticationFailed }),
|
||||
aiConnectionService: () => ({ markAuthenticationFailed: mockMarkAuthenticationFailed, select: mockSelectAiConnection, credential: mockAiCredential }),
|
||||
}));
|
||||
|
||||
function mockManagedRuntime(method: "api_key" | "subscription") {
|
||||
@@ -259,6 +262,44 @@ describe("agent test-environment route", () => {
|
||||
await unregisterTestAdapter("external_test");
|
||||
});
|
||||
|
||||
it("lets a company task author discover Copilot models without agent configuration permission", async () => {
|
||||
mockAccessService.decide.mockResolvedValue({ allowed: false, explanation: "No configuration grant" });
|
||||
mockAccessService.canUser.mockResolvedValue(false);
|
||||
mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1" });
|
||||
const response = await request(await createApp()).get("/api/companies/company-1/adapters/paperclip_runner/models")
|
||||
.query({ provider: "acpx", acpxAgent: "copilot", agentId: "agent-1" });
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual([{ id: "gpt-5.6-luna", label: "GPT-5.6 Luna" }]);
|
||||
expect(response.headers["cache-control"]).toBe("no-store");
|
||||
expect(mockAccessService.decide).not.toHaveBeenCalled();
|
||||
expect(mockSelectAiConnection).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
|
||||
companyId: "company-1", userId: "local-board", agentId: "agent-1", runnerProvider: "acpx", acpxAgent: "copilot",
|
||||
binding: { provider: "github", method: "api_key", mode: "responsible_user" }, allowUninstalledShared: false,
|
||||
}));
|
||||
expect(mockCopilotModels).toHaveBeenCalledExactlyOnceWith(expect.anything(), "company-1", "synthetic-copilot-token", null, undefined, expect.anything());
|
||||
});
|
||||
|
||||
it("refuses model discovery for a foreign agent before selecting credentials", async () => {
|
||||
mockAgentService.getById.mockResolvedValue({ id: "agent-foreign", companyId: "company-2" });
|
||||
const response = await request(await createApp()).get("/api/companies/company-1/adapters/paperclip_runner/models")
|
||||
.query({ provider: "acpx", acpxAgent: "copilot", agentId: "agent-foreign" });
|
||||
expect(response.status).toBe(404);
|
||||
expect(mockSelectAiConnection).not.toHaveBeenCalled();
|
||||
expect(mockAiCredential).not.toHaveBeenCalled();
|
||||
expect(mockCopilotModels).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps saved connection access enforced before Copilot model probing", async () => {
|
||||
const { forbidden } = await import("../errors.js");
|
||||
mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1" });
|
||||
mockSelectAiConnection.mockRejectedValueOnce(forbidden("Saved connection access denied"));
|
||||
const response = await request(await createApp()).get("/api/companies/company-1/adapters/paperclip_runner/models")
|
||||
.query({ provider: "acpx", acpxAgent: "copilot", agentId: "agent-1" });
|
||||
expect(response.status).toBe(403);
|
||||
expect(mockAiCredential).not.toHaveBeenCalled();
|
||||
expect(mockCopilotModels).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tests the instance default sandbox when the agent inherits its environment", async () => {
|
||||
const environmentId = "11111111-1111-4111-8111-111111111111";
|
||||
mockInstanceSettingsService.get.mockResolvedValue({ defaultEnvironmentId: environmentId });
|
||||
|
||||
@@ -3415,7 +3415,7 @@ export function agentRoutes(
|
||||
if (modelAgentId) {
|
||||
const modelAgent = await svc.getById(modelAgentId);
|
||||
if (!modelAgent || modelAgent.companyId !== companyId) throw notFound("Agent not found");
|
||||
await assertCanUpdateAgent(req, modelAgent);
|
||||
await assertCanReadAgent(req, modelAgent);
|
||||
} else await assertCanCreateAgentsForCompany(req, companyId);
|
||||
const allowUninstalledShared = !modelAgentId && await canInstallSharedAiConnectionForNewAgent(db, req, companyId, binding);
|
||||
const service = aiConnectionService(db);
|
||||
|
||||
@@ -734,6 +734,7 @@ const recoveryFakeCodex = resolve(
|
||||
const previousStateBase = process.env.PAPERCLIP_RUNNER_STATE_DIR;
|
||||
const server = createServer();
|
||||
let firstSession: NativeSession | undefined;
|
||||
let restoreCodexCommand: (() => void) | undefined;
|
||||
const ownedSessions = new Set<NativeSession>();
|
||||
const ownedSpawns: Array<{ pid: number; processGroupId: number | null; startedAt: string }> = [];
|
||||
const onSpawn = async (meta: typeof ownedSpawns[number]) => { ownedSpawns.push(meta); };
|
||||
@@ -759,6 +760,11 @@ const recoveryFakeCodex = resolve(
|
||||
`#!/bin/sh\nexec ${quote(process.execPath)} ${quote(recoveryFakeCodex)} "$CODEX_HOME/fake-state.json" 16\n`,
|
||||
);
|
||||
await chmod(command, 0o700);
|
||||
// Installed dependency selection intentionally precedes PATH on mainline.
|
||||
// Bind this fixture's synthetic provider explicitly at that boundary.
|
||||
const codexCommandModule = await import("../../../../packages/paperclip-runner/src/drivers/codex/codex-command.js");
|
||||
const codexCommandSelection = vi.spyOn(codexCommandModule, "resolveCodexCommand").mockReturnValue(command);
|
||||
restoreCodexCommand = () => codexCommandSelection.mockRestore();
|
||||
await new Promise<void>((done) => server.listen(0, "127.0.0.1", done));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string")
|
||||
@@ -1048,6 +1054,11 @@ const recoveryFakeCodex = resolve(
|
||||
async onContinuityBreak(value) {
|
||||
continuity = value;
|
||||
},
|
||||
}).then((result) => {
|
||||
if (result.terminal.runTerminalState !== "succeeded") {
|
||||
throw new Error(`Disposable recovery fixture failed: ${JSON.stringify(result)}`);
|
||||
}
|
||||
return result;
|
||||
}).catch(async (error: unknown) => {
|
||||
const eventRows = await db
|
||||
.select({
|
||||
@@ -1227,6 +1238,7 @@ const recoveryFakeCodex = resolve(
|
||||
Promise.resolve().then(() => session.close({ reason: "Recovery fixture cleanup" })),
|
||||
));
|
||||
const closeFailures = closed.filter((result) => result.status === "rejected");
|
||||
restoreCodexCommand?.();
|
||||
runnerPrpWebSocketInternals.resetForTests();
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((done) => server.close(() => done()));
|
||||
|
||||
@@ -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