Files
PaperClipAI/doc/opencode-cold-recovery.md
T
DottaandPaperclip 909f4d1a33 fix(native): recover exact historical provider pack code (#19779)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native sessions retain the provider pack that admitted their saved
state.
> - An image update can replace that pack's executable, proxy, or
contract.
> - Older sessions can predate the durable three-file launch artifact
receipt.
> - The current image's archive cannot satisfy a different pinned
manifest.
> - This pull request obtains the exact public code from its recorded
release image.
> - Full source, archive, and installed-file checks still control
provider admission.

## Linked Issues or Issue Description

Refs #19558, #19622, #19755.

A saved OpenCode session can fail with
`runner_remote_provider_archive_invalid` after an image update. Its
original provider state and checkpoint remain valid, but its pinned pack
differs from the controller's pack. The old manifest can name a release
that still has the exact code in GHCR. Matching the OpenCode version
does not prove matching proxy or contract bytes.

## What Changed

- Resolve a pinned historical pack from the recorded source-SHA image in
the public Paperclip GHCR repository. Verify the platform manifest, raw
config, source identity, dedicated code layer, selected pack manifest,
archive contents, paths, and hashes.
- Keep a private temporary cache of verified public code. Rebuild it on
an empty host. Bound each source to 4 GiB and three minutes, with two
active imports or cache verifications, 32 distinct queued sources, and
disk-headroom checks. Share warm-cache verification by manifest while
each caller keeps its own cancellation.
- Select this verified archive through the existing uploader. Preserve
installed-pack and preinstalled-pack checks. Refuse unavailable
historical transport before clearing the target. Keep the current-source
identity guard from #19755. Refuse an oversized layer argument before
any target effect.
- Attach the source wait to the actual per-turn startup. Stop, lease
loss, and startup completion cancel it. Keep existing Runner admission
and turn limits.
- Document the registry retention requirement and the distinction
between public code recovery and encrypted provider-state backup.

## Verification

- Genuine baseline RED: the real uploader rejects the selected old pack
before any remote effect. Treatment installs the exact old code through
that uploader.
- Current head: all 19 source/integrity/cache/deadline controls, all 45
archive controls, and 29 actual-caller and manifest/startup controls
pass (93 total). Four review regressions fail against the prior head and
pass with the fixes. The server and colocated test TypeScript graph
passes. Module-boundary and whitespace checks pass.
- Actual public GHCR cold reconstruction: 1,054,587,008 compressed code
bytes, 115.4 s, about 248 MiB maximum owner RSS. All 114 file layers and
the overlay passed source validation.
- Linux Node 24.22.2 fixture: the real historical pack passes the normal
uploader, full installed-file verifier, and pinned OpenCode version
check. It used one transfer, no application startup, no network, and no
provider calls. Revision12b0d2ba passed in 220.2 s with about 282 MiB
owner RSS. The earlier prototype passed in 108.5 s; both measurements
are retained. The three-minute source limit and the fixture's
four-minute limit were unchanged. The later review fixes add shared
cache verification, hash cancellation, and an early size guard. They do
not change extraction or the accepted Linux fixture input.
- Earlier fixture failures are retained: unsafe hardlink classification
was corrected with confined-link controls; a cached image lacked the
required zstd CLI; Docker Desktop bind storage caused GNU tar
directory-identity errors. The passing fixture uses the official Debian
zstd binary and an owned Linux tmpfs target. Production extraction code
is unchanged.
- Fresh Greptile review is 5/5 on
7b1a249478 with no open findings. All
required CI gates passed on the reviewed head. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42353011485),
[fresh
review](https://github.com/paperclipai/paperclip/pull/19779#issuecomment-6704065994).
No hosted session was retried by this change.

## Risks

- Cold historical recovery reads about 1 GiB of public code for the
measured release. Registry latency or missing images can block it. The
source deadline remains three minutes.
- GHCR administrators can delete release images. Source-SHA tags and
this temporary cache are not a permanent code-retention guarantee. A
provider-state S3 backup does not replace legacy executable bytes.
- Targets must support the existing qualified archive transport and
selected platform. Failed checks preserve the saved state and prevent
provider dispatch.
- The Linux fixture proves code installation and verification. Hosted
conversation continuation still needs acceptance after normal delivery.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools, and local
code execution. The host does not expose the exact deployment ID or
context-window size.

## 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 references)
- [x] My branch name describes the change 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-07 02:06:02 -06:00

12 KiB

OpenCode launch artifacts and cold recovery

OpenCode records the absolute paths and hashes of its Node executable, proxy, and native executable in its durable provider state. An image upgrade can change these files. A saved session must keep its admitted files. The Runner still rejects a different launch profile.

For a new managed remote OpenCode session with durable object storage, Core puts these three qualified files in the runtime account's private, content-addressed launch directory. Core reads the home and UID from the runtime OS account database. It does not infer ownership from HOME, a workspace path, or a provider response. The launch receipt records this UID, the absolute directory, the pack manifest, and the encrypted artifact identity.

Core saves the files before it releases the first provider turn. Artifact capture uses the current native coordinator claim and the recorded Core admission. A brief admission lock waits for at most one second and checks the current owner after it acquires the lock. A stale, revoked, or unknown owner cannot publish. The artifact operation does not grant execution or tool authority.

The archive is limited to 512 MiB of launch files. Core streams files and checks hashes and file identity before and after capture. Pack-internal package links can resolve to regular source files. Links outside the selected pack are refused. The published launch directory contains regular files and private, owned parents. Node and OpenCode must be qualified native ELF executables. A script wrapper or a changed account fails closed. The selected runtime must still supply the qualified native executables' platform and system dependencies.

The artifact object uses authenticated encryption and a company-bound key wrapper. A company can reuse a ready object with the same file descriptor. Canonical run and harness-backup references keep the object alive. Large launch files are excluded from each later harness archive. A pending upload records its exact encrypted object reference before upload, so an interrupted reply can be retired without guessing an object key.

A ready company closure is reused. Concurrent first admissions may independently upload that closure: each admission retains its own cancellation, ownership fence, and pending receipt rather than depending on another controller's live transfer. Each capture has the 512 MiB limit and five-minute deadline; a run can retain at most three pending uploads. Publication selects one verified ready object under the closure lock. A loser pins that winner before deleting only its own object. This bounds each admitted run, not total simultaneous company egress. A burst can therefore transfer duplicate bytes; distributed upload election is not included.

On cold recovery, Core restores the canonical Runner checkpoint, workspace snapshot, and native harness data through their normal paths. It also verifies and decrypts the recorded launch files. The runtime must have the same owned account UID and absolute home. Core stages the exact files at the recorded content-addressed paths. It does not replace files in the current image.

Published workspaces retain their recorded directory basename across lease changes. A replacement allocation with the same provider base therefore keeps the native session's absolute cwd. The workspace service can restore files to a different base, but that alone does not authorize changing a native session's cwd. This change adds no directory aliases and makes no change to the Rust profile guard.

An explicitly governed continuity break archives the controller launch cache with the prior session state, receipt, and backups. The cache is not a new execution authority and must not remain active for the fresh provider session.

Live Runner reattachment does not download or replace the launch files. Historical sessions that recorded image-rooted paths keep those paths. A governed fresh-session transition can use a new pack after existing effects and receipts are reconciled. It must not replay a previous turn or treat a missing result as proof that no effect occurred.

Historical provider-pack code sources

An older session can retain its exact pack manifest while the current server image contains a different pack. When neither the staged nor the preinstalled pack verifies, the normal uploader resolves the selected historical code from ghcr.io/paperclipai/paperclip. It uses the manifest's recorded source revision and Linux architecture. The full source-SHA image tag is a lookup hint; a tag name alone does not prove the code identity.

The resolver verifies the OCI index, selected platform manifest, image config, source revision, and dedicated provider-pack transport layer. It then verifies the selected canonical pack manifest, compressed layer hashes, tar paths and links, and each file layer's plaintext bytes. The final installed pack still passes the normal verification against the original selected manifest. The OpenCode version alone cannot admit a different proxy or contract. This source selection also serves other providers that use the full provider-pack uploader; it does not change their compatibility or qualification rules.

The code source is downloaded and verified before the uploader can clear or transfer anything to the target. A historical source requires the existing qualified archive transport. An unavailable transport leaves the target intact. Missing images, changed bytes, invalid source identity, and unsupported packs fail closed. The resolver never resets provider state, substitutes the current pack, changes the stored launch paths, or starts an old application image.

The temporary cache contains public code and source provenance only. It uses private, process-account-owned directories and exact manifest keys. An empty host can reconstruct the cache from the registry. A cached source is checked again before upload. Imports stream data with a four-GiB code limit and a three-minute deadline, allow two active imports or cache verifications, and bound queued distinct sources to 32. Concurrent callers share verification of the same cached pack. Import waits and downloads respond to cancellation; one cancelled caller does not cancel a shared import still needed by another caller. Inactive instances perform no registry reads or cache scans. Each import requires disk headroom before writing; completed cached code remains until temporary storage is removed.

The normal per-turn startup owns the source wait, including OpenCode starts without a warm execution context. Stop, execution-lease loss, and startup completion cancel that wait. An existing execution context can cancel it sooner. The source deadline applies before Runner admission. It does not increase the separate cold-admission or provider-turn time limits.

The resolver uses only an anonymous token scoped to reading this public code repository. It does not use database, sandbox, model, or object-store credentials. Its cache and registry dependency are separate from the encrypted S3 backup of provider conversation state and workspace files.

The Docker publication workflow publishes full source-SHA tags. There is no image-deletion workflow in this repository, but this is not a permanent registry retention guarantee. GitHub package administrators can delete package versions. Historical continuation therefore depends on retaining the corresponding code image in GHCR, or retaining a verified source cache. Deleting both keeps the session blocked with its original state intact; an S3 provider-state backup alone does not reconstruct legacy image-owned executable code.

Artifact cleanup uses the existing bounded checkpoint and backup expiration owners. It requires closed work, settled native ownership, and exact stopped remote execution receipts. Backup retirement releases only that backup reference. The separate run reference lasts until fenced checkpoint expiration, including when an interrupted backup upload is retired while a resume handle still exists. A remaining backup or run reference keeps the object. A failed external delete keeps its receipt and reference for a later normal expiration attempt. There is no new idle scan or timer.

Checkpoint expiration checks current resume handles under its existing locks. It then fences old generations and atomically replaces the expired header with a versioned, company/run-scoped retirement marker. An earlier handle keeps the original checkpoint and files. After retirement, readers and header writers refuse the old checkpoint. Source consumers also check retirement in their existing ownership query, so a cached old snapshot cannot bypass the barrier. The marker stays in the existing header index until external artifact deletion succeeds, including after a server restart. No transaction waits for a network delete. Families with more than 100 related runs keep every pin and checkpoint. Missing object storage keeps the marker and pins for a later normal cleanup attempt.

The capture wait responds to Stop, startup completion, and the execution deadline. Native bulk transfer does not offer a cancellation signal. Core keeps that actual transfer registered as pending startup work after it stops waiting. The late operation can write only its private local scratch files. It cannot publish after the context closes. It removes its scratch files and releases its worker responsibility only after actual settlement. Stop does not imply that this transport has finished.

Local qualification

Run the focused server suites with the repository's supported pnpm version:

pnpm --filter @paperclipai/server exec vitest run --config vitest.config.ts \
  src/services/native-runtime/native-harness-backup-storage.integration.test.ts \
  src/services/native-runtime/native-harness-backup-archive.test.ts \
  src/services/native-runtime/remote-opencode-artifacts.test.ts \
  src/services/native-runtime/native-provider-pack-admission.test.ts \
  src/services/native-runtime/native-provider-artifact-capture.test.ts \
  src/services/native-runtime/native-runner-checkpoint-retirement.test.ts \
  src/services/native-runtime/native-session-executor.test.ts \
  src/__tests__/native-harness-backup-expiration.test.ts \
  src/__tests__/native-harness-backup-expiration-config.test.ts

The opt-in Linux qualification requires two locally cached Paperclip images with full immutable image IDs and different OpenCode proxy hashes. Both images must have the same Linux architecture and the standard qualified provider pack. The new image must include the packaged Runner binary at /app/server/dist/vendor/paperclip-runner/bin/paperclip-runnerd. Build the current checkout's Runner TypeScript SDK first. Use the local Docker context that has the cached images:

pnpm --filter @paperclipai/paperclip-runner build:typescript
pnpm --filter @paperclipai/server exec tsx scripts/qualify-opencode-cold-recovery.mts \
  --context desktop-linux \
  --old-image "$OLD_IMAGE" \
  --new-image "$NEW_IMAGE" \
  --output /tmp/opencode-cold-recovery-result.json

The command accepts only full sha256: image IDs and a local Unix Docker endpoint. It never pulls an image. It creates isolated PostgreSQL data and two owned containers. The containers have no network, a read-only image filesystem, resource limits, and a private fixture OS account. The synthetic model runs on container loopback. No external model credential or API is used.

The command uses production workspace save/restore, encrypted checkpoint and harness storage, artifact publication, admission, SDK transport, Rust Runner, and actual OpenCode. It destroys the original allocation's account and controller files, changes the image and allocation lease, and restores the same provider session. It requires the useful workspace file and earlier model context to survive. Exactly one model request is allowed in each turn. The result records image IDs, pack digests, checkout revision and dirty state, lease identities, byte counts, session continuity, and useful output. The command removes only its own containers and PostgreSQL server. It retains its temporary evidence directory.

The independent object-store fixture uses the real StorageService and encrypted streams with an owned local directory outside both destroyed allocations. This qualifies the cold restore contract. It does not qualify a hosted S3 service, a task's HTTP/actor workflow, or completion of an existing hosted task.