Files
PaperClipAI/docs/adapters/overview.md
T
Devin FoleyandPaperclip 724fa6144d fix(opencode-local): preserve the agent during sandbox tool OOMs (#17398)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Legacy OpenCode adapter runs an agent and its shell tools in the
same sandbox.
> - A large build can exceed the sandbox memory limit and trigger a
kernel OOM kill.
> - The kernel can kill OpenCode before its tool, which loses the active
turn.
> - OpenCode provides a shell environment hook that can change only the
tool subprocess.
> - This pull request uses that hook to prefer Bash tool processes as
OOM victims.
> - The benefit is that OpenCode can report a failed tool and continue
the task.

## Linked Issues or Issue Description

**What happened?**

An OpenCode sandbox run ended with exit 137 after a TypeScript process
had also been killed. Its cgroup limit was 4 GiB. It had completed
useful tool work and had not reached its execution deadline. The
original sandbox was deleted, so its historical OOM counters are
unavailable. Memory pressure is the leading explanation, not a proven
historical kernel diagnosis.

**Expected behavior**

When a shell tool exhausts sandbox memory, the kernel should prefer that
tool over OpenCode where possible. The adapter must preserve configured
shell startup, model, permissions and resource limits.

**Steps to reproduce**

Use a disposable 4 GiB Linux sandbox with OpenCode. Retain a 1,200 MiB
buffer in the agent and run a Bash tool with fourteen 250 MiB workers.
Compare the same workload before and after installing the generated
hook. Record the cgroup memory events, agent exit and final response.

Refs: #16844. That PR covers the same protection for Claude. This PR
adds the OpenCode-owned hook. Refs: #17254 for the separate shutdown
recovery path. This PR does not change retry policy.

## What Changed

- Generate a dependency-free OpenCode `shell.env` plugin and a Bash
startup file in the isolated runtime config.
- Stage those assets only for sandbox execution. Resolve the startup
file relative to the copied plugin.
- Set child `oom_score_adj` to `1000` on Linux. Keep the parent
environment unchanged and honor existing `BASH_ENV` values, including an
empty value.
- Keep command execution available if the optional plugin cannot be
installed or the process priority cannot be changed.
- Materialize linked plugin directories inside the isolated config
before writing. Keep the operator directory and its plugins unchanged.
- Cover relocation, shell output and exit status, operator settings,
local execution, remote staging and failed installation. Update the
adapter documentation and verification recipe.

## Verification

- Red before the fix: the new runtime regression failed because the
sandbox shell plugin was absent. The existing 15 runtime config tests
passed. The same regression passes with the fix.
- Apex first found a host-write risk through linked plugins directories
(4/5). A new regression reproduced the host directory modification, then
passed after the installer materialized the copied link. The finding is
resolved. Apex reviewed current head
`df82a8f51725a197c0ea3ebeb942488509f32b34` at 5/5 with no actionable
issues remaining.
- The full OpenCode adapter suite passes: 87 source tests. The adapter
typecheck and map checks pass.
- Real sandbox acceptance used the plugin generated from
`df82a8f51725a197c0ea3ebeb942488509f32b34` and OpenCode 1.18.34 in a
disposable 4 GiB Daytona sandbox. A controlled local model endpoint
issued an actual Bash tool call; no real model credentials were used.
The tool reported priority `1000`; the parent reported `0`.
- Controlled OOM comparison: without the guard, OpenCode was killed by
SIGKILL after 8.95 seconds and the cgroup recorded one OOM kill. With
the guard, five workers were killed, OpenCode exited `0` after 21.87
seconds, and its final response was `fixture-complete`. The cgroup
counter increased by five. Both attempts used the same allocation and
workload. The fixture started with a linked operator plugins directory.
The installer preserved that directory, staged a real plugins directory,
and passed both the process-priority probe and OOM comparison. The
fixture allocation was deleted afterward.
- The first live probe was not accepted because the harness could not
parse the provider response framing. After correcting the decoder, the
unchanged plugin passed the probe and the controlled OOM comparison.
Both initial disposable allocations were deleted. After the review fix,
a third allocation repeated the probe and OOM comparison with a linked
plugins directory. That allocation was also deleted. The initial
comparison had the same agent-killed/agent-survived result (8.42 seconds
without the guard; 19.42 seconds with it).
- Repository `pnpm -r typecheck` and `pnpm build` pass. The full CI
matrix passes on `df82a8f51725a197c0ea3ebeb942488509f32b34`, including
tests, browser shards, Runner checks, build, policy and Apex. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/40043947375).
- The first full local test attempt had 15 embedded PostgreSQL startup
failures. Direct `initdb --version` and `postgres --version` probes
showed missing bundled library symlinks and SIGABRT. Running the bundled
symlink hydration script restored both executables. All 15 affected
cases pass after that setup repair. No product code changed for it.
- Two existing workspace tests assumed that new Git repositories have no
`main` branch. They failed under the host default branch setting of
`main`, then passed with a per-command `init.defaultBranch=master`
setting. The global setting was preserved. The full local test command
was restarted with hydrated libraries and that controlled fixture
default. It had no reported assertion failures when it was stopped after
the complete CI matrix passed. It did not complete locally; full-suite
verification comes from CI. The first failed invocation was also
stopped.
- The original tenant task has not been retried or recovered by this PR.
Its board API credential is expired. The original hosted workflow and
the separate shutdown recovery remain outside this acceptance result.

