Merge branch 'codex/copilot-saved-token-setup-v30' into codex/copilot-qualification-v30

* codex/copilot-saved-token-setup-v30:
  Authorize shared Copilot discovery before credential use
This commit is contained in:
Dotta committed 2026-10-08 11:54:34 -05:00
commit a26ac42ff7
2 files changed
+40 -2

No files matched your search

@@ -164,6 +164,37 @@ describe("managed AI connections", () => {
} finally { probe.mockRestore(); }
});
it("keeps Copilot model discovery behind shared installation and agent configuration authority", async () => {
const { errorHandler } = await import("../middleware/index.js");
const owner = "copilot-discovery-owner", audience = "copilot-discovery-audience";
await db.insert(companyMemberships).values([owner, audience].map(principalId => ({ companyId, principalId, principalType: "user", status: "active", membershipRole: "member" })));
await db.insert(principalPermissionGrants).values([owner, audience].map(principalId => ({ companyId, principalId, principalType: "user", permissionKey: "agents:create" })));
const token = "github_pat_fixture_shared_discovery";
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "shared", name: "Shared discovery", apiKey: token, agentIds: [], allAgents: false }, token);
await db.insert(connectionGrantMembers).values([owner, audience].map(subjectId => ({ companyId, grantId: account.grantId, subjectType: "user", subjectId })));
const binding = { provider: "github", method: "api_key", mode: "shared", ...account };
const probe = vi.spyOn(copilotProbe, "probeCopilotConnection").mockResolvedValue({ status: "verified", version: "1.0.88", profileDigest: "fixture-profile", promptSent: false, models: [{ id: "gpt-5.6-luna", label: "GPT" }] });
const app = express(); app.use(express.json());
app.use((req, _res, next) => { req.actor = { type: "board", source: "session", userId: String(req.headers["x-test-user"] ?? audience), companyIds: [companyId], memberships: [{ companyId, membershipRole: "member", status: "active" }] }; next(); });
app.use("/api", agentRoutes(db)); app.use(errorHandler);
const url = `/api/companies/${companyId}/adapters/paperclip_runner/models`;
const query = { provider: "acpx", acpxAgent: "copilot", aiConnection: JSON.stringify(binding) };
try {
const denied = await request(app).get(url).query(query);
expect(denied.status, JSON.stringify(denied.body)).toBe(403);
expect(denied.body.error).toContain("not permitted for this agent");
expect(probe).not.toHaveBeenCalled();
const allowed = await request(app).get(url).set("x-test-user", owner).query(query);
expect(allowed.status, JSON.stringify(allowed.body)).toBe(200);
expect(probe).toHaveBeenCalledExactlyOnceWith(db, companyId, token, null, undefined, { pluginWorkerManager: undefined });
probe.mockClear();
const foreign = await request(app).get(url).set("x-test-user", owner).query({ ...query, agentId: randomUUID() });
expect(foreign.status).toBe(404);
expect(probe).not.toHaveBeenCalled();
expect(JSON.stringify([denied.body, allowed.body, foreign.body])).not.toContain(token);
} finally { probe.mockRestore(); }
});
it("preserves repository GitHub credentials for managed non-Copilot execution", async () => {
const owner = "repo-github-owner";
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
+9 -2
View File
@@ -3404,9 +3404,16 @@ export function agentRoutes(
catch { throw unprocessable("Invalid AI connection binding"); }
}
if (binding.provider !== "github") throw unprocessable("Select a GitHub Copilot AI connection");
const modelAgentId = asNonEmptyString(req.query.agentId);
if (modelAgentId) {
const modelAgent = await svc.getById(modelAgentId);
if (!modelAgent || modelAgent.companyId !== companyId) throw notFound("Agent not found");
await assertCanUpdateAgent(req, modelAgent);
} else await assertCanCreateAgentsForCompany(req, companyId);
const allowUninstalledShared = !modelAgentId && await canInstallSharedAiConnectionForNewAgent(db, req, companyId, binding);
const service = aiConnectionService(db);
const selection = await service.select({ companyId, userId, agentId: asNonEmptyString(req.query.agentId) ?? "00000000-0000-0000-0000-000000000000",
adapterType: type, runnerProvider: "acpx", acpxAgent: "copilot", binding, allowUninstalledPersonal: true, allowUninstalledShared: req.actor.type === "board" });
const selection = await service.select({ companyId, userId, agentId: modelAgentId ?? "00000000-0000-0000-0000-000000000000",
adapterType: type, runnerProvider: "acpx", acpxAgent: "copilot", binding, allowUninstalledPersonal: true, allowUninstalledShared });
const metadata = await probeCopilotConnection(db, companyId, await service.credential(selection), environmentId, undefined, { pluginWorkerManager: options.pluginWorkerManager });
res.setHeader("Cache-Control", "no-store");
res.json(metadata.models);