Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 85 additions & 3 deletions cli/src/__tests__/company.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ describe("import selection catalog", () => {
});

describe("default adapter overrides", () => {
it("maps process-only imported agents to claude_local", () => {
it("returns no overrides unless a default adapter is provided", () => {
const preview: CompanyPortabilityPreviewResult = {
include: {
company: false,
Expand Down Expand Up @@ -578,9 +578,91 @@ describe("default adapter overrides", () => {
errors: [],
};

expect(buildDefaultImportAdapterOverrides(preview)).toEqual({
expect(buildDefaultImportAdapterOverrides(preview)).toBeUndefined();
});

it("maps process-only imported agents to the provided default adapter", () => {
const preview: CompanyPortabilityPreviewResult = {
include: {
company: false,
agents: true,
projects: false,
issues: false,
skills: false,
},
targetCompanyId: null,
targetCompanyName: null,
collisionStrategy: "rename",
selectedAgentSlugs: ["legacy-agent", "explicit-agent"],
plan: {
companyAction: "none",
agentPlans: [],
projectPlans: [],
issuePlans: [],
},
manifest: {
schemaVersion: 1,
generatedAt: "2026-03-23T18:20:00.000Z",
source: null,
includes: {
company: false,
agents: true,
projects: false,
issues: false,
skills: false,
},
company: null,
sidebar: null,
agents: [
{
slug: "legacy-agent",
name: "Legacy Agent",
path: "agents/legacy-agent/AGENT.md",
skills: [],
role: "agent",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
{
slug: "explicit-agent",
name: "Explicit Agent",
path: "agents/explicit-agent/AGENT.md",
skills: [],
role: "agent",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
],
skills: [],
projects: [],
issues: [],
envInputs: [],
},
files: {},
envInputs: [],
warnings: [],
errors: [],
};

expect(buildDefaultImportAdapterOverrides(preview, "codex_local")).toEqual({
"legacy-agent": {
adapterType: "claude_local",
adapterType: "codex_local",
},
});
});
Expand Down
8 changes: 6 additions & 2 deletions cli/src/commands/client/company.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,12 @@ export function buildSelectedFilesFromImportSelection(

export function buildDefaultImportAdapterOverrides(
preview: Pick<CompanyPortabilityPreviewResult, "manifest" | "selectedAgentSlugs">,
defaultAdapterType?: string | null,
): Record<string, { adapterType: string }> | undefined {
const normalizedDefaultAdapterType = defaultAdapterType?.trim();
if (!normalizedDefaultAdapterType) {
return undefined;
}
const selectedAgentSlugs = new Set(preview.selectedAgentSlugs);
const overrides = Object.fromEntries(
preview.manifest.agents
Expand All @@ -359,8 +364,7 @@ export function buildDefaultImportAdapterOverrides(
.map((agent) => [
agent.slug,
{
// TODO: replace this temporary claude_local fallback with adapter selection in the import TUI.
adapterType: "claude_local",
adapterType: normalizedDefaultAdapterType,
},
]),
);
Expand Down
2 changes: 2 additions & 0 deletions doc/spec/agent-runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ Runs local `codex` CLI directly.
}
```

If `dangerouslyBypassApprovalsAndSandbox` is omitted for `codex_local`, Paperclip currently defaults it to `true` for normal create/update flows and company imports so imported Codex agents behave like manually created ones.

### Invocation

- Base command: `codex exec --json <prompt>`
Expand Down
7 changes: 7 additions & 0 deletions docs/guides/board-operator/importing-and-exporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ my-company/
- **AGENT.md** files contain agent identity, role, and instructions.
- **SKILL.md** files are compatible with the Agent Skills ecosystem.
- **.paperclip.yaml** holds Paperclip-specific config (adapter types, env inputs, budgets) as an optional sidecar.
- Packages without `.paperclip.yaml` stay vendor-neutral. Their agents import as `process` agents until Paperclip resolves them to a concrete adapter during import.

## Exporting a Company

Expand Down Expand Up @@ -114,6 +115,10 @@ paperclipai company import org/repo/companies/acme

If `--target` is not specified, Paperclip infers it: if a `--company-id` is provided (or one exists in context), it defaults to `existing`; otherwise `new`.

For `--target new`, vendor-neutral packages without explicit adapter metadata use the instance default adapter from **Instance Settings → General**.

On a brand-new instance, Paperclip also seeds that instance default from the first agent ever created unless an operator explicitly chose a default adapter first.

### Collision Strategies

When importing into an existing company, agent or project names may conflict with existing ones:
Expand Down Expand Up @@ -142,6 +147,8 @@ The preview shows:

Imported agents always land with timer heartbeats disabled. Assignment/on-demand wake behavior from the package is preserved, but scheduled runs stay off until a board operator re-enables them.

When previewing a new-company import in the UI, vendor-neutral `process` agents are prefilled with the instance default adapter so operators can review or override that choice before applying the import.

### Common Workflows

**Clone a company template from GitHub:**
Expand Down
2 changes: 1 addition & 1 deletion packages/adapters/codex-local/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Core fields:
- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high) passed via -c model_reasoning_effort=...
- promptTemplate (string, optional): run prompt template
- search (boolean, optional): run codex with --search
- dangerouslyBypassApprovalsAndSandbox (boolean, optional): run with bypass flag
- dangerouslyBypassApprovalsAndSandbox (boolean, optional): run with bypass flag; defaults to true when omitted in Paperclip-managed agent create/update/import flows
- command (string, optional): defaults to "codex"
- extraArgs (string[], optional): additional CLI args
- env (object, optional): KEY=VALUE environment variables
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/types/instance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { AgentAdapterType } from "../constants.js";

export interface InstanceGeneralSettings {
censorUsernameInLogs: boolean;
defaultAdapterType: AgentAdapterType;
}

export interface InstanceExperimentalSettings {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/validators/instance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { z } from "zod";
import { AGENT_ADAPTER_TYPES } from "../constants.js";

export const instanceGeneralSettingsSchema = z.object({
censorUsernameInLogs: z.boolean().default(false),
defaultAdapterType: z.enum(AGENT_ADAPTER_TYPES).default("claude_local"),
}).strict();

export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema.partial();
Expand Down
118 changes: 118 additions & 0 deletions server/src/__tests__/agents-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { agentService } from "../services/agents.ts";

const mockInstanceSettingsService = vi.hoisted(() => ({
seedDefaultAdapterType: vi.fn(),
}));

vi.mock("../services/instance-settings.js", () => ({
instanceSettingsService: () => mockInstanceSettingsService,
}));

function buildCreatedAgent(adapterType: string) {
return {
id: "agent-1",
companyId: "company-1",
name: "CEO",
role: "ceo",
title: null,
reportsTo: null,
capabilities: null,
adapterType,
adapterConfig: {},
runtimeConfig: {},
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
metadata: null,
permissions: null,
status: "idle",
pauseReason: null,
pausedAt: null,
lastHeartbeatAt: null,
createdAt: new Date("2026-01-01T00:00:00.000Z"),
updatedAt: new Date("2026-01-01T00:00:00.000Z"),
};
}

function createAgentCreateDb(options: {
existingAgents?: Array<{ id: string; name: string; status: string }>;
totalAgentCountAfterCreate: number;
createdAgent: ReturnType<typeof buildCreatedAgent>;
}) {
let selectCallCount = 0;

return {
select: vi.fn(() => {
selectCallCount += 1;
if (selectCallCount === 1) {
return {
from: () => ({
where: () => Promise.resolve(options.existingAgents ?? []),
}),
};
}
return {
from: () => Promise.resolve([{ count: options.totalAgentCountAfterCreate }]),
};
}),
insert: vi.fn(() => ({
values: vi.fn(() => ({
returning: vi.fn(async () => [options.createdAgent]),
})),
})),
};
}

describe("agent service", () => {
beforeEach(() => {
vi.clearAllMocks();
mockInstanceSettingsService.seedDefaultAdapterType.mockResolvedValue(undefined);
});

it("seeds the instance default adapter after the first agent is created", async () => {
const createdAgent = buildCreatedAgent("codex_local");
const dbStub = createAgentCreateDb({
totalAgentCountAfterCreate: 1,
createdAgent,
});

const svc = agentService(dbStub as any);
await svc.create("company-1", {
name: "CEO",
role: "ceo",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
status: "idle",
lastHeartbeatAt: null,
});

expect(mockInstanceSettingsService.seedDefaultAdapterType).toHaveBeenCalledWith("codex_local");
});

it("does not reseed the instance default adapter after later agent creations", async () => {
const createdAgent = buildCreatedAgent("codex_local");
const dbStub = createAgentCreateDb({
existingAgents: [{ id: "agent-existing", name: "Existing", status: "idle" }],
totalAgentCountAfterCreate: 2,
createdAgent,
});

const svc = agentService(dbStub as any);
await svc.create("company-1", {
name: "CEO",
role: "ceo",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
status: "idle",
lastHeartbeatAt: null,
});

expect(mockInstanceSettingsService.seedDefaultAdapterType).not.toHaveBeenCalled();
});
});
Loading
Loading