## Risks

- This is a preference, not a memory reservation. It cannot guarantee
that OpenCode survives every OOM condition, including memory growth
inside OpenCode itself.
- The guard applies to non-interactive Bash tools. Other shells ignore
`BASH_ENV`. Existing operator startup settings take precedence.
- Older OpenCode releases without the shell environment hook will not
apply the preference. No model, permission, deadline, memory quota or
retry limit changes.
- No schema change or migration.

## Model Used

OpenAI GPT-6 through Codex, with repository tools and code execution.
The exact deployment variant and context window are not exposed by this
session.

## 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>
2026-10-23 17:49:00 -07:00

10 KiB

title, summary
title summary
Adapters Overview What adapters are and how they connect agents to Paperclip

Adapters are the bridge between Paperclip's orchestration layer and agent runtimes. Each adapter knows how to invoke a specific type of AI agent and capture its results.

How Adapters Work

When a heartbeat fires, Paperclip:

  1. Looks up the agent's adapterType and adapterConfig
  2. Calls the adapter's execute() function with the execution context
  3. The adapter spawns or calls the agent runtime
  4. The adapter captures stdout, parses usage/cost data, and returns a structured result

Built-in Adapters

Adapter Type Key Description
Claude Code claude_local Runs Claude Code CLI locally, with a native ACP engine when available
Codex codex_local Runs OpenAI Codex CLI locally, with a native ACP engine when available
Gemini CLI gemini_local Runs Gemini CLI locally (experimental — adapter package exists, not yet in stable type enum)
Kimi Code CLI kimi_local Runs Kimi Code CLI locally through ACP, with explicitly selectable headless -p mode
OpenCode opencode_local Runs OpenCode CLI locally (multi-provider provider/model)
Cursor cursor Runs Cursor in background mode
Pi pi_local Runs an embedded Pi agent locally
Hermes hermes_local Runs the local Hermes CLI through @paperclipai/hermes-paperclip-adapter
Hermes Gateway hermes_gateway Calls an already-running Hermes API server through @paperclipai/hermes-paperclip-adapter/gateway
OpenClaw Gateway openclaw_gateway Connects to an OpenClaw gateway endpoint
Process process Executes arbitrary shell commands
HTTP http Sends webhooks to external agents

Credential ownership for sandbox targets

Local CLI adapters can run on the Paperclip host, SSH targets, or managed sandbox targets. The adapter decides which credential home is authoritative before the CLI starts:

Adapter Credential topology Which credential file wins on managed sandbox targets
codex_local Host-owns-auth for Paperclip-managed CODEX_HOME A host-owned auth.json is symlinked into the managed CODEX_HOME and uploaded to the sandbox. If a per-agent OPENAI_API_KEY is configured, Paperclip writes an API-key auth.json instead and that file wins. A login baked into the sandbox image is shadowed because Codex runs with Paperclip's uploaded CODEX_HOME.
claude_local Snapshot-owns-auth for managed remote Claude config A configured ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN (agent or environment env) wins over any stored login. Otherwise Paperclip uploads only sanitized settings and skill/runtime assets, and when the remote managed config has no Claude credential files it copies .credentials.json or credentials.json from the sandbox image's own $HOME/.claude, so the image's login wins.

Worked examples:

  • Codex sandbox with host ChatGPT login: the host ~/.codex/auth.json is symlinked into the managed home, then uploaded as the sandbox CODEX_HOME. Codex reads that uploaded file and does not use any auth.json already present inside the sandbox image.
  • Claude sandbox with image login: Paperclip materializes a remote CLAUDE_CONFIG_DIR, then fills missing .credentials.json / credentials.json from the sandbox image's own $HOME/.claude. The snapshot's Claude login is the credential source for the run.

OpenCode sandbox memory pressure

On managed Linux sandboxes, the Legacy OpenCode adapter adds a local plugin through OpenCode's shell.env hook. The hook supplies a Bash startup file only to shell subprocesses. It sets their Linux oom_score_adj to 1000, so the kernel prefers to kill memory-heavy tool commands before OpenCode when the sandbox runs out of memory.

