paperclip/scripts
Jannes Stubbemann c21f70ef1c
fix: skip gosu when already running as target user (#2908)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The reference container image must be deployable on both Docker
Compose (where it starts as root and `gosu`'s a `USER_UID`/`USER_GID`
switch) and Kubernetes (where the pod is typically constrained by
PodSecurity)
> - The Kubernetes operator (paperclipinc/paperclip-operator#45) sets
`runAsNonRoot: true`, `runAsUser: 1000`, `allowPrivilegeEscalation:
false`, and `drop: ALL` capabilities by default — the unconditional
`usermod` + `gosu` flow in the entrypoint requires root + `CAP_SETUID` /
`CAP_SETGID`, making the image undeployable on any cluster enforcing
baseline or restricted PodSecurity
> - Without root, neither the user remap nor `gosu` can ever succeed —
so the fix is a runtime branch: non-root starts `exec` the command
directly (warning if the runtime UID/GID differs from the requested
one), while root starts keep the existing `usermod`+`gosu` flow
> - This also covers platforms that assign arbitrary UIDs (OpenShift
restricted SCC), which previously crashed with a cryptic `usermod:
Permission denied`
> - The benefit is one image that works for both deployment shapes with
no operator-side workaround — pure superset, no breaking change

## Linked Issues or Issue Description

Refs paperclipinc/paperclip-operator#45 (cross-repo) — the operator's
default pod security context (`runAsNonRoot: true`, `runAsUser: 1000`,
`allowPrivilegeEscalation: false`, `drop: ALL`) is blocked by this
entrypoint behavior. The operator shipped a stopgap
(paperclipinc/paperclip-operator#46 lets the CRD override the security
context); this PR is the image-side fix that makes the secure defaults
work out of the box. Supersedes #2904 (v1 of this branch).

No in-repo issue covers this directly — problem described in-PR
following the bug-report template:

**What happened**

The entrypoint unconditionally runs `usermod`/`groupmod`/`chown` + `exec
gosu node`, which requires root plus `CAP_SETUID` / `CAP_SETGID` — any
non-root start crashes (`gosu` cannot drop privileges; a mismatched UID
dies earlier at `usermod: Permission denied`), making the reference
image undeployable on clusters enforcing baseline or restricted
PodSecurity.

**Expected behavior**

A non-root container `exec`s the command directly (with a clear warning
if its UID/GID differs from the requested `USER_UID`/`USER_GID`, since a
remap is impossible without root). The existing root + `usermod` +
`gosu` flow is preserved for Docker Compose, where the container starts
as root and switches to the requested UID/GID.

**Deployment mode**

Kubernetes with baseline/restricted PodSecurity and OpenShift-style
arbitrary-UID platforms (failing cases); Docker Compose root-start (must
keep working).

## What Changed

- **`scripts/docker-entrypoint.sh`** — branch on the runtime UID:
- **Non-root start** → `exec "$@"` directly. If the runtime UID/GID
differs from `USER_UID`/`USER_GID`, print a one-line warning to stderr
first (the remap is impossible without root; the warning keeps
volume-permission mismatches diagnosable instead of failing cryptically
inside `usermod`).
- **Root start** → unchanged: `usermod`/`groupmod` remap when requested,
`chown` of `/paperclip` when a remap happened, then `exec gosu node
"$@"`.

## Verification

**Automated:** `server/src/__tests__/docker-entrypoint.test.ts` runs the
real entrypoint with `id`/`usermod`/`groupmod`/`chown`/`gosu` stubbed
via PATH and asserts all five privilege branches (root+defaults,
root+remap, non-root match, arbitrary non-root UID, GID mismatch) — runs
in the regular server suite, no Docker needed.

**Manual (Docker):** ran the entrypoint on `node:lts-trixie-slim` (the
image's actual base) across the full matrix, with `gosu` stubbed to a
marker:

- [x] Root start, defaults → no remap, `gosu node` invoked (Docker
Compose flow unchanged)
- [x] Root start, `USER_UID=1001`/`USER_GID=1001` → `Updating node
UID/GID to 1001` + `gosu node` (remap flow unchanged)
- [x] Non-root `--user 1000:1000` (the operator's `runAsUser: 1000`
shape) → silent direct exec, command runs as 1000:1000
- [x] Non-root `--user 1234:1234` (arbitrary UID) → warning `running
unprivileged as 1234:1234; cannot remap to requested 1000:1000`, then
direct exec (previously: crash)
- [x] Non-root `--user 1000:1001` (GID mismatch) → warning, then direct
exec
- [x] Baseline check: master's entrypoint fails for any non-root start
(gosu/usermod require root)

End-to-end cluster verification under restricted PodSecurity exercises
the same branch as the `--user 1000:1000` case above; the operator
repo's deploy is the natural place for that smoke test once this ships
in an image tag.

## Risks

- **Backward-compatible.** Docker Compose / root-entrypoint path is
byte-for-byte the same flow — `usermod`/`gosu` runs whenever the
container starts as root.
- **Behavior change only for previously-broken starts.** Non-root
containers used to crash; they now run. The only observable difference
for a *working* deployment is none.
- **Mismatched non-root UID/GID warns instead of failing.** Deliberate:
the remap is impossible without root, and arbitrary-UID platforms
(OpenShift) rely on group-writable volumes; a hard fail would keep them
broken. The stderr warning preserves diagnosability.
- **No new env vars, no API surface.** Pure entrypoint behavior change
gated on the runtime UID.
- **Restricted PodSecurity ready.** The non-root branch needs no Linux
capabilities — works under `drop: ALL`.

## Model Used

Claude Opus 4.6; rebase, non-root generalization, and verification
matrix by Claude Fable 5 (1M context).

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Checked ROADMAP.md — not in conflict with planned core work
(agent-runtime sandbox images use `tini`, no gosu — unaffected)
- [x] Tests run locally and pass (new `docker-entrypoint.test.ts`
covering all five privilege branches, plus a manual Docker matrix on the
real base image; see Verification)
- [x] No UI changes
- [x] Documented risks above
- [x] Will address all Greptile and reviewer comments before merge

Unblocks the default (non-overridden) security context of
paperclipinc/paperclip-operator#45 / #46.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-12 10:33:52 -07:00
..
smoke [codex] Move maintainer task skills under .agents (#7658) 2026-06-06 10:47:59 -05:00
backfill-issue-reference-mentions.ts Add first-class issue references (#4214) 2026-04-21 10:02:52 -05:00
backup-db.sh Add paperclipai db:backup CLI command 2026-03-05 06:02:12 -06:00
bootstrap-npm-package.mjs fix(ci): gate new release packages on npm bootstrap (#5146) 2026-05-03 19:31:28 -07:00
bootstrap-npm-package.test.mjs [codex] Add workspace diff viewer plugin (#6071) 2026-05-18 08:50:06 -05:00
build-npm.sh fix: validate canary release path in CI 2026-03-17 15:35:59 -05:00
build-standalone-public-packages.mjs Fix release packaging for standalone public packages (#4494) 2026-04-25 12:16:23 -07:00
capture-acpx-skills-screenshots.mjs Add ACPX local adapter runtime (#4893) 2026-04-30 19:57:05 -05:00
capture-pap-2351-binding-picker.mjs Add secrets provider vaults and remote import (#5429) 2026-05-09 18:22:17 -05:00
check-docker-deps-stage.mjs Add E2B sandbox provider plugin (#4452) 2026-04-25 11:01:11 -07:00
check-forbidden-tokens.mjs Redact current user in comments and token checks 2026-03-11 22:17:21 -05:00
check-no-git-push.mjs PAPA-430: workspace finalize gates + no-remote-git enforcement (#6969) 2026-05-29 08:25:29 -07:00
check-no-git-push.test.mjs PAPA-430: workspace finalize gates + no-remote-git enforcement (#6969) 2026-05-29 08:25:29 -07:00
check-release-package-bootstrap.mjs fix(ci): gate new release packages on npm bootstrap (#5146) 2026-05-03 19:31:28 -07:00
check-release-package-bootstrap.test.mjs fix(ci): gate new release packages on npm bootstrap (#5146) 2026-05-03 19:31:28 -07:00
clean-onboard-git.sh http clone 2026-03-09 10:03:45 -05:00
clean-onboard-npm.sh scripts 2026-03-03 14:00:10 -06:00
clean-onboard-ref.sh feat: add committed-ref onboarding smoke script 2026-03-09 09:59:43 -05:00
create-github-release.sh fix: use origin for github release creation in actions 2026-03-18 09:10:00 -05:00
dev-runner-output.mjs Guard dev health JSON parsing 2026-04-06 21:23:33 -05:00
dev-runner-output.ts fix(runtime): handle empty dev runner responses 2026-04-06 21:56:13 -05:00
dev-runner-paths.mjs [codex] Add backup endpoint and dev runtime hardening (#4087) 2026-04-20 06:08:55 -05:00
dev-runner-snapshot.mjs Address dev runner snapshot review feedback 2026-06-01 22:03:51 +00:00
dev-runner.mjs [codex] Add skills CLI and catalog management (#6782) 2026-05-28 07:33:51 -10:00
dev-runner.ts Add dev runner snapshot race regression test 2026-06-01 21:55:30 +00:00
dev-service-profile.ts Introduce bind presets for deployment setup 2026-04-11 07:09:07 -05:00
dev-service.ts Add idempotent local dev service management 2026-03-29 10:55:26 -05:00
discord-daily-digest.sh [codex] Harden heartbeat scheduling and runtime controls (#4223) 2026-04-21 12:24:11 -05:00
docker-build-test.sh chore(docker): improve base image and organize docker files 2026-04-01 11:36:27 +00:00
docker-entrypoint.sh fix: skip gosu when already running as target user (#2908) 2026-06-12 10:33:52 -07:00
docker-onboard-smoke.sh chore(docker): improve base image and organize docker files 2026-04-01 11:36:27 +00:00
ensure-plugin-build-deps.mjs [codex] Add skills CLI and catalog management (#6782) 2026-05-28 07:33:51 -10:00
ensure-workspace-package-links.ts [codex] fix worktree dev dependency ergonomics (#3743) 2026-04-15 09:47:29 -05:00
generate-company-assets.ts Address Greptile review on portability PR 2026-03-23 17:23:59 -05:00
generate-npm-package-json.mjs fix: add npm provenance package metadata 2026-03-17 16:01:48 -05:00
generate-org-chart-images.ts Add standalone Playwright-based org chart image generator 2026-03-20 05:55:33 -05:00
generate-org-chart-satori-comparison.ts feat: multi-style pure SVG org chart renderer (no Playwright needed) 2026-03-20 06:33:29 -05:00
generate-plugin-package-json.mjs Add E2B sandbox provider plugin (#4452) 2026-04-25 11:01:11 -07:00
generate-ui-package-json.mjs Publish @paperclipai/ui from release automation 2026-03-26 11:13:11 -05:00
kill-agent-browsers.sh [codex] Stabilize tests and local maintenance assets (#4423) 2026-04-24 15:11:42 -05:00
kill-dev.sh kill chrome test servers too 2026-04-09 06:14:12 -05:00
kill-vitest.sh [codex] Harden heartbeat scheduling and runtime controls (#4223) 2026-04-21 12:24:11 -05:00
kill-workspaces.sh Add workspace runtime kill script 2026-06-05 03:28:07 +00:00
link-plugin-dev-sdk.mjs Add E2B sandbox provider plugin (#4452) 2026-04-25 11:01:11 -07:00
measure-issue-chat-long-thread.mjs Improve issue thread scale and markdown polish (#4861) 2026-04-30 13:18:01 -05:00
migrate-inline-env-secrets.ts refactor: rename packages to @paperclipai and CLI binary to paperclipai 2026-03-03 08:45:26 -06:00
paperclip-commit-metrics.ts feat: implement multi-user access and invite flows (#3784) 2026-04-17 09:44:19 -05:00
paperclip-issue-update.sh chore: improve worktree tooling and security docs 2026-04-10 22:26:30 -05:00
prepare-server-ui-dist.sh ci: speed up PR verify workflow (#6137) 2026-05-16 11:28:25 -07:00
provision-worktree.sh [codex] Add backup endpoint and dev runtime hardening (#4087) 2026-04-20 06:08:55 -05:00
release-lib.sh [codex] Recover duplicate npm provenance canary publishes (#7839) 2026-06-09 15:35:07 -05:00
release-lib.test.mjs [codex] Recover duplicate npm provenance canary publishes (#7839) 2026-06-09 15:35:07 -05:00
release-package-manifest.json feat(plugin-kubernetes): self-hostable Kubernetes sandbox provider (stage 1/3: plugin package) (#5790) 2026-06-10 21:07:00 -07:00
release-package-map.mjs fix(ci): gate new release packages on npm bootstrap (#5146) 2026-05-03 19:31:28 -07:00
release-package-map.test.mjs fix(ci): gate new release packages on npm bootstrap (#5146) 2026-05-03 19:31:28 -07:00
release.sh [codex] Recover duplicate npm provenance canary publishes (#7839) 2026-06-09 15:35:07 -05:00
rollback-latest.sh chore: switch release calver to mdd patch 2026-03-18 07:57:36 -05:00
run-typecheck-build-gaps.mjs ci: speed up PR verify workflow (#6137) 2026-05-16 11:28:25 -07:00
run-vitest-stable.mjs refactor(deps-dev): bump vitest from 3.2.4 to 4.1.8 (#7581) 2026-06-05 21:11:32 -07:00
screenshot-blocked-inbox.mjs [codex] Add blocked inbox attention view (#5603) 2026-05-13 16:41:36 -05:00
screenshot-file-viewer.mjs Add workspace file viewer and artifact links (#7681) 2026-06-09 17:17:43 -05:00
screenshot-one.mjs Add workspace file viewer and artifact links (#7681) 2026-06-09 17:17:43 -05:00
screenshot-pap2373.mjs Cancel stale queued heartbeats when issue graph changes (PAP-2314) (#4534) 2026-04-26 21:17:38 -05:00
screenshot-recovery-card.cjs [codex] Add source-scoped recovery actions (#5599) 2026-05-12 09:37:15 -05:00
screenshot-subissues.mjs Present ordered sub-issues as a workflow checklist (#4523) 2026-04-26 07:36:49 -05:00
screenshot.cjs feat: add authenticated screenshot utility (#2622) 2026-04-03 10:51:26 -07:00
verify-release-registry-state.mjs fix: harden release registry verification against npm lag (#4816) 2026-05-09 22:18:12 -07:00
verify-release-registry-state.test.mjs fix: harden release registry verification against npm lag (#4816) 2026-05-09 22:18:12 -07:00