mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
fix(docker): forward tool action signing secret in Compose (#15202)
## Thinking Path > - Paperclip can run through Docker Compose. > - Approval-gated tools need an independent signing secret. > - The guide sets that secret before starting Compose. > - Neither Compose file passes it into the container. > - This pull request forwards the operator's configured secret. > - Documented setup can issue signed tool-action approvals. ## Linked Issues or Issue Description Fixes: #15064 ## What Changed - Forward PAPERCLIP_TOOL_ACTION_SIGNING_SECRET in both shipped Compose environments. - Document persistent deployment secrets and reuse on restart. - Explain that it is independent from the session secret. - Keep an empty default so deployments without approval-gated tools retain their existing startup behavior. ## Verification - Docker Compose config before the fix drops a supplied synthetic secret in both files. - After the fix, both preserve the supplied value. - Both also resolve successfully when the optional signing secret is absent. - Used --env-file /dev/null and synthetic values. No containers were started. - `node --test scripts/docker-compose-secrets.test.mjs`: 2 passed. - The Docker context CI lane runs this integration test. - git diff --check passes. ## Risks The value is visible to the container as intended. Operators must protect it as a secret. This does not change admission rules or require a new startup value for existing deployments. ## Model Used OpenAI Codex, based on GPT-6, assisted with source review, implementation and local test execution. The exact deployment model ID and context-window size are not exposed in this session. ## 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 and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass (targeted local checks and full remote CI pass) - [x] I have added or updated tests where applicable (Hermes regression / real Compose interpolation checks) - [x] I have updated relevant documentation to reflect my changes (Hermes has no configuration changes; Compose documentation is updated) - [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
This commit is contained in:
1 parent
7a8b358365
commit
95692b7785
5 files changed
+79
-6
No files matched your search
@@ -572,6 +572,14 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
|
||||
- name: Set up Docker Compose
|
||||
uses: docker/setup-compose-action@54042514f505b273907334ae2b9cdbb9a0213c1a # v2
|
||||
with:
|
||||
version: v5.5.1
|
||||
|
||||
- name: Verify Compose signing secret forwarding
|
||||
run: node --test scripts/docker-compose-secrets.test.mjs
|
||||
|
||||
# Same .dockerignore semantics as the real image builds: a
|
||||
# context-slimming change that strips a committed build input must
|
||||
# fail here, on the pull request, instead of failing every
|
||||
|
||||
+22
-5
@@ -96,14 +96,27 @@ All persisted under your bind mount (`./data/docker-paperclip` in the example ab
|
||||
|
||||
## Docker Compose
|
||||
|
||||
### Persistent signing secrets
|
||||
|
||||
Before the first launch, generate two independent values with `openssl rand -hex 32`
|
||||
and save them in `docker/.env`. Keep any existing deployment's values unchanged:
|
||||
|
||||
```dotenv
|
||||
BETTER_AUTH_SECRET=<session-signing-secret>
|
||||
PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=<tool-action-signing-secret>
|
||||
```
|
||||
|
||||
Restrict this file to its owner (`chmod 600 docker/.env`) and keep it out of version
|
||||
control. Compose reads it for both commands below. Reuse the values on every
|
||||
restart. Rotate the tool-action secret only after outstanding approvals are
|
||||
resolved, because pending signatures use the previous value.
|
||||
|
||||
### Quickstart (embedded SQLite)
|
||||
|
||||
Single container, no external database. Data persists via a bind mount.
|
||||
|
||||
```sh
|
||||
BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
|
||||
PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=$(openssl rand -hex 32) \
|
||||
docker compose -f docker/docker-compose.quickstart.yml up --build
|
||||
docker compose -f docker/docker-compose.quickstart.yml up --build
|
||||
```
|
||||
|
||||
Defaults:
|
||||
@@ -129,12 +142,16 @@ Pass `OPENAI_API_KEY` and/or `ANTHROPIC_API_KEY` to enable local adapter runs.
|
||||
Paperclip server + PostgreSQL 17. The database is health-checked before the server starts.
|
||||
|
||||
```sh
|
||||
BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
|
||||
docker compose -f docker/docker-compose.yml up --build
|
||||
docker compose -f docker/docker-compose.yml up --build
|
||||
```
|
||||
|
||||
PostgreSQL data persists in a named Docker volume (`pgdata`). Paperclip data persists in `paperclip-data`.
|
||||
|
||||
Both Compose files pass `PAPERCLIP_TOOL_ACTION_SIGNING_SECRET` to the server.
|
||||
Set it to enable signed tool-action approvals, and keep it independent from
|
||||
`BETTER_AUTH_SECRET`. Leaving it unset preserves startup without approval-gated
|
||||
tool actions; those calls report `signing_secret_unconfigured`.
|
||||
|
||||
### Untrusted PR review
|
||||
|
||||
Isolated container for reviewing untrusted pull requests with Codex or Claude, without exposing your host machine. See `doc/UNTRUSTED-PR-REVIEW.md` for the full workflow.
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
# Copy this file and set the required env vars before running.
|
||||
#
|
||||
# cd docker
|
||||
# export BETTER_AUTH_SECRET="$(openssl rand -hex 32)"
|
||||
# Set BETTER_AUTH_SECRET and PAPERCLIP_TOOL_ACTION_SIGNING_SECRET in .env
|
||||
# once, using a separate `openssl rand -hex 32` value for each. Reuse them
|
||||
# on restarts; do not replace existing deployment secrets.
|
||||
# export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
# docker compose -f docker-compose.quickstart.yml up --build
|
||||
#
|
||||
@@ -41,6 +43,8 @@ services:
|
||||
|
||||
# ── Required secret for session signing (generate with: openssl rand -hex 32) ──
|
||||
BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET must be set}"
|
||||
# Independent secret for signed approval-gated tool actions.
|
||||
PAPERCLIP_TOOL_ACTION_SIGNING_SECRET: "${PAPERCLIP_TOOL_ACTION_SIGNING_SECRET:-}"
|
||||
volumes:
|
||||
# Persistent data directory (database, agent workspaces, uploads)
|
||||
- "${PAPERCLIP_DATA_DIR:-../data/docker-paperclip}:/paperclip"
|
||||
@@ -40,6 +40,8 @@ services:
|
||||
PAPERCLIP_DEPLOYMENT_EXPOSURE: "private"
|
||||
PAPERCLIP_PUBLIC_URL: "${PAPERCLIP_PUBLIC_URL:-http://localhost:3100}"
|
||||
BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET must be set}"
|
||||
# Independent secret for signed approval-gated tool actions.
|
||||
PAPERCLIP_TOOL_ACTION_SIGNING_SECRET: "${PAPERCLIP_TOOL_ACTION_SIGNING_SECRET:-}"
|
||||
volumes:
|
||||
- paperclip-data:/paperclip
|
||||
depends_on:
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
const baseEnv = Object.fromEntries(
|
||||
["PATH", "HOME", "SystemRoot", "ComSpec", "TEMP", "TMP", "USERPROFILE"]
|
||||
.filter((key) => process.env[key] !== undefined)
|
||||
.map((key) => [key, process.env[key]]),
|
||||
);
|
||||
const compose = spawnSync("docker", ["compose", "version"], { env: baseEnv });
|
||||
assert.equal(compose.status, 0, "Docker Compose is required for this integration check.");
|
||||
|
||||
for (const [file, service] of [
|
||||
["docker/docker-compose.quickstart.yml", "paperclip"],
|
||||
["docker/docker-compose.yml", "server"],
|
||||
]) {
|
||||
test(`${file} forwards the independent tool signing secret`, () => {
|
||||
const temporary = mkdtempSync(path.join(os.tmpdir(), "paperclip-compose-secrets-"));
|
||||
const envFile = path.join(temporary, "empty.env");
|
||||
writeFileSync(envFile, "");
|
||||
try {
|
||||
for (const signingSecret of ["test-tool-signing-secret", undefined]) {
|
||||
const env = { ...baseEnv, BETTER_AUTH_SECRET: "test-session-secret" };
|
||||
if (signingSecret !== undefined) env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET = signingSecret;
|
||||
const result = spawnSync("docker", [
|
||||
"compose", "--env-file", envFile, "-f", file, "config", "--format", "json",
|
||||
], { cwd: repoRoot, env, encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const containerEnv = JSON.parse(result.stdout).services[service].environment;
|
||||
assert.equal(containerEnv.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET, signingSecret ?? "");
|
||||
assert.equal(containerEnv.BETTER_AUTH_SECRET, "test-session-secret");
|
||||
}
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user