From 6d40e43b8e187c97d274be0828d9a68d76ebf310 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 3 Jun 2026 19:51:47 +0000 Subject: [PATCH] PAP-10277 Inject safe claude_local adapter defaults for bundled team installs Bundled catalog agents declare no adapter in AGENTS.md, so the agent_safe importer rejects them with `Adapter type "process" is not allowed`. Inject a per-agent `claude_local` default for every imported catalog agent the caller did not explicitly override, scoped to the catalog install path only. Explicit caller overrides (adapterType + adapterConfig) are preserved untouched, and the generic company-portability fallback / board_full behavior is unchanged. Co-Authored-By: Paperclip --- .../__tests__/teams-catalog-service.test.ts | 65 +++++++++++++++++++ server/src/services/teams-catalog.ts | 33 +++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/teams-catalog-service.test.ts b/server/src/__tests__/teams-catalog-service.test.ts index 378b9bd5fea..55e7b87cef0 100644 --- a/server/src/__tests__/teams-catalog-service.test.ts +++ b/server/src/__tests__/teams-catalog-service.test.ts @@ -134,6 +134,71 @@ describe("teamsCatalogService", () => { ); }); + it("injects safe claude_local adapter defaults for every bundled agent when no overrides are supplied", async () => { + const svc = teamsCatalogService({} as any); + + await svc.installCatalogTeam("company-1", "core-exec-team"); + + const [importInput] = mockCompanyPortabilityService.importBundle.mock.calls.at(-1)!; + expect(importInput.adapterOverrides).toEqual({ + ceo: { adapterType: "claude_local" }, + cto: { adapterType: "claude_local" }, + qa: { adapterType: "claude_local" }, + }); + }); + + it("supplies safe adapter defaults for product-design and product-engineering installs", async () => { + const svc = teamsCatalogService({} as any); + + await svc.installCatalogTeam("company-1", "product-design"); + const [designInput] = mockCompanyPortabilityService.importBundle.mock.calls.at(-1)!; + expect(designInput.adapterOverrides).toEqual({ + "ux-designer": { adapterType: "claude_local" }, + }); + + await svc.installCatalogTeam("company-1", "product-engineering"); + const [engineeringInput] = mockCompanyPortabilityService.importBundle.mock.calls.at(-1)!; + expect(engineeringInput.adapterOverrides).toEqual({ + cto: { adapterType: "claude_local" }, + qa: { adapterType: "claude_local" }, + "senior-coder": { adapterType: "claude_local" }, + }); + }); + + it("never sends a forbidden process adapter type from the default catalog path", async () => { + const svc = teamsCatalogService({} as any); + + await svc.installCatalogTeam("company-1", "core-exec-team"); + + const [importInput] = mockCompanyPortabilityService.importBundle.mock.calls.at(-1)!; + const adapterTypes = Object.values(importInput.adapterOverrides as Record) + .map((override) => override.adapterType); + expect(adapterTypes).not.toContain("process"); + expect(adapterTypes).not.toContain("http"); + }); + + it("preserves an explicit caller adapter override for the affected slug", async () => { + const svc = teamsCatalogService({} as any); + + const callerOverrides = { + cto: { adapterType: "opencode_local", adapterConfig: { model: "anthropic/claude-opus-4" } }, + }; + await svc.installCatalogTeam("company-1", "core-exec-team", { + adapterOverrides: callerOverrides, + }); + + const [importInput] = mockCompanyPortabilityService.importBundle.mock.calls.at(-1)!; + expect(importInput.adapterOverrides).toEqual({ + ceo: { adapterType: "claude_local" }, + cto: { adapterType: "opencode_local", adapterConfig: { model: "anthropic/claude-opus-4" } }, + qa: { adapterType: "claude_local" }, + }); + // Caller-supplied object must not be mutated in place. + expect(callerOverrides).toEqual({ + cto: { adapterType: "opencode_local", adapterConfig: { model: "anthropic/claude-opus-4" } }, + }); + }); + it("passes install secretValues through to company portability import", async () => { const svc = teamsCatalogService({} as any); diff --git a/server/src/services/teams-catalog.ts b/server/src/services/teams-catalog.ts index ef246677bb0..e0663d2f71f 100644 --- a/server/src/services/teams-catalog.ts +++ b/server/src/services/teams-catalog.ts @@ -12,6 +12,7 @@ import type { CatalogTeamSkillRequirementType, } from "@paperclipai/teams-catalog"; import type { + CompanyPortabilityAdapterOverride, CompanyPortabilityAgentSelection, CompanyPortabilityCollisionStrategy, CompanyPortabilityFileEntry, @@ -649,6 +650,33 @@ async function readCatalogTeamSourceFiles(team: CatalogTeam): Promise { + const merged: Record = { ...(callerOverrides ?? {}) }; + for (const slug of agentSlugs) { + if (merged[slug]) continue; + merged[slug] = { adapterType: DEFAULT_SAFE_CATALOG_ADAPTER_TYPE }; + } + return merged; +} + function buildPortabilityInput( companyId: string, source: CompanyPortabilitySource, @@ -827,7 +855,10 @@ export function teamsCatalogService(db: Db) { ]; const importInput: CompanyPortabilityImport = { ...buildPortabilityInput(companyId, prepared.source, options), - adapterOverrides: options.adapterOverrides, + adapterOverrides: withSafeCatalogAdapterDefaults( + prepared.team.agentSlugs, + options.adapterOverrides, + ), secretValues: options.secretValues, }; const result = await portability.importBundle(