## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Runner, plugin, and Playwright tests verify how those agents run, connect, and appear in the board > - Most package tsconfigs exclude tests, and Vitest strips types, so a type error in those tests fails nothing > - The tests drifted to 803 real type errors, and some of those errors hid assertions that checked nothing > - paperclipai/paperclip#18739 and paperclipai/paperclip#18738 check the server test tree and four test projects, but not these files > - This pull request adds a required check for these test files, fixes every error, and fails CI if a test file is in no program > - The benefit is that a type error in any test file outside `server/src` now fails CI ## Linked Issues or Issue Description Refs #18739 Refs #18738 **What happened?** 517 test files were in no program that required CI checks. A type error in those files did not fail CI. **Expected behavior** CI fails on a type error in any test file. **Steps to reproduce** Add `export const value: number = "text";` to a test file under `packages/paperclip-runner/src` on master. CI stays green, because `tsconfig.json` excludes test files and Vitest strips types. **Measured gap** Each row is a program with its tests included, on one thread. File and error counts are for head `7d4b5e907a`. Memory peaks were measured before the two latest merges of master. | Group | Test files | Real errors | Peak | | --- | ---: | ---: | ---: | | `paperclip-runner` Node source | 290 | 644 | 1.09 GiB | | `paperclip-runner` surfaces | 9 | 12 | 0.42 GiB | | `paperclip-runner` devtools and examples | 14 | 0 | 0.68 GiB | | `sandbox-providers/kubernetes` | 21 | 65 | 0.44 GiB | | 8 other sandbox providers and `bridge-template` | 32 | 4 | 0.34 GiB | | `plugins/sdk` | 22 | 15 | 0.41 GiB | | `plugin-workspace-diff` | 5 | 0 | 0.37 GiB | | `plugin-orchestration-smoke-example` | 1 | 1 | 0.15 GiB | | `kv-demo-mcp-server` | 3 | 0 | 0.13 GiB | | `packages/db` scripts | 1 | 0 | 1.01 GiB | | 8 Playwright suites under `tests/` | 52 | 25 | 1.72 GiB | | `tests/runner-acceptance` | 3 | 0 | 0.58 GiB | | `tests/e2e`, through layer declarations | 63 | 37 | at most 1.59 GiB | | `ui` `AttentionInteractionResolver.test.tsx` | 1 | 0 | n/a | | **Total** | **517** | **803** | | 27 of the 803 errors came from master while this pull request was open. Each merge of master brought new test code that no check had read. Errors that were config errors, not real errors: - One TS2688 for each provider, because the root install does not install provider dependencies. - 9 in the Runner Node program (TS7016 and TS6059). - 2 in `tsconfig.browser.json`. TypeScript 7 removed `baseUrl`, so that config was invalid. - 213 in the Playwright suites (`@/*` aliases and Vite types). - About 60 in `tests/e2e` (import extensions and aliases). `packages/plugins/create-paperclip-plugin` has no gap. Its tsconfig excludes its test, but `cli/tsconfig.json` reaches that test through an import. `server/test-baselines` also has no gap, because `tests/lifecycle-baseline` reads it. ## What Changed - Added `scripts/typecheck-test-programs.mjs` and its tests. The `Typecheck + Release Registry` job runs it. It checks each program in `PROGRAMS` on one thread. - The script fails if a tracked `*.test.ts(x)` or `*.spec.ts(x)` file outside `server/src` is in no program. The compiler file list decides this, because `include` drops `foo.test.tsx` when `foo.test.ts` exists. - Added a `tsconfig.test.json` to each uncovered package, and `tests/tsconfig.playwright.json`. - Added `tests/e2e` to `PROJECTS` in `scripts/typecheck-test-projects.mjs`, with a `tsconfig.json` and a `tsconfig.layers.json`. - Added `scripts/typecheck-sandbox-providers`. It is a small workspace that installs provider SDKs from one lockfile. I did not add a lockfile inside each provider, because `scripts/build-standalone-public-packages.mjs` uses a frozen install when one exists, and that changes what a release resolves. - Corrected `packages/paperclip-runner/tsconfig.browser.json` for TypeScript 7. - Set `rootDir` to `..` in `packages/paperclip-runner/tsconfig.test.json`, because one Runner test imports from `packages/adapter-utils/src`. The program does not emit files, so this changes no build output. - Moved the Kubernetes provider from `vitest ^3.2.4` to `^4.1.8`, the version the other providers use. Vitest 3 requires `tinypool ^1.1.1`, which has two critical advisories (GHSA-85c8-ppgw-ccpr and GHSA-5gmw-xhrv-c9v3). The first patched version is 2.1.2. Its tests have the same result on both versions: 189 passed, and the same 12 `file-sync` tests failed. - Moved the Kubernetes provider from `zod ^3.24.2` to `^4.6.6`, and gave its two `z.record` calls a key schema. This is the only production change, and it stays in this pull request. See "Production change for review". - Renamed `AttentionInteractionResolver.test.ts` to `AttentionInteractionResolver.replace.test.ts`, so TypeScript reads the `.test.tsx` beside it. - Fixed all 803 errors, with changes to test files and test support only. The fix for `contracts/native-execution.test.ts` is the same as the fix in paperclipai/paperclip#18739, so those two changes merge cleanly. - Updated `doc/DEVELOPING.md`. This pull request does not edit `server/scripts/typecheck-layers.mjs`. paperclipai/paperclip#18739 and paperclipai/paperclip#18738 have both merged, and this branch includes them. ### Production change for review The new lockfile made dependency review flag `zod@3.25.76` as a high-severity advisory (GHSA-mrhx-c7xc-m3w5). The first patched version is 4.6.6, so 3.x has no fix. The published Kubernetes plugin uses zod to validate operator config. The zod 3 source does not compile against zod 4, so a typecheck-only override cannot fix this. The existing tests do not prove this change, because they also passed with the invalid one-argument `z.record`. So I parsed 45 inputs with the same schema on zod 3.25.76 and zod 4.6.7: | Result | Inputs | Detail | | --- | ---: | --- | | Identical | 23 | Includes defaults, valid config, and a `__proto__` key | | Same outcome, new error wording | 21 | For example, `Invalid url` is now `Invalid URL`. `validateConfig` shows this text to operators. | | Different outcome | 1 | `jobTtlSecondsAfterFinished: 2 ** 60` was valid and is now rejected. The new limit is `Number.MAX_SAFE_INTEGER`. | I did not test this against a real cluster. The decision is to keep this change in this pull request. Since then, paperclipai/paperclip#17364 has moved the rest of the repository to zod 4.6.x, so the Kubernetes plugin now matches it. ### Behavior change for review `reducedMotion` under `use` is not a Playwright option. It had no effect in 11 configs and 2 `test.use` calls, so those suites ran with `no-preference`. The options are now under `contextOptions`, so reduced motion now takes effect. `app-shell-design.spec.ts` asserts a real transition, so it now asks for `no-preference` itself. The baseline manifest has `snapshotCount: 0`, so this invalidates no published baseline. A frozen trial reference PNG can differ. ### Findings These are product questions. I did not change them, and I did not hide them with casts: 1. **`PrpEvent` and its schema disagree.** The TypeScript type in `src/protocol/replay-contract.ts` requires `normalizedSessionId`. The `required` list in `protocol/schemas/event.schema.json` does not include it. `validatePrpEvent` accepts events that the type forbids. 2. **`requestCheckboxConfirmation` is stricter than its runtime.** It uses the output type, so plugin authors must pass `continuationPolicy` and `allowDeclineReason`, although the schema gives them defaults. `z.input` would fix this. 3. **`RetainedRunnerdMaintenanceInput.backend.kind` is a free string.** The quarantine domain comes from `descriptor.kind`, but the release uses `proof.backend.kind`. If they differ, `completeRetainedNativeSessionCleanup` returns 0 and the quarantine stays. The production caller passes `"runner"` today, so this is not a live bug. 4. **`CodexHarnessSession.close(input?)` is optional, but the contract requires `{ reason }`.** These are tests that verified nothing. I corrected them, and they pass: - `backends/native-instruction-measurement.test.ts` filtered on `placement === "core"`, which is not a valid placement. It measured an empty tool list. It now measures 14 tools. - `drivers/acpx/codex-acpx-driver.test.ts` checked for `usage.updated`, which is not an event type. - The `hostFixture()` helper in `drivers/acpx/runtime-host.test.ts` ignored two overrides, so `expect(prepareSandbox).not.toHaveBeenCalled()` could not fail. - `live/live-session.test.ts` set `agent: "pi"` before a spread that overwrote it. - `native-distribution-integrity.test.ts` passed an AbortSignal where `openCommand` expects `{ signal }`. These are gaps that this pull request does not close: - **The root lockfile still has some old zod versions.** paperclipai/paperclip#17364 moved the direct zod dependencies to 4.6.x. `pnpm-lock.yaml` still resolves `zod@3.25.76`, `4.1.11`, and `4.4.3`, which are below the patched 4.6.6. `packages/paperclip-runner/scripts/pi-distribution/package.json` pins `zod` to `3.25.76`, and some agent SDKs resolve `4.4.3`. Dependency review does not flag them, because this pull request does not add them. - **Dead GitHub coverage.** `chat-adapters-ui-providers.spec.ts` returned early for GitHub, so four later `provider === "github"` branches never ran. I removed them. They included the private key file and paste match, and the reconnect flow. Nothing asserts those behaviors in that spec now. - **Provider tests do not run in CI.** The root `vitest.config.ts` lists only Daytona. The other 8 providers, `bridge-template` included, are typechecked now, but CI does not run their tests. - **One `packages/db` test never runs.** `scripts/clean-poisoned-claude-sessions.test.ts` is outside the `include` of `packages/db/vitest.config.ts`. - **`plugin-orchestration-smoke-example` does not collect.** `@paperclipai/shared` is not linked in its `node_modules`. It passes 3 of 3 with an alias. ## Verification Run these commands: ```sh node scripts/typecheck-test-programs.mjs --low-memory pnpm --filter @paperclipai/server typecheck node scripts/typecheck-test-projects.mjs --low-memory --reuse-declarations node --test scripts/typecheck-test-programs.test.mjs ``` Results on macOS (arm64), Node 26.12. CI uses Node 24, and its `Typecheck + Release Registry` job also passes. At head `7d4b5e907a`, the test programs, `tests/e2e`, and the script tests pass again with 0 errors. The times and peaks below come from the run before the two latest merges of master: | Check | Result | | --- | --- | | Test programs, `--low-memory` | Exit 0, 0 errors, 42 seconds, peak 1.78 GiB | | Server layers, low memory | Exit 0, 0 errors, 38 seconds, peak 3.41 GiB | | Test projects, including `tests/e2e` | Exit 0, 0 errors, 13 seconds, peak 1.59 GiB | | Script tests | 12 passed | | Injected type error in each of 23 places | Each one failed its program with TS2322 | | Coverage check | 0 unchecked files, 10 seconds | I did not run this in a 4 GiB container. The peaks above are `maximum resident set size` for the largest single process. **Runtime check of the fixes.** Types do not prove that a test still works, so I ran the suites before the two latest merges of master: | Suite | Result | | --- | --- | | Runner, full suite | 4,885 passed, 3 failed, 13 skipped | | Sandbox providers, 10 packages | 939 passed, 12 failed, 36 skipped | | Plugin SDK | 200 passed | | `kv-demo-mcp-server` | 12 passed | | Renamed UI test and its sibling | 4 passed | All 15 failures exist without these fixes or come from the local machine: - **3 Cursor Cloud timeouts.** `artifact-delivery.test.ts` has 1 and `cursor-cloud-driver.test.ts` has 2. They time out in a full local run, and all 133 tests in those files with `codex-security-config.test.ts` pass when run alone. They fail in the same way with those two files at their unedited versions. - **12 Kubernetes `file-sync.test.ts` failures.** They fail in the same way before and after the fixes. The first failure is that `/proc/self/fd` does not exist on macOS. After each of the two latest merges of master, I ran only the Runner test files that I changed: | Merge | Result | | --- | --- | | `885c249cb9`, 8 files | 534 passed | | `7d4b5e907a`, 7 files | 672 passed, 16 failed | The 16 failures are in the two `source-reconciliation` test files. They fail in the same way with master's unedited versions of those files. 15 of them fail with `ACPX sidecar exited at initialize`, so I think my local build of the Rust and sidecar binaries is out of date. I did not rebuild them to check. CI builds them from scratch, and all Runner jobs pass in CI at `7d4b5e907a`. **Not run:** - `tests/e2e` and the 8 Playwright suites need a server, so most of them have only compile checks. `playwright test --list` loads 308 tests from the main e2e config. - `packages/plugins/sandbox-providers/kubernetes/test/integration` needs a cluster, so it has only compile checks. - I did not run the full `pnpm test:run` or root `pnpm build`. Required CI runs them. **Reduced motion.** I ran 6 motion-sensitive Storybook specs in system Chrome with a temporary config, once with `reduce` and once with `no-preference`. Both runs had 45 passed and the same 5 failed. So reduced motion did not change those results. The 5 failures do not depend on motion. At least one comes from the browser, because system Chrome prints `oklch(55.6% 0 0)` where the test expects `oklch(0.556 0 0)`. I did not run the other Storybook specs. The `storybook-visual` label runs that workflow. ## Risks - **A new lockfile to maintain.** A change to a provider `package.json` now fails the typecheck job until someone refreshes `scripts/typecheck-sandbox-providers/pnpm-lock.yaml`. The failure message gives the command. - **The job has a new install.** The step downloads provider SDKs, so it depends on the npm registry. It uses `--frozen-lockfile` and `--ignore-scripts`. - **The local install replaces provider `node_modules`.** A contributor with a standalone provider install must reinstall that provider after running the script. - **Reduced motion can change Storybook results.** See "Behavior change for review". That workflow is not part of required CI. - **Kubernetes plugin validation changed.** A release of this plugin ships zod 4. Error wording changes, and very large integers are now rejected. See "Production change for review". - **Master moves fast.** Each merge of master brought new test code with type errors, 27 so far. After this merges, the check reports those errors in the pull request that adds them. ## Model Used - Provider: Anthropic Claude, through Claude Code - Model: Claude Opus 5.5 (`claude-opus-5-5`) - Capabilities: tool use, code execution, and subagents for the type fixes in separate files ## 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) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details. The app generated the branch name. - [ ] I have run tests locally and they pass. See the failures explained in Verification. - [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 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
KV Demo MCP Server
A standalone, self-contained MCP server for demos. One process exposes four key/value MCP tools and a tiny web UI that renders the values those tools mutate — so you can call a tool from a Paperclip agent and watch the value appear in a browser tab.
The shared in-memory store is the whole point: the same Map<string, string>
backs every tool call and every UI render. There is no database, no file, no
persistence. Restart the process and the store is empty again. That makes this
the right fixture for showing what Paperclip stores versus what the demo
package stores — the package stores the values, Paperclip stores the
connection, the profile/policy decisions, and the audit log.
This package pairs with the operator guide in doc/MCP-ACCESS-GOVERNANCE.md and the recorded walkthrough in doc/MCP-DEMO-SCRIPT.md.
What you get
- Four MCP tools (over the Streamable HTTP transport):
kv_list(read) — list every key/value, optionally filtered byprefix.kv_get(read) — read one key.kv_set(write) — set one key to a string value.kv_delete(destructive) — delete a key. CarriesdestructiveHint: trueso Paperclip's catalog quarantines it on first sight.
- A Values UI at
/— an auto-refreshing HTML table over the same store. - A JSON state route at
/api/state— what the UI polls.
Local startup
From the repo root:
pnpm --filter @paperclipai/kv-demo-mcp-server build
pnpm --filter @paperclipai/kv-demo-mcp-server start
Or run the source directly during development:
cd packages/kv-demo-mcp-server
node --experimental-strip-types src/main.ts # Node 24+
By default it listens on http://127.0.0.1:8848 and prints three URLs to
stderr on startup:
- MCP endpoint —
POST http://127.0.0.1:8848/mcp(Streamable HTTP) - Values UI —
GET http://127.0.0.1:8848/(auto-refreshes every 2s) - JSON state —
GET http://127.0.0.1:8848/api/state
Open the Values UI in a browser tab and keep it visible. Every successful
kv_set / kv_delete lands in that table within ~2 seconds.
Configuration
All configuration is via environment variables:
| Variable | Default | Purpose |
|---|---|---|
PORT (or KV_DEMO_PORT) |
8848 |
Listen port. Use 0 for a random free port. |
KV_DEMO_HOST |
127.0.0.1 |
Bind host. Use 0.0.0.0 to accept connections from another machine on the LAN. |
KV_DEMO_TOKEN |
unset | Optional shared secret. When set, data and MCP routes require it. |
When KV_DEMO_TOKEN is set, present it as Authorization: Bearer <token>.
For the browser UI, open http://127.0.0.1:8848/#token=<token>; the fragment is
not sent to the server and the page removes it from the address bar before
polling /api/state with the bearer header. The token is a convenience guard
for local demos, not a hardened auth scheme; do not expose this server to
untrusted networks.
Connecting from Paperclip
The KV demo is meant for the remote_http connection path. The server speaks
Streamable HTTP at /mcp, runs in a single process so the Values UI and the
MCP tools share state, and listens on a fixed loopback port. Paperclip's
remote-HTTP gateway proxies every call through policy and audit while leaving
process supervision to you (just Ctrl+C the server when you are done).
Via the Connect-an-app wizard (recommended)
- Open the company's Tools UI at
/<prefix>/companies/<companyId>/tools. - Go to Connect an app and pick Connect with a link.
- Paste
http://127.0.0.1:8848/mcpand give it a name (e.g. "KV demo"). - If you launched the server with
KV_DEMO_TOKEN, paste the token in the App key field. The wizard stores it as anAuthorization: Bearer …header secret. - Pick the profile defaults (read/write/destructive) and finish. Paperclip
imports the four tools and quarantines
kv_delete.
The wizard hits this API under the hood:
curl -fsS -X POST \
-H "Authorization: Bearer $BOARD_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_URL/api/companies/$COMPANY_ID/tools/apps/connect" \
-d '{
"link": "http://127.0.0.1:8848/mcp",
"name": "KV demo"
}'
If a token is set, add the credentialValues block:
{
"link": "http://127.0.0.1:8848/mcp",
"name": "KV demo",
"credentialValues": {
"credentials.authorization": "my-demo-secret"
}
}
Transport tradeoffs
remote_http(recommended for this demo) — required if you want the Values UI to reflect what the agent just did. The KV demo intentionally holds state in one process and exposes both the MCP endpoint and the UI from that process. Paperclip'sremote_httpgateway forwards every call to the same loopback URL, so the UI always sees the same store the tools mutated.local_stdio(not used here) — runs MCP servers as supervised child processes inside Paperclip's runtime slots. Reserved for trusted local deployments (developer laptop,local_trustedorauthenticated/privatewithPAPERCLIP_TRUSTED_MCP_RUNTIME_HOSTset on a single trusted worker). Each runtime slot has its own process, which would give each slot its own in-memory store — you would lose the shared-state property that makes this demo work. Use the approved stdio templates that ship in the Paperclip build when you need stdio; do not try to shoehorn this server into a local-stdio template.
For the full transport policy across deployment modes, see MCP-ACCESS-GOVERNANCE.md → Local trusted deployment.
What you should see in Paperclip
After the wizard finishes, expect:
- Catalog — four tools imported from this connection.
kv_listandkv_getare taggedread;kv_setis taggedwrite;kv_deleteis taggeddestructiveand starts inquarantinedstatus. - Tools panel on an agent —
kv_list,kv_get, and (with the default ask-first policy)kv_set.kv_deleteis not listed until you take it out of quarantine. - Audit feed — one row per call. Reads show
tool_gateway.call_completedwithdecision: allow. Writes that hit the default ask-first policy showtool_gateway.approval_requestedfollowed bytool_gateway.call_allowedandtool_gateway.call_completedonce you approve. Calls tokv_deleteshowtool_gateway.call_deniedwithreasonCode: quarantined_catalog_entry.
The recorded walkthrough in doc/MCP-DEMO-SCRIPT.md runs all three cases end-to-end against this package and matches the audit rows above.
What lives where
| Concern | Stored in this package | Stored in Paperclip |
|---|---|---|
Key/value entries (kv_* data) |
In-memory Map, lost on restart. |
Not stored. Paperclip never sees the values directly; the gateway only sees the MCP request/response envelope and the redacted-by-policy view the audit log keeps. |
| Connection record (URL, token, transport) | Not stored. | Persisted in tool_connections. The optional token becomes a secret. |
| Profile / policy / binding decisions | Not stored. | Persisted under tool_profiles, tool_policies, and tool_profile_bindings. |
| Approval action requests | Not stored. | Persisted under tool_action_requests, linked to issue-thread interactions. |
| Audit rows for each call | Not stored. | Persisted under tool_call_events. Append-only. |
Cleanup and reset
Resetting the demo state usually means resetting this process; Paperclip's records stay intact unless you also archive the connection.
-
Empty the KV store —
Ctrl+C(orkill) the server and start it again. The new process starts with zero keys and revision0. There is no in-process reset endpoint by design; restart is the single supported reset. -
Free the port — if startup logs
EADDRINUSE, anotherkv-demoprocess is still bound to8848. Find and kill it:lsof -nP -iTCP:8848 -sTCP:LISTEN kill <pid> -
Quiesce the Paperclip side — disable the connection so the gateway stops trying to reach the now-stopped server:
curl -fsS -X PATCH \ -H "Authorization: Bearer $BOARD_API_KEY" \ -H "Content-Type: application/json" \ "$PAPERCLIP_URL/api/tool-connections/$CONNECTION_ID" \ -d '{ "enabled": false, "status": "disabled" }' -
Archive the application when you are fully done. Audit history is retained, but no new calls can land:
curl -fsS -X PATCH \ -H "Authorization: Bearer $BOARD_API_KEY" \ -H "Content-Type: application/json" \ "$PAPERCLIP_URL/api/tool-applications/$APPLICATION_ID" \ -d '{ "status": "archived" }'
The KV demo is a fixture, not a piece of infrastructure. Treat each session as disposable: start it, run the demo, kill it.