Files
PaperClipAI/packages/adapter-utils
Devin FoleyandPaperclip 6a43890326 Fix image attachments in remote native workspaces (#20939)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native runs must receive the files that users attach to their
messages.
> - Remote wake preparation returned file metadata without readable
bytes.
> - The historical chat reader also rejected every remote target.
> - Command-backed sandboxes and SSH targets already provide a bound
command transport.
> - This pull request stages verified files through that transport and
clears them after the turn.
> - Agents can then open attached images with their normal image reader.

## Linked Issues or Issue Description

Refs #19740. This change covers SSH and command-backed sandbox
execution, including Daytona. Plugin Runner tasks without a command
transport still need a separate versioned file-transfer contract. This
PR does not close that broader issue. The open attachment-recovery PR
#20778 concerns provider-session attachment journals, not input files.

**What happened?**

A user uploaded an image and sent it to an agent. The remote native run
received its filename and size, but no readable file. Wake preparation
reported `remote_workspace_staging_unsupported`. The historical read
fallback also refused remote execution.

**Expected behavior**

Copy the exact admitted attachment into the bound remote workspace. Let
the provider read the image. Preserve company, task, source-comment,
size, and hash checks. Clear temporary file contents when the turn ends.

**Steps to reproduce**

1. Select a native agent on a command-backed sandbox or Linux SSH
environment.
2. Attach a PNG to its current task or in-app chat message.
3. Ask the agent to inspect it.
4. Check that wake preparation supplies a readable workspace-relative
image path.

## What Changed

- Add a remote staging owner with bounded commands, stdin transfer, hash
verification, opaque paths, and safe image extensions.
- Confine Linux filesystem operations through directory descriptors.
Refuse symlinks and replacement files during cleanup.
- Bind current wake files and authorized historical external-chat reads
to the selected lease transport.
- Join active writes during cancellation. Retain failed cleanup work
until scope close.
- Tell providers to open image files with an image-capable reader.
- Handle stdin stream errors in the shared SSH transport. Failed uploads
return failure instead of raising an unhandled EPIPE or reporting
success.
- Add database, transport, cancellation, integrity, cleanup,
executor-wiring, and Linux filesystem regressions. Document support and
limits.

## Verification

- Prepared an isolated source checkout with `node
scripts/agent-preflight.mjs --skip-rust-debug`.
- Focused suites: `pnpm exec vitest run
server/src/services/native-runtime/remote-attachment-staging.test.ts
server/src/services/native-runtime/native-runner-file-handoff.test.ts
server/src/services/native-runtime/chat-attachment-read.test.ts` — 74
passed, 4 Linux-only tests skipped.
- Executor checks: `pnpm exec vitest run
server/src/services/native-runtime/native-session-executor.test.ts -t
'attachment staging to the remote|stages from the authenticated|verifies
frozen staged admission'` — 7 passed.
- Server typecheck: `pnpm --filter @paperclipai/server typecheck`,
followed by the server layered check after the final changes.
- `git diff --check` passed. The local secret, PII, and
private-reference diff scan found no matches.
- `pnpm verify` returned exit 2 because the documentation change
requires broad CI. No full local suite is claimed.
- The four Linux filesystem tests were skipped locally because this
macOS host has no available Docker daemon. Linux CI executed all 17
remote-staging tests successfully, including those four confinement
tests: [server shard
10](https://github.com/paperclipai/paperclip/actions/runs/43102035096/job/130796646266).
- SSH regression: the real pipe test reproduced two unhandled EPIPE
errors before the fix, plus false success when the peer exited with code
zero. The fixed SSH tests and existing argv tests pass (12 tests). The
remote-staging transport suite also passes (13 tests; 4 Linux tests
require CI).
- `pnpm --filter @paperclipai/adapter-utils typecheck` passed after the
SSH change.
- Final revision `1c5a9288b8d5f267ddeac84638ff6075dbbbf8ec`: all
applicable CI jobs passed, including typecheck, build, general and
serialized tests, Runner checks, canary dry run, and all eight browser
shards. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/43102035096).
Linux CI also passed all 6 native SSH tests: [server shard
5](https://github.com/paperclipai/paperclip/actions/runs/43102035096/job/130796646349).
- Greptile reviewed that final revision at 5/5 with no remaining
findings. The review thread is resolved. GitHub reports the PR mergeable
with a clean merge state.
- No live model turn or deployed release is claimed.

## Risks

- Remote staging requires Linux, Node, and a command transport bound to
the selected lease. Plugin Runner tasks without that transport retain
their unavailable result.
- Each file uses bounded independent commands. Slow transports can delay
preparation or cleanup. File bytes stay out of command arguments and
provider-facing error messages.
- An abrupt controller or remote-host failure can bypass cleanup.
Ephemeral sandbox disposal remains the backstop. Retained files are
never blindly overwritten or deleted.
- Previous-release provider sessions and checkpoints retain their format
and identity. New turns receive newly staged input files. Existing
restart-admission checks still apply. No migration, runner-image change,
or account-credential transfer is required.

## Model Used

OpenAI `gpt-6.1-sol` through the Codex harness in T3 Code, with xhigh
reasoning, repository inspection, code execution, and tests. No
subagents were used. The context-window size is not exposed by this
runtime.

## 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-11-11 14:12:09 -08:00
..

@paperclipai/adapter-utils

Shared utilities for Paperclip adapters: process spawning, environment injection, sandbox/SSH transport, workspace sync, and the round-trip helpers that move code between the local execution-workspace cwd and wherever the agent actually runs.

For the adapter-author guide see docs/adapters/creating-an-adapter.md and the in-repo notes at packages/adapters/AUTHORING.md.

Sandbox bridge deadlines

Command-managed bridge control operations (queue reads, input delivery, setup, and cleanup) have a host-enforced deadline of at most 30 seconds per shell command. A shorter configured timeout still applies. These operations cannot inherit the agent run's hours-long lifetime or wait forever on a provider that ignores its timeout. Long-lived agent session commands keep their own limits.

A control timeout reports a transport failure. It does not prove that the remote command stopped and does not replay an uncertain write. The process session closes failed input delivery and uses its existing shutdown path; normal execution settlement must still verify termination.

Native archive transfer deadlines

Native syncIn and syncOut calls accept an optional minimumTimeoutMs budget. The server validates this budget and extends the worker RPC deadline. Providers can honor this field for file transfers without changing their configuration. Daytona uses the larger of this minimum and its configured timeout. Other providers keep their existing behavior. The minimum is limited to 30 minutes and does not change sandbox lifetimes, stored settings or the timeout for other commands.

The remote-workspace archive path uses the verified capture size for downloads and the local archive size for uploads. Large archives get one second per MiB plus 60 seconds, capped at 30 minutes. Small archives keep the existing provider timeout when that calculation is no greater than five minutes. Archive size, checksum, confinement and host space checks still apply.

An exact retained-sandbox resume keeps its recorded provider timeout and RPC overhead. It uses the normal one-minute fallback only when no timeout was recorded. A five-minute cold restore must not be cut off by that fallback. Resume still targets the recorded allocation and does not acquire a replacement.

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.

Adapters that run the agent on a different host should use the SSH round-trip helpers in src/ssh.ts:

  • prepareWorkspaceForSshExecution({ spec, localDir, remoteDir }) — bundles the local cwd (tracked files, dirty edits, untracked additions, and the git history needed to reconstruct it) to remoteDir before the run starts. Runs with no git remote configured.
  • restoreWorkspaceFromSshExecution({ spec, localDir, remoteDir, ... }) — syncs the remote cwd back into localDir after the run, including any new commits the agent created. Also runs with no git remote configured.

prepareRemoteManagedRuntime in src/remote-managed-runtime.ts wraps both calls for adapters that want a per-run remote workspace and an automatic restoreWorkspace() finally hook.

The invariant is pinned by the no-remote-git contract case in src/ssh-fixture.test.ts, which asserts that a remote-only commit propagates to the local worktree through the prepare → restore round-trip with no git remote configured at any point. Do not regress that test.