mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
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:
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);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user