chore: enable boxd plugin release publishing (#19020)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Sandbox provider plugins let those agents run in remote
environments.
> - The boxd provider exists, but its release entry excludes it from CI
publishing.
> - npm now has the `@paperclipai/[email protected]` bootstrap
placeholder.
> - This pull request enables CI publishing and documents npm
installation.
> - A pre-existing OpenCode fixture blocked CI because it qualified a
group-writable host Node binary.
> - This also gives that test its own Linux Node copy with strict
permissions.
> - Operators can install a real release after CI publishes it.

## Linked Issues or Issue Description

Related implementation:
https://github.com/paperclipai/paperclip/pull/17284.
No matching public issue or duplicate release-enrollment PR was found.

**What existing behavior does this improve?**

Publishing and installing the existing boxd sandbox provider.

**Subsystem affected**

`packages/plugins` and the npm release manifest.

**Current behavior**

`publishFromCi` is false. npm has only the `0.0.0` placeholder. The
README says the package is not published and requires a local-path
install.

**Proposed behavior**

Include boxd in CI releases. Document stable installation, explicit
published versions, and the placeholder limit.

**Reason and benefit**

Operators can install the provider through the existing npm plugin
installer after a real release ships.

**Breaking changes**

None. This changes release enrollment and documentation only.

**Additional context**

This follows the existing plugin and sandbox roadmap items. It adds no
new runtime capability.

## What Changed

- Set `publishFromCi: true` for `@paperclipai/plugin-boxd` in the
release manifest.
- Replace the README's unpublished-package notice with npm installation
guidance.
- Explain that the `0.0.0` placeholder has no plugin functionality and
that `latest` can point to it until the first stable release.
- Keep the local development instructions available.
- Fix the existing ingress-retry test to use an owned Linux Node copy
and explicit executable permissions. This follows the existing OpenCode
transport fixture. It does not change production launch checks.

## Verification

- Passed: `node --test scripts/release-package-map.test.mjs
scripts/check-release-package-bootstrap.test.mjs
scripts/bootstrap-npm-package.test.mjs` (31 tests).
- Passed: `node scripts/release-package-map.mjs check`.
- Passed: `PAPERCLIP_RELEASE_BOOTSTRAP_BASE_SHA=origin/master node
scripts/check-release-package-bootstrap.mjs
scripts/release-package-manifest.json
packages/plugins/sandbox-providers/boxd/README.md`.
- Passed: `git diff --check`.
- Confirmed that npm resolves `@paperclipai/plugin-boxd` to version
`0.0.0`.
- Full workspace typecheck, test suite, and build were not run locally.
CI checks remain required before merge.
- Initial CI failure: [native Runner
job](https://github.com/paperclipai/paperclip/actions/runs/41889969053/job/126785636239),
OpenCode ingress retry, `qualified OpenCode proxy command must not be
group- or world-writable`. The failing fixture is byte-identical to the
base commit `3a2249dd576`. It passed `process.execPath` to
qualification, while other OpenCode tests already use a private Linux
Node copy.
- Local focused run: `pnpm exec vitest run
server/src/services/native-runtime/native-ingress-bootstrap-retry.integration.test.ts`
built the real Rust Runner and passed the Codex case. The OpenCode case
stopped at `runner_remote_provider_artifact_incompatible: OpenCode proxy
path belongs to the controller host` on macOS (`/Users/...`). This does
not exercise the new Linux-only copy.
- An unchanged-base OpenCode comparison was attempted with `-t
"reacquires opencode ingress"`, but PostgreSQL setup failed with
`ENOSPC` before the case ran. The local baseline comparison is therefore
inconclusive. Task-owned Rust build outputs were cleaned to recover disk
space.
- At this stage Linux verification was pending. The successful final run
is recorded below.
- No new tests were needed. The existing release-tool tests cover
manifest enrollment, dependency ordering, and bootstrap checks.

- Current-head CI interruption: [workspaces-a shard
1/2](https://github.com/paperclipai/paperclip/actions/runs/41893808561/job/126799012853)
was cancelled after the runner received a shutdown signal. The log shows
no assertion failure. The job rerun request was rejected with HTTP 403
because the workflow was still running. The subsequent fixture
correction starts a new full CI run and supersedes that pending rerun.

- CI at `7fffa315`: [native Runner
job](https://github.com/paperclipai/paperclip/actions/runs/41893808561/job/126799011711)
passed executable permission checks, then failed because Node tried to
resolve `/memfd:paperclip-verified-launch (deleted)`. The private
executable was named `opencode-node`, which bypassed Runner's Node
interpreter recognition. The fixture now names its copy `node`, matching
the existing transport fixture and selecting the CommonJS descriptor
loader.
- Passed a local smoke test of the production CommonJS descriptor loader
using an open, unlinked script file. This is macOS evidence only; the
successful final Linux run below provides the complete ingress retry
evidence.

- Final verification on `bcacac542aa0a1b641b26c099c1e9fc4a1653ec4`: all
54 reported checks passed; three unrelated checks were skipped. [Native
Runner integration
job](https://github.com/paperclipai/paperclip/actions/runs/41895787712/job/126805844773)
passed, including the OpenCode ingress retry regression. The fresh full
run also passed the workspace shard that was interrupted on the previous
attempt. Greptile reports 5/5 for this exact head with no unresolved
review threads. Earlier failures and local verification limits remain
recorded above.

## Risks

- The maintainer confirmed that npm trusted publishing is configured for
this package. The first real CI publish will verify OIDC authentication.
- Merging enables publication in the next successful canary run. An
installation that uses `latest` can still resolve to the placeholder
until the first stable release includes this change.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`, with xhigh reasoning effort,
repository tool use, and shell execution. The runtime does not expose
the 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 `#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 <[email protected]>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-11-04 11:36:04 -08:00
1 parent 89703c3ff7
commit cf0ab4f7f4
3 files changed
+37 -9

No files matched your search

@@ -3,10 +3,31 @@
boxd sandbox provider for Paperclip. [boxd](https://boxd.sh) runs each sandbox
as an isolated KVM microVM. This package lives alongside E2B, Daytona and the
other sandbox providers, outside the root pnpm workspace. It uses the
[`@boxd-sh/sdk`](https://www.npmjs.com/package/@boxd-sh/sdk) package. The
package is not published yet; use a local-path install. Its release manifest
entry has `publishFromCi: false` until a maintainer bootstraps the first npm
publish.
[`@boxd-sh/sdk`](https://www.npmjs.com/package/@boxd-sh/sdk) package. Paperclip's
CI release pipeline publishes this plugin to npm.
## Install from npm
Once a stable release is available, install the package from the Plugins page
by name, or use the CLI:
```sh
paperclipai plugin install @paperclipai/plugin-boxd
```
Before the first stable release, the `latest` tag can still point to the
`0.0.0` bootstrap placeholder. That version reserves the package name and has
no plugin functionality. Check the published tags and choose a real release:
```sh
npm view @paperclipai/plugin-boxd dist-tags --json
paperclipai plugin install @paperclipai/plugin-boxd --version <published-version>
```
Replace `<published-version>` with the version shown for the desired tag, such
as `canary`. If no real release is available yet, use the local-path install
below. See the [publishing guide](../../../../doc/PUBLISHING.md) for the release
and bootstrap process.
## Build and install locally
+1 -1
View File
@@ -127,7 +127,7 @@
{
"dir": "packages/plugins/sandbox-providers/boxd",
"name": "@paperclipai/plugin-boxd",
"publishFromCi": false
"publishFromCi": true
},
{
"dir": "packages/plugins/sandbox-providers/cloudflare",
@@ -1,7 +1,7 @@
import { execFileSync, type ChildProcess } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { chmod, copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import https, { createServer as createHttpsServer } from "node:https";
import { createServer as createTcpServer } from "node:net";
import { join, resolve } from "node:path";
@@ -85,11 +85,18 @@ describeEmbeddedPostgres("native ingress bootstrap retry through the real Runner
identity: { kind: provider } }, normalizedSessionId: native.normalizedSessionId }));
const digest = (file: string) => `sha256:${createHash("sha256").update(readFileSync(file)).digest("hex")}`;
const opencode = join(runtimeRoot, "fake-opencode-ingress");
const providerNode = provider === "opencode" && process.platform === "linux"
? join(runtimeRoot, "node") : process.execPath;
const providerCalls = join(sandbox, "provider-calls.log");
if (provider === "opencode") {
if (process.platform === "linux") {
await copyFile(process.execPath, providerNode);
await chmod(providerNode, 0o500);
}
const script = resolve(runnerWorkspace, "../test/fixtures/fake-opencode-server.mjs");
execFileSync("cc", ["-x", "c", "-o", opencode, "-"], { input:
`#include <unistd.h>\n#include <stdlib.h>\n#include <stdio.h>\nint main(int argc, char **argv) { FILE *log = fopen(${JSON.stringify(providerCalls)}, "a"); if (!log) return 126; fputs("process-start\\n", log); fclose(log); char **args = calloc(argc + 2, sizeof(char *)); args[0] = ${JSON.stringify(process.execPath)}; args[1] = ${JSON.stringify(script)}; for (int i = 1; i < argc; i++) args[i + 1] = argv[i]; execv(args[0], args); return 127; }` });
`#include <unistd.h>\n#include <stdlib.h>\n#include <stdio.h>\nint main(int argc, char **argv) { FILE *log = fopen(${JSON.stringify(providerCalls)}, "a"); if (!log) return 126; fputs("process-start\\n", log); fclose(log); char **args = calloc(argc + 2, sizeof(char *)); args[0] = ${JSON.stringify(providerNode)}; args[1] = ${JSON.stringify(script)}; for (int i = 1; i < argc; i++) args[i + 1] = argv[i]; execv(args[0], args); return 127; }` });
await chmod(opencode, 0o755);
}
const proxy = resolve(runnerWorkspace, "../dist/cli/opencode-app-server-proxy.cjs");
const identity = { runnerInstanceId: native.runnerInstanceId, environmentLeaseId: runId, runId,
@@ -187,8 +194,8 @@ describeEmbeddedPostgres("native ingress bootstrap retry through the real Runner
onAccountingEvent: async event => { await usageDelivery?.capture(event); },
environment: { CODEX_HOME: codexHome, PATH: process.env.PATH, OPENROUTER_API_KEY: "synthetic-fixture-key" }, sourceCodexHome: codexHome,
...(provider === "opencode" ? { opencodeCommand: opencode, opencodeCommandSha256: digest(opencode),
opencodeProxyPath: proxy, opencodeProxySha256: digest(proxy), providerNodeCommand: process.execPath,
providerNodeCommandSha256: digest(process.execPath), opencodePermissionMode: "allow" as const } : {}),
opencodeProxyPath: proxy, opencodeProxySha256: digest(proxy), providerNodeCommand: providerNode,
providerNodeCommandSha256: digest(providerNode), opencodePermissionMode: "allow" as const } : {}),
prpIdentity: identity, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
controlPlaneRegistration: async authority => {
const controller = await currentNativeControllerIdentity();