fix(build): enforce Node 24 across Paperclip (#11792)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip runs across the CLI, server, adapters, plugins, CI, and container images. > - These surfaces declared different Node.js versions from 20 through 24. > - A newer `@types/node` major can expose APIs that the supported runtime does not provide. > - Node.js 20 is no longer a suitable project baseline, and Node.js 24 is the current LTS line. > - This pull request sets Node.js 24.11.0 as one repository-wide baseline, adds a drift check, and gives users actionable startup guidance when their runtime is too old. > - The benefit is one clear runtime contract for development, release, installation, and published packages. ## Linked Issues or Issue Description Refs #2734 Refs #11727 Refs #739 ## What Changed - Require Node.js 24.11.0 or newer in all 42 package manifests and runtime checks. - Use Node.js 24 in GitHub Actions, Docker images, smoke images, sandbox setup, portable installs, and esbuild targets. - Align every direct `@types/node` declaration on `^24.0.0`. - Prevent Dependabot from opening major `@types/node` upgrades without a matching runtime decision. - Add `.nvmrc` and a CI policy check for Node version drift. - Update ACP version gates, tests, and user documentation for the new minimum. - Print a non-blocking warning on CLI and server startup when Node is unsupported, with remediation through a version manager or the documented downloaded `install.sh` workflow. - Deduplicate that warning when `paperclipai run` boots the CLI and server in the same process. ## Verification - `node scripts/check-node-version-policy.mjs` - `node --check scripts/check-node-version-policy.mjs` - `node --check cli/esbuild.config.mjs` - `node --check scripts/generate-npm-package-json.mjs` - `bash -n scripts/install.sh scripts/test-install-sh-docker.sh scripts/e2e-install-lifecycle.sh` - Parsed all 42 package manifests and confirmed `engines.node` is `>=24.11.0`. - `git diff --check` - `vitest run packages/adapter-utils/src/sandbox-install-command.test.ts` passed with 3 tests. - `vitest run cli/src/node-version.test.ts` passed with 4 tests. - Directly exercised the shared warning helper for unsupported-version messaging and same-process deduplication. - The focused exe.dev suite could not resolve the locally unbuilt plugin SDK from this isolated worktree. A full offline workspace install was also blocked because the package-manager signature verifier requires registry access. The full suite was not run locally; draft CI performs a clean install and evaluates the wider impact. ## Risks - This is a breaking runtime change for users, plugins, and deployments that still use Node.js 20 or 22. - Published workspace packages will now produce an engine warning or failure in strict package managers on older Node.js releases. - Node.js 24 can reveal dependency, native module, Playwright, or agent CLI compatibility issues in CI. - The bootstrap installer now installs Node.js 24 when the current runtime is older than 24.11.0. - The portable sandbox fallback is pinned to Node.js 24.11.0 and depends on that upstream tarball remaining available. - Unsupported runtimes continue booting after a warning, so a later incompatibility can still fail at its point of use. - The CLI and server share the warning policy through the published `@paperclipai/shared` package; packaging checks must keep that subpath export available. - This PR does not commit `pnpm-lock.yaml` because repository policy assigns lockfile generation to CI. > 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 based on GPT-5. The exact deployment ID and context window are not exposed in this session. Reasoning, repository tools, shell execution, and GitHub tools were enabled. ## 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>
This commit is contained in:
parent
33eb68b3ae
commit
38d8f37172
|
|
@ -14,6 +14,13 @@ updates:
|
|||
open-pull-requests-limit: 20
|
||||
labels:
|
||||
- "dependencies"
|
||||
ignore:
|
||||
# @types/node describes the APIs available in the supported Node runtime.
|
||||
# Runtime major upgrades are deliberate compatibility changes, so keep
|
||||
# Dependabot on the current major until the runtime baseline moves too.
|
||||
- dependency-name: "@types/node"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
- name: Set up Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
|
||||
- name: Generate commitperclip token
|
||||
id: token
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
|
||||
- name: Refresh lockfile for Docker build context
|
||||
run: |
|
||||
|
|
@ -257,7 +257,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
|
||||
- name: Refresh lockfile for Docker build context
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ jobs:
|
|||
- name: Validate Dockerfile deps stage
|
||||
run: node ./scripts/check-docker-deps-stage.mjs
|
||||
|
||||
- name: Validate Node version policy
|
||||
run: pnpm check:node-version
|
||||
|
||||
- name: Reject git push in adapter/runtime code
|
||||
run: node ./scripts/check-no-git-push.mjs
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Refresh pnpm lockfile
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# syntax=docker/dockerfile:1.20
|
||||
FROM node:lts-trixie-slim AS base
|
||||
FROM node:24-trixie-slim AS base
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=1000
|
||||
RUN apt-get update \
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ fi
|
|||
bash install.sh
|
||||
```
|
||||
|
||||
The installer ensures Node.js 20 or newer is available, installs a managed
|
||||
The installer ensures Node.js 24.11 or newer is available, installs a managed
|
||||
Paperclip CLI under `~/.paperclip/cli`, and starts interactive onboarding. It
|
||||
can also install Paperclip as a background service on supported Linux and
|
||||
macOS systems. The checksum detects transfer or publishing mistakes, but it is
|
||||
|
|
@ -376,7 +376,7 @@ pnpm dev
|
|||
|
||||
This starts the API server at `http://localhost:3100`. An embedded PostgreSQL database is created automatically — no setup required.
|
||||
|
||||
> **Requirements:** Node.js 20+, pnpm 9.15+
|
||||
> **Requirements:** Node.js 24.11+, pnpm 9.15+
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ pnpm dev
|
|||
|
||||
This starts the API server at `http://localhost:3100`. An embedded PostgreSQL database is created automatically — no setup required.
|
||||
|
||||
> **Requirements:** Node.js 20+, pnpm 9.15+
|
||||
> **Requirements:** Node.js 24.11+, pnpm 9.15+
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default {
|
|||
entryPoints: ["src/index.ts"],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
target: "node24",
|
||||
format: "esm",
|
||||
outfile: "dist/index.js",
|
||||
banner: { js: "#!/usr/bin/env node" },
|
||||
|
|
|
|||
|
|
@ -59,8 +59,11 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"tsx": "^4.23.12",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
type InstallStorePaths,
|
||||
} from "../install-store.js";
|
||||
import type { CheckResult } from "./index.js";
|
||||
import { isSupportedNodeVersion, MINIMUM_NODE_VERSION } from "@paperclipai/shared/node-version";
|
||||
|
||||
function pathContains(directory: string): boolean {
|
||||
const normalized = path.resolve(directory);
|
||||
|
|
@ -33,14 +34,13 @@ function hasManagedArtifacts(paths: InstallStorePaths): boolean {
|
|||
}
|
||||
|
||||
export function nodeRuntimeCheck(): CheckResult {
|
||||
const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
||||
return major >= 20
|
||||
return isSupportedNodeVersion(process.versions.node)
|
||||
? { name: "Node.js runtime", status: "pass", message: `Node.js ${process.versions.node}` }
|
||||
: {
|
||||
name: "Node.js runtime",
|
||||
status: "fail",
|
||||
message: `Node.js ${process.versions.node} is unsupported`,
|
||||
repairHint: "Install Node.js 20 or newer before installing or running Paperclip",
|
||||
repairHint: `Install Node.js ${MINIMUM_NODE_VERSION} or newer before installing or running Paperclip`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { execFile } from "node:child_process";
|
|||
import { promisify } from "node:util";
|
||||
import * as p from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isSupportedNodeVersion, MINIMUM_NODE_VERSION } from "@paperclipai/shared/node-version";
|
||||
import {
|
||||
addManagedPathBlock,
|
||||
assertManagedShimWritable,
|
||||
|
|
@ -84,9 +85,8 @@ export function resolveGitInstallWorkspacePackages(checkoutPath: string): Releas
|
|||
}
|
||||
|
||||
function assertSupportedNodeVersion(): void {
|
||||
const major = Number(process.versions.node.split(".")[0]);
|
||||
if (!Number.isFinite(major) || major < 20) {
|
||||
throw new Error(`Managed installs require Node.js 20 or newer (found ${process.version}).`);
|
||||
if (!isSupportedNodeVersion(process.versions.node)) {
|
||||
throw new Error(`Managed installs require Node.js ${MINIMUM_NODE_VERSION} or newer (found ${process.version}).`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Command } from "commander";
|
||||
import { warnIfUnsupportedNodeVersion } from "@paperclipai/shared/node-version";
|
||||
import { onboard } from "./commands/onboard.js";
|
||||
import { doctor } from "./commands/doctor.js";
|
||||
import { envCommand } from "./commands/env.js";
|
||||
|
|
@ -250,6 +251,8 @@ auth
|
|||
registerClientAuthCommands(auth);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
warnIfUnsupportedNodeVersion(process.versions.node, (message) => console.warn(message));
|
||||
|
||||
let failed = false;
|
||||
try {
|
||||
await program.parseAsync();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatNodeVersionWarning,
|
||||
isSupportedNodeVersion,
|
||||
MINIMUM_NODE_VERSION,
|
||||
NODE_VERSION_INSTALL_GUIDE_URL,
|
||||
warnIfUnsupportedNodeVersion,
|
||||
} from "@paperclipai/shared/node-version";
|
||||
|
||||
describe("isSupportedNodeVersion", () => {
|
||||
it("accepts the Node 24 LTS floor and newer releases", () => {
|
||||
expect(MINIMUM_NODE_VERSION).toBe("24.11.0");
|
||||
expect(isSupportedNodeVersion("v24.11.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion("24.19.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion("v26.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects pre-LTS Node 24 and older major releases", () => {
|
||||
expect(isSupportedNodeVersion("v24.10.0")).toBe(false);
|
||||
expect(isSupportedNodeVersion("v22.23.0")).toBe(false);
|
||||
expect(isSupportedNodeVersion("unknown")).toBe(false);
|
||||
});
|
||||
|
||||
it("provides non-blocking remediation text for unsupported runtimes", () => {
|
||||
expect(formatNodeVersionWarning("v24.11.0")).toBeNull();
|
||||
const warning = formatNodeVersionWarning("v22.23.0");
|
||||
expect(warning).toContain("Node.js v22.23.0 is unsupported");
|
||||
expect(warning).toContain("requires Node.js 24.11.0 or newer");
|
||||
expect(warning).toContain(NODE_VERSION_INSTALL_GUIDE_URL);
|
||||
expect(warning).toContain("piped install.sh form cannot upgrade");
|
||||
expect(warning).toContain("Restart Paperclip after upgrading");
|
||||
});
|
||||
|
||||
it("emits at most one warning when CLI and server boot in the same process", () => {
|
||||
const warnings: string[] = [];
|
||||
expect(
|
||||
warnIfUnsupportedNodeVersion("22.23.0", (message) => warnings.push(message)),
|
||||
).toBe(true);
|
||||
expect(
|
||||
warnIfUnsupportedNodeVersion("22.23.0", (message) => warnings.push(message)),
|
||||
).toBe(false);
|
||||
expect(warnings).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -12,7 +12,7 @@ Current implementation status:
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- Node.js 24.11+
|
||||
- pnpm 9+
|
||||
|
||||
## Dependency Lockfile Policy
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ bash install.sh
|
|||
The bootstrap script:
|
||||
|
||||
1. verifies that the platform is supported;
|
||||
2. ensures Node.js 20 or newer is available;
|
||||
2. ensures Node.js 24.11 or newer is available;
|
||||
3. delegates installation to `paperclipai install`;
|
||||
4. starts interactive onboarding when stdin and stdout are terminals.
|
||||
|
||||
|
|
@ -241,6 +241,12 @@ paperclipai service status
|
|||
presence and drift, running state, configured port ownership, and the running
|
||||
server version.
|
||||
|
||||
The CLI and server also print a non-blocking startup warning when Node.js is
|
||||
below the supported minimum. Upgrade Node.js with a version manager or follow
|
||||
the downloaded `install.sh` workflow under **Recommended Install**. Do not use
|
||||
the piped form for this repair because it requires a supported Node.js runtime
|
||||
before it starts.
|
||||
|
||||
## Uninstall
|
||||
|
||||
Remove the background service and managed CLI payloads:
|
||||
|
|
|
|||
|
|
@ -1313,7 +1313,7 @@ Required UX behaviors:
|
|||
|
||||
## 15.1 Environment
|
||||
|
||||
- Node 20+
|
||||
- Node 24.11+
|
||||
- `DATABASE_URL` optional
|
||||
- if unset, auto-use embedded PostgreSQL under `~/.paperclip/instances/default/db`
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ pause controls, and consistent audits instead of hidden daemon behavior.
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 22+ and `pnpm`.
|
||||
- Node.js 24.11+ and `pnpm`.
|
||||
- A local Paperclip checkout you can run from source. Local plugin installs read source from disk, so the running server must be able to see the path you give it.
|
||||
|
||||
## The five steps
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
FROM ubuntu:24.04
|
||||
|
||||
ARG NODE_MAJOR=20
|
||||
ARG NODE_MAJOR=24
|
||||
ARG PAPERCLIPAI_VERSION=latest
|
||||
ARG HOST_UID=10001
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# syntax=docker/dockerfile:1.6
|
||||
ARG NODE_VERSION=22
|
||||
ARG NODE_VERSION=24
|
||||
ARG TARGETARCH
|
||||
|
||||
# ---------- Stage 1: build agent-shim ----------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Container images for running coding-agent harnesses in sandboxed environments (f
|
|||
|
||||
## Image Lineup
|
||||
|
||||
- **`agent-runtime-base`**: Foundation. Ubuntu 22.04 + Node 22 + git + tini + non-root user (uid 1000) + the agent shim.
|
||||
- **`agent-runtime-base`**: Foundation. Ubuntu 22.04 + Node 24 + git + tini + non-root user (uid 1000) + the agent shim.
|
||||
- **`agent-runtime-opencode`**: Extends base with `opencode-ai` globally installed.
|
||||
- **`agent-runtime-pi`**: Extends base with `@mariozechner/pi-coding-agent`.
|
||||
- **`agent-runtime-codex`**: Extends base with `@openai/codex`.
|
||||
|
|
@ -16,7 +16,7 @@ Container images for running coding-agent harnesses in sandboxed environments (f
|
|||
|
||||
**OS & Runtime:**
|
||||
- Ubuntu 22.04
|
||||
- Node.js 22 (via NodeSource APT repo)
|
||||
- Node.js 24 (via NodeSource APT repo)
|
||||
- git
|
||||
- tini (PID-1 init, ensures signal propagation)
|
||||
- Non-root user `paperclip` (uid/gid 1000)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# syntax=docker/dockerfile:1.6
|
||||
ARG HERMES_VERSION=0.17.0
|
||||
FROM node:22-bookworm-slim
|
||||
FROM node:24-bookworm-slim
|
||||
|
||||
ARG HERMES_VERSION
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:22-alpine
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY server.mjs /app/server.mjs
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:lts-trixie-slim
|
||||
FROM node:24-trixie-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ my-adapter/
|
|||
"picocolors": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Run Paperclip locally with zero external dependencies.
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- Node.js 24.11+
|
||||
- pnpm 9+
|
||||
|
||||
## Start Dev Server
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ This issue does not affect the Docker Sandbox approach.
|
|||
|
||||
### Node version mismatch in community template images
|
||||
|
||||
Some community-built sandbox templates (e.g. `olegselajev241/openclaw-dmr:latest`) ship Node 20, but OpenClaw requires Node >=22.12.0. Use our locally built `openclaw:local` image as the sandbox template instead, which includes Node 22.
|
||||
Some community-built sandbox templates (e.g. `olegselajev241/openclaw-dmr:latest`) ship Node 20, but OpenClaw requires Node >=22.12.0. Use our locally built `openclaw:local` image as the sandbox template instead, which includes Node 24.
|
||||
|
||||
### Gateway takes ~15 seconds to respond after start
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ Paperclip is a monorepo with four main layers.
|
|||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Frontend | React 19, Vite 6, React Router 7, Radix UI, Tailwind CSS 4, TanStack Query |
|
||||
| Backend | Node.js 20+, Express.js 5, TypeScript |
|
||||
| Backend | Node.js 24.11+, Express.js 5, TypeScript |
|
||||
| Database | PostgreSQL 17 (or embedded PGlite), Drizzle ORM |
|
||||
| Auth | Better Auth (sessions + API keys) |
|
||||
| Adapters | Claude Code CLI, Codex CLI, shell process, HTTP webhook |
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ npx paperclipai run
|
|||
|
||||
## Local Development
|
||||
|
||||
For contributors working on Paperclip itself. Prerequisites: Node.js 20+ and pnpm 9+.
|
||||
For contributors working on Paperclip itself. Prerequisites: Node.js 24.11+ and pnpm 9+.
|
||||
|
||||
Clone the repository, then:
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
"release:bootstrap-package": "node scripts/bootstrap-npm-package.mjs",
|
||||
"check:tokens": "node scripts/check-forbidden-tokens.mjs",
|
||||
"check:token-gates": "node scripts/check-token-gates.mjs",
|
||||
"check:node-version": "node scripts/check-node-version-policy.mjs",
|
||||
"check:no-git-push": "node scripts/check-no-git-push.mjs",
|
||||
"test:check-no-git-push": "node --test scripts/check-no-git-push.test.mjs",
|
||||
"test:install-sh-docker": "./scripts/test-install-sh-docker.sh",
|
||||
|
|
@ -76,7 +77,7 @@
|
|||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=24.11.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"pnpm": {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ describe("buildSandboxNpmInstallCommand", () => {
|
|||
expect(command).toContain("npm install -g --prefix \"$HOME/.local\" '@google/gemini-cli'");
|
||||
});
|
||||
|
||||
it("bootstraps npm from a portable Node tarball when missing", () => {
|
||||
it("bootstraps npm from a portable Node tarball when missing or unsupported", () => {
|
||||
const command = buildSandboxNpmInstallCommand("@google/gemini-cli");
|
||||
expect(command).toContain("if ! command -v npm >/dev/null 2>&1; then");
|
||||
expect(command).toContain("if ! command -v npm >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1 ||");
|
||||
expect(command).toContain("process.versions.node");
|
||||
expect(command).toContain("v[0]>24||(v[0]===24&&v[1]>=11)");
|
||||
expect(command).toContain("https://nodejs.org/dist/");
|
||||
expect(command).toContain('export PATH="$HOME/.local/bin:$PATH"');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@ function shellSingleQuote(value: string): string {
|
|||
// $HOME/.local/bin.
|
||||
const ENSURE_NPM_PREAMBLE =
|
||||
"PAPERCLIP_NPM_BOOTSTRAPPED=; " +
|
||||
'if ! command -v npm >/dev/null 2>&1; then ' +
|
||||
"if ! command -v npm >/dev/null 2>&1 || " +
|
||||
"! command -v node >/dev/null 2>&1 || " +
|
||||
"! node -e 'const v=process.versions.node.split(\".\").map(Number);" +
|
||||
"process.exit(v[0]>24||(v[0]===24&&v[1]>=11)?0:1)' >/dev/null 2>&1; then " +
|
||||
'NODE_ARCH="$(uname -m)"; ' +
|
||||
'case "$NODE_ARCH" in ' +
|
||||
"x86_64) NODE_ARCH=x64 ;; " +
|
||||
"aarch64|arm64) NODE_ARCH=arm64 ;; " +
|
||||
"esac; " +
|
||||
'NODE_VERSION="v22.11.0"; ' +
|
||||
'NODE_VERSION="v24.11.0"; ' +
|
||||
'NODE_TARBALL="node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz"; ' +
|
||||
'mkdir -p "$HOME/.local"; ' +
|
||||
'curl -fsSL "https://nodejs.org/dist/${NODE_VERSION}/${NODE_TARBALL}" -o "/tmp/${NODE_TARBALL}" && ' +
|
||||
|
|
|
|||
|
|
@ -59,7 +59,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ Operational fields:
|
|||
|
||||
Notes:
|
||||
- filesystemScope and networkScope are spawn-level confinement and are orthogonal to Claude permission flags. Both require Bubblewrap on the host and select the CLI engine in auto mode; engine="acp" is rejected because ACP confinement is not yet supported. networkScope="allowlist" injects HTTP_PROXY/HTTPS_PROXY for the CLI while its private network namespace blocks direct sockets, so every required provider/API hostname must be listed explicitly.
|
||||
- The Claude ACP lane requires Node >=22.12.0 and @agentclientprotocol/claude-agent-acp to be installed with this adapter package. Auto engine selection falls back to CLI when those prerequisites are unavailable; explicit engine="acp" fails loudly.
|
||||
- The Claude ACP lane requires Node >=24.11.0 and @agentclientprotocol/claude-agent-acp to be installed with this adapter package. Auto engine selection falls back to CLI when those prerequisites are unavailable; explicit engine="acp" fails loudly.
|
||||
- For ACP runs, model selection is passed through ANTHROPIC_MODEL at ACP server startup; Paperclip-managed Claude permissions and ephemeral skill materialization are handled by the shared ACP engine.
|
||||
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -259,9 +259,9 @@ describe("claude_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("checks the Node version required by the Claude ACP runtime", () => {
|
||||
setNodeVersion("v22.11.0");
|
||||
setNodeVersion("v24.10.0");
|
||||
expect(nodeVersionMeetsClaudeAcpMinimum()).toBe(false);
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
expect(nodeVersionMeetsClaudeAcpMinimum()).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -270,7 +270,7 @@ describe("claude_local ACP lane", () => {
|
|||
const commandPath = path.join(root, "bin", "claude-agent-acp");
|
||||
await fs.mkdir(path.dirname(commandPath), { recursive: true });
|
||||
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
|
||||
expect(resolveClaudeExecutionEngine({})).toEqual({ engine: "acp", explicit: false });
|
||||
await expect(
|
||||
|
|
@ -286,7 +286,7 @@ describe("claude_local ACP lane", () => {
|
|||
}),
|
||||
).resolves.toEqual({ engine: "cli", explicit: true });
|
||||
|
||||
setNodeVersion("v22.11.0");
|
||||
setNodeVersion("v24.10.0");
|
||||
await expect(
|
||||
resolveClaudeExecutionEngineForRun({
|
||||
config: { agentCommand: commandPath },
|
||||
|
|
@ -341,7 +341,7 @@ describe("claude_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("uses ACP for bridged sandbox auto runs when the ACP command is configured as a shell command", async () => {
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveClaudeExecutionEngineForRun({
|
||||
config: { agentCommand: "claude-agent-acp" },
|
||||
|
|
@ -367,7 +367,7 @@ describe("claude_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for one-shot sandbox auto runs", async () => {
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveClaudeExecutionEngineForRun({
|
||||
config: {},
|
||||
|
|
@ -386,7 +386,7 @@ describe("claude_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for non-sandbox remote auto runs", async () => {
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveClaudeExecutionEngineForRun({
|
||||
config: {},
|
||||
|
|
@ -418,7 +418,7 @@ describe("claude_local ACP lane", () => {
|
|||
const commandPath = path.join(root, "bin", "claude-agent-acp");
|
||||
await fs.mkdir(path.dirname(commandPath), { recursive: true });
|
||||
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
|
||||
const result = await testClaudeAcpEnvironment({
|
||||
adapterType: "claude_local",
|
||||
|
|
@ -833,7 +833,7 @@ describe("claude_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for a runner-less sandbox even when the ACP command is set", async () => {
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveClaudeExecutionEngineForRun({
|
||||
config: { agentCommand: "claude-agent-acp" },
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
|||
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageRootDir = path.resolve(moduleDir, "../..");
|
||||
const MIN_ACP_NODE_VERSION = "22.12.0";
|
||||
const MIN_ACP_NODE_VERSION = "24.11.0";
|
||||
|
||||
export type ClaudeExecutionEngine = "cli" | "acp";
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,5 +129,5 @@ Notes:
|
|||
- Some model/tool combinations reject certain effort levels (for example minimal with web search enabled).
|
||||
- Fast mode is supported on GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`.
|
||||
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
|
||||
- Codex ACP is the preferred auto lane when Node >=22.13.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
|
||||
- Codex ACP is the preferred auto lane when Node >=24.11.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ describe("codex_local ACP lane", () => {
|
|||
const commandPath = path.join(root, "bin", "codex-acp");
|
||||
await fs.mkdir(path.dirname(commandPath), { recursive: true });
|
||||
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
|
||||
expect(resolveCodexExecutionEngine({})).toEqual({ engine: "acp", explicit: false });
|
||||
await expect(
|
||||
|
|
@ -296,7 +296,7 @@ describe("codex_local ACP lane", () => {
|
|||
explicit: true,
|
||||
});
|
||||
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.10.0");
|
||||
await expect(
|
||||
resolveCodexExecutionEngineForRun({
|
||||
config: { agentCommand: commandPath },
|
||||
|
|
@ -375,7 +375,7 @@ describe("codex_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("uses ACP for bridged sandbox auto runs when the ACP command is configured as a shell command", async () => {
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveCodexExecutionEngineForRun({
|
||||
config: { agentCommand: "codex-acp" },
|
||||
|
|
@ -401,7 +401,7 @@ describe("codex_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for one-shot sandbox auto runs", async () => {
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveCodexExecutionEngineForRun({
|
||||
config: {},
|
||||
|
|
@ -420,7 +420,7 @@ describe("codex_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for non-sandbox remote auto runs", async () => {
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveCodexExecutionEngineForRun({
|
||||
config: {},
|
||||
|
|
@ -479,9 +479,9 @@ describe("codex_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("checks the Node version required by the ACPX runtime", () => {
|
||||
setNodeVersion("v22.12.0");
|
||||
setNodeVersion("v24.10.0");
|
||||
expect(nodeVersionMeetsCodexAcpMinimum()).toBe(false);
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
expect(nodeVersionMeetsCodexAcpMinimum()).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -490,7 +490,7 @@ describe("codex_local ACP lane", () => {
|
|||
const commandPath = path.join(root, "bin", "codex-acp");
|
||||
await fs.mkdir(path.dirname(commandPath), { recursive: true });
|
||||
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
|
||||
const result = await testCodexAcpEnvironment({
|
||||
adapterType: "codex_local",
|
||||
|
|
@ -543,7 +543,7 @@ describe("codex_local ACP lane", () => {
|
|||
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
|
||||
await fs.mkdir(sharedCodexHome, { recursive: true });
|
||||
await fs.writeFile(path.join(sharedCodexHome, "auth.json"), '{"OPENAI_API_KEY":"sk-shared"}', "utf8");
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
process.env.CODEX_HOME = sharedCodexHome;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
|
||||
|
|
@ -591,7 +591,7 @@ describe("codex_local ACP lane", () => {
|
|||
await fs.mkdir(path.dirname(commandPath), { recursive: true });
|
||||
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
|
||||
await fs.mkdir(sharedCodexHome, { recursive: true });
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
process.env.CODEX_HOME = sharedCodexHome;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
|
||||
|
|
@ -632,7 +632,7 @@ describe("codex_local ACP lane", () => {
|
|||
"codex-home",
|
||||
);
|
||||
await fs.mkdir(sharedCodexHome, { recursive: true });
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
process.env.CODEX_HOME = sharedCodexHome;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
|
||||
|
|
@ -1136,7 +1136,7 @@ describe("codex_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for a runner-less sandbox even when the ACP command is set", async () => {
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
// Isolate the missing bidirectional runner as the sole fallback cause:
|
||||
// provide a valid ACP command and Node version so the only difference from
|
||||
// the runner-backed ACP case is the absent `runner`.
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js";
|
|||
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageRootDir = path.resolve(moduleDir, "../..");
|
||||
const MIN_ACP_NODE_VERSION = "22.13.0";
|
||||
const MIN_ACP_NODE_VERSION = "24.11.0";
|
||||
|
||||
export type CodexExecutionEngine = "cli" | "acp";
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ Operational fields:
|
|||
- graceSec (number, optional): SIGTERM grace period in seconds
|
||||
|
||||
Notes:
|
||||
- Gemini ACP is the preferred auto lane when Node >=20 and the local Gemini CLI command is available. It runs Gemini CLI's native \`gemini --acp\` server through Paperclip's shared ACP engine, including selected skill links, Paperclip runtime prompt/env guidance, model config, and persistent ACP session state. Auto selection falls back to the CLI lane when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
|
||||
- Gemini ACP is the preferred auto lane when Node >=24.11.0 and the local Gemini CLI command is available. It runs Gemini CLI's native \`gemini --acp\` server through Paperclip's shared ACP engine, including selected skill links, Paperclip runtime prompt/env guidance, model config, and persistent ACP session state. Auto selection falls back to the CLI lane when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
|
||||
- Runs use --prompt for non-interactive execution, not stdin.
|
||||
- The adapter sets a headless-safe terminal/browser environment for Gemini CLI child processes so unattended runs do not wait on browser auth or 256-color terminal prompts.
|
||||
- Sessions resume with --resume when stored session cwd matches the current cwd.
|
||||
|
|
|
|||
|
|
@ -244,9 +244,9 @@ describe("gemini_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("checks the Node version required by the Gemini ACP runtime", () => {
|
||||
setNodeVersion("v19.9.0");
|
||||
setNodeVersion("v24.10.0");
|
||||
expect(nodeVersionMeetsGeminiAcpMinimum()).toBe(false);
|
||||
setNodeVersion("v20.0.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
expect(nodeVersionMeetsGeminiAcpMinimum()).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -255,7 +255,7 @@ describe("gemini_local ACP lane", () => {
|
|||
const commandPath = path.join(root, "bin", "gemini");
|
||||
await fs.mkdir(path.dirname(commandPath), { recursive: true });
|
||||
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
|
||||
setNodeVersion("v20.0.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
|
||||
expect(resolveGeminiExecutionEngine({})).toEqual({ engine: "acp", explicit: false });
|
||||
await expect(
|
||||
|
|
@ -275,7 +275,7 @@ describe("gemini_local ACP lane", () => {
|
|||
explicit: true,
|
||||
});
|
||||
|
||||
setNodeVersion("v19.9.0");
|
||||
setNodeVersion("v24.10.0");
|
||||
await expect(
|
||||
resolveGeminiExecutionEngineForRun({
|
||||
config: { command: commandPath },
|
||||
|
|
@ -295,7 +295,7 @@ describe("gemini_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for non-sandbox remote auto runs", async () => {
|
||||
setNodeVersion("v20.0.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveGeminiExecutionEngineForRun({
|
||||
config: { agentCommand: "gemini --acp" },
|
||||
|
|
@ -323,7 +323,7 @@ describe("gemini_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for one-shot sandbox auto runs", async () => {
|
||||
setNodeVersion("v20.0.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveGeminiExecutionEngineForRun({
|
||||
config: {},
|
||||
|
|
@ -342,7 +342,7 @@ describe("gemini_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("uses ACP for bridged sandbox auto runs when the ACP command is configured as a shell command", async () => {
|
||||
setNodeVersion("v20.0.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveGeminiExecutionEngineForRun({
|
||||
config: { agentCommand: "gemini --acp" },
|
||||
|
|
@ -681,7 +681,7 @@ describe("gemini_local ACP lane", () => {
|
|||
});
|
||||
|
||||
it("falls back to the CLI lane for a runner-less sandbox even when the ACP command is set", async () => {
|
||||
setNodeVersion("v22.13.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
await expect(
|
||||
resolveGeminiExecutionEngineForRun({
|
||||
config: { agentCommand: "gemini --acp" },
|
||||
|
|
@ -706,7 +706,7 @@ describe("gemini_local ACP lane", () => {
|
|||
await fs.writeFile(path.join(bin, "gemini"), "#!/usr/bin/env sh\n", "utf8");
|
||||
process.env.PATH = `${bin}${path.delimiter}${process.env.PATH ?? ""}`;
|
||||
process.env.GEMINI_API_KEY = "test-key";
|
||||
setNodeVersion("v20.0.0");
|
||||
setNodeVersion("v24.11.0");
|
||||
|
||||
const result = await testGeminiAcpEnvironment({
|
||||
adapterType: "gemini_local",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
|||
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageRootDir = path.resolve(moduleDir, "../..");
|
||||
const MIN_ACP_NODE_VERSION = "20.0.0";
|
||||
const MIN_ACP_NODE_VERSION = "24.11.0";
|
||||
|
||||
export type GeminiExecutionEngine = "cli" | "acp";
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,11 +71,11 @@
|
|||
"@paperclipai/hermes-paperclip-adapter": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,11 +103,11 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,8 +55,11 @@
|
|||
"ws": "^8.21.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@
|
|||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,13 @@
|
|||
"postgres": "^3.4.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"tsx": "^4.23.12",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,11 @@
|
|||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Or run the source directly during development:
|
|||
|
||||
```sh
|
||||
cd packages/kv-demo-mcp-server
|
||||
node --experimental-strip-types src/main.ts # Node 22+/24
|
||||
node --experimental-strip-types src/main.ts # Node 24+
|
||||
```
|
||||
|
||||
By default it listens on `http://127.0.0.1:8848` and prints three URLs to
|
||||
|
|
|
|||
|
|
@ -48,8 +48,11 @@
|
|||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,11 @@
|
|||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,10 @@
|
|||
"@paperclipai/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ export function scaffoldPluginProject(options: ScaffoldPluginOptions): string {
|
|||
"@paperclipai/plugin-sdk": sdkDependency,
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.0.8",
|
||||
esbuild: "^0.27.3",
|
||||
rollup: "^4.38.0",
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"esbuild": "^0.28.1",
|
||||
"rollup": "^4.62.4",
|
||||
|
|
@ -41,5 +41,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"codemirror": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"esbuild": "^0.28.1",
|
||||
|
|
@ -38,5 +38,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
"@paperclipai/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"react": "^19.2.8",
|
||||
|
|
@ -31,5 +31,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.28.1",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"react": "^19.2.8",
|
||||
|
|
@ -33,5 +33,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
"@paperclipai/shared": "workspace:*",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"esbuild": "^0.28.1",
|
||||
"rollup": "^4.62.4",
|
||||
|
|
@ -42,5 +42,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@
|
|||
"@paperclipai/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
"@paperclipai/plugin-sdk": "workspace:*",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"esbuild": "^0.28.1",
|
||||
|
|
@ -51,5 +51,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
"@pierre/diffs": "^1.2.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"esbuild": "^0.28.1",
|
||||
|
|
@ -67,5 +67,8 @@
|
|||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,8 @@
|
|||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.8",
|
||||
"wrangler": "^4.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,8 +51,11 @@
|
|||
"postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,8 +53,11 @@
|
|||
"@daytonaio/sdk": "0.203.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,8 +53,11 @@
|
|||
"e2b": "^2.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ Operational notes:
|
|||
- Reusable leases keep the VM alive between runs. exe.dev does not expose a documented "stop and later resume" command in the public CLI docs, so `reuseLease: true` means "retain the VM" rather than "suspend it."
|
||||
- The provisioning path uses `https://exe.dev/exec`, which exe.dev documents as a command-style HTTPS API with a 30-second request timeout. Typical `new` calls are expected to fit inside that limit; command execution itself does not use `/exec`.
|
||||
- Probes still create and delete a real exe.dev VM through `/exec`, and so do the `new`/`rm` calls inside the normal acquire/release lifecycle. Treat all of those as real provisioning cost, not just probes.
|
||||
- exe.dev runs `--setup-script` as the unprivileged `exedev` user, not as root. That user has passwordless `sudo`, so any system-level steps in a custom `setupScript` must invoke `sudo` explicitly (for example `sudo apt-get install -y …`). When you omit `setupScript`, the plugin supplies a default that installs Node 20 via the official nodesource script — Paperclip's sandbox callback bridge is a Node program, so the VM needs `node` on `PATH` before the bridge can launch.
|
||||
- exe.dev runs `--setup-script` as the unprivileged `exedev` user, not as root. That user has passwordless `sudo`, so any system-level steps in a custom `setupScript` must invoke `sudo` explicitly (for example `sudo apt-get install -y …`). When you omit `setupScript`, the plugin supplies a default that installs Node 24 via the official nodesource script — Paperclip's sandbox callback bridge is a Node program, so the VM needs `node` on `PATH` before the bridge can launch.
|
||||
|
||||
## Local development
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,11 @@
|
|||
"postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -372,7 +372,9 @@ describe("exe.dev sandbox provider plugin", () => {
|
|||
|
||||
const body = String(fetchMock.mock.calls[0]?.[1]?.body ?? "");
|
||||
expect(body).toContain("--setup-script=");
|
||||
expect(body).toContain("nodesource.com/setup_20.x");
|
||||
expect(body).toContain("process.versions.node");
|
||||
expect(body).toContain("v[0]>24||(v[0]===24&&v[1]>=11)");
|
||||
expect(body).toContain("nodesource.com/setup_24.x");
|
||||
expect(body).toContain("sudo apt-get install -y nodejs");
|
||||
});
|
||||
|
||||
|
|
@ -425,7 +427,7 @@ describe("exe.dev sandbox provider plugin", () => {
|
|||
await acquirePromise?.catch((error: Error) => {
|
||||
// Operator did not supply a setupScript, so the visible default install
|
||||
// is not a secret and stays in the error for debuggability.
|
||||
expect(error.message).toContain("nodesource.com/setup_20.x");
|
||||
expect(error.message).toContain("nodesource.com/setup_24.x");
|
||||
expect(error.message).not.toContain("[REDACTED]");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,11 +74,13 @@ const UUID_SECRET_REF_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-
|
|||
// exe.dev's `--setup-script` runs at VM init as the unprivileged `exedev` user, which
|
||||
// has passwordless sudo. The Paperclip sandbox callback bridge is a Node script, so
|
||||
// every Paperclip workload on this provider needs node on PATH before the bridge can
|
||||
// start. When the operator hasn't supplied their own setup script, install Node 20 via
|
||||
// start. When the operator hasn't supplied their own setup script, install Node 24 via
|
||||
// nodesource so the VM comes up ready for Paperclip out of the box.
|
||||
const DEFAULT_SETUP_SCRIPT =
|
||||
"command -v node >/dev/null 2>&1 || " +
|
||||
"(curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && " +
|
||||
"(command -v node >/dev/null 2>&1 && " +
|
||||
"node -e 'const v=process.versions.node.split(\".\").map(Number);" +
|
||||
"process.exit(v[0]>24||(v[0]===24&&v[1]>=11)?0:1)' >/dev/null 2>&1) || " +
|
||||
"(curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && " +
|
||||
"sudo apt-get install -y nodejs)";
|
||||
|
||||
class ExeDevApiError extends Error {
|
||||
|
|
|
|||
|
|
@ -52,8 +52,11 @@
|
|||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ The host plugin installer runs `npm install` into the managed plugin directory,
|
|||
|
||||
## Runtime support note
|
||||
|
||||
Modal's official JS SDK README pins support to **Node 22 or later**. Paperclip's repo baseline is currently `node >= 20`; empirically `modal@0.7.4` imports and operates against the Modal API under Node 20, so the plugin runs there today, but the vendor support contract is Node 22+. The plugin logs a startup warning when it detects Node `< 22`. Operators who can pin their Paperclip runtime to Node 22+ should do so; treat Node-20 usage as best-effort until the host bumps its baseline.
|
||||
|
||||
The empirical Node 20 compatibility check is recorded in [PAPA-352](/PAPA/issues/PAPA-352).
|
||||
Paperclip and this plugin require **Node 24 or later**. The plugin logs a startup warning when it detects an older host runtime.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -27,7 +25,7 @@ Configure Modal from `Instance Settings -> Environments`, not from the plugin's
|
|||
| Field | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `appName` | yes | Modal App name. The plugin calls `modal.apps.fromName(appName, { createIfMissing: true })`, so the App is created on first acquire if it does not already exist. |
|
||||
| `image` | yes | Container image passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:20`. |
|
||||
| `image` | yes | Container image passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:24`. |
|
||||
| `tokenId` / `tokenSecret` | yes | Modal auth tokens. Both must be provided together. Paperclip stores pasted values as company secrets. The plugin worker runs in a child process that does not inherit host env vars, so `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET` set on the Paperclip server are **not** read by the plugin — provide the tokens in this form. |
|
||||
| `environment` | no | Optional Modal environment name. Falls back to the SDK profile default. |
|
||||
| `workdir` | no | Remote working directory inside the sandbox. Defaults to `/workspace/paperclip`. |
|
||||
|
|
|
|||
|
|
@ -53,8 +53,11 @@
|
|||
"modal": "^0.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ const manifest: PaperclipPluginManifestV1 = {
|
|||
image: {
|
||||
type: "string",
|
||||
description:
|
||||
"Container image reference passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:22`. The default `node:22` satisfies the sandbox runtime contract (node, sh, and tar on PATH).",
|
||||
default: "node:22",
|
||||
"Container image reference passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:24`. The default `node:24` satisfies the sandbox runtime contract (node, sh, and tar on PATH).",
|
||||
default: "node:24",
|
||||
},
|
||||
tokenId: {
|
||||
type: "string",
|
||||
|
|
|
|||
|
|
@ -717,7 +717,7 @@ describe("modal manifest form defaults", () => {
|
|||
|
||||
it("pre-fills the required app name and image so the form works out of the box", () => {
|
||||
expect(properties.appName?.default).toBe("paperclip");
|
||||
expect(properties.image?.default).toBe("node:22");
|
||||
expect(properties.image?.default).toBe("node:24");
|
||||
});
|
||||
|
||||
it("declares no default on secret-ref fields, which would be persisted as a company secret", () => {
|
||||
|
|
|
|||
|
|
@ -318,8 +318,8 @@ async function getSandboxOrNull(
|
|||
|
||||
function warnIfUnsupportedNode(logger: { warn: (msg: string) => void } | undefined): void {
|
||||
const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
||||
if (Number.isFinite(major) && major < 22) {
|
||||
const message = `Modal sandbox provider is running on Node ${process.versions.node}; Modal officially supports Node 22+. The plugin will attempt to operate but vendor support is not guaranteed below Node 22.`;
|
||||
if (Number.isFinite(major) && major < 24) {
|
||||
const message = `Modal sandbox provider is running on Node ${process.versions.node}; Paperclip requires Node 24+. Upgrade the host runtime before using this plugin.`;
|
||||
logger?.warn(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,8 +54,11 @@
|
|||
"novita-sandbox": "^1.0.3-b2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@
|
|||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
|
|
@ -123,5 +123,8 @@
|
|||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
|||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
target: "node24",
|
||||
sourcemap,
|
||||
minify,
|
||||
external: ["react", "react-dom"],
|
||||
|
|
@ -92,7 +92,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
|||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
target: "node24",
|
||||
sourcemap,
|
||||
external: ["@paperclipai/plugin-sdk"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@
|
|||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
export const MINIMUM_NODE_VERSION = "24.11.0";
|
||||
export const NODE_VERSION_INSTALL_GUIDE_URL =
|
||||
"https://github.com/paperclipai/paperclip/blob/master/doc/INSTALLING.md#recommended-install";
|
||||
const NODE_VERSION_WARNING_EMITTED = Symbol.for("@paperclipai/node-version-warning-emitted");
|
||||
|
||||
function parseVersion(version: string): [number, number, number] | null {
|
||||
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
||||
if (!match) return null;
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
||||
}
|
||||
|
||||
export function isSupportedNodeVersion(version: string): boolean {
|
||||
const current = parseVersion(version);
|
||||
const minimum = parseVersion(MINIMUM_NODE_VERSION);
|
||||
if (!current || !minimum) return false;
|
||||
|
||||
for (let index = 0; index < current.length; index += 1) {
|
||||
if (current[index] > minimum[index]) return true;
|
||||
if (current[index] < minimum[index]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function formatNodeVersionWarning(version: string): string | null {
|
||||
if (isSupportedNodeVersion(version)) return null;
|
||||
const currentVersion = version.trim() || "unknown";
|
||||
return [
|
||||
`[paperclip] warning: Node.js ${currentVersion} is unsupported. Paperclip requires Node.js ${MINIMUM_NODE_VERSION} or newer.`,
|
||||
"Upgrade Node.js with your version manager, or follow the recommended downloaded install.sh workflow:",
|
||||
` ${NODE_VERSION_INSTALL_GUIDE_URL}`,
|
||||
"The piped install.sh form cannot upgrade an unsupported Node.js runtime.",
|
||||
"Restart Paperclip after upgrading.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function warnIfUnsupportedNodeVersion(
|
||||
version: string,
|
||||
warn: (message: string) => void,
|
||||
): boolean {
|
||||
const warning = formatNodeVersionWarning(version);
|
||||
if (!warning) return false;
|
||||
|
||||
const warningState = globalThis as unknown as Record<symbol, boolean | undefined>;
|
||||
if (warningState[NODE_VERSION_WARNING_EMITTED]) return false;
|
||||
|
||||
warningState[NODE_VERSION_WARNING_EMITTED] = true;
|
||||
warn(warning);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -52,6 +52,9 @@
|
|||
"@paperclipai/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1"
|
||||
"@types/node": "^24.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@
|
|||
"test": "node scripts/build-native.mjs && vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@
|
|||
"validate": "node ../../cli/node_modules/tsx/dist/cli.mjs scripts/validate-catalog.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1"
|
||||
"@types/node": "^24.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const expectedEngine = ">=24.11.0";
|
||||
const expectedTypes = "^24.0.0";
|
||||
const failures = [];
|
||||
const skippedDirectories = new Set([".git", ".paperclip", "coverage", "data", "dist", "node_modules"]);
|
||||
|
||||
function relative(filePath) {
|
||||
return path.relative(repoRoot, filePath) || ".";
|
||||
}
|
||||
|
||||
function walk(directory, visit) {
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && skippedDirectories.has(entry.name)) continue;
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(entryPath, visit);
|
||||
else if (entry.isFile()) visit(entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
walk(repoRoot, (filePath) => {
|
||||
if (path.basename(filePath) !== "package.json") return;
|
||||
const manifest = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
for (const section of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
||||
const specifier = manifest[section]?.["@types/node"];
|
||||
if (specifier && specifier !== expectedTypes) {
|
||||
failures.push(`${relative(filePath)}: ${section}.@types/node must be ${expectedTypes}, found ${specifier}`);
|
||||
}
|
||||
}
|
||||
if (manifest.engines?.node !== expectedEngine) {
|
||||
failures.push(`${relative(filePath)}: engines.node must be ${expectedEngine}, found ${manifest.engines?.node ?? "missing"}`);
|
||||
}
|
||||
});
|
||||
|
||||
const workflowRoot = path.join(repoRoot, ".github", "workflows");
|
||||
walk(workflowRoot, (filePath) => {
|
||||
if (!/\.ya?ml$/.test(filePath)) return;
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
for (const match of source.matchAll(/node-version:\s*["']?([^\s"'#]+)/g)) {
|
||||
if (match[1] !== "24") failures.push(`${relative(filePath)}: node-version must be 24, found ${match[1]}`);
|
||||
}
|
||||
});
|
||||
|
||||
walk(repoRoot, (filePath) => {
|
||||
if (!path.basename(filePath).startsWith("Dockerfile")) return;
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
for (const match of source.matchAll(/^\s*FROM\s+node:([^\s]+)/gm)) {
|
||||
if (!match[1].startsWith("24-")) failures.push(`${relative(filePath)}: Node base image must use the Node 24 major, found node:${match[1]}`);
|
||||
}
|
||||
for (const match of source.matchAll(/^\s*ARG\s+NODE_(?:MAJOR|VERSION)=([^\s]+)/gm)) {
|
||||
if (match[1] !== "24") failures.push(`${relative(filePath)}: Node build argument must be 24, found ${match[1]}`);
|
||||
}
|
||||
});
|
||||
|
||||
const requiredSourceFragments = [
|
||||
[".nvmrc", "24"],
|
||||
["packages/shared/src/node-version.ts", 'MINIMUM_NODE_VERSION = "24.11.0"'],
|
||||
["scripts/install.sh", "MIN_NODE_VERSION=\"${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}.${MIN_NODE_PATCH}\""],
|
||||
["scripts/install.sh", "MIN_NODE_MAJOR=24"],
|
||||
["scripts/install.sh", "MIN_NODE_MINOR=11"],
|
||||
["scripts/install.sh", "DEFAULT_NODE_MAJOR=24"],
|
||||
["docker/agent-runtime/Dockerfile.base", "ARG NODE_VERSION=24"],
|
||||
["packages/adapter-utils/src/sandbox-install-command.ts", "NODE_VERSION=\"v24.11.0\""],
|
||||
["packages/plugins/sandbox-providers/exe-dev/src/plugin.ts", "nodesource.com/setup_24.x"],
|
||||
["packages/plugins/sandbox-providers/modal/src/manifest.ts", 'default: "node:24"'],
|
||||
["cli/esbuild.config.mjs", 'target: "node24"'],
|
||||
["packages/plugins/sdk/src/bundlers.ts", 'target: "node24"'],
|
||||
["scripts/generate-npm-package-json.mjs", `engines: { node: "${expectedEngine}" }`],
|
||||
];
|
||||
|
||||
for (const [filePath, fragment] of requiredSourceFragments) {
|
||||
const source = fs.readFileSync(path.join(repoRoot, filePath), "utf8");
|
||||
if (!source.includes(fragment)) failures.push(`${filePath}: missing Node 24 policy fragment ${JSON.stringify(fragment)}`);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("Node version policy check failed:");
|
||||
for (const failure of failures) console.error(`- ${failure}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Node version policy check passed (Node >=24.11.0, @types/node 24.x).");
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
# -> update --check -> update --rollback -> reinstall (payload reuse)
|
||||
# -> bad-ref failure hygiene -> service lifecycle -> uninstall (data preserved)
|
||||
#
|
||||
# Machine requirements: bash, curl, tar, node >= 20 (with corepack), npm.
|
||||
# Machine requirements: bash, curl, tar, node >= 24.11 (with corepack), npm.
|
||||
# The machine's $HOME must not already contain a managed install.
|
||||
#
|
||||
# Env knobs:
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ const publishPkg = {
|
|||
homepage: cliPkg.homepage,
|
||||
bugs: cliPkg.bugs,
|
||||
files: cliPkg.files,
|
||||
engines: { node: ">=20" },
|
||||
engines: { node: ">=24.11.0" },
|
||||
dependencies: sortedDeps,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MIN_NODE_MAJOR=20
|
||||
DEFAULT_NODE_MAJOR=22
|
||||
MIN_NODE_MAJOR=24
|
||||
MIN_NODE_MINOR=11
|
||||
MIN_NODE_PATCH=0
|
||||
MIN_NODE_VERSION="${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}.${MIN_NODE_PATCH}"
|
||||
DEFAULT_NODE_MAJOR=24
|
||||
PAPERCLIP_PACKAGE="paperclipai"
|
||||
PUBLIC_NPM_REGISTRY="https://registry.npmjs.org"
|
||||
HOMEBREW_INSTALL_COMMIT="99e13e96cbbdc1ac1ac09c0a40b450bf219ef3aa"
|
||||
HOMEBREW_INSTALL_SHA256="99287f194a8b3c9e6b0203a11a5fa54518be57209343e6bb954dec4635796d9d"
|
||||
NODESOURCE_DISTRIBUTIONS_COMMIT="9b431d8ae0f10df272598585855c6eca6c0e1bd2"
|
||||
NODESOURCE_DEB_SHA256="575583bbac2fccc0b5edd0dbc03e222d9f9dc8d724da996d22754d6411104fd1"
|
||||
NODESOURCE_RPM_SHA256="b0ed2b9b66002e7ee802e8777cf3a92b25f1ecc0129812dc6f59a43a536810cc"
|
||||
NODESOURCE_DEB_SHA256="6e3d580f5bd7ccf2aa1e8df8d35c60d78e873c3ff8beb282c9bebd914904ad72"
|
||||
NODESOURCE_RPM_SHA256="5550ad302050f887377a0451e720b800466573ebc83392fea80924393dba642b"
|
||||
|
||||
CANARY=0
|
||||
VERSION=""
|
||||
|
|
@ -190,19 +193,19 @@ esac
|
|||
|
||||
log "Detected $OS_NAME/$ARCH_NAME"
|
||||
|
||||
node_major() {
|
||||
local version
|
||||
has_supported_node() {
|
||||
local version major minor patch
|
||||
command -v node >/dev/null 2>&1 || return 1
|
||||
version="$(node --version 2>/dev/null || true)"
|
||||
version="${version#v}"
|
||||
printf '%s' "${version%%.*}"
|
||||
}
|
||||
|
||||
has_supported_node() {
|
||||
local major
|
||||
command -v node >/dev/null 2>&1 || return 1
|
||||
major="$(node_major)"
|
||||
[[ "$major" =~ ^[0-9]+$ ]] || return 1
|
||||
[ "$major" -ge "$MIN_NODE_MAJOR" ] || return 1
|
||||
IFS=. read -r major minor patch <<< "$version"
|
||||
patch="${patch%%-*}"
|
||||
[[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ && "$patch" =~ ^[0-9]+$ ]] || return 1
|
||||
if [ "$major" -lt "$MIN_NODE_MAJOR" ] ||
|
||||
{ [ "$major" -eq "$MIN_NODE_MAJOR" ] && [ "$minor" -lt "$MIN_NODE_MINOR" ]; } ||
|
||||
{ [ "$major" -eq "$MIN_NODE_MAJOR" ] && [ "$minor" -eq "$MIN_NODE_MINOR" ] && [ "$patch" -lt "$MIN_NODE_PATCH" ]; }; then
|
||||
return 1
|
||||
fi
|
||||
command -v npm >/dev/null 2>&1 || return 1
|
||||
command -v npx >/dev/null 2>&1 || return 1
|
||||
}
|
||||
|
|
@ -339,7 +342,7 @@ if has_supported_node; then
|
|||
log "Using Node.js $(node --version)"
|
||||
else
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
log "Node.js $(node --version 2>/dev/null || printf unknown) is too old; Node.js >= $MIN_NODE_MAJOR is required"
|
||||
log "Node.js $(node --version 2>/dev/null || printf unknown) is too old; Node.js >= $MIN_NODE_VERSION is required"
|
||||
else
|
||||
log "Node.js was not found"
|
||||
fi
|
||||
|
|
@ -353,7 +356,7 @@ else
|
|||
else
|
||||
install_node_linux
|
||||
fi
|
||||
has_supported_node || fail "Node.js installation finished, but Node.js >= $MIN_NODE_MAJOR with npm/npx is not available"
|
||||
has_supported_node || fail "Node.js installation finished, but Node.js >= $MIN_NODE_VERSION with npm/npx is not available"
|
||||
log "Installed Node.js $(node --version)"
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ run_with_node() {
|
|||
-v "$RESULTS_DIR:/results" \
|
||||
-e "PAPERCLIP_INSTALL_TEST_LOG=/results/$name.args" \
|
||||
-e PATH="/paperclip-scripts/install-sh-fixtures:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
|
||||
node:22-bookworm-slim \
|
||||
node:24-bookworm-slim \
|
||||
"$@"
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ docker run --rm \
|
|||
-e npm_config_registry=http://attacker-registry.invalid \
|
||||
-e PAPERCLIP_INSTALL_TEST_LOG=/results/hostile.args \
|
||||
-e PATH="/paperclip-scripts/install-sh-fixtures:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
|
||||
node:22-bookworm-slim \
|
||||
node:24-bookworm-slim \
|
||||
bash /paperclip-scripts/install.sh --no-prompt --no-onboard
|
||||
assert_line "$RESULTS_DIR/hostile.args" "--registry=https://registry.npmjs.org"
|
||||
assert_line "$RESULTS_DIR/hostile.args" "NPM_CONFIG_REGISTRY=https://registry.npmjs.org"
|
||||
|
|
@ -140,7 +140,7 @@ docker run --rm \
|
|||
-e PAPERCLIP_INSTALL_NO_ONBOARD=1 \
|
||||
-e PAPERCLIP_INSTALL_NO_PROMPT=1 \
|
||||
-e PATH="/paperclip-scripts/install-sh-fixtures:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
|
||||
node:22-bookworm-slim \
|
||||
node:24-bookworm-slim \
|
||||
bash /paperclip-scripts/install.sh
|
||||
assert_line "$RESULTS_DIR/env.args" "paperclipai@2026.722.0"
|
||||
assert_line "$RESULTS_DIR/env.args" "--version"
|
||||
|
|
@ -161,8 +161,8 @@ assert_line "$RESULTS_DIR/no-node.args" "paperclipai@latest"
|
|||
node_version="$(cat "$RESULTS_DIR/no-node.args.node")"
|
||||
node_major="${node_version#v}"
|
||||
node_major="${node_major%%.*}"
|
||||
[ "$node_major" -ge 20 ] || {
|
||||
printf 'Expected Node >= 20, got %s\n' "$node_version" >&2
|
||||
[ "$node_major" -ge 24 ] || {
|
||||
printf 'Expected Node >= 24, got %s\n' "$node_version" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
"@types/express-serve-static-core": "^5.1.3",
|
||||
"@types/jsdom": "^30.0.0",
|
||||
"@types/multer": "^2.2.0",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
|
@ -97,5 +97,8 @@
|
|||
"typescript": "^5.7.3",
|
||||
"vite": "^6.4.3",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ describe("adapter routes", () => {
|
|||
agentId: "codex",
|
||||
skillsMode: "ephemeral",
|
||||
prerequisites: {
|
||||
nodeRange: ">=22.13.0",
|
||||
nodeRange: ">=24.11.0",
|
||||
packages: ["@agentclientprotocol/codex-acp"],
|
||||
},
|
||||
});
|
||||
|
|
@ -203,7 +203,7 @@ describe("adapter routes", () => {
|
|||
agentId: "gemini",
|
||||
skillsMode: "ephemeral",
|
||||
prerequisites: {
|
||||
nodeRange: ">=20.0.0",
|
||||
nodeRange: ">=24.11.0",
|
||||
packages: ["@google/gemini-cli"],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ const claudeLocalAdapter: ServerAdapterModule = {
|
|||
agentId: "claude",
|
||||
skillsMode: "ephemeral",
|
||||
prerequisites: {
|
||||
nodeRange: ">=22.12.0",
|
||||
nodeRange: ">=24.11.0",
|
||||
packages: ["@agentclientprotocol/claude-agent-acp"],
|
||||
},
|
||||
},
|
||||
|
|
@ -317,7 +317,7 @@ const codexLocalAdapter: ServerAdapterModule = {
|
|||
agentId: "codex",
|
||||
skillsMode: "ephemeral",
|
||||
prerequisites: {
|
||||
nodeRange: ">=22.13.0",
|
||||
nodeRange: ">=24.11.0",
|
||||
packages: ["@agentclientprotocol/codex-acp"],
|
||||
},
|
||||
},
|
||||
|
|
@ -382,7 +382,7 @@ const geminiLocalAdapter: ServerAdapterModule = {
|
|||
agentId: "gemini",
|
||||
skillsMode: "ephemeral",
|
||||
prerequisites: {
|
||||
nodeRange: ">=20.0.0",
|
||||
nodeRange: ">=24.11.0",
|
||||
packages: ["@google/gemini-cli"],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue