Integrate current Copilot admission and setup repairs

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/copilot-saved-token-setup-v30:
  Preserve Copilot task catalogs and repair ordinary startup
  Align Copilot contract history tests with profile v33
This commit is contained in:
DottaandPaperclip committed 2026-10-08 12:49:28 -05:00
commit 6a47aed265
10 files changed
+107 -11

No files matched your search

+3 -3
View File
@@ -4266,7 +4266,7 @@
"checkConstraints": {
"ai_connection_defaults_provider_check": {
"name": "ai_connection_defaults_provider_check",
"value": "\"ai_connection_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google','github')"
"value": "\"ai_connection_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google')"
},
"ai_connection_defaults_method_check": {
"name": "ai_connection_defaults_method_check",
@@ -4739,7 +4739,7 @@
"checkConstraints": {
"ai_provider_defaults_provider_check": {
"name": "ai_provider_defaults_provider_check",
"value": "\"ai_provider_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google','github')"
"value": "\"ai_provider_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google')"
}
},
"isRLSEnabled": false
@@ -56355,4 +56355,4 @@
"schemas": {},
"tables": {}
}
}
}
@@ -391,7 +391,7 @@ describe("NativeExecutionInputV1", () => {
})).toThrow("does not match");
});
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => {
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => {
const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId,
permissionPolicy, qualificationStatus: _qualificationStatus, modelPolicy: _modelPolicy,
...snapshot } = QUALIFIED_ACPX_PROFILES.copilot;
@@ -403,7 +403,7 @@ describe("NativeExecutionInputV1", () => {
const parsed = parseNativeExecutionInput(value);
expect(parsed.provider).toEqual(provider);
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
for (const unsupportedVersion of [0, 33, 1.5, "32", null]) {
for (const unsupportedVersion of [0, 34, 1.5, "33", null]) {
expect(() => parseNativeExecutionInput({
...value, provider: { ...provider, profile: { ...provider.profile, agentProfileVersion: unsupportedVersion } },
})).toThrow("qualified ACPX v1 profile");
@@ -49,6 +49,20 @@ describe("idle admission", () => {
expect(idleWorkSnapshot().active).toBe(0);
});
it("tracks ordinary middleware with non-router stack metadata", async () => {
const server = app(), done = deferred();
const middleware = Object.assign(async (_req: express.Request, res: express.Response) => {
res.sendStatus(202); await done.promise;
}, { stack: { name: "middleware metadata" } });
server.use(middleware);
expect(() => trackIdleRequestHandlers(server)).not.toThrow();
await request(server).get("/work").expect(202);
expect(idleWorkSnapshot().active).toBe(1);
done.resolve();
await new Promise(resolve => setImmediate(resolve));
expect(idleWorkSnapshot().active).toBe(0);
});
it("does not mistake a client disconnect for completed accepted work", async () => {
const server = app();
const entered = deferred(), done = deferred(), closed = deferred();
+1 -1
View File
@@ -61,7 +61,7 @@ export function trackIdleRequestHandlers(app: Application): void {
if (seen.has(layer)) continue;
seen.add(layer);
if (layer.route) { visit(layer.route.stack, true); continue; }
if (layer.handle.stack) { visit(layer.handle.stack); continue; }
if (Array.isArray(layer.handle.stack)) { visit(layer.handle.stack); continue; }
const original = layer.handle;
if (original === idleAdmissionMiddleware) continue;
const invoke = (req: Request, args: unknown[]) => {
@@ -2359,6 +2359,23 @@ describe("IssueProperties", () => {
act(() => root.unmount());
});
it("discovers Copilot overrides with the assignee connection and environment", async () => {
const binding = { provider: "github", method: "api_key", mode: "shared", connectionId: "11111111-1111-4111-8111-111111111111", grantId: "33333333-3333-4333-8333-333333333333" };
mockAgentsApi.list.mockResolvedValue([{ id: "agent-1", name: "Copilot", role: "engineer", status: "active",
adapterType: "paperclip_runner", defaultEnvironmentId: "daytona-1",
adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" },
runtimeConfig: { aiConnection: binding } }]);
mockAgentsApi.adapterModels.mockResolvedValue([{ id: "gpt-5.6-luna", label: "Copilot Luna" }]);
const root = renderProperties(container, {
issue: createIssue({ assigneeAgentId: "agent-1", assigneeAdapterOverrides: { adapterConfig: { model: "gpt-5.6-luna" } } }),
childIssues: [], onUpdate: vi.fn(),
});
await waitForAssertion(() => expect(mockAgentsApi.adapterModels).toHaveBeenCalledWith("company-1", "paperclip_runner", {
environmentId: "daytona-1", provider: "acpx", acpxAgent: "copilot", agentId: "agent-1", aiConnection: binding,
}));
act(() => root.unmount());
});
it("edits existing custom assignee model options from the properties pane", async () => {
const onUpdate = vi.fn();
mockAgentsApi.list.mockResolvedValue([
@@ -34,6 +34,7 @@ import { projectsApi } from "../../api/projects";
import { useCompany } from "../../context/CompanyContext";
import { useSidebar } from "../../context/SidebarContext";
import { queryKeys } from "../../lib/queryKeys";
import { copilotTaskModelCatalogOptions } from "../../lib/copilot-task-model-catalog";
import { buildCompanyUserInlineOptions, buildCompanyUserLabelMap, buildCompanyUserProfileMap, isAgentTaskTarget } from "../../lib/company-members";
import { ISSUE_OVERRIDE_ADAPTER_TYPES, type IssueModelLane } from "../../lib/issue-assignee-overrides";
import { useProjectOrder } from "../../hooks/useProjectOrder";
@@ -729,11 +730,12 @@ export function IssueProperties({
const assignee = issue.assigneeAgentId
? agents?.find((a) => a.id === issue.assigneeAgentId)
: null;
const copilotCatalog = copilotTaskModelCatalogOptions(assignee);
const assigneeAdapterType = assignee?.adapterType ?? null;
const assigneeAdapterOverrides = issue.assigneeAdapterOverrides ?? null;
const showAssigneeAdapterOptions = assigneeAdapterOverrides !== null;
const supportsAssigneeOverrides = Boolean(
assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType),
assigneeAdapterType && (ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType) || copilotCatalog),
);
const assigneeOverrideLane = overrideLane(assigneeAdapterOverrides);
const assigneeOverrideAdapterConfig = asRecord(assigneeAdapterOverrides?.adapterConfig);
@@ -750,12 +752,16 @@ export function IssueProperties({
const assigneeOverrideChrome = assigneeAdapterType === "claude_local"
&& assigneeOverrideAdapterConfig.chrome === true;
const catalogProvider = assigneeAdapterType === "paperclip_runner" ? String(normalizeLegacyRunnerProvider(assigneePrimaryAdapterConfig).provider ?? "codex") : undefined;
const catalogEnvironmentId = copilotCatalog ? assignee?.defaultEnvironmentId ?? null : null;
const { data: assigneeAdapterModels } = useQuery({
queryKey:
companyId && assigneeAdapterType
? queryKeys.agents.adapterModels(companyId, assigneeAdapterType, null, catalogProvider)
? [...queryKeys.agents.adapterModels(companyId, assigneeAdapterType, catalogEnvironmentId, catalogProvider), copilotCatalog ?? null]
: ["agents", "none", "adapter-models", assigneeAdapterType ?? "none"],
queryFn: () => agentsApi.adapterModels(companyId!, assigneeAdapterType!, { provider: catalogProvider }),
queryFn: () => agentsApi.adapterModels(companyId!, assigneeAdapterType!, {
provider: catalogProvider,
...(copilotCatalog ? { environmentId: catalogEnvironmentId, ...copilotCatalog } : {}),
}),
enabled: Boolean(companyId) && showAssigneeAdapterOptions && supportsAssigneeOverrides,
});
const modelOverrideOptions = useMemo<InlineEntityOption[]>(() => {
@@ -259,6 +259,26 @@ describe("composer assignee picker", () => {
expect(choices.some((item) => item.includes("GPT-6 Sol"))).toBe(false);
});
it("discovers Copilot task models with the assignee connection and environment", async () => {
const binding = { provider: "github", method: "api_key", mode: "shared", connectionId: "11111111-1111-4111-8111-111111111111", grantId: "33333333-3333-4333-8333-333333333333" } as const;
const copilot = { ...agent, adapterType: "paperclip_runner", defaultEnvironmentId: "daytona-1",
adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" },
runtimeConfig: { aiConnection: binding } } as Agent;
const loadModels = vi.spyOn(agentsApi, "adapterModels").mockResolvedValueOnce([
{ id: "gpt-5.6-luna", label: "Copilot Luna" },
]);
render(vi.fn(), vi.fn(), true, { agents: new Map([[agent.id, copilot]]), settings: null });
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); });
expect(loadModels).toHaveBeenCalledWith("company-1", "paperclip_runner", {
environmentId: "daytona-1", provider: "acpx", poolId: undefined,
acpxAgent: "copilot", agentId: agent.id, aiConnection: binding,
});
await click("Select model and effort");
await click("Choose exact model");
expect(document.body.textContent).toContain("Copilot Luna");
expect(document.body.textContent).not.toContain("Claude Sonnet");
});
it("preserves settings when the selected assignee is chosen again", async () => {
const onAssigneeChange = vi.fn();
const onSettingsChange = vi.fn();
@@ -4,6 +4,7 @@ import { ArrowLeft, Check, ChevronDown, Plus, RotateCcw, Search, X, Zap } from "
import { aiRuntimeConnectionBindingSchema, type Agent, type IssueAssigneeAdapterOverrides } from "@paperclipai/shared";
import { agentsApi, type AdapterModel } from "@/api/agents";
import { queryKeys } from "@/lib/queryKeys";
import { copilotTaskModelCatalogOptions } from "@/lib/copilot-task-model-catalog";
import { cn } from "@/lib/utils";
import { useMobileEntityPickerViewportStyle } from "@/hooks/useMobileEntityPickerViewportStyle";
import { getLastComposerEffort, rememberComposerEffort } from "@/lib/recent-composer-effort";
@@ -106,12 +107,13 @@ export function ComposerRunSettingsPicker({
const provider = composerCatalogProvider(agent);
const binding = aiRuntimeConnectionBindingSchema.safeParse(agent?.runtimeConfig?.aiConnection).data;
const poolId = binding?.mode === "router" ? binding.connectionId : undefined;
const copilotCatalog = copilotTaskModelCatalogOptions(agent);
const { data: fetchedModels = [], isPending: modelsPending } = useQuery({
queryKey: agent && modelSupported
? [...queryKeys.agents.adapterModels(companyId, agent.adapterType, agent.defaultEnvironmentId ?? null, provider), poolId ?? null]
? [...queryKeys.agents.adapterModels(companyId, agent.adapterType, agent.defaultEnvironmentId ?? null, provider), poolId ?? null, copilotCatalog ?? null]
: ["agents", "composer-models", "none"],
queryFn: () => agentsApi.adapterModels(companyId, agent!.adapterType, {
environmentId: agent!.defaultEnvironmentId ?? null, provider, poolId,
environmentId: agent!.defaultEnvironmentId ?? null, provider, poolId, ...copilotCatalog,
}),
enabled: Boolean(agent && modelSupported && !modelOptionsOverride),
});
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import type { Agent } from "@paperclipai/shared";
import { copilotTaskModelCatalogOptions } from "./copilot-task-model-catalog";
describe("Copilot task model catalog identity", () => {
const agent = { id: "copilot", adapterType: "paperclip_runner", adapterConfig: {
provider: "acpx", acpxAgent: "copilot",
} } as unknown as Agent;
it("changes the catalog identity when a saved connection changes", () => {
const a = copilotTaskModelCatalogOptions({ ...agent, runtimeConfig: { aiConnection: {
provider: "github", method: "api_key", mode: "shared", connectionId: "11111111-1111-4111-8111-111111111111", grantId: "33333333-3333-4333-8333-333333333333",
} } });
const b = copilotTaskModelCatalogOptions({ ...agent, runtimeConfig: { aiConnection: {
provider: "github", method: "api_key", mode: "shared", connectionId: "22222222-2222-4222-8222-222222222222", grantId: "33333333-3333-4333-8333-333333333333",
} } });
expect(a).not.toEqual(b);
});
it("allows normal responsible-user discovery when no explicit binding is saved", () => {
expect(copilotTaskModelCatalogOptions(agent)).toEqual({ acpxAgent: "copilot", agentId: "copilot", aiConnection: undefined });
});
it("does not change catalogs for other runners", () => {
expect(copilotTaskModelCatalogOptions({ ...agent, adapterConfig: { provider: "acpx", acpxAgent: "claude" } })).toBeUndefined();
});
});
+13
View File
@@ -0,0 +1,13 @@
import { aiConnectionBindingSchema, type Agent } from "@paperclipai/shared";
/** Task overrides must discover models through the assignee's saved connection. */
export function copilotTaskModelCatalogOptions(agent: Agent | null | undefined) {
if (agent?.adapterType !== "paperclip_runner"
|| agent.adapterConfig.provider !== "acpx"
|| agent.adapterConfig.acpxAgent !== "copilot") return undefined;
return {
acpxAgent: "copilot",
agentId: agent.id,
aiConnection: aiConnectionBindingSchema.safeParse(agent.runtimeConfig?.aiConnection).data,
};
}