paperclip/packages/plugins/sandbox-providers/daytona
Nicky Leach e31951a17d
feat: Claude agent setup-token login in a sandbox (#11286)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Claude agents that run in a remote sandbox need a safe in-product
login path
> - The existing host login route cannot open a pseudo-terminal inside
that sandbox
> - The login flow must protect the browser code, the login URL, and the
OAuth token at every step
> - This pull request adds the parser, the runner, a Daytona
pseudo-terminal transport, and a guarded, owner-bound session route
behind an injectable transport
> - The route stays inert in the default build and fails closed until a
sandbox provider binds the live transport
> - The benefit is a company-scoped setup-token flow with one-time
secret delivery, redaction, and fail-closed transport checks, ready for
a later staged production rollout

## Linked Issues or Issue Description

**Agent or provider**

Claude Code setup-token login for sandbox agents.

**Why this adapter is useful**

Sandbox agents need a supported way to sign in without host credentials.
An authorized owner completes the browser step and receives the token
one time.

**How the agent is invoked**

When a sandbox provider binds the injectable transport, the server
starts `claude setup-token` through a sandbox pseudo-terminal, sends the
browser code to the matched prompt, and returns the token through the
guarded session route. The default build does not bind the transport. In
that state the start route fails closed with a fixed no-secret `503`. It
does not start a process and it does not hold a sandbox lease.

**Additional context**

The transport is injectable, so each sandbox provider binds its own
pseudo-terminal. This pull request adds the Daytona transport but does
not bind it in the production server. A production wiring needs a lease
manager, a live pseudo-terminal factory, a durable token store, and its
own security review. The route keeps secrets out of logs, activity
details, errors, telemetry, and non-owner responses.

## What Changed

- Add strict parsers for the setup-token URL, the prompt, and the
success token.
- Add a login runner that drives the `claude setup-token` command
through a pseudo-terminal.
- Add the Daytona pseudo-terminal transport and the sandbox plugin
wiring.
- Add a company-scoped, owner-bound login session service with rate
limits, a reaper, cleanup, and one-time token delivery.
- Add the guarded session routes at
`/agents/:id/setup-token-login-sessions/*` behind an injectable
transport. The routes become the live login path only when a provider
binds the transport.
- Keep the start route fail-closed in the default build. It returns a
fixed no-secret `503` and it does not bind `setupTokenLogin`.
- Keep the existing host route `POST /agents/:id/claude-login` in place.
This pull request does not replace it.
- Keep confidential responses behind a fail-closed TLS transport guard
with `Cache-Control: no-store`, and extend redaction for the new fields.
- Export the parser and the runner from the Claude local server entry,
and document the new session routes in the OpenAPI spec.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run setup-token-route
setup-token-session`
- `pnpm --filter @paperclipai/adapter-claude-local exec vitest run`
- `pnpm --filter @paperclipai/server run typecheck`
- Confirm that the pull request checks pass on GitHub.

## Risks

- Low user-facing risk on merge. The default build does not bind the
transport, so the production start route stays fail-closed with a `503`.
The merge does not change the production login behavior.
- When a provider later binds the transport, the flow starts a live
sandbox process and holds a short-lived in-memory secret. Cleanup must
stop the child before it releases the sandbox lease.
- The transport guard fails closed when the deployment does not provide
a trusted TLS path. A wrong proxy allowlist can block a valid request.
- The production wiring is out of scope. It needs a lease manager, a
live pseudo-terminal factory, a durable token store, and its own
security review before the server binds `setupTokenLogin`.

## Model Used

Anthropic Claude Opus 4.8 assisted the implementation. It used extended
reasoning, code execution, repository tool use, and a 200,000-token
context window.

## 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 (the
OpenAPI spec covers the new session routes; no user-facing documentation
needs 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>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 13:02:49 -07:00
..
src feat: Claude agent setup-token login in a sandbox (#11286) 2026-08-12 13:02:49 -07:00
DIRECTORY-CONSTRAINT-FINDINGS.md feat(plugin-daytona): persistent session model with plain command dispatch (#10941) 2026-08-06 17:38:00 -07:00
README.md fix(daytona): replace bwrap user-namespace with su privilege drop (#10805) 2026-08-03 21:17:17 -07:00
package.json feat(sandbox-providers): pre-fill environment form with default sizing and image values (#11004) 2026-08-06 12:04:33 -07:00
tsconfig.json Add reusable sandbox custom images (#8794) 2026-06-30 09:57:49 -07:00
vitest.config.ts Add reusable sandbox custom images (#8794) 2026-06-30 09:57:49 -07:00

README.md

@paperclipai/plugin-daytona

Published Daytona sandbox provider plugin for Paperclip.

This package lives in the Paperclip monorepo, but it is intentionally excluded from the root pnpm workspace and shaped to publish and install like a standalone npm package. That lets operators install it from the Plugins page by package name without introducing root lockfile churn for Daytona's SDK dependencies.

Install

From a Paperclip instance, install:

@paperclipai/plugin-daytona

The host plugin installer runs npm install into the managed plugin directory, so transitive dependencies such as @daytonaio/sdk are pulled in during installation.

Configuration

Configure Daytona from Instance Settings -> Environments, not from the plugin's plugin page.

  • Put the Daytona API key on the sandbox environment itself.
  • When you save an environment, Paperclip stores pasted API keys as company secrets.
  • DAYTONA_API_KEY remains an optional host-level fallback when an environment omits the key.
  • Optional apiUrl and target settings map directly to the Daytona SDK/client configuration. If apiUrl is omitted, the Daytona SDK uses its default endpoint.

Notes:

  • The current published Daytona SDK package is @daytonaio/sdk.
  • The driver supports both snapshot-based and image-based sandbox creation. If both are set, validation rejects the config as ambiguous.
  • Reusable leases map to Daytona stop/start semantics. Non-reusable leases are deleted on release.

Advisory bwrap wrapper

The driver wraps a sandbox command with an advisory bubblewrap (bwrap) wrapper. The wrapper is advisory, best-effort, and automatic. At lease time the driver probes the sandbox for the wrapper capability and records the result on the lease metadata. At execute time the driver wraps the command when the capability is present. The command builder is a pure function.

  • The wrapper adds no security. The ephemeral sandbox stays the only security posture. The wrapper only gives an agent real-time feedback when the agent tries to change a file that the ephemeral sandbox will not keep.
  • The read-only root is a feedback signal. The wrapper binds the root as read-only (--ro-bind / /) and re-binds only the writable directories. A write to a path outside the writable set fails at once, so the agent learns the change is not durable.
  • A capability probe records the wrapper capability. No configuration field turns it on. At lease time the driver reads the sandbox username with id -un, then probes the end-to-end bwrap capability by running sudo -n bwrap with a workspace bind and an su user switch. It stores bwrapAvailable and sandboxUsername on the lease metadata.
  • The probe is best-effort. A missing bwrap binary, a missing passwordless sudo -n rule, a missing su binary, or an inaccessible workspace bind records bwrapAvailable: false and never fails the lease.
  • The writable set is the workspace plus the read-write sync destinations. The wrapper binds the workspace directory read-write as the baseline; the workspace is always durable. It adds the read-write sync destinations that a sync-in recorded for the same lease. The set deduplicates the directories. The baseline keeps a safe result even when the collected set is empty.
  • The wrapper runs at execute time when the capability is present. The driver wraps the command only when the lease reports bwrapAvailable: true and a username is known. It binds the workspace and the read-write sync destinations, keeps the root read-only for feedback, and re-binds the stdin file after the fresh /tmp. It runs the plain command when the capability or the username is missing. A wrap without a username would run as root and give the agent's files root ownership, so the driver keeps the plain command in that case.

Operator enablement (advisory bwrap)

The advisory bwrap wrapper needs three run-time prerequisites on the image or snapshot. The repository does not build the Daytona image or snapshot. It references an external image or snapshot. So the three prerequisites are image facts, not code facts. The runtime only probes for the capability and degrades when the capability is absent.

The wrapper is advisory, best-effort, and automatic. It adds no security. The ephemeral sandbox model stays the only security posture. A missing prerequisite degrades to the plain command. It never fails the lease. So the enablement below is optional. It gives the agent real-time feedback on a non-durable write. It does not change the security posture.

The install and the sudoers change are environment provisioning at the image or snapshot layer. Route them to DevOps through the board. Do not run the steps from the runtime and do not commit a provisioning script to the repository.

1. Install the bubblewrap package

The repository does not state the Daytona base distribution. Confirm the distribution on the referenced image or snapshot first, then run the matching command:

# Debian/Ubuntu
apt-get install -y bubblewrap
# Alpine
apk add bubblewrap
# Fedora/RHEL
dnf install -y bubblewrap

Confirm the binary path is /usr/bin/bwrap after the install.

2. Add the passwordless sudoers rule

The wrapper runs bwrap as root with sudo -n. Add this exact sudoers line. Use the real sandbox user name and the real bwrap path:

<sandbox-user> ALL=(root) NOPASSWD: /usr/bin/bwrap

The <sandbox-user> is the account name that id -un returns inside the sandbox. Use the account name, not a numeric id, in the sudoers line. The probe reads the username with id -un. The driver resolves the sandbox work directory first, then the user home directory. It uses /home/daytona only as a fallback default when both are empty. Confirm the real home directory for your image or snapshot. Install the sudo and util-linux (for su) packages in the image or snapshot if they are absent.

3. Verify the prerequisites

Run this exact command as the sandbox user, replacing <sandbox-user> and <workspace> with real values:

sudo -n bwrap --ro-bind / / --bind-try <workspace> <workspace> -- su -s /bin/sh <sandbox-user> -c true

A zero exit code means all prerequisites are met. A non-zero exit code means one prerequisite is missing. The wrapper then stays off and runs the plain command.

The probe binds the workspace directory and switches to the sandbox user with su. This matches the exact invocation the live wrapper uses, so a passing probe guarantees that execution commands will also succeed.

Local development

cd packages/plugins/sandbox-providers/daytona
pnpm install --ignore-workspace --no-lockfile
pnpm build
pnpm test
pnpm typecheck

These commands assume the repo root has already been installed once so the local @paperclipai/plugin-sdk workspace package is available to the compiler during development.

Package layout

  • src/manifest.ts declares the sandbox-provider driver metadata
  • src/plugin.ts implements the environment lifecycle hooks
  • paperclipPlugin.manifest and paperclipPlugin.worker point the host at the built plugin entrypoints in dist/