fix(deploy): address review feedback on Cloudflare deployment
- Remove sharp from resolution entirely (high-severity libvips CVEs, GHSA-f88m-g3jw-g9cj; unused optional wrangler dep) — unblocks the Dependency Review gate - Gate unclaimed operator invite behind a BOOTSTRAP_TOKEN secret (Greptile P1): query-param-once + cookie, constant-time compare, documented claim-then-delete flow - Serialize cold-start boots (Greptile P1): shared in-flight ensure promise per isolate + flock --nonblock in start-paperclip.sh across isolates - Pin exact npm versions in the container image (Greptile/Superagent P2) and reject mutable tags in config tests - Use request origin for PAPERCLIP_PUBLIC_URL fallback so wrangler dev http:// works (Copilot) - Declare packages list in standalone pnpm-workspace.yaml (Copilot); plain pnpm install now works without --ignore-workspace - Document Node.js 22+ requirement (Copilot/Greptile) Claude-Session: https://claude.ai/code/session_01NWpwXBqxrPZdv75ud8xszr
This commit is contained in:
parent
26687784b7
commit
5ea8a82157
|
|
@ -7,8 +7,9 @@ running `paperclipai` with embedded Postgres and the local-adapter agent CLIs
|
|||
preinstalled. No custom domain required.
|
||||
|
||||
```sh
|
||||
pnpm install --ignore-workspace
|
||||
pnpm install
|
||||
npx wrangler login
|
||||
npx wrangler secret put BOOTSTRAP_TOKEN # gate access until you claim the operator account
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
|
|
@ -30,10 +31,11 @@ troubleshooting): **[docs/deploy/cloudflare.md](../../docs/deploy/cloudflare.md)
|
|||
| `wrangler.jsonc` | Worker + container + Durable Object config |
|
||||
| `test/` | Unit tests + cross-file config consistency checks |
|
||||
|
||||
This package is intentionally **not** part of the pnpm workspace (same
|
||||
pattern as `packages/plugins/sandbox-providers/*`) so its Cloudflare
|
||||
toolchain never churns the root lockfile. Install with
|
||||
`pnpm install --ignore-workspace`.
|
||||
This package is intentionally **not** part of the root pnpm workspace (same
|
||||
pattern as `packages/plugins/sandbox-providers/*`) — its own
|
||||
`pnpm-workspace.yaml` makes it a standalone single-package workspace, so the
|
||||
Cloudflare toolchain never churns the root lockfile and a plain
|
||||
`pnpm install` here does the right thing.
|
||||
|
||||
```sh
|
||||
pnpm test # vitest: lib + config invariants (Dockerfile↔SDK version pin)
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@ RUN apt-get update \
|
|||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Paperclip (npm release) plus the local-adapter agent CLIs, mirroring the
|
||||
# root Dockerfile's production stage.
|
||||
# root Dockerfile's production stage. Versions are pinned exactly so image
|
||||
# rebuilds are reproducible and supply-chain review applies to a known set
|
||||
# (test/config.test.ts rejects mutable tags); bump them deliberately.
|
||||
RUN npm install -g \
|
||||
paperclipai@latest \
|
||||
@anthropic-ai/claude-code@latest \
|
||||
@openai/codex@latest \
|
||||
opencode-ai \
|
||||
@google/gemini-cli@latest
|
||||
paperclipai@2026.722.0 \
|
||||
@anthropic-ai/claude-code@2.1.218 \
|
||||
@openai/codex@0.145.0 \
|
||||
opencode-ai@1.18.4 \
|
||||
@google/gemini-cli@0.52.0
|
||||
|
||||
# Paperclip runtime defaults (mirrors the root Dockerfile / quadlet unit).
|
||||
ENV HOST=0.0.0.0 \
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ set -euo pipefail
|
|||
find /paperclip -path /paperclip/instances/default/data/storage -prune \
|
||||
-o -exec chown paperclip:paperclip {} +
|
||||
|
||||
exec runuser -u paperclip -- bash -c '
|
||||
# Serialize boots: concurrent Worker isolates can race ensurePaperclip() and
|
||||
# start this script twice. The non-blocking lock makes every duplicate exit
|
||||
# immediately instead of fighting over onboarding and port 3100.
|
||||
exec flock --nonblock /paperclip/.boot.lock runuser -u paperclip -- bash -c '
|
||||
set -euo pipefail
|
||||
export HOME=/home/paperclip
|
||||
if [ ! -f /paperclip/instances/default/config.json ]; then
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ settings:
|
|||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
sharp: '-'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
|
|
@ -103,9 +106,6 @@ packages:
|
|||
'@emnapi/runtime@1.11.1':
|
||||
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
||||
|
||||
'@emnapi/runtime@1.11.2':
|
||||
resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
|
||||
|
||||
'@emnapi/wasi-threads@1.2.2':
|
||||
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
|
||||
|
||||
|
|
@ -265,159 +265,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
|
@ -779,15 +626,6 @@ packages:
|
|||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
|
|
@ -1011,11 +849,6 @@ snapshots:
|
|||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.11.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.2.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
|
@ -1099,102 +932,6 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.2
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
|
@ -1463,7 +1200,6 @@ snapshots:
|
|||
miniflare@4.20260721.0:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
sharp: 0.34.5
|
||||
undici: 7.28.0
|
||||
workerd: 1.20260721.1
|
||||
ws: 8.21.0
|
||||
|
|
@ -1511,39 +1247,6 @@ snapshots:
|
|||
'@rolldown/binding-win32-arm64-msvc': 1.1.5
|
||||
'@rolldown/binding-win32-x64-msvc': 1.1.5
|
||||
|
||||
semver@7.8.5: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
# not attach it to the repo root workspace (same isolation rationale as
|
||||
# packages/plugins/sandbox-providers/*): the Cloudflare toolchain
|
||||
# (wrangler/workerd) never churns the root pnpm-lock.yaml.
|
||||
# sharp stays false: it is an optional wrangler dependency (static-asset
|
||||
# image processing) with no prebuilt binary on some platforms, and its
|
||||
# source build fails without node-addon-api. Wrangler works without it.
|
||||
packages:
|
||||
- "."
|
||||
# sharp is an optional wrangler dependency (static-asset image processing)
|
||||
# that this Worker never uses; it is removed from resolution entirely
|
||||
# because sharp@0.34.x carries high-severity libvips CVEs
|
||||
# (GHSA-f88m-g3jw-g9cj) that trip dependency review.
|
||||
overrides:
|
||||
sharp: "-"
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: false
|
||||
workerd: true
|
||||
|
|
|
|||
|
|
@ -16,12 +16,16 @@
|
|||
import { getSandbox, type Sandbox as SandboxType } from "@cloudflare/sandbox";
|
||||
import {
|
||||
ARTIFACTS_BINDING,
|
||||
BOOTSTRAP_COOKIE,
|
||||
BOOTSTRAP_PARAM,
|
||||
PAPERCLIP_PORT,
|
||||
SANDBOX_ID,
|
||||
START_COMMAND,
|
||||
STORAGE_MOUNT_PATH,
|
||||
accessDeniedPage,
|
||||
bootingResponse,
|
||||
buildPaperclipEnv,
|
||||
getCookie,
|
||||
isMountAlreadyInUse,
|
||||
isPaperclipRunning,
|
||||
isTransientBootError,
|
||||
|
|
@ -49,6 +53,13 @@ interface Env {
|
|||
/** Secrets (wrangler secret put …); forwarded to the container when set. */
|
||||
ANTHROPIC_API_KEY?: string;
|
||||
DATABASE_URL?: string;
|
||||
/**
|
||||
* When set, every request must present this token (?bootstrap_token=…,
|
||||
* which sets a cookie) — protects the unclaimed operator invite between
|
||||
* first boot and the operator's first login. Delete the secret after
|
||||
* claiming the account to open the login page to your team.
|
||||
*/
|
||||
BOOTSTRAP_TOKEN?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,6 +69,62 @@ interface Env {
|
|||
*/
|
||||
let paperclipEnsured = false;
|
||||
|
||||
/**
|
||||
* Shared in-flight boot so concurrent cold-start requests in one isolate
|
||||
* issue a single ensure pass instead of racing startProcess. Cross-isolate
|
||||
* duplicates are additionally serialized by the flock in
|
||||
* container/start-paperclip.sh — duplicates exit immediately.
|
||||
*/
|
||||
let ensureInFlight: Promise<void> | null = null;
|
||||
|
||||
/** Constant-time comparison via digest so token checks don't leak timing. */
|
||||
async function tokensMatch(presented: string, expected: string): Promise<boolean> {
|
||||
const encoder = new TextEncoder();
|
||||
const [a, b] = await Promise.all([
|
||||
crypto.subtle.digest("SHA-256", encoder.encode(presented)),
|
||||
crypto.subtle.digest("SHA-256", encoder.encode(expected)),
|
||||
]);
|
||||
const av = new Uint8Array(a);
|
||||
const bv = new Uint8Array(b);
|
||||
let diff = 0;
|
||||
for (let i = 0; i < av.length; i++) diff |= av[i] ^ bv[i];
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap gate: when BOOTSTRAP_TOKEN is set, only requests presenting it
|
||||
* (query param once, cookie afterwards) reach Paperclip. Returns null when
|
||||
* the request may proceed, otherwise the response to serve.
|
||||
*/
|
||||
async function enforceBootstrapGate(request: Request, env: Env, url: URL): Promise<Response | null> {
|
||||
if (!env.BOOTSTRAP_TOKEN) return null;
|
||||
|
||||
const presented = url.searchParams.get(BOOTSTRAP_PARAM);
|
||||
if (presented !== null && (await tokensMatch(presented, env.BOOTSTRAP_TOKEN))) {
|
||||
// Strip the token from the URL and persist access in a cookie.
|
||||
url.searchParams.delete(BOOTSTRAP_PARAM);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
location: url.toString(),
|
||||
"set-cookie":
|
||||
`${BOOTSTRAP_COOKIE}=${encodeURIComponent(env.BOOTSTRAP_TOKEN)}; ` +
|
||||
"HttpOnly; Secure; SameSite=Lax; Path=/",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const cookie = getCookie(request.headers.get("Cookie"), BOOTSTRAP_COOKIE);
|
||||
if (cookie !== undefined && (await tokensMatch(decodeURIComponent(cookie), env.BOOTSTRAP_TOKEN))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Response(accessDeniedPage(), {
|
||||
status: 401,
|
||||
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
async function ensurePaperclip(sandbox: SandboxType, env: Env, requestUrl: URL): Promise<void> {
|
||||
const processes = await sandbox.listProcesses();
|
||||
if (isPaperclipRunning(processes)) return;
|
||||
|
|
@ -76,7 +143,9 @@ async function ensurePaperclip(sandbox: SandboxType, env: Env, requestUrl: URL):
|
|||
|
||||
await sandbox.startProcess(START_COMMAND, {
|
||||
env: buildPaperclipEnv({
|
||||
publicUrl: env.PAPERCLIP_PUBLIC_URL || `https://${requestUrl.host}`,
|
||||
// origin (not a hardcoded https:// prefix) so wrangler dev's http://
|
||||
// origin round-trips correctly and auth cookies behave locally.
|
||||
publicUrl: env.PAPERCLIP_PUBLIC_URL || requestUrl.origin,
|
||||
deploymentMode: env.PAPERCLIP_DEPLOYMENT_MODE,
|
||||
deploymentExposure: env.PAPERCLIP_DEPLOYMENT_EXPOSURE,
|
||||
anthropicApiKey: env.ANTHROPIC_API_KEY,
|
||||
|
|
@ -101,9 +170,15 @@ export default {
|
|||
const sandbox = getSandbox(env.Sandbox, SANDBOX_ID);
|
||||
const url = new URL(request.url);
|
||||
|
||||
const denied = await enforceBootstrapGate(request, env, url);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
if (!paperclipEnsured) {
|
||||
await ensurePaperclip(sandbox, env, url);
|
||||
ensureInFlight ??= ensurePaperclip(sandbox, env, url).finally(() => {
|
||||
ensureInFlight = null;
|
||||
});
|
||||
await ensureInFlight;
|
||||
paperclipEnsured = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,54 @@ export function isWebSocketUpgrade(headers: Headers): boolean {
|
|||
return headers.get("Upgrade")?.toLowerCase() === "websocket";
|
||||
}
|
||||
|
||||
/** Cookie set once a visitor presents the bootstrap token. */
|
||||
export const BOOTSTRAP_COOKIE = "paperclip_bootstrap";
|
||||
|
||||
/** Query parameter used to present the bootstrap token on first visit. */
|
||||
export const BOOTSTRAP_PARAM = "bootstrap_token";
|
||||
|
||||
/** Minimal cookie-header lookup (no parsing library needed for one value). */
|
||||
export function getCookie(cookieHeader: string | null, name: string): string | undefined {
|
||||
if (!cookieHeader) return undefined;
|
||||
for (const part of cookieHeader.split(";")) {
|
||||
const eq = part.indexOf("=");
|
||||
if (eq === -1) continue;
|
||||
if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 401 page shown while the deployment is gated by BOOTSTRAP_TOKEN. */
|
||||
export function accessDeniedPage(): string {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>Paperclip — access restricted</title>
|
||||
<style>
|
||||
:root{color-scheme:light dark}
|
||||
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
|
||||
font-family:ui-sans-serif,system-ui,sans-serif;background:#0d1017;color:#e6e6e6}
|
||||
main{max-width:34rem;padding:2rem}
|
||||
h1{font-size:1.1rem;font-weight:600;margin:0 0 .5rem}
|
||||
p{margin:.35rem 0;color:#9aa4bf;font-size:.9rem}
|
||||
code{color:#7dd3fc}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Access restricted</h1>
|
||||
<p>This Paperclip deployment is gated by a bootstrap token.</p>
|
||||
<p>Open the URL with <code>?${BOOTSTRAP_PARAM}=<your token></code> —
|
||||
the value you set with <code>wrangler secret put BOOTSTRAP_TOKEN</code>.</p>
|
||||
<p>Once the operator account is claimed, the operator can remove the gate
|
||||
with <code>wrangler secret delete BOOTSTRAP_TOKEN</code>.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/** True when a live Paperclip boot process already exists in the sandbox. */
|
||||
export function isPaperclipRunning(processes: ProcessLike[]): boolean {
|
||||
return processes.some(
|
||||
|
|
|
|||
|
|
@ -69,9 +69,11 @@ describe("container image", () => {
|
|||
expect(dockerfile).toContain(`EXPOSE ${PAPERCLIP_PORT}`);
|
||||
});
|
||||
|
||||
it("installs Paperclip and the agent CLIs", () => {
|
||||
expect(dockerfile).toContain("paperclipai@latest");
|
||||
expect(dockerfile).toContain("@anthropic-ai/claude-code");
|
||||
it("installs Paperclip and the agent CLIs with exact version pins", () => {
|
||||
expect(dockerfile).toMatch(/paperclipai@\d+\.\d+\.\d+/);
|
||||
expect(dockerfile).toMatch(/@anthropic-ai\/claude-code@\d+\.\d+\.\d+/);
|
||||
// Mutable tags make image rebuilds non-reproducible and un-reviewable.
|
||||
expect(dockerfile).not.toContain("@latest");
|
||||
});
|
||||
|
||||
it("pins the paperclip uid the R2 mount options rely on", () => {
|
||||
|
|
@ -87,4 +89,8 @@ describe("boot script", () => {
|
|||
expect(script).toContain(`-path ${STORAGE_MOUNT_PATH} -prune`);
|
||||
expect(script).not.toMatch(/chown -R paperclip:paperclip \/paperclip\s*$/m);
|
||||
});
|
||||
|
||||
it("serializes duplicate boots with a non-blocking lock", () => {
|
||||
expect(script).toContain("flock --nonblock");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BOOTSTRAP_PARAM,
|
||||
PAPERCLIP_PORT,
|
||||
PAPERCLIP_UID,
|
||||
START_COMMAND,
|
||||
STORAGE_MOUNT_PATH,
|
||||
accessDeniedPage,
|
||||
getCookie,
|
||||
bootingPage,
|
||||
bootingResponse,
|
||||
buildPaperclipEnv,
|
||||
|
|
@ -117,6 +120,25 @@ describe("transient boot detection", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("bootstrap gate helpers", () => {
|
||||
it("extracts a single cookie value", () => {
|
||||
expect(getCookie("a=1; paperclip_bootstrap=tok; b=2", "paperclip_bootstrap")).toBe("tok");
|
||||
expect(getCookie("paperclip_bootstrap=tok", "paperclip_bootstrap")).toBe("tok");
|
||||
});
|
||||
|
||||
it("returns undefined for missing header, missing cookie, or name prefixes", () => {
|
||||
expect(getCookie(null, "paperclip_bootstrap")).toBeUndefined();
|
||||
expect(getCookie("other=1", "paperclip_bootstrap")).toBeUndefined();
|
||||
expect(getCookie("xpaperclip_bootstrap=evil", "paperclip_bootstrap")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("access-denied page names the param and secret", () => {
|
||||
const html = accessDeniedPage();
|
||||
expect(html).toContain(BOOTSTRAP_PARAM);
|
||||
expect(html).toContain("BOOTSTRAP_TOKEN");
|
||||
});
|
||||
});
|
||||
|
||||
describe("storage mount", () => {
|
||||
it("targets Paperclip's local_disk storage path, never the DB dir", () => {
|
||||
expect(STORAGE_MOUNT_PATH).toBe("/paperclip/instances/default/data/storage");
|
||||
|
|
|
|||
|
|
@ -61,26 +61,45 @@ Notes:
|
|||
available on the free tier)
|
||||
- [Docker](https://docs.docker.com/get-docker/) running locally (wrangler
|
||||
builds the container image and, for `wrangler dev`, runs it)
|
||||
- Node.js 20+ and pnpm
|
||||
- Node.js 22+ (required by the pinned wrangler toolchain) and pnpm
|
||||
|
||||
## Deploy
|
||||
|
||||
```sh
|
||||
cd deploy/cloudflare
|
||||
pnpm install --ignore-workspace
|
||||
pnpm install
|
||||
npx wrangler login
|
||||
|
||||
# strongly recommended: gate the deployment until you claim the operator
|
||||
# account — any value you choose, e.g. `openssl rand -hex 16`
|
||||
npx wrangler secret put BOOTSTRAP_TOKEN
|
||||
|
||||
npx wrangler deploy
|
||||
|
||||
# optional: give in-container agents an API key
|
||||
npx wrangler secret put ANTHROPIC_API_KEY
|
||||
```
|
||||
|
||||
Then open the printed `https://paperclip-sandbox.<your-subdomain>.workers.dev`
|
||||
URL. The **first request** provisions the container and onboards Paperclip
|
||||
Then open
|
||||
`https://paperclip-sandbox.<your-subdomain>.workers.dev/?bootstrap_token=<your token>`.
|
||||
The **first request** provisions the container and onboards Paperclip
|
||||
(a minute or two) — you'll see a self-refreshing status page until the app is
|
||||
up. Paperclip boots in `authenticated` mode with a pending bootstrap invite;
|
||||
the first login claims the operator account, so open the URL yourself promptly
|
||||
after deploying.
|
||||
up.
|
||||
|
||||
Paperclip boots in `authenticated` mode with a pending bootstrap invite, and
|
||||
**the first visitor to reach the app can claim the operator account**. The
|
||||
`BOOTSTRAP_TOKEN` gate exists to make sure that visitor is you: without the
|
||||
token (query param once; cookie afterwards), the Worker serves a 401 and
|
||||
nothing reaches Paperclip. After you claim the account, remove the gate so
|
||||
your team can reach the login page:
|
||||
|
||||
```sh
|
||||
npx wrangler secret delete BOOTSTRAP_TOKEN
|
||||
```
|
||||
|
||||
If you skip the token, deploy and claim the account immediately — an
|
||||
unclaimed invite on a public `workers.dev` URL is claimable by anyone who
|
||||
finds it.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -91,6 +110,7 @@ Set via `vars` in `wrangler.jsonc` or `wrangler secret put`:
|
|||
| `PAPERCLIP_PUBLIC_URL` | var | request origin | Public URL Paperclip advertises |
|
||||
| `PAPERCLIP_DEPLOYMENT_MODE` | var | `authenticated` | See [Deployment Modes](/deploy/deployment-modes) |
|
||||
| `PAPERCLIP_DEPLOYMENT_EXPOSURE` | var | `private` | Embedded Postgres currently requires `private`; use `public` only with an external `DATABASE_URL` |
|
||||
| `BOOTSTRAP_TOKEN` | secret | — | Gates all access until the operator account is claimed (see Deploy) |
|
||||
| `ANTHROPIC_API_KEY` | secret | — | Forwarded to in-container agent CLIs |
|
||||
| `DATABASE_URL` | secret | — | External Postgres (strongly recommended, see above) |
|
||||
| `ARTIFACTS` | R2 binding | — | Durable attachment storage (see above) |
|
||||
|
|
@ -117,7 +137,7 @@ want real memory; shrink with care.
|
|||
|
||||
```sh
|
||||
cd deploy/cloudflare
|
||||
pnpm install --ignore-workspace
|
||||
pnpm install
|
||||
pnpm dev # wrangler dev — builds and runs the container via Docker
|
||||
```
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue