mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 01:50:13 +01:00
## 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>
207 lines
12 KiB
Markdown
207 lines
12 KiB
Markdown
# 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](https://docs.github.com/en/packages/learn-github-packages/deleting-and-restoring-a-package).
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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.
|