mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 21:03:51 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Adapter names are part of the board-facing agent setup and management experience. > - The product now treats adapters as harnesses, while execution environments are modeled separately. > - Several built-in adapter labels still carried legacy local wording from the older harness-by-environment model. > - That wording makes the UI noisier and implies a distinction users no longer need to reason about. > - This pull request normalizes adapter display labels while keeping persisted adapter type identifiers unchanged. > - The benefit is clearer adapter selection and management copy without a database migration. ## Linked Issues or Issue Description No public GitHub issue was found for this exact cleanup. Related public PRs: - Supersedes #8910, an earlier branch for the same cleanup that did not include the later docs/gateway/Cursor alignment. - Refs #8819, which is related display-registry work for external multi-segment adapter labels, but not a duplicate of this built-in label cleanup. Feature request details: - Subsystem affected: Cross-cutting (`ui/`, `packages/adapters`, and docs). - Problem or motivation: user-facing adapter names include legacy local qualifiers even though adapters map to harnesses and environments are first-class elsewhere. - Proposed solution: remove the legacy local wording from built-in display labels, keep machine-readable adapter type ids unchanged, and keep gateway disambiguation where it is useful. - Alternatives considered: changing persisted adapter type ids was ruled out because it would create migration and compatibility risk; one-off UI replacements were ruled out because the display registry is already the correct central label boundary. - Roadmap alignment: this is small adapter UX polish, not a new roadmap-level core feature. ## What Changed - Updated the adapter display registry so known adapter labels are final and no built-in local adapter renders a legacy local suffix. - Preserved clean derived labels for unknown plugin local types while keeping gateway disambiguation for unknown gateway types. - Updated `AdapterManager` to prefer registry labels when the server reports raw adapter type ids for built-ins. - Removed legacy local wording from built-in adapter metadata labels in UI and adapter packages. - Aligned Cursor adapter metadata with the central display registry label. - Updated adapter docs and Storybook fixtures to match the new display names. - Added focused registry coverage for built-in labels and unknown plugin suffix behavior. ## Verification - `pnpm check:tokens` - `git diff --check origin/master...fix/adapter-display-labels` - Patch-addition scan for added secrets, private paths, and internal links: no matches. - GitHub duplicate search for open adapter-label/local-suffix issues and PRs; #8910 was identified as the older superseded public PR. - `pnpm exec vitest run ui/src/adapters/adapter-display-registry.test.ts` - `pnpm --filter @paperclipai/ui typecheck` - Stale-label scan found no remaining user-facing display-label suffixes; remaining local wording is operational/test terminology such as adapter ids, docs about running locally, and test descriptions. ## Risks Low risk. The change is display-label and documentation focused, and adapter type ids remain unchanged. The main risk is ambiguous gateway naming, mitigated by keeping explicit gateway labels where variants need disambiguation. ## Model Used OpenAI GPT-5 via Codex, tool-enabled coding agent in a local repository workspace. Context window size is not exposed by this environment. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
282 lines
12 KiB
Markdown
282 lines
12 KiB
Markdown
---
|
|
title: Creating an Adapter
|
|
summary: Guide to building a custom adapter
|
|
---
|
|
|
|
Build a custom adapter to connect Paperclip to any agent runtime.
|
|
|
|
<Tip>
|
|
If you're using Claude Code, the `.agents/skills/create-agent-adapter` skill can guide you through the full adapter creation process interactively. Just ask Claude to create a new adapter and it will walk you through each step.
|
|
</Tip>
|
|
|
|
## Two Paths
|
|
|
|
| | Built-in | External Plugin |
|
|
|---|---|---|
|
|
| Source | Inside `paperclip-fork` | Separate npm package |
|
|
| Distribution | Ships with Paperclip | Independent npm publish |
|
|
| UI parser | Static import | Dynamic load from API |
|
|
| Registration | Edit 3 registries | Auto-loaded at startup |
|
|
| Best for | Core adapters, contributors | Third-party adapters, internal tools |
|
|
|
|
For most cases, **build an external adapter plugin**. It's cleaner, independently versioned, and doesn't require modifying Paperclip's source. See [External Adapters](/adapters/external-adapters) for the full guide.
|
|
|
|
The rest of this page covers the shared internals that both paths use.
|
|
|
|
## Package Structure
|
|
|
|
```
|
|
packages/adapters/<name>/ # built-in
|
|
── or ──
|
|
my-adapter/ # external plugin
|
|
package.json
|
|
tsconfig.json
|
|
src/
|
|
index.ts # Shared metadata
|
|
server/
|
|
index.ts # Server exports (createServerAdapter)
|
|
execute.ts # Core execution logic
|
|
parse.ts # Output parsing
|
|
test.ts # Environment diagnostics
|
|
ui/
|
|
index.ts # UI exports (built-in only)
|
|
parse-stdout.ts # Transcript parser (built-in only)
|
|
build-config.ts # Config builder
|
|
ui-parser.ts # Self-contained UI parser (external — see [UI Parser Contract](/adapters/adapter-ui-parser))
|
|
cli/
|
|
index.ts # CLI exports
|
|
format-event.ts # Terminal formatter
|
|
```
|
|
|
|
## Step 1: Root Metadata
|
|
|
|
`src/index.ts` is imported by all three consumers. Keep it dependency-free.
|
|
|
|
```ts
|
|
export const type = "my_agent"; // snake_case, globally unique
|
|
export const label = "My Agent";
|
|
export const models = [
|
|
{ id: "model-a", label: "Model A" },
|
|
];
|
|
export const agentConfigurationDoc = `# my_agent configuration
|
|
Use when: ...
|
|
Don't use when: ...
|
|
Core fields: ...
|
|
`;
|
|
|
|
// Required for external adapters (plugin-loader convention)
|
|
export { createServerAdapter } from "./server/index.js";
|
|
```
|
|
|
|
## Step 2: Server Execute
|
|
|
|
`src/server/execute.ts` is the core. It receives an `AdapterExecutionContext` and returns an `AdapterExecutionResult`.
|
|
|
|
Key responsibilities:
|
|
|
|
1. Read config using safe helpers (`asString`, `asNumber`, etc.) from `@paperclipai/adapter-utils/server-utils`
|
|
2. Build environment with `buildPaperclipEnv(agent)` plus context vars
|
|
3. Resolve session state from `runtime.sessionParams`
|
|
4. Render prompt with `renderTemplate(template, data)`
|
|
5. Spawn the process with `runChildProcess()` or call via `fetch()`
|
|
6. Parse output for usage, costs, session state, errors
|
|
7. Handle unknown session errors (retry fresh, set `clearSession: true`)
|
|
|
|
### Available Helpers
|
|
|
|
| Helper | Source | Purpose |
|
|
|--------|--------|---------|
|
|
| `runChildProcess(cmd, opts)` | `@paperclipai/adapter-utils/server-utils` | Spawn with timeout, grace, streaming |
|
|
| `buildPaperclipEnv(agent)` | `@paperclipai/adapter-utils/server-utils` | Inject `PAPERCLIP_*` env vars |
|
|
| `renderTemplate(tpl, data)` | `@paperclipai/adapter-utils/server-utils` | `{{variable}}` substitution |
|
|
| `asString(v)` | `@paperclipai/adapter-utils` | Safe config value extraction |
|
|
| `asNumber(v)` | `@paperclipai/adapter-utils` | Safe number extraction |
|
|
|
|
### AdapterExecutionContext
|
|
|
|
```ts
|
|
interface AdapterExecutionContext {
|
|
runId: string;
|
|
agent: { id: string; companyId: string; name: string; adapterConfig: unknown };
|
|
runtime: { sessionId: string | null; sessionParams: Record<string, unknown> | null };
|
|
config: Record<string, unknown>; // agent's adapterConfig
|
|
context: Record<string, unknown>; // task, wake reason, etc.
|
|
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
|
onMeta?: (meta: AdapterInvocationMeta) => Promise<void>;
|
|
onSpawn?: (meta: { pid: number; startedAt: string }) => Promise<void>;
|
|
}
|
|
```
|
|
|
|
### AdapterExecutionResult
|
|
|
|
```ts
|
|
interface AdapterExecutionResult {
|
|
exitCode: number | null;
|
|
signal: string | null;
|
|
timedOut: boolean;
|
|
errorMessage?: string | null;
|
|
usage?: { inputTokens: number; outputTokens: number };
|
|
sessionParams?: Record<string, unknown> | null; // persist across heartbeats
|
|
sessionDisplayId?: string | null;
|
|
provider?: string | null;
|
|
model?: string | null;
|
|
costUsd?: number | null;
|
|
clearSession?: boolean; // set true to force fresh session on next wake
|
|
}
|
|
```
|
|
|
|
## Step 3: Environment Test
|
|
|
|
`src/server/test.ts` validates the adapter config before running.
|
|
|
|
Return structured diagnostics:
|
|
|
|
| Level | Meaning | Effect |
|
|
|-------|---------|--------|
|
|
| `error` | Invalid or unusable setup | Blocks execution |
|
|
| `warn` | Non-blocking issue | Shown with yellow indicator |
|
|
| `info` | Successful check | Shown in test results |
|
|
|
|
```ts
|
|
export async function testEnvironment(
|
|
ctx: AdapterEnvironmentTestContext,
|
|
): Promise<AdapterEnvironmentTestResult> {
|
|
return {
|
|
adapterType: ctx.adapterType,
|
|
status: "pass", // "pass" | "warn" | "fail"
|
|
checks: [
|
|
{ level: "info", message: "CLI v1.2.0 detected", code: "cli_detected" },
|
|
{ level: "warn", message: "No API key found", hint: "Set ANTHROPIC_API_KEY", code: "no_key" },
|
|
],
|
|
testedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
```
|
|
|
|
## Step 4: UI Module (Built-in Only)
|
|
|
|
For built-in adapters registered in Paperclip's source:
|
|
|
|
- `parse-stdout.ts` — converts stdout lines to `TranscriptEntry[]` for the run viewer
|
|
- `build-config.ts` — converts form values to `adapterConfig` JSON
|
|
- Config fields React component in `ui/src/adapters/<name>/config-fields.tsx`
|
|
|
|
For external adapters, use a self-contained `ui-parser.ts` instead. See the [UI Parser Contract](/adapters/adapter-ui-parser).
|
|
|
|
## Step 5: CLI Module
|
|
|
|
`format-event.ts` — pretty-prints stdout for `paperclipai run --watch` using `picocolors`.
|
|
|
|
```ts
|
|
export function formatStdoutEvent(line: string, debug: boolean): void {
|
|
if (line.startsWith("[tool-done]")) {
|
|
console.log(chalk.green(` ✓ ${line}`));
|
|
} else {
|
|
console.log(` ${line}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Step 6: Register (Built-in Only)
|
|
|
|
Add the adapter to all three registries:
|
|
|
|
1. `server/src/adapters/registry.ts`
|
|
2. `ui/src/adapters/registry.ts`
|
|
3. `cli/src/adapters/registry.ts`
|
|
|
|
For external adapters, registration is automatic — the plugin loader handles it.
|
|
|
|
## Session Persistence
|
|
|
|
If your agent runtime supports conversation continuity across heartbeats:
|
|
|
|
1. Return `sessionParams` from `execute()` (e.g., `{ sessionId: "abc123" }`)
|
|
2. Read `runtime.sessionParams` on the next wake to resume
|
|
3. Optionally implement a `sessionCodec` for validation and display
|
|
|
|
```ts
|
|
export const sessionCodec: AdapterSessionCodec = {
|
|
deserialize(raw) { /* validate raw session data */ },
|
|
serialize(params) { /* serialize for storage */ },
|
|
getDisplayId(params) { /* human-readable session label */ },
|
|
};
|
|
```
|
|
|
|
## Capability Flags
|
|
|
|
Adapters can declare what "local" capabilities they support by setting optional fields on the `ServerAdapterModule`. The server and UI use these flags to decide which features to enable for agents using the adapter (instructions bundle editor, skills sync, JWT auth, etc.).
|
|
|
|
| Flag | Type | Default | What it controls |
|
|
|------|------|---------|------------------|
|
|
| `supportsLocalAgentJwt` | `boolean` | `false` | Whether heartbeat generates a local JWT for the agent |
|
|
| `supportsInstructionsBundle` | `boolean` | `false` | Managed instructions bundle (AGENTS.md) — server-side resolution + UI editor |
|
|
| `instructionsPathKey` | `string` | `"instructionsFilePath"` | The `adapterConfig` key that holds the instructions file path |
|
|
| `requiresMaterializedRuntimeSkills` | `boolean` | `false` | Whether runtime skill entries must be written to disk before execution |
|
|
|
|
These flags are exposed via `GET /api/adapters` in a `capabilities` object, along with a derived `supportsSkills` flag (true when `listSkills` or `syncSkills` is defined).
|
|
|
|
### Example
|
|
|
|
```ts
|
|
export function createServerAdapter(): ServerAdapterModule {
|
|
return {
|
|
type: "my_k8s_adapter",
|
|
execute: myExecute,
|
|
testEnvironment: myTestEnvironment,
|
|
listSkills: myListSkills,
|
|
syncSkills: mySyncSkills,
|
|
|
|
// Capability flags
|
|
supportsLocalAgentJwt: true,
|
|
supportsInstructionsBundle: true,
|
|
instructionsPathKey: "instructionsFilePath",
|
|
requiresMaterializedRuntimeSkills: true,
|
|
};
|
|
}
|
|
```
|
|
|
|
With these flags set, the Paperclip UI will automatically show the instructions bundle editor, skills management tab, and working directory field for agents using this adapter — no Paperclip source changes required.
|
|
|
|
If capability flags are not set, the server falls back to legacy hardcoded lists for built-in adapter types. External adapters that omit the flags will default to `false` for all capabilities.
|
|
|
|
## Skills Injection
|
|
|
|
Make Paperclip skills discoverable to your agent runtime without writing to the agent's working directory:
|
|
|
|
1. **Best: tmpdir + flag** — create tmpdir, symlink skills, pass via CLI flag, clean up after
|
|
2. **Acceptable: global config dir** — symlink to the runtime's global plugins directory
|
|
3. **Acceptable: env var** — point a skills path env var at the repo's `skills/` directory
|
|
4. **Last resort: prompt injection** — include skill content in the prompt template
|
|
|
|
## Cross-run workspace persistence (no-remote-git contract)
|
|
|
|
The local execution-workspace cwd is the **only** persistence boundary across runs. No adapter may depend on a git remote for cross-run state.
|
|
|
|
The supported round-trip:
|
|
|
|
- **Per-run, on the remote side.** `prepareWorkspaceForSshExecution` (in `packages/adapter-utils/src/ssh.ts`) git-bundles the local worktree and ships it to the run's remote dir. No `git remote` is set anywhere; the bundle is the transport.
|
|
- **End-of-run, in the adapter's `finally` block.** The adapter invokes `restoreRemoteWorkspace` (e.g. claude-local's `execute.ts`), which calls `restoreWorkspaceFromSshExecution` → `exportGitWorkspaceFromSsh` → `integrateImportedGitHead`. Remote commits made during the run land back in the local Mac worktree with no `git push` and no remote configured.
|
|
|
|
The invariant adapters must preserve:
|
|
|
|
- **Never `git push`** from adapter or runtime code. Operator-supplied configuration may opt in, but the default contract is no remote operations.
|
|
- **Never assume a remote exists.** The local cwd is the source of truth between runs.
|
|
- **Surface restore failures.** A failed sync-back must propagate as a run-level error, not a silent warning. The heartbeat records a `workspace_finalize` row (`succeeded`/`failed`) around `adapter.execute` so dependent issues do not wake on a stale worktree.
|
|
|
|
The invariant is pinned by the "no-remote-git contract" case in `packages/adapter-utils/src/ssh-fixture.test.ts`: it asserts `git remote` is empty before and after the round-trip and that a remote-only commit still lands locally via restore alone.
|
|
|
|
## Security
|
|
|
|
- Treat agent output as untrusted (parse defensively, never execute)
|
|
- Inject secrets via environment variables, not prompts
|
|
- Configure network access controls if the runtime supports them
|
|
- Always enforce timeout and grace period
|
|
- The UI parser module runs in a browser sandbox — zero runtime imports, no side effects
|
|
|
|
## Next Steps
|
|
|
|
- [External Adapters](/adapters/external-adapters) — build a standalone adapter plugin
|
|
- [UI Parser Contract](/adapters/adapter-ui-parser) — ship a custom run-log parser
|
|
- [How Agents Work](/guides/agent-developer/how-agents-work) — the heartbeat lifecycle
|