An existing BASH_ENV takes precedence. Local and SSH execution keep their existing behavior. The guard does not increase the sandbox memory limit, apply to other shells, or prevent OpenCode itself from exhausting memory. If the runtime cannot set the process preference, the command still runs.

Hermes local vs gateway

Use hermes_local when Paperclip should start the local hermes CLI on the same host for each heartbeat. Use hermes_gateway when Hermes is already running as an HTTP/SSE API server and Paperclip should call that server instead of spawning a process. Both type keys are stable built-ins.

The unified Hermes package owns both built-in adapters. The older @paperclipai/adapter-hermes-gateway package remains only as a deprecated compatibility shim that re-exports the gateway entrypoints for one release. New plugin overrides should target @paperclipai/hermes-paperclip-adapter and set the desired type key (hermes_local or hermes_gateway).

External (plugin) adapters

These adapters ship as standalone npm packages and are installed via the plugin system:

Adapter Package Type Key Description
Droid @henkey/droid-paperclip-adapter droid_local Runs Factory Droid locally

External Adapters

You can build and distribute adapters as standalone packages — no changes to Paperclip's source code required. External adapters are loaded at startup via the plugin system.

# Install from npm via API
curl -X POST http://localhost:3102/api/adapters \
  -d '{"packageName": "my-paperclip-adapter"}'

# Or link from a local directory
curl -X POST http://localhost:3102/api/adapters \
  -d '{"localPath": "/home/user/my-adapter"}'

See External Adapters for the full guide.

Adapter Architecture

Each adapter is a package with modules consumed by three registries:

my-adapter/
  src/
    index.ts            # Shared metadata (type, label, models)
    server/
      execute.ts        # Core execution logic
      parse.ts          # Output parsing
      test.ts           # Environment diagnostics
    ui-parser.ts        # Self-contained UI transcript parser (for external adapters)
    cli/
      format-event.ts   # Terminal output for `paperclipai run --watch`
Registry What it does Source
Server Executes agents, captures results createServerAdapter() from package root
UI Renders run transcripts, provides config forms ui-parser.js (dynamic) or static import (built-in)
CLI Formats terminal output for live watching Static import

Choosing an Adapter

  • Need a coding agent? Use claude_local, codex_local, opencode_local, hermes_local, or install droid_local as an external plugin
  • Need the richest live run feedback? Use claude_local, codex_local, or gemini_local with adapterConfig.engine set to acp when the execution environment satisfies the ACP prerequisites — see Feedback granularity
  • Need Hermes on another host or already running as a service? Use hermes_gateway
  • Need to run a script or command? Use process
  • Need to call a custom external service? Use http
  • Need something custom? Create your own adapter or build an external adapter plugin

Feedback Granularity

Adapter choice determines how much structured, live detail a run's transcript can show while the agent is still working. Every adapter's stdout is streamed to the run log and rendered live in the UI — including runs on sandbox execution targets, whose logs are tailed and delivered incrementally — but the granularity of what you see depends on the event stream the adapter emits.

Rough tiers, richest first:

  1. Native ACP engine (claude_local, codex_local, or gemini_local with engine: "acp") — full structured event stream. ACP emits a JSONL event per meaningful runtime moment: acpx.session (agent, mode, session identity), acpx.status (progress text plus context-window usage), acpx.text_delta (assistant/thinking token deltas), acpx.tool_call (tool title, call id, and status updates as the call progresses), acpx.result (stop reason summary), and acpx.error (code, message, retryability). The transcript renders these as live-updating message, thinking, tool, and status blocks, and repeated acpx.tool_call status updates fold into a single tool card instead of stacking duplicates.
  2. CLI wrappers (claude_local, codex_local, cursor, opencode_local, …). These parse each CLI's own streaming JSON output. You get assistant text, tool calls/results, and a final usage/cost summary, but granularity is limited to what the CLI prints — some emit tool progress, others only call/finish pairs.
  3. Generic adapters (process, http). Plain stdout/stderr lines with no structured transcript — you see raw output only.

Recommendation: use the native ACP engine on claude_local, codex_local, or gemini_local when the selected execution environment supports it. Rich ACP status events (including context usage) and incremental tool-call updates give the closest thing to watching the agent work locally.

UI Parser Contract

External adapters can ship a self-contained UI parser that tells the Paperclip web UI how to render their stdout. Without it, the UI uses a generic shell parser. See the UI Parser Contract for details.

Cursor failure details

The Cursor CLI adapter uses structured error output first, then the first nonempty diagnostic line. It skips the informational cursor-retrieval: tracing to ... file-location notice. If the CLI exits unsuccessfully with only that notice, the run shows the exit code. The original stdout and stderr remain in the local run result and log for troubleshooting. Environment probes use the same diagnostic selection. This does not change retry or success decisions.