Commit Graph
12 Commits
Author SHA1 Message Date
Jannes StubbemannandClaude Opus 4.8 9e750d3e92 feat(codex-local): env-driven gateway routing via PAPERCLIP_CODEX_PROVIDERS config.toml (#7919)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The `codex-local` adapter runs the OpenAI Codex CLI; Paperclip
already maintains a managed `CODEX_HOME` per company and ships it to
remote/sandboxed execution targets
> - Deployments increasingly put an OpenAI-compatible LLM gateway
between the harness and the model for cost, governance, or
data-residency reasons: LiteLLM, OpenRouter, Portkey, Kong, a corporate
proxy, self-hosted models (vLLM/Ollama), or region-pinned/sovereign
endpoints. But Codex has no CLI flag or env var for a custom endpoint:
its only mechanism is `[model_providers.<id>]` tables (with `base_url`,
`env_key`, `wire_api`) in `$CODEX_HOME/config.toml`, selected by a
root-level `model_provider` key
> - Today there is no supported way to get such provider config into the
managed `CODEX_HOME`, so gateway routing requires hand-editing files the
adapter owns and regenerates
> - This pull request adds the codex analogue of #7837's opencode
mechanism: a `PAPERCLIP_CODEX_PROVIDERS` JSON env var whose shape maps
1:1 onto codex's TOML schema, merged into the managed `config.toml` so
the existing asset-shipping + `env.CODEX_HOME` mechanics deliver it to
local and sandboxed runs alike; nothing here is specific to one hosting
setup
> - The benefit is Codex works behind any OpenAI-compatible gateway with
config only; with no env set, behavior is unchanged

## Linked Issues or Issue Description

No existing issue; describing in-PR (feature / adapter enhancement).

- **Gap:** there is no supported way to register a custom/gateway
`[model_providers.*]` endpoint for `codex-local`. Codex's only
custom-endpoint mechanism is `config.toml` (`base_url` + `env_key` +
`wire_api`, selected via the root `model_provider` key), and the adapter
owns/regenerates the managed `CODEX_HOME`, so operators cannot durably
hand-edit it.
- Related: #7837 (the opencode-local analogue of this change, same
env-driven gateway-routing pattern). Searched for duplicate/related PRs:
no existing codex-local gateway/provider-routing PR found.

> Note on ROADMAP: this is adapter-level, opt-in config (defaults
unchanged) that *enables* gateway routing for one harness; it is not the
core "Cloud / Sandbox agents" platform work itself.

## What Changed

- New `prepareCodexRuntimeConfig()`
(`packages/adapters/codex-local/src/server/runtime-config.ts`): reads
`PAPERCLIP_CODEX_PROVIDERS` (run env first, then `process.env`), shaped
as `{"providers": {"<id>": {base_url, env_key, wire_api, ...}},
"model_provider": "<id>"}`, and merges it into the managed
`CODEX_HOME`'s `config.toml`. No-op when unset or empty.
- A malformed value (invalid JSON, not a JSON object, no `providers`
object, no usable provider entries, or individual entries with empty
names or non-object values, which are skipped by name) is never silently
dropped: each case surfaces a distinct, user-visible note (via the
prepare notes, which flow into command notes + `onLog`) and unusable
input leaves `config.toml` untouched.
- Merge is marker-delimited and TOML-correct: existing `config.toml`
content is preserved between two managed blocks. Root keys (e.g.
`model_provider`) are prepended **before the first table header** (TOML
root-region rule), `[model_providers.*]` tables are appended.
Pre-existing same-name provider sections and root `model_provider` keys
are excised so the managed definitions win without duplicate-table parse
errors.
- `{env:VAR}` placeholders are expanded server-side for
literal-credential fields; `env_key` indirection remains the preferred
path.
- Crash-safe restore: prepare writes a pre-run backup
(`config.toml.paperclip-backup`) before the merged file; `cleanup()`
restores the original in the execute `finally` and removes the backup.
If a run never reaches `cleanup()` (a throw during the setup between
prepare and execution, or SIGKILL), the next prepare restores the
original from the backup with full fidelity, including user
`[model_providers.*]` sections the merge excised (review feedback, P2);
plain block-stripping remains the fallback for pre-backup state.
- An explicit adapter-config `env.CODEX_HOME` override is treated as
user-managed: no merge, surfaced as a command note.
- Dependency-free hand-emitted TOML (strings/numbers/booleans, arrays of
scalars, plain objects as inline tables); basic strings escape
U+0000-U+001F and U+007F per TOML 1.0 (review feedback, P2). Merged
output was additionally validated locally with python tomllib during
development; the committed tests assert the structural invariants.
- `execute.ts` wiring: `prepareCodexRuntimeConfig` runs after
`prepareManagedCodexHome` (before the home ships to the remote target),
notes surface via `onLog` + command notes, and the `finally` calls
`cleanup()`.

