Files
PaperClipAI/doc/architecture/paperclip-runner-daytona-networking.md
Dotta 0bdbf61564 feat(runner): add secure remote transport (#12639)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip Runner gives native runs a durable and governed execution
path.
> - The lower stack PR adds authenticated remote execution targets and
provider ingress.
> - The Rust daemon currently accepts only loopback plaintext WebSocket
connections.
> - Remote Codex needs authenticated WSS dialing and provider-ingress
listener mode.
> - This pull request adds the bounded Rust transport contract.
> - The benefit is a secure transport layer for the Codex remote
vertical slice.

## Linked Issues or Issue Description

Refs #12638.
Refs #12616.
Refs #12352.

**Subsystem affected**

Paperclip Runner Rust transport and remote runner networking.

**Problem or motivation**

The runner daemon cannot connect to a public control plane with TLS. It
also cannot accept a provider preview connection on the run-bound
ingress path.

**Proposed solution**

Add WSS with native trust roots and an optional private CA bundle. Add a
fixed authenticated listener mode for provider ingress. Advertise the
exact transport contract through build metadata.

**Alternatives considered**

Plaintext public WebSocket connections would weaken the transport
boundary. A general listener would expose more network surface than the
run-bound provider ingress requires.

**Roadmap alignment**

This work supports the Cloud and Sandbox agents milestone. It also
supports self-healing native runs.

## Stack

- Lower merged PR: #12638.
- This PR contains only its 13-file delta against `master`.
- Later stack PRs add the task workspace and administrator UI.

## What Changed

- Added WSS dialing with rustls and native certificate roots.
- Added an optional bounded private CA bundle that augments native
roots.
- Kept plaintext WebSocket dialing restricted to loopback addresses.
- Pinned resolved dial addresses for the process lifetime.
- Added a fixed `0.0.0.0:43127` listener with an exact run-bound path.
- Rejected listener queries, ambiguous paths, and WebSocket extensions.
- Kept frame and message size bounds.
- Added bounded reconnect grace and exponential jitter.
- Retried bootstrap failures only before authentication proof
transmission begins.
- Kept post-proof failures fail-closed and bounded the welcome exchange
at two seconds.
- Added runnerd build metadata for the versioned transport contract.
- Updated Rust dependencies and `Cargo.lock` only for TLS and
certificate handling.
- Did not add provider dispatch, Pi, AWS, `pnpm-lock.yaml`, migrations,
or workflows.

## Verification

- GitHub Actions will run Cargo formatting, Rust tests, repository
tests, typecheck, build, security, and policy gates.
- Rust tests cover URL validation, listener path validation, build
metadata, durable recovery, and the existing Codex provider path.
- Local tests were not run. The requested verification policy uses
GitHub Actions for this series.
- `git diff --check master...HEAD` passes.
- The delta contains 13 files.

## Risks

- TLS and listener changes affect the runner trust boundary.
- Public plaintext transport remains rejected.
- The listener uses one fixed port and one exact run-bound path.
- PRP authentication remains required after the WebSocket upgrade.
- The optional CA file uses the existing private-file checks and a 4 MiB
limit.
- This PR does not enable another provider or change direct adapters.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex with GPT-5.6. The work used high-reasoning agent mode,
repository tools, GitHub tools, and parallel code-audit agents.

## 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 or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [ ] 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 risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-09-01 02:19:11 -05:00

3.7 KiB

Paperclip Runner in Daytona sandboxes

Intended topology

Paperclip creates the run and a short-lived, one-use runner bootstrap ticket. The Daytona sandbox starts only paperclip-runnerd; runnerd then starts the configured provider (Codex initially). Runnerd listens on a fixed sandbox-local port and Paperclip connects through Daytona's authenticated preview WebSocket ingress. The provider never receives a Paperclip API credential or Daytona preview credential.

Paperclip control plane
       |
       | outbound WSS + X-Daytona-Preview-Token
       v
Daytona preview proxy -> paperclip-runnerd:43127 -> Codex app-server

The PRP identity binds company, issue, agent, run, environment lease, runner instance, normalized session, artifact version, artifact digest, and catalog digest. After the one-use ticket challenge succeeds, runnerd receives a renewable connection lease. Revocation, expiry, replay cursors, and the durable outbox continue to work across transient network loss and sandbox/provider restarts.

Network policy

  • Allow Paperclip to make outbound TCP 443 connections to the configured Daytona preview origin. The Paperclip host does not need public ingress.
  • Allow the sandbox provider's private preview proxy to reach runnerd on TCP 43127. Do not expose the port with a public sandbox or signed URL.
  • Allow the sandbox outbound access only to destinations explicitly required by the provider runtime.
  • Use Daytona's wss:// preview URL with normal certificate and hostname validation. The preview token is sent only as the X-Daytona-Preview-Token header by Paperclip.
  • Runnerd rejects public plaintext ws:// destinations. Public dial targets require wss:// with hostname validation and either the platform trust store or an explicitly staged private CA bundle.
  • Listener mode binds only 0.0.0.0:43127, accepts only the run-specific /api/runner/v1/connect/:runId path, and rejects WebSocket extension negotiation. PRP authentication and application-layer secure frames remain mandatory above the preview-proxy hop.
  • Do not put the bootstrap ticket in argv, files, provider environment, logs, or model context. Inject it into runnerd's initial environment/secret channel; runnerd already removes it from its environment immediately.
  • Runnerd is the only process allowed to reach PRP. The provider communicates with runnerd over inherited pipes.

Lifecycle

  1. Paperclip allocates the Daytona environment and persists its environment lease.
  2. Paperclip creates a runner ticket with a very short expiry and binds it to the run, environment lease, runner digest/version, and allowed catalog digest.
  3. The sandbox startup command launches the pinned runnerd artifact in listener mode on 0.0.0.0:43127 and the run-specific PRP path. The ticket is supplied separately as secret environment material.
  4. Paperclip acquires an authenticated Daytona preview endpoint and dials runnerd. After PRP authentication, Paperclip sends run.prepare; only then may runnerd start Codex and advertise the run-authorized tools.
  5. Suspend/drain/revoke commands stop new turns and durably flush terminal events. Paperclip revokes the lease before destroying or recycling the sandbox.
  6. A recovered sandbox reuses its durable runner state and an unexpired connection lease; it must not mint a second provider session when a resumable one exists.

Deployment-mode boundary

Daytona always selects provider ingress for paperclip_runner; it does not fall back to direct outbound WSS or a legacy callback bridge. Same-host native runs keep loopback ws://, and other remote providers may use direct outbound wss:// only when an operator explicitly configures a reachable Paperclip runner URL. Legacy adapters retain their existing transport paths.