## 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> |
||
|---|---|---|
| .. | ||
| deploy | ||
| scripts | ||
| src | ||
| README.md | ||
| package.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
README.md
Paperclip Tailscale HTTPS broker
Least-privilege host broker that manages only Paperclip-owned, tailnet-only, same-number HTTPS-to-loopback listeners for managed branch runtimes.
It exists so the Paperclip app/agent account never gains Tailscale operator authority (see PAP-16989) while still getting automatic trusted HTTPS previews per branch runtime. Design: PAP-17049 plan; security contract: PAP-17050 threat-model verdict.
Runtime services opt in explicitly; existing services and the primary :443
route are unchanged:
{
"port": { "type": "auto", "envKey": "PORT" },
"expose": {
"type": "tailscale_https",
"hostname": "auto",
"publicPort": "same",
"includePaperclipViteHmr": true,
"failurePolicy": "fail_closed"
}
}
What it can and cannot do
Supported operations (over a Unix socket, one runtime-service at a time):
list— the caller's own exposures (never returns lease handles).reserve— atomically reserve an app/HMR pair before either backend binds, returning an unguessable, short-lived lease handle bound to the caller, runtime ID, ports, purposes, and generation.expose— redeem that reservation only after/procproves both listeners are loopback-only and owned by the configured managed-runtime UID.remove— remove the caller's own listeners, proven by exact lease handle.
Hard-denied, deny-by-default: Funnel, certificates, Tailscale Services,
serve reset / set-config, path handlers, arbitrary targets, non-loopback or
wildcard/dual-stack backends, port 443, privileged/reserved ports, ports
outside the dedicated runtime range, unknown fields, and removal of any mapping
not matching an exact registry + lease + live Serve entry. The primary
:443 → 127.0.0.1:3100 route is verified structurally before and after every
mutation and is never modified.
The socket transport reads Linux SO_PEERCRED before admission, admits at most
8 concurrent sockets per resolved UID, and reserves 4 of its 32 global slots
for the configured Paperclip service UID. Connection deadlines destroy the
socket so timed-out peers cannot retain kernel-level connection slots. Missing
or invalid native credentials fail closed; socket permissions are not used as
a substitute identity.
One-time host installation (paperclip-dev)
These steps require root and must be run by CloudOps/host owner, not the Paperclip agent account. They install the broker as a dedicated Tailscale-operator service account distinct from the Paperclip app account.
-
Preconditions. Tailscale is installed and up on the node, the node has an HTTPS-capable trusted cert (MagicDNS + HTTPS enabled), and the existing
:443 → 127.0.0.1:3100Serve mapping is present. -
Create the dedicated operator account and socket group.
sudo useradd --system --home /var/lib/paperclip-tailscale-broker \ --shell /usr/sbin/nologin paperclip-tsbroker sudo groupadd --system paperclip-tsbroker-sock # The Paperclip *app* service account must have this as its PRIMARY group so # its SO_PEERCRED gid matches the socket group (supplemental membership is # intentionally NOT accepted). sudo usermod -g paperclip-tsbroker-sock <paperclip-app-account> -
Grant Tailscale operator authority to the broker account only.
sudo tailscale set --operator=paperclip-tsbrokerDo not grant
--operatorto the Paperclip app/agent account (that grant was explicitly rejected in PAP-16989). -
Create state directories (not writable by the Paperclip app). The packaged unit creates these automatically; for a manual install use:
sudo install -d -o paperclip-tsbroker -g paperclip-tsbroker-sock -m 0750 /run/paperclip-tailscale-broker sudo install -d -o paperclip-tsbroker -g paperclip-tsbroker-sock -m 0700 /var/lib/paperclip-tailscale-broker sudo install -d -o paperclip-tsbroker -g paperclip-tsbroker-sock -m 0700 /var/log/paperclip-tailscale-brokerThe broker refuses to start if the registry path's parent is group/other writable.
-
Build, install the package under
/opt/paperclip, and install the packaged systemd unit. The unit'sExecStart(and the doctor command below) run the build output from/opt/paperclip/packages/tailscale-https-broker/dist, so copy it there explicitly. The Linux build requires a C compiler and Node.js headers to compile the dependency-free N-APISO_PEERCREDaddon. The output is self-contained (Node builtins plus the compiled addon; nonode_modulesneeded).pnpm --filter @paperclipai/tailscale-https-broker build sudo install -d -m 0755 /opt/paperclip/packages/tailscale-https-broker sudo cp -r packages/tailscale-https-broker/dist \ /opt/paperclip/packages/tailscale-https-broker/ sudo install -D -m 0644 \ packages/tailscale-https-broker/deploy/paperclip-tailscale-https-broker.service \ /etc/systemd/system/paperclip-tailscale-https-broker.service sudo install -d -m 0750 /etc/paperclip sudoedit /etc/paperclip/tailscale-https-broker.envThe packaged unit is equivalent to:
[Unit] Description=Paperclip Tailscale HTTPS broker After=tailscaled.service Requires=tailscaled.service [Service] Type=simple User=paperclip-tsbroker # Socket must end up 0660 paperclip-tsbroker:paperclip-tsbroker-sock. Set the group here and # the broker chmods the socket to 0660 on bind. Group=paperclip-tsbroker-sock EnvironmentFile=/etc/paperclip/tailscale-https-broker.env ExecStart=/usr/bin/node /opt/paperclip/packages/tailscale-https-broker/dist/main.js Restart=on-failure NoNewPrivileges=true ProtectSystem=strict ReadWritePaths=/run/paperclip-tailscale-broker /var/lib/paperclip-tailscale-broker /var/log/paperclip-tailscale-broker [Install] WantedBy=multi-user.targetPut the
BROKER_*values from the table below in the environment file. SetPAPERCLIP_TAILSCALE_BROKER_SOCKET=/run/paperclip-tailscale-broker/broker.sockon the Paperclip service only if overriding its default.Environment variables (defaults in
src/config.ts):Var Required Default Meaning BROKER_NODE_IDENTITYyes — hostname + boot id; a change forces quarantine + operator reconciliation BROKER_SERVICE_UIDyes — UID of the Paperclip app account allowed to connect BROKER_SERVICE_GIDyes — GID of the dedicated socket group (caller's primary GID) BROKER_RUNTIME_UIDyes — UID that owns Paperclip-managed runtime processes (normally the Paperclip app service account); only its loopback listeners are eligible BROKER_TAILSCALE_BINno /usr/bin/tailscaleabsolute path to the Tailscale CLI BROKER_SOCKET_PATHno /run/paperclip-tailscale-broker/broker.sockUnix socket path BROKER_REGISTRY_PATHno /var/lib/paperclip-tailscale-broker/registry.jsonroot-owned 0600ownership registryBROKER_AUDIT_PATHno /var/log/paperclip-tailscale-broker/audit.logappend-only security audit log BROKER_PROTECTED_PORTSno (empty) comma/space separated ports the broker must never create, remove, or reclaim — even when its own registry holds a valid lease for them (see below) BROKER_PROTECTED_PORTS— operator-declared preservation (PAP-17285)The long-standing "unknown/manual entries are never modified" invariant is provenance-blind: it protects only entries the broker has no lease for. It therefore could not protect the
42000/52000mappings, because the broker had itself created them for a canary lane that was later retired — so its registry still called them owned, while operators had reclassified them as must-preserve after failing to attribute them to any live lane. Both views were internally consistent, they disagreed, and a fully authorized, shape-valid,:443-preserving removal destroyed them with no guard able to object.A protected port is an operator assertion that outranks the broker's own ownership record. Enforcement is fail-closed and layered: refused during argv construction, denied in
reserve/expose/removewithprotected_port, excluded from the allocatable allowlist so no lane can acquire one, and asserted byte-unchanged across every before/after snapshot (protected_entry_violation). A malformed list makes the broker refuse to start rather than silently protect nothing;443is rejected because the primary route already has a stronger, non-optional invariant.BROKER_PROTECTED_PORTS=42000,52000Confirm it took effect before trusting it —
--doctorechoes the parsed set:sudo -u paperclip-tsbroker \ env $(cat /etc/paperclip/tailscale-https-broker.env | xargs) \ node /opt/paperclip/packages/tailscale-https-broker/dist/main.js --doctor -
Preflight (read-only, no mutation).
sudo -u paperclip-tsbroker \ BROKER_NODE_IDENTITY=$(hostname) BROKER_SERVICE_UID=... BROKER_SERVICE_GID=... BROKER_RUNTIME_UID=... \ node /opt/paperclip/packages/tailscale-https-broker/dist/main.js --doctorVerifies: supported Tailscale CLI version, Serve status is readable, the primary
:443route is intact, the registry path is safe, and prints the node identity. Exit 0 = ready. It never mutates Serve state. -
Enable.
sudo systemctl daemon-reload && sudo systemctl enable --now paperclip-tailscale-https-broker. Confirm the socket is0660 paperclip-tsbroker:paperclip-tsbroker-sock.
Upgrade
Deploy new package output to
/opt/paperclip/packages/tailscale-https-broker/dist, then
sudo systemctl restart paperclip-tailscale-https-broker. On
restart the broker re-reads its root-owned registry and adopts only exact-lease
matches; a changed BROKER_NODE_IDENTITY (host reimage / boot-id change) forces
quarantine and operator reconciliation rather than silently re-adopting.
Uninstall / rollback / opt-out
Rollback disables new exposure and removes only broker-owned listeners; it never resets Serve or changes the primary route.
- Disable the exposure flag on the project runtime (Paperclip stops requesting
expose). Existing previews drain on runtime stop. - Drain owned listeners: stop each managed runtime so Paperclip issues
removefor its own leases (proven by handle). sudo systemctl disable --now paperclip-tailscale-https-broker.- Optional cleanup: remove the state dirs and
sudo tailscale set --operator=to drop the operator grant. Do not runtailscale serve reset— remove only the specific per-port Serve entries if any remain.
Recovery
If a mutation fails partway, the broker removes only the exact listeners it
applied; if exact cleanup cannot be proven it quarantines the affected ports and
reports cleanup_pending (partial app+HMR exposure is never reported healthy).
Quarantined ports are not reused until an operator clears them. The append-only
audit log at BROKER_AUDIT_PATH records every allow/deny and mutation outcome
(peer UID/GID/PID, operation, runtime UUID, ports, decision reason, before/after
state digests, quarantine/recovery) with lease handles and raw CLI output
redacted.
Tests
pnpm --filter @paperclipai/tailscale-https-broker test # 72 tests
pnpm --filter @paperclipai/tailscale-https-broker typecheck
pnpm --filter @paperclipai/tailscale-https-broker build