**Note for reviewers:** current codex removed `wire_api = "chat"`
(openai/codex#10157, Feb 2026), so gateway provider configs must use
`wire_api = "responses"`, i.e. the gateway must speak `/v1/responses`.
The adapter passes the value through verbatim; this is a codex-side
constraint worth knowing when configuring it.

## Verification

- `pnpm --filter @paperclipai/adapter-codex-local build` and
`typecheck`: tsc clean against current `master`
- `pnpm exec vitest run packages/adapters/codex-local`: 45 passing
(incl. 17 `runtime-config` tests: fresh-merge + cleanup restore,
root-region placement, same-name provider override, inline
tables/arrays, DEL escaping, `{env:}` expansion from run env +
`process.env`, per-case malformed-input notes with `config.toml`
untouched, skipped-entry notes alongside a successful merge, silent
no-op when unset/empty, explicit-`CODEX_HOME` skip note, backup restore
of excised user sections after an interrupted run, backup removal on
cleanup, stale-block self-heal, re-run replacement)
- Verified end-to-end: a codex agent in a hardened Kubernetes (gVisor)
sandbox completed a real task routed through an OpenAI-compatible
gateway's `/v1/responses`, with a billed usage row recorded on the
gateway. That deployment supplies the verification evidence; the
mechanism is gateway-agnostic.

## Risks

Low. Entirely env-driven and opt-in; with `PAPERCLIP_CODEX_PROVIDERS`
unset the adapter never touches `config.toml` and behavior is
byte-identical to before. The merge preserves user content, restores the
original file on cleanup, and survives interrupted runs via the pre-run
backup; malformed input surfaces a visible note and is ignored without
touching `config.toml`. No migration/UI impact.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`, 1M context), extended thinking +
tool use, via Claude Code.

## 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 (adapter-level opt-in config enabling
gateway routing; not the core sandbox-platform work, noted above)
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (#7837 is the opencode analogue; no codex-local duplicate
found)
- [x] I have either (a) linked existing issues OR (b) described the
issue in-PR following the relevant issue template
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (n/a, no UI)
- [ ] I have updated relevant documentation to reflect my changes (env
var documented inline; no central doc references the adapter env yet)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green (green on the previous head;
re-running on the final note-copy polish commit)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(both review P2s are fixed at head: the interrupted-run restore via the
pre-run backup and the U+007F escaping; a re-review is requested for the
note-copy polish)
- [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 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:13:31 -07:00
Jannes StubbemannandClaude Opus 4.8 6e4aca9c67 feat(pi-local): env-driven gateway routing via PAPERCLIP_PI_PROVIDERS models.json (#7920)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The `pi-local` adapter runs the Pi coding agent, including inside
remote/sandboxed execution targets; Pi resolves `--provider P --model M`
by an exact (provider, id) match against its model registry, and it has
no base-url CLI flag or env var: a `models.json` in its agent config dir
(`$PI_CODING_AGENT_DIR`, falling back to `$HOME/.pi/agent`) is its only
mechanism for custom or OpenAI/Anthropic-compatible endpoints
> - Deployments increasingly put an LLM gateway between the harness and
the model for cost, governance, or data-residency reasons: LiteLLM,
OpenRouter, Portkey, Kong, a corporate proxy, self-hosted models
(vLLM/Ollama), or region-pinned/sovereign endpoints. Today there is no
supported way to get such provider config into Pi's registry for
orchestrated runs
> - The opencode adapter gained the equivalent capability in #7837 and
codex in #7919; this pull request is the Pi analogue, so the harness
layer stays gateway-agnostic regardless of which CLI an agent uses;
nothing here is specific to one hosting setup
> - This pull request reads `PAPERCLIP_PI_PROVIDERS` (Pi's `models.json`
`providers` shape), materialises a managed `models.json` in a temp
agent-config dir, points `PI_CODING_AGENT_DIR` at it, and ships it to
remote execution targets with the run
> - The benefit is Pi works behind any compatible gateway with config
only; with no env set, behavior is unchanged

## Linked Issues or Issue Description

No existing issue; describing in-PR (feature / adapter enhancement).

- **Gap:** there is no supported way to register custom/gateway
providers + models for `pi-local`. Pi's only custom-endpoint mechanism
is a `models.json` in its agent config dir, and orchestrated (especially
sandboxed) runs have no way to provision one declaratively.
- Related: #7837 (the opencode-local analogue, same env-driven
gateway-routing pattern) and #7919 (the codex-local analogue). Searched
for duplicate or related PRs: no existing pi-local
gateway/provider-routing PR found.

> Note on ROADMAP: this is adapter-level, opt-in config (defaults
unchanged) that *enables* gateway routing for one harness; it is not the
core "Cloud / Sandbox agents" platform work itself.

## What Changed

- New `packages/adapters/pi-local/src/server/runtime-config.ts`:
`preparePiRuntimeConfig()` reads `PAPERCLIP_PI_PROVIDERS` (a JSON object
in pi's `models.json` `providers` shape) from the run env, then
`process.env`. When set, it expands `{env:VAR}` placeholders (run env
first, then process env; unresolvable placeholders left intact), writes
`{"providers": ...}` to a managed temp dir as `models.json`, and returns
env with `PI_CODING_AGENT_DIR` pointing at it plus a cleanup handle.
- `execute.ts`: the prepared dir ships to remote execution targets as
the managed-runtime asset `agentConfig` (same mechanism as opencode's
`xdgConfig`), and `PI_CODING_AGENT_DIR` is repointed to the in-target
path; cleanup runs in `finally`.
- Misconfiguration is visible, not silent: a set-but-unusable
`PAPERCLIP_PI_PROVIDERS` (invalid JSON, not an object, no provider
objects) surfaces an explanatory note instead of proceeding unconfigured
into an opaque model-not-found failure later, and provider entries with
non-object values are skipped with a note naming them. Unset/empty stays
a silent no-op (feature off).
- Defaults unchanged: with `PAPERCLIP_PI_PROVIDERS` unset, the adapter
behaves byte-for-byte as before, for local runs and for every existing
sandbox provider.

## Verification

- All pi-local tests green against this base (new: providers written
verbatim, `{env:VAR}` expansion from run env/process env/unresolvable,
no-op when unset, `PI_CODING_AGENT_DIR` set and shipped, the
misconfiguration notes incl. skipped non-object entries, remote asset
sync + env repoint). Typecheck and build clean.
- Production end-to-end evidence (our deployment, used as verification,
not as the scope of the change): a pi agent in a Kubernetes gVisor
sandbox resolved a custom provider from the shipped `models.json`,
completed an assigned issue through an Anthropic-compatible gateway, and
landed a billed usage row.

## Risks

Low. The entire feature is opt-in behind one env var; the only behavior
change when it is set is the intended one. The managed dir replaces the
host agent dir for the run by design (credentials travel inside the
provider config or via env-key indirection), which is the correct
posture for orchestrated runs. No migration/UI impact.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`, 1M context), extended thinking +
tool use, via Claude Code.

