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 <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-05 03:20:50 +00:00
1 parent 4344ad6b1a
commit 6d40e43b8e
2 files changed
+97 -1

No files matched your search

@@ -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<string, { adapterType: string }>)
.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);
+32 -1
View File
@@ -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<Record<str
return files;
}
/**
* Default safe adapter for catalog agents imported through the agent-safe path.
*/
const DEFAULT_SAFE_CATALOG_ADAPTER_TYPE = "claude_local";
/**
* Bundled catalog agents declare no adapter in their `AGENTS.md` frontmatter, so
* `parsePortableAgentFrontmatter` defaults them to `process` — which the
* `agent_safe` importer rejects (see `IMPORT_FORBIDDEN_ADAPTER_TYPES` in
* `company-portability`). Inject a safe per-agent adapter default for every
* catalog agent the caller did not explicitly override so default-trust teams
* install without manual adapter flags. Explicit caller overrides win and are
* left untouched (both `adapterType` and `adapterConfig`). This is scoped to the
* catalog install path; the generic portability fallback is unchanged.
*/
function withSafeCatalogAdapterDefaults(
agentSlugs: string[],
callerOverrides: CompanyPortabilityImport["adapterOverrides"],
): Record<string, CompanyPortabilityAdapterOverride> {
const merged: Record<string, CompanyPortabilityAdapterOverride> = { ...(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(