From c9db03bcabdcebd378cc214ccd8b58e17c920d98 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 22 Sep 2026 12:32:18 -0700 Subject: [PATCH] Redact runtime capabilities and require the SDK isolation check HTTP request logs retained the runtime GitHub capability header. Add it to the shared redaction policy and cover success, denied, and failed requests with canary credentials. Run the real optional Sentry SDK regression in a dedicated CI job with the audited peer installed outside the workspace. Require the SDK in that job so missing installation cannot silently skip the regression. Validation: 123 focused tests, the canonical real-SDK CI command, server typecheck, module boundaries, YAML parse, and secret scans passed. Co-Authored-By: Paperclip --- .github/workflows/sentry-contract.yml | 62 +++++++++++++++++++ .../src/__tests__/http-log-redaction.test.ts | 29 +++++++++ .../run-failure-sentry-real-sdk.test.ts | 4 ++ server/src/middleware/http-log-redaction.ts | 2 + 4 files changed, 97 insertions(+) create mode 100644 .github/workflows/sentry-contract.yml diff --git a/.github/workflows/sentry-contract.yml b/.github/workflows/sentry-contract.yml new file mode 100644 index 0000000000..f4bb2bcebb --- /dev/null +++ b/.github/workflows/sentry-contract.yml @@ -0,0 +1,62 @@ +name: Sentry SDK contract + +on: + pull_request: + paths: + - .github/workflows/sentry-contract.yml + - server/package.json + - server/src/sentry*.ts + - server/src/peer-version-check.ts + - server/src/__tests__/*sentry*.test.ts + push: + branches: [master] + paths: + - .github/workflows/sentry-contract.yml + - server/package.json + - server/src/sentry*.ts + - server/src/peer-version-check.ts + - server/src/__tests__/*sentry*.test.ts + +permissions: + contents: read + +concurrency: + group: sentry-contract-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + sentry-contract: + name: Real Sentry SDK isolation + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + package-manager-cache: false + + - name: Setup pnpm + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + with: + version: 9.15.4 + + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + + - name: Install the audited optional SDK outside the workspace + shell: bash + run: | + sentry_version=$(node -p 'require("./server/package.json").peerDependencies["@sentry/node"]') + npm install --prefix "$RUNNER_TEMP/sentry-contract-sdk" --ignore-scripts --no-audit --no-fund --package-lock=false "@sentry/node@$sentry_version" + + - name: Verify run failure isolation with the real SDK + env: + NODE_PATH: ${{ runner.temp }}/sentry-contract-sdk/node_modules + PAPERCLIP_REQUIRE_SENTRY_TEST_SDK: "1" + run: pnpm --filter @paperclipai/server exec vitest run src/__tests__/run-failure-sentry-real-sdk.test.ts diff --git a/server/src/__tests__/http-log-redaction.test.ts b/server/src/__tests__/http-log-redaction.test.ts index a16e56e69a..62def738d8 100644 --- a/server/src/__tests__/http-log-redaction.test.ts +++ b/server/src/__tests__/http-log-redaction.test.ts @@ -399,6 +399,35 @@ describe("HTTP logger redaction", () => { expect(log.res.headers["set-cookie"]).toBe("[Redacted]"); }); + it.each([200, 403, 500])("redacts runtime GitHub capabilities from HTTP %i logs", async (status) => { + const capability = "runtime-github-capability-canary"; + const chunks: string[] = []; + const stream = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(chunk.toString()); + callback(); + }, + }); + const app = express(); + app.use(createHttpLogger(pino({ redact: [...HTTP_LOG_REDACT_PATHS] }, stream))); + app.post("/runtime-tools/github/credentials", (_req, res) => { + res.status(status).json({ status }); + }); + + await request(app) + .post("/runtime-tools/github/credentials") + .set("X-Paperclip-Github-Capability", capability) + .send({}) + .expect(status); + + const output = chunks.join(""); + expect(output).not.toContain(capability); + const log = JSON.parse(output.trim()); + expect(log.req.headers["x-paperclip-github-capability"]).toBe("[Redacted]"); + expect(log.req.url).toBe("/runtime-tools/github/credentials"); + expect(log.res.statusCode).toBe(status); + }); + it("drops OAuth callback query data from the message and structured request", async () => { const chunks: string[] = []; const stream = new Writable({ diff --git a/server/src/__tests__/run-failure-sentry-real-sdk.test.ts b/server/src/__tests__/run-failure-sentry-real-sdk.test.ts index 75f4707edf..524b52445c 100644 --- a/server/src/__tests__/run-failure-sentry-real-sdk.test.ts +++ b/server/src/__tests__/run-failure-sentry-real-sdk.test.ts @@ -20,6 +20,10 @@ const sentryPackage = (() => { } })(); +if (process.env.PAPERCLIP_REQUIRE_SENTRY_TEST_SDK === "1" && !sentryPackage) { + throw new Error("The Sentry SDK contract job requires the audited optional peer"); +} + afterEach(async () => { await sentryPackage?.close(2000); vi.unstubAllEnvs(); diff --git a/server/src/middleware/http-log-redaction.ts b/server/src/middleware/http-log-redaction.ts index a7d7ed05f2..8b50c5cf37 100644 --- a/server/src/middleware/http-log-redaction.ts +++ b/server/src/middleware/http-log-redaction.ts @@ -10,6 +10,8 @@ export const HTTP_LOG_REDACT_PATHS = [ 'req.headers["x-csrf-token"]', 'req.headers["x-xsrf-token"]', 'req.headers["x-api-key"]', + // Runtime GitHub capabilities authorize credential acquisition for a live run. + 'req.headers["x-paperclip-github-capability"]', // Telegram's optional webhook verification header is a reusable bearer // secret sent on every provider callback. 'req.headers["x-telegram-bot-api-secret-token"]',