## 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 (adapter-level opt-in config enabling
gateway routing; not the core sandbox-platform work, noted above)
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (#7837 and #7919 are the opencode/codex analogues; no
pi-local duplicate found)
- [x] I have either (a) linked existing issues OR (b) described the
issue in-PR following the relevant issue template
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (n/a, no UI)
- [ ] I have updated relevant documentation to reflect my changes (env
var documented inline; no central doc references the adapter env yet)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green (green on the previous head;
re-running on the final note-copy polish commit)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(both prior review findings are fixed at head: the indirect notes-based
guard is now an explicit `agentConfigDir` handle, and a failed
`models.json` write no longer leaks the temp dir; a re-review is
requested for the note-copy polish)
- [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 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:12:23 -07:00
Jannes StubbemannandClaude Opus 4.8 1ac1ba5442 feat(opencode-local): env-driven gateway routing (custom providers, small/cheap model, remote allow-all) (#7837)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The `opencode-local` adapter runs the OpenCode harness; its
model/provider routing assumes built-in providers (anthropic/openai/...)
and their default models
> - Deployments increasingly put an OpenAI/Anthropic-compatible LLM
gateway between the harness and the model for cost, governance, or
data-residency reasons: LiteLLM, OpenRouter, Portkey, Kong, a corporate
proxy, self-hosted models (vLLM/Ollama), or region-pinned/sovereign
endpoints. But OpenCode only resolves `--model provider/model` when the
model is registered in a provider's `models` map, and
`OPENCODE_ALLOW_ALL_MODELS` does NOT bypass its internal `getModel()`
> - Several lanes also fall back to built-in default models the gateway
may not serve: the auxiliary/title model (e.g. `claude-haiku-*`) and the
budget/recovery "cheap" lane (`openai/gpt-5.1-codex-mini`); these abort
runs with "no keys found that support model"
> - This pull request makes the adapter's provider/model wiring
declarative via env, so any such deployment can register gateway models
+ pin the auxiliary/budget lanes without code changes; nothing here is
specific to one hosting setup
> - The benefit is OpenCode works behind any compatible gateway with
config only; with no env set, behavior is unchanged

## Linked Issues or Issue Description

No existing issue; describing in-PR (feature / adapter enhancement).

- **Gap:** there is no supported way to register custom/gateway
providers + models for `opencode-local`, nor to pin the auxiliary
(title-gen) and budget (recovery) model lanes, so routing OpenCode
through a gateway fails at `getModel()` or on the default helper models.
- Related: #5737 (exe.dev sandbox installs for gemini/opencode local),
#5823 (unblock claude_local on remote sandbox providers).

> Note on ROADMAP: this is adapter-level, opt-in config (defaults
unchanged) that *enables* gateway routing for one harness; it is not the
core "Cloud / Sandbox agents" platform work itself. Happy to
redirect/discuss in #dev if preferred.

## What Changed

- `PAPERCLIP_OPENCODE_PROVIDERS`: merge custom/extended providers
(OpenCode `provider` shape) into the runtime `opencode.json`, so gateway
models are registered and `--model provider/model` resolves. `{env:VAR}`
placeholders are expanded server-side (so a key need not depend on the
sandbox run env).
- A malformed `PAPERCLIP_OPENCODE_PROVIDERS` is no longer silently
ignored: invalid JSON, a non-object value, and individual provider
entries with non-object values (which are skipped by name) each append a
visible note to the run notes so the misconfiguration is diagnosable
(addresses both review P1s).
- `PAPERCLIP_OPENCODE_SMALL_MODEL` / `PAPERCLIP_OPENCODE_CHEAP_MODEL`:
pin the auxiliary (title-generation) and budget (recovery-retry) lanes
to gateway-served models; defaults unchanged.
- Honour `OPENCODE_ALLOW_ALL_MODELS` on the **remote** execution path
too (was local-only, a parity gap).
- `PAPERCLIP_OPENCODE_PRINT_LOGS`: optional toggle adding `--print-logs`
so OpenCode logs surface on stderr for diagnosing remote/sandbox runs.
- `buildOpenCodeModelProfiles()` guards its `process.env` default with
`typeof process` so the shared client/server module stays browser-safe
(a bare `process.env` at module load threw ReferenceError in the browser
under Vite dev middleware and broke UI rendering in the e2e lane).

## Verification

- `pnpm --filter @paperclipai/adapter-opencode-local build` and
`typecheck` (tsc clean)
- `pnpm exec vitest run packages/adapters/opencode-local/src` shows 33
passing (incl. new tests for the provider merge, `{env:}` expansion, the
malformed/non-object/skipped-entry provider notes, small/cheap-model
resolution, and the remote allow-all bypass)
- Manually verified end-to-end against a real
OpenAI-/Anthropic-compatible gateway: with the providers + small/cheap
model set, both the title-gen and main task route to the configured
gateway model and the agent completes (a real completion is returned and
billed). That deployment supplies the verification evidence; the
mechanism is gateway-agnostic.

## Risks

Low. Everything is env-driven and opt-in; with no env set the generated
config output is unchanged, and the cheap model profile keeps its model
(the only difference is its updated human-readable description).
Defaults preserved: built-in providers, Codex-mini cheap lane with
`variant: low`, no `--print-logs`. No migration/UI impact.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`, 1M context), extended thinking +
tool use, via Claude Code.

## 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 (adapter-level opt-in config enabling
gateway routing; not the core sandbox-platform work, noted above)
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (#5737, #5823)
- [x] I have either (a) linked existing issues OR (b) described the
issue in-PR following the relevant issue template
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (n/a, no UI)
- [ ] I have updated relevant documentation to reflect my changes (env
vars documented inline via comments; no central doc references the
adapter env yet)
- [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
(the P1 about silently dropped malformed providers JSON is addressed in
6eeb803, the follow-up P1 about silently skipped non-object entries in
2f4045a; the latest review has no further findings, and a re-review is
requested for the final note-copy/test-fixture polish at head)
- [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 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:11:49 -07:00
Jannes StubbemannandClaude Opus 4.8 398d746093 build(agent-runtime): harness runtime images for sandboxed execution (stage 3/3) (#7934)
> [!NOTE]
> This is **stage 3 of 3** of the staged Kubernetes contribution: stage
1 is the kubernetes sandbox-provider plugin (#5790), stage 2 is the
provider backend/hardening refresh filed separately, and this stage
ships the runtime images those sandboxes run.

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandboxed agent execution (Refs #248) runs each agent turn in an
isolated environment; the kubernetes sandbox provider (stage 1, #5790)
schedules those runs as hardened pods
> - A sandbox pod needs a runtime image with the harness CLI
preinstalled: installing CLIs at run start is slow, flaky, and needs
network egress the sandbox should not have
> - There is no first-party image family for this, so every deployer
would have to hand-roll Ubuntu + Node + CLI images per harness and solve
signal handling, non-root, and image chaining themselves
> - This PR ships the agent-runtime image family: a hardened base
(non-root uid 1000, tini, git, the agent shim) plus one derived image
per harness, a buildx bake file that chains them, and a publish workflow
with cosign keyless signing
> - The benefit is that any sandbox infrastructure, the kubernetes
provider or otherwise, gets ready-made, signed, security-hardened
per-harness runtime images that are verified in production across five
harnesses

## Linked Issues or Issue Description

Refs #248 (sandboxed agent execution proposal) and #5790 (the kubernetes
sandbox provider, stage 1 of this contribution, which consumes these
images as per-run runtime images via its adapter defaults).

No issue covers the image gap itself, described in-PR: sandbox providers
reference `ghcr.io/paperclipai/agent-runtime-*` images, but the
repository contains neither the Dockerfiles nor the workflow that builds
and publishes them. Without this, self-deployers cannot reproduce or
audit the images their agent runs execute in.

## What Changed

- `docker/agent-runtime/Dockerfile.base`: foundation image. Ubuntu 22.04
+ Node 22 + git + tini (PID 1, signal propagation) + non-root
`paperclip` user (uid/gid 1000) + the agent shim compiled in a Go build
stage. `WORKDIR /workspace`, entrypoint `tini -- paperclip-agent-shim`.
- One derived Dockerfile per harness: `opencode` (opencode-ai), `pi`
(@mariozechner/pi-coding-agent), `codex` (@openai/codex), `gemini`
(@google/gemini-cli, plus headless auth-mode settings), `claude`
(@anthropic-ai/claude-code, symlinked as `claude-code`). Each installs
the CLI as root, returns to uid 1000, and asserts the binary is on PATH
at build time.
- `acpx` and `hermes` Dockerfiles are included in the bake group but are
not in the default publish scope (hermes is a stub until a CLI package
exists).
- `docker/agent-runtime/buildx-bake.hcl`: builds the whole family in one
pass. Derived targets chain off the `base` target through bake
`contexts` (the literal registry in each `FROM` is overridden to
`target:base` at build time, so no intermediate push is needed).
`REGISTRY` (default `ghcr.io/paperclipai`) and `VERSION` are overridable
variables.
- `tools/agent-shim/`: a small Go shim that runs as the container
command. It reads `/run/paperclip/runtime-command.json` (`{ "command",
"args" }`), resolves the harness CLI on PATH, and `syscall.Exec`s it so
SIGTERM from the kubelet reaches the harness directly. Harness-agnostic,
with unit tests.
- `.github/workflows/agent-runtime-images.yml`: builds and pushes the
default scope (base, opencode, pi, codex, gemini, claude) for
linux/amd64 on `workflow_dispatch` (explicit version tag) or pushes to
`master` touching these paths, then signs every digest with cosign
keyless OIDC. Uses only `GITHUB_TOKEN`; no extra secrets.
- `docker/agent-runtime/README.md`: image lineup, base contents, local
build instructions, the runtime-command contract, and the security
model.

Additive only: nothing in the product loads these images. Deployments
opt in via their sandbox provider configuration (for example the
kubernetes plugin's image settings).

## Verification

- `cd tools/agent-shim && go build ./... && go test ./... && go vet
./...`: all passing.
- `docker buildx bake -f docker/agent-runtime/buildx-bake.hcl --print
base opencode pi codex gemini claude`: resolves cleanly; every tag and
build context lands on `ghcr.io/paperclipai/agent-runtime-*` and derived
targets map the base ref to `target:base`.
- Workflow YAML validated (parses, single job, no org-specific secrets).
- This exact image family (built from these Dockerfiles, bake file, and
workflow) is what runs agent execution in production on paperclip.inc,
verified end-to-end across five harnesses (opencode, pi, codex, gemini,
claude): each as a full loop from assigned issue to per-run runtime
image in a sandboxed pod to completed run.

## Risks

- Low risk: purely additive, nothing in paperclip-server or the UI
references these files. The workflow only triggers on its own paths.
- Derived images install harness CLIs `@latest` at build time; a broken
upstream CLI release would surface at image build, not at run time, and
the PATH assertion fails the build rather than shipping a broken image.
- The hermes image is an explicit stub (documented in its Dockerfile)
until a hermes CLI package exists; it is outside the default publish
scope.
- cosign signing is keyless OIDC with the workflow identity; no
long-lived signing keys are introduced.

## Model Used

Claude Opus 4.8 (claude-opus-4-8, 1M context, extended thinking, tool
use via Claude Code).

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (no UI changes)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:10:23 -07:00
Jannes StubbemannandClaude Opus 4.8 4ad94d0bde feat(server): kubernetes execution integration for sandbox-provider plugins (stage 2/3) (#7938)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The execution subsystem runs those agents in environments (local,
ssh, sandbox), and sandbox-provider plugins let an environment
materialize per-run sandboxes
> - Stage 1 (#5790) contributed a first-party Kubernetes
sandbox-provider plugin, but the server core has no way to adopt it
operationally: no per-run adapter selection, no way to force an instance
onto sandboxed execution, no declarative adapter/model configuration,
and the plugin must be installed by hand
> - Without this, a multi-tenant or security-conscious deployment cannot
guarantee that agent runs never execute on the host, and a single
environment cannot serve agents with different harnesses
> - This pull request adds the server + SDK integration: per-run
adapterType on the lease protocol, an env-gated forced-Kubernetes
execution policy with provisioning and a per-run allowlist guard, a
declarative adapter registry and model list, in-cluster env passthrough
for sandbox plugin workers, fail-safe auto-install of the bundled
plugin, and the matching UI affordance
> - The benefit is that sandbox-provider plugins become fully usable for
Kubernetes execution: operators configure everything via environment
variables and GitOps, while self-hosters who set none of the variables
see exactly the behavior they have today

## Linked Issues or Issue Description

Refs #5790 (stage 1 of 3: the Kubernetes sandbox-provider plugin
package).

No existing issue. Feature description: the server core lacks the
integration seams to operate a sandbox-provider plugin as the mandatory
execution path of an instance. This PR is stage 2 of 3 of the staged
Kubernetes contribution; stage 3 will contribute the agent runtime
images and their build pipeline.

## What Changed

One line per piece:

- `packages/plugins/sdk/protocol.ts`: optional `adapterType` on
`PluginEnvironmentAcquireLeaseParams` so a provider can select the
runtime image per run; existing providers simply ignore it
- `server/services/environment-runtime.ts` +
`environment-run-orchestrator.ts`: thread the agent's adapter type into
both lease-acquiring drivers, including the heartbeat path (the two call
sites have historically drifted, hence the pinned test)
- `server/services/environments.ts`: `ensureKubernetesEnvironment` /
`findKubernetesEnvironment`, an idempotent managed Kubernetes
environment per company, identified by a metadata marker and refreshed
(not recreated) on config change; `timeoutMs` rides on the config for
slow cold-start leases
- `server/services/execution-allowlist.ts`: pure (driver, provider,
policy) -> allow/deny guard; `executionMode=kubernetes` only allows the
kubernetes sandbox provider
- `server/services/execution-policy-bootstrap.ts` + startup hook in
`server/index.ts`: parse `PAPERCLIP_EXECUTION_MODE` / `PAPERCLIP_K8S_*`,
persist `executionMode` into instance general settings, and provision
the managed environment for every company; fails loud on
misconfiguration
- `server/services/heartbeat.ts`: when the policy forces Kubernetes, pin
run selection to the managed environment (also overriding any persisted
workspace environment id), refuse to fall back to local, and re-check
the actually acquired environment against the allowlist as defense in
depth
- `server/services/adapter-registry-bootstrap.ts` + shared
`AdapterRegistryEntry` type/validator: declarative `PAPERCLIP_ADAPTERS`
registry (inline JSON or file) that reconciles adapter availability at
startup and rides on the Kubernetes environment config
- `server/services/adapter-models-env.ts` + `adapters/registry.ts`:
`PAPERCLIP_ADAPTER_MODELS` lets an operator declare picker model lists
the server cannot CLI-discover
- `server/services/plugin-loader.ts`: pass
`KUBERNETES_SERVICE_HOST/PORT(_HTTPS)` through to plugin workers that
register environment drivers, so in-cluster API clients can be
constructed; all other host env stays stripped
- `server/app.ts`: fail-safe auto-install of the bundled kubernetes
plugin at boot; no-ops when the bundle is absent and never blocks
startup on error
- `packages/shared` types/validators: `InstanceExecutionMode` on general
settings (optional, strict schema)
- `ui/lib/forced-kubernetes-environment.ts` + `AgentConfigForm`: when
the policy is active, show a read-only Kubernetes environment instead of
the environment picker and default new agents onto the managed
environment
- Tests for every new module plus the adapterType pin in
`heartbeat-plugin-environment` and the managed-environment lifecycle in
`environment-service`

Everything is gated: with `PAPERCLIP_EXECUTION_MODE`,
`PAPERCLIP_ADAPTERS`, and `PAPERCLIP_ADAPTER_MODELS` unset (and no
bundled plugin present), every code path reduces to current behavior.
The per-run `adapterType` is an optional SDK parameter that existing
providers ignore.

## Verification

- `cd server && npx tsc --noEmit`: clean (0 errors); `ui` typecheck also
clean
- Targeted suites all green (11 files, 90 tests): `npx vitest run
server/src/__tests__/heartbeat-plugin-environment.test.ts
server/src/__tests__/environment-service.test.ts
server/src/__tests__/environment-runtime.test.ts
server/src/__tests__/environment-run-orchestrator.test.ts
server/src/__tests__/plugin-database.test.ts
server/src/services/execution-policy-bootstrap.test.ts
server/src/services/execution-allowlist.test.ts
server/src/services/adapter-registry-bootstrap.test.ts
server/src/services/adapter-registry-bootstrap.reconcile.test.ts
server/src/services/adapter-models-env.test.ts
packages/shared/src/validators/adapter-registry.test.ts`
- `npx vitest run ui/src/components/AgentConfigForm.test.ts`: green (6
tests)
- Full `npx vitest run server/src/__tests__`: 2323 passed, 1 skipped;
the only failures (heartbeat-process-recovery pid-retry,
workspace-runtime symbolic-ref/git tests) reproduce identically on
pristine `master` in the same environment, so they are
machine-environment issues unrelated to this change;
`server-startup-feedback-export` needed its `services/index.js` mock
extended with the new export and is green
- This integration has been running in production on a hosted
multi-tenant deployment, where it executes agent runs across five
different harnesses through the stage 1 plugin

## Risks

- Low for existing deployments: every behavior is env-gated and the
defaults preserve current semantics; the auto-install block is wrapped
fail-safe and skips silently when the plugin bundle is absent
- `executionMode` is a new optional field on a strict zod schema; absent
input normalizes exactly as before
- The forced policy intentionally fails runs loudly (rather than falling
back to local) when no managed Kubernetes environment exists; this only
affects instances that explicitly set
`PAPERCLIP_EXECUTION_MODE=kubernetes`

## Model Used

Claude Opus 4.8 (claude-opus-4-8, 1M context), extended thinking,
agentic tool use via Claude Code.

## UI screenshots

The UI change is a new read-only "Execution" section in
`AgentConfigForm`, shown only when the instance execution policy forces
Kubernetes (`executionMode=kubernetes`); there is no "before" state for
it (the section did not exist, and instances without the forced policy
render the existing picker unchanged). Captured from the new Storybook
stories added in this PR (`Product/Agent Management`):

Managed Kubernetes environment present (read-only display, no local/SSH
picker):

![AgentConfigForm with forced Kubernetes
execution](https://raw.githubusercontent.com/paperclipinc/paperclip/296ad06e8/screenshots/PR-7938-agent-config-forced-kubernetes.png)

No managed environment available yet (warning notice, no silent local
fallback):

![AgentConfigForm forced Kubernetes, missing environment
warning](https://raw.githubusercontent.com/paperclipinc/paperclip/296ad06e8/screenshots/PR-7938-agent-config-forced-kubernetes-missing-env.png)

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] 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 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:09:02 -07:00
Jannes StubbemannandClaude Opus 4.8 05ab45225a feat(plugin-kubernetes): self-hostable Kubernetes sandbox provider (stage 1/3: plugin package) (#5790)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox providers are the seam that lets agent runs execute in
isolated environments; today the only first-party remote provider is
Daytona, a hosted third-party service
> - Self-hosters running Paperclip on their own infrastructure (often
Kubernetes already) have no first-party way to run agent sandboxes on a
cluster they control
> - That gap matters for teams with data-residency, sovereignty, or cost
constraints who cannot or will not send workloads to a hosted sandbox
service
> - This pull request adds a Kubernetes sandbox-provider plugin as a
standalone, workspace-excluded package: it implements every
SandboxProvider hook the Daytona provider does, on infrastructure the
operator owns
> - The benefit is that any Paperclip deployment with a Kubernetes
cluster gets multi-tenant, network-isolated, quota-bounded agent
sandboxes with zero new external dependencies

## Linked Issues or Issue Description

No existing issue. Following the feature template:

- **Problem:** Paperclip's remote sandbox execution requires a hosted
third-party provider. Self-hosters cannot run agent sandboxes on their
own Kubernetes clusters with a first-party provider.
- **Proposed solution:** A `@paperclipai/plugin-kubernetes`
sandbox-provider plugin with two backends: long-lived sandboxes via the
[kubernetes-sigs/agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox)
CRD (multi-command exec, adapter-install pattern) and one-shot
`batch/v1` Jobs (stable APIs only, no extra controllers).
- **Alternatives considered:** Driving kubectl from a generic shell
provider (no lifecycle/lease semantics), or requiring a hosted provider
(exactly the constraint this removes).

## What Changed

This is **stage 1 of 3** of a staged contribution (direction agreed with
maintainers): the plugin package alone. Stage 2 (server integration:
lease params, provider registration) and stage 3 (agent runtime images +
CI) are companion PRs that will be cross-linked from a comment here.

- New package `packages/plugins/sandbox-providers/kubernetes`
(workspace-excluded, like the path already carved out in
`pnpm-workspace.yaml`): src, unit + kind integration tests, operator
prerequisite manifests, README, smoke-test guide
- Implements the full SandboxProvider hook surface the Daytona provider
implements: `validateConfig`, `probe`, `acquireLease`, `resumeLease`,
`releaseLease`, `destroyLease`, `realizeWorkspace`, `execute`
- Two backends: `sandbox-cr` (default; long-lived pod via the
agent-sandbox `Sandbox` CR, supports multi-command exec) and `job`
(one-shot `batch/v1` Job; nothing beyond k8s 1.27+ required)
- Per-run adapter resolution: one environment serves mixed harnesses;
the per-run `adapterType` hint is read through a local optional type
extension, so the plugin typechecks and builds against the current
plugin SDK and simply falls back to the environment's configured default
adapter until stage 2 lands
- Exec-env wrapping: the Kubernetes exec API carries no environment, so
commands are wrapped to receive the run's env
- Fast-upload interception for workspace realization, scoped per lease
- Per-tenant isolation: derived namespace per company, RBAC,
ResourceQuota, restricted-PSS pod security (runAsNonRoot, drop ALL,
seccomp RuntimeDefault, no SA token automount)
- Network egress policy in two flavors: native `NetworkPolicy` and
`CiliumNetworkPolicy` (FQDN allowlists)
- Image allowlist with glob matching, registry override, and per-run
image override validation
- Per-run Kubernetes Secrets carrying agent credentials, ownerRef'd to
the Job or Sandbox CR for cascade GC

## Verification

- Standalone build, exactly as the README documents:
  ```bash
  cd packages/plugins/sandbox-providers/kubernetes
  pnpm install --ignore-workspace
  pnpm test        # 147 unit tests, 17 files, all green
  pnpm typecheck   # clean against the in-repo plugin SDK on master
pnpm build # dist/ emitted, manifest + worker entrypoints present
  ```
- A kind-cluster end-to-end integration test is included
(`RUN_K8S_INTEGRATION_TESTS=1 pnpm test
test/integration/end-to-end-run.test.ts`)
- Beyond CI: this provider has been verified in a production
multi-tenant deployment against five harnesses (opencode, pi, codex,
gemini, claude code) with real billed runs

## Risks

- **Zero behavior change for any existing deployment.** The package is
workspace-excluded; nothing in the server imports or loads it until
stage 2's integration lands. No existing code paths are touched.
- The default `sandbox-cr` backend depends on an alpha CRD
(`agents.x-k8s.io/v1alpha1`); the README flags this and the `job`
backend uses only stable APIs as a fallback.
- Risk surface is confined to deployments that explicitly install and
configure the plugin.
- The default runtime images (`ghcr.io/paperclipai/agent-runtime-*`) are
published by the stage 3 companion PR (#7934); until that lands,
deployments must point `runtimeImage` at their own images.

## Model Used

Claude Opus 4.8 (1M context), extended thinking, with tool use (Claude
Code).

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (no UI changes)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending this push)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:07:00 -07:00
Jannes StubbemannandClaude Opus 4.8 93cdc5c1ce fix(adapter-utils): tar sandbox workspace by entry, not '.', to avoid EPERM on unowned target dir (#7836)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can run in remote/sandboxed environments via the shared
sandbox managed-runtime in `@paperclipai/adapter-utils` (used by
SSH/E2B/Daytona and other sandbox providers), which syncs the workspace
into the sandbox by tarring it up and extracting it inside the pod/host
> - When the sandbox runs the harness as a non-root user whose
home/workspace dir it does not own (for example a hardened, non-root,
gVisor pod with an `emptyDir`-mounted workspace), the workspace upload
aborts before the agent can start
> - Root cause: `createTarballFromDirectory` archives `.`, embedding a
`./` self-entry whose mode/mtime tar then tries to restore onto the
**extraction target directory**; `chmod`/`utime` of `.` fails with
`Operation not permitted` for a non-owner
> - This is not specific to any one deployment: the `.` self-entry EPERM
can bite every sandbox provider built on the shared managed runtime as
soon as the extracting user does not own the target directory, which is
the norm for hardened non-root sandboxes
> - This pull request archives the directory's top-level entries by name
instead of `.`, so there is no `./` self-entry and extraction never
touches the target dir's metadata
> - The benefit is that workspace sync works in any sandbox where the
target dir is non-root or not owned by the extracting user, without
GNU-only tar flags

## Linked Issues or Issue Description

No existing issue; describing in-PR (bug).

- **What happens:** managed sandbox runs that sync the workspace fail at
upload with `tar: .: Cannot utime: Operation not permitted` / `tar: .:
Cannot change mode to ... : Operation not permitted`, aborting the run
before the harness starts.
- **Where:** `packages/adapter-utils/src/sandbox-managed-runtime.ts`, in
`createTarballFromDirectory` (archives `.`).
- **When:** the extraction target directory is not owned by the
(non-root) user extracting the tar inside the sandbox.
- Closely related (different root cause): #6560 (E2B workspace upload +
lease idle failures).

## What Changed

- `createTarballFromDirectory` enumerates the directory's top-level
entries with `fs.readdir` and passes them by name after `--` (guards
flag-like filenames) instead of archiving `.`, eliminating the `./`
self-entry that triggers the EPERM.
- Empty workspaces (legitimate for blank-workspace runs) write a valid
1024-byte all-zero EOF tar instead of invoking `tar` with no paths.
- `--exclude` patterns continue to apply (to nested matches and any
named entry).

## Verification

- `pnpm --filter @paperclipai/adapter-utils build` (tsc clean)
- `pnpm exec vitest run
packages/adapter-utils/src/sandbox-managed-runtime.test.ts` runs green
- New tests: uploaded workspace/asset tarballs contain no `.`/`./`
member yet still extract correctly; empty workspace produces a valid
(no-op) tarball. Existing managed-runtime sync test unchanged.
- Manually verified in a hardened (non-root, gVisor) sandbox pod: with
the fix, the workspace upload that previously aborted with the EPERM now
succeeds. That deployment is the reproduction and verification
environment; the fix itself is provider-agnostic.

## Risks

Low. Behavior is unchanged for owned/root targets; the archive contents
are the same minus the `./` self-entry (which tar recreates implicitly
on extract). Portable across GNU/BSD/busybox tar (no GNU-only
`--no-overwrite-dir`). No API/migration/UI impact.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`, 1M context), extended thinking +
tool use, via Claude Code.

## 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 (bug fix in shared sandbox utils, not core feature
work)
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (#6560)
- [x] I have either (a) linked existing issues OR (b) described the
issue in-PR following the relevant issue template
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (n/a, no UI)
- [ ] I have updated relevant documentation to reflect my changes (n/a,
internal behavior, no docs reference this)
- [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
(the only finding was the description-template P2, resolved by this
description; the latest review covers the current head with no code
findings and all CI gates are green)
- [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 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:36:19 -07:00
Jannes Stubbemann 7463479fc8 fix: disable HTTP caching on run log endpoints (#3724)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Every run emits a streaming log that the web UI polls so humans can
watch what the agent is doing
> - Log responses go out without explicit cache directives, so Express
adds an ETag
> - If the first poll lands before any bytes have been written, the
browser caches the empty / partial snapshot and keeps getting `304 Not
Modified` on every subsequent poll
> - The transcript pane then stays stuck on "Waiting for transcript…"
even after the log has plenty of content
> - This pull request sets `Cache-Control: no-cache, no-store` on both
run-log endpoints so the conditional-request path is defeated

## What Changed

- `server/src/routes/agents.ts` — `GET /heartbeat-runs/:runId/log` now
sets `Cache-Control: no-cache, no-store` on the response.
- Same change applied to `GET /workspace-operations/:operationId/log`
(same structure, same bug).

## Verification

- Reproduction: start a long-running agent, watch the transcript pane.
Before the fix, open devtools and observe `304 Not Modified` on each
poll after the initial 200 with an empty body; the UI never updates.
After the fix, each poll is a 200 with fresh bytes.
- Existing tests pass.

## Risks

Low. Cache headers only affect whether the browser revalidates; the
response body is unchanged. No API surface change.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:53:25 -05:00
Jannes Stubbemann 390502736c chore(ui): drop console.* and legal comments in production builds (#3728)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The web UI is a single-page app built with Vite and shipped as a
static bundle to every deployment
> - Production bundles carry `console.log` / `console.debug` calls from
dev code and `/*! … */` legal-comment banners from third-party packages
> - The console calls leak internals to anyone opening devtools and
waste bytes per call site; the legal banners accumulate throughout the
bundle
> - Both problems affect every self-hoster, since they all ship the same
UI bundle
> - This pull request configures esbuild (via `vite.config.ts`) to strip
`console` and `debugger` statements and drop inline legal comments from
production builds only

## What Changed

- `ui/vite.config.ts`:
  - Switch to the functional `defineConfig(({ mode }) => …)` form.
- Add `build.minify: "esbuild"` (explicit — it's the existing default).
- Add `esbuild.drop: ["console", "debugger"]` and
`esbuild.legalComments: "none"`, gated on `mode === "production"` so
`vite dev` is unaffected.

## Verification

- `pnpm --filter @paperclipai/ui build` then grep the
`ui/dist/assets/*.js` bundle for `console.log` — no occurrences.
- `pnpm --filter @paperclipai/ui dev` — `console.log` calls in source
still reach the browser console.
- Bundle size: small reduction (varies with project but measurable on a
fresh build).

## Risks

Low. No API surface change. Production code should not depend on
`console.*` for side effects; any call that did is now a dead call,
which is the same behavior most minifiers apply.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:46:12 -05:00
Jannes Stubbemann 0d87fd9a11 fix: proper cache headers for static assets and SPA fallback (#3734)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Every deployment serves the same Vite-built UI bundle from the same
express app
> - Vite emits JS/CSS under `/assets/<name>.<hash>.<ext>` — the hash
rolls whenever the content rolls, so these files are inherently
immutable
> - `index.html` references specific hashed filenames, so it has the
opposite lifecycle: whenever we deploy, the file changes but the URL
doesn't
> - Today the static middleware sends neither with cache headers, and
the SPA fallback serves `index.html` for any unmatched route — including
paths under `/assets/` that no longer exist after a deploy
> - That combination produces the familiar "blank screen after deploy" +
`Failed to load module script: Expected a JavaScript MIME type but
received 'text/html'` bug
> - This pull request caches hashed assets immutably, forces
`index.html` to `no-cache` everywhere it gets served, and returns 404
for missing `/assets/*` paths

## What Changed

- `server/src/app.ts`:
- Serve `/assets/*` with `Cache-Control: public, max-age=31536000,
immutable`.
- Serve the remaining static files (favicon, manifest, robots.txt) with
a 1-hour cache, but override to `no-cache` specifically for `index.html`
via the `setHeaders` hook — because `express.static` serves it directly
for `/` and `/index.html`.
- The SPA fallback (`app.get(/.*/, …)`) sets `Cache-Control: no-cache`
on its `index.html` response.
- The fallback returns 404 for paths under `/assets/` so browsers don't
cache the HTML shell as a JavaScript module.

## Verification

- `curl -i http://localhost:3100/assets/index-abc123.js` →
`cache-control: public, max-age=31536000, immutable`.
- `curl -i http://localhost:3100/` → `cache-control: no-cache`.
- `curl -i http://localhost:3100/assets/missing.js` → `404`.
- `curl -i http://localhost:3100/some/spa/route` → `200` HTML with
`cache-control: no-cache`.

## Risks

Low. Asset URLs and HTML content are unchanged; only response headers
and the 404 behavior for missing asset paths change. No API surface
affected.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:45:22 -05:00
Jannes Stubbemann 6059c665d5 fix(a11y): remove maximum-scale and user-scalable=no from viewport (#3726)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Humans watch and oversee those agents through a web UI
> - Accessibility matters for anyone who cannot read small text
comfortably — they rely on browser zoom
> - The app shell's viewport meta tag includes `maximum-scale=1.0,
user-scalable=no`
> - Those tokens disable pinch-zoom and are a WCAG 2.1 SC 1.4.4 (Resize
Text) failure
> - The original motivation — suppressing iOS Safari's auto-zoom on
focused inputs — is actually a font-size issue, not a viewport issue,
and modern Safari only auto-zooms when input font-size is below 16px
> - This pull request drops the two tokens, restoring pinch-zoom while
leaving the real fix (inputs at ≥16px) to CSS

## What Changed

- `ui/index.html` — remove `maximum-scale=1.0, user-scalable=no` from
the viewport meta tag. Keep `width=device-width, initial-scale=1.0,
viewport-fit=cover`.

## Verification

- Manual on iOS and Chrome mobile: pinch-to-zoom now works across the
app.
- Manual on desktop: Ctrl+/- zoom already worked via
`initial-scale=1.0`; unchanged.

## Risks

Low. Users who were relying on auto-zoom-suppression for text inputs
will notice nothing (modern Safari only auto-zooms below 16px). No API
surface change.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:43:45 -05:00
Jannes Stubbemann f460f744ef fix: trust PAPERCLIP_PUBLIC_URL in board mutation guard (#3731)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Humans interact with the system through a web UI that authenticates
a session and then issues mutations against the board
> - A CSRF-style guard (`boardMutationGuard`) protects those mutations
by requiring the request origin match a trusted set built from the
`Host` / `X-Forwarded-Host` header
> - Behind certain reverse proxies, neither header matches the public
URL — TLS terminates at the edge and the inbound `Host` carries an
internal service name (cluster-local hostname, IP, or an Ingress backend
reference)
> - Mutations from legitimate browser sessions then fail with `403 Board
mutation requires trusted browser origin`
> - `PAPERCLIP_PUBLIC_URL` is already the canonical "what operators told
us the public URL is" value — it's used by better-auth and `config.ts`
> - This pull request adds it to the trusted-origin set when set, so
browsers reaching the legit public URL aren't blocked

## What Changed

- `server/src/middleware/board-mutation-guard.ts` — parse
`PAPERCLIP_PUBLIC_URL` and add its origin to the trusted set in
`trustedOriginsForRequest`. Additive only.

## Verification

- `PAPERCLIP_PUBLIC_URL=https://example.com pnpm start` then issue a
mutation from a browser pointed at `https://example.com`: 200, as
before. From an unrecognized origin: 403, as before.
- Without `PAPERCLIP_PUBLIC_URL` set: behavior is unchanged.

## Risks

Low. Additive only. The default dev origins and the
`Host`/`X-Forwarded-Host`-derived origins continue to be trusted; this
just adds the operator-configured public URL on top.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:42:55 -05:00