mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
fix: fail closed on unusable Codex auth during sandbox auth merge
When reusing a Codex sandbox, the workspace-restore step now performs an auth-merge check before adopting the sandbox's auth.json. The merge fails closed when: - host or sandbox Codex auth is unusable (missing token or account) - the subscription payload shapes differ between host and sandbox - subscription account identities do not match Additional hardening: node absence on PATH during the embedded decision script now exits non-zero instead of silently falling through. Co-change comments link parseAuth (the embedded script) to hasUsableAuthPayload (codex-home.ts) so a future auth-format change cannot silently diverge between the two implementations. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
98df214917
3422 files changed
+1614966
No files matched your search
@@ -0,0 +1,37 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
ARG NODE_VERSION=22
|
||||
ARG TARGETARCH
|
||||
|
||||
# ---------- Stage 1: build agent-shim ----------
|
||||
FROM golang:1.25-bookworm AS shim-build
|
||||
ARG TARGETARCH
|
||||
WORKDIR /src
|
||||
COPY tools/agent-shim/ ./
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH:-amd64} \
|
||||
go build -ldflags='-s -w' -o /out/paperclip-agent-shim .
|
||||
|
||||
# ---------- Stage 2: runtime base image ----------
|
||||
# Ubuntu + Node + git + tini + non-root paperclip user + the agent-shim. This is
|
||||
# everything the sandbox exec-in model needs: the pod idles and paperclip-server
|
||||
# execs the harness CLI in, so the harness images only add their CLI on top of
|
||||
# this. Git workspace population (an init-container workspace bootstrap) is
|
||||
# intentionally NOT built here yet: the current scope is a blank workspace.
|
||||
# Add it when git workspace population lands.
|
||||
FROM ubuntu:22.04 AS base
|
||||
ARG NODE_VERSION
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl git tini gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd -g 1000 paperclip \
|
||||
&& useradd -u 1000 -g 1000 -d /home/paperclip -m -s /bin/bash paperclip
|
||||
|
||||
COPY --from=shim-build /out/paperclip-agent-shim /usr/local/bin/paperclip-agent-shim
|
||||
|
||||
USER 1000:1000
|
||||
WORKDIR /workspace
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/usr/local/bin/paperclip-agent-shim"]
|
||||
@@ -0,0 +1,17 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
ARG BASE_TAG=dev
|
||||
FROM paperclipai/agent-runtime-base:${BASE_TAG}
|
||||
|
||||
USER root
|
||||
RUN npm install -g @anthropic-ai/claude-code@latest \
|
||||
&& { chown -R 1000:1000 /usr/lib/node_modules || true; }
|
||||
|
||||
# @anthropic-ai/claude-code installs as 'claude', but the shim expects 'claude-code'.
|
||||
# Resolve the real install path and fail the build if claude is missing.
|
||||
RUN CLAUDE_BIN="$(command -v claude)" \
|
||||
&& ln -s "$CLAUDE_BIN" /usr/bin/claude-code
|
||||
|
||||
USER 1000:1000
|
||||
|
||||
# Verify the CLI is on PATH for the shim's exec.LookPath
|
||||
RUN command -v claude-code >/dev/null 2>&1 || (echo "claude-code not on PATH"; exit 1)
|
||||
@@ -0,0 +1,12 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
ARG BASE_TAG=dev
|
||||
FROM paperclipai/agent-runtime-base:${BASE_TAG}
|
||||
|
||||
USER root
|
||||
RUN npm install -g @openai/codex@latest \
|
||||
&& { chown -R 1000:1000 /usr/lib/node_modules || true; }
|
||||
|
||||
USER 1000:1000
|
||||
|
||||
# Verify the CLI is on PATH for the shim's exec.LookPath
|
||||
RUN command -v codex >/dev/null 2>&1 || (echo "codex not on PATH"; exit 1)
|
||||
@@ -0,0 +1,21 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
ARG BASE_TAG=dev
|
||||
FROM paperclipai/agent-runtime-base:${BASE_TAG}
|
||||
|
||||
USER root
|
||||
RUN npm install -g @google/gemini-cli@latest \
|
||||
&& { chown -R 1000:1000 /usr/lib/node_modules || true; }
|
||||
|
||||
USER 1000:1000
|
||||
|
||||
# Verify the CLI is on PATH for the shim's exec.LookPath
|
||||
RUN command -v gemini >/dev/null 2>&1 || (echo "gemini not on PATH"; exit 1)
|
||||
|
||||
# Pre-select the gemini-api-key auth mode: gemini-cli refuses headless runs with
|
||||
# "Invalid auth method selected." unless settings.json picks an auth type (the
|
||||
# GEMINI_DEFAULT_AUTH_TYPE env alone does NOT work, proven empirically). Both
|
||||
# schema generations are written (selectedAuthType = legacy, security.auth
|
||||
# .selectedType = current). Correct for managed-gateway AND BYOK api-key modes;
|
||||
# OAuth flows are interactive and out of scope for sandbox pods.
|
||||
RUN mkdir -p /home/paperclip/.gemini \
|
||||
&& printf '%s\n' '{"selectedAuthType":"gemini-api-key","security":{"auth":{"selectedType":"gemini-api-key"}}}' > /home/paperclip/.gemini/settings.json
|
||||
@@ -0,0 +1,17 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
ARG BASE_TAG=dev
|
||||
FROM paperclipai/agent-runtime-base:${BASE_TAG}
|
||||
|
||||
# hermes_local is in the legacy sessioned-adapter set but has no upstream
|
||||
# npm package wired into Paperclip locally yet (see
|
||||
# packages/adapter-utils/src/session-compaction.ts vs the absent
|
||||
# packages/adapters/hermes-local/). This stub image preserves a cloud
|
||||
# runtime slot for hermes_local; whoever ports the hermes binary to
|
||||
# Paperclip locally adds the `npm install -g <pkg>` and the PATH check
|
||||
# to this Dockerfile.
|
||||
#
|
||||
# Until then, runs targeting hermes_local on the cloud adapter will boot
|
||||
# the container but `agent-shim` will fail with "hermes not on PATH" when
|
||||
# it tries to invoke the CLI.
|
||||
|
||||
USER 1000:1000
|
||||
@@ -0,0 +1,13 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
ARG BASE_TAG=dev
|
||||
FROM paperclipai/agent-runtime-base:${BASE_TAG}
|
||||
|
||||
USER root
|
||||
# opencode-ai npm package; binary on PATH is `opencode`.
|
||||
RUN npm install -g opencode-ai@latest \
|
||||
&& { chown -R 1000:1000 /usr/lib/node_modules || true; }
|
||||
|
||||
USER 1000:1000
|
||||
|
||||
# Verify the CLI is on PATH for the shim's exec.LookPath
|
||||
RUN command -v opencode >/dev/null 2>&1 || (echo "opencode not on PATH"; exit 1)
|
||||
@@ -0,0 +1,15 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
ARG BASE_TAG=dev
|
||||
FROM paperclipai/agent-runtime-base:${BASE_TAG}
|
||||
|
||||
USER root
|
||||
# pi is the multi-provider Pi coding-agent router. Verified npm package:
|
||||
# @mariozechner/pi-coding-agent (binary 'pi' → dist/cli.js). Same package
|
||||
# is used by SANDBOX_INSTALL_COMMAND in pi-local/src/index.ts.
|
||||
RUN npm install -g @mariozechner/pi-coding-agent@latest \
|
||||
&& { chown -R 1000:1000 /usr/lib/node_modules || true; }
|
||||
|
||||
USER 1000:1000
|
||||
|
||||
# Verify the CLI is on PATH for the shim's exec.LookPath
|
||||
RUN command -v pi >/dev/null 2>&1 || (echo "pi not on PATH"; exit 1)
|
||||
@@ -0,0 +1,85 @@
|
||||
# Agent Runtime Image Family
|
||||
|
||||
Container images for running coding-agent harnesses in sandboxed environments (for example the kubernetes sandbox provider, stage 1 of the k8s contribution). Images are named `agent-runtime-{harness}:{version}` and published to `ghcr.io/paperclipai/` by the `agent-runtime-images` workflow. The registry is overridable: every reference flows through the `REGISTRY` bake variable.
|
||||
|
||||
## Image Lineup
|
||||
|
||||
- **`agent-runtime-base`**: Foundation. Ubuntu 22.04 + Node 22 + git + tini + non-root user (uid 1000) + the agent shim.
|
||||
- **`agent-runtime-opencode`**: Extends base with `opencode-ai` globally installed.
|
||||
- **`agent-runtime-pi`**: Extends base with `@mariozechner/pi-coding-agent`.
|
||||
- **`agent-runtime-codex`**: Extends base with `@openai/codex`.
|
||||
- **`agent-runtime-gemini`**: Extends base with `@google/gemini-cli` plus headless auth-mode settings.
|
||||
- **`agent-runtime-claude`**: Extends base with `@anthropic-ai/claude-code` (symlinked as `claude-code`).
|
||||
- **`agent-runtime-hermes`**: Dockerfile included in the bake group, not in the default publish scope (stub until a CLI package exists).
|
||||
|
||||
## Base Image Contents
|
||||
|
||||
**OS & Runtime:**
|
||||
- Ubuntu 22.04
|
||||
- Node.js 22 (via NodeSource APT repo)
|
||||
- git
|
||||
- tini (PID-1 init, ensures signal propagation)
|
||||
- Non-root user `paperclip` (uid/gid 1000)
|
||||
|
||||
**Paperclip Binaries:**
|
||||
- `/usr/local/bin/paperclip-agent-shim`: Go binary compiled from `tools/agent-shim/`. Reads `/run/paperclip/runtime-command.json` and `syscall.Exec`s the harness CLI.
|
||||
|
||||
**Defaults:**
|
||||
- `USER`: 1000:1000 (paperclip, non-root)
|
||||
- `WORKDIR`: `/workspace` (mount workspace volumes here)
|
||||
- `ENTRYPOINT`: `/usr/bin/tini --` (PID-1 reaper, forwards signals)
|
||||
- `CMD`: `/usr/local/bin/paperclip-agent-shim`
|
||||
|
||||
## Building Locally
|
||||
|
||||
All targets build `linux/amd64` by default (see `buildx-bake.hcl`). Derived images chain off the `base` target through bake `contexts`, so the literal registry in each `FROM` line is overridden at build time and the whole family builds in one pass without pushing intermediates.
|
||||
|
||||
```bash
|
||||
docker buildx bake -f docker/agent-runtime/buildx-bake.hcl --load
|
||||
```
|
||||
|
||||
### Custom tag or registry
|
||||
|
||||
```bash
|
||||
REGISTRY=myregistry VERSION=mytag \
|
||||
docker buildx bake -f docker/agent-runtime/buildx-bake.hcl --load
|
||||
```
|
||||
|
||||
## Quickstart Smoke Test
|
||||
|
||||
Build and verify the `agent-runtime-claude` image runs locally:
|
||||
|
||||
```bash
|
||||
docker buildx bake -f docker/agent-runtime/buildx-bake.hcl base claude --load
|
||||
docker run --rm ghcr.io/paperclipai/agent-runtime-claude:dev claude-code --version
|
||||
```
|
||||
|
||||
## Agent Container (paperclip-agent-shim)
|
||||
|
||||
The main agent process runs as the shim (PID 1 under tini). The shim:
|
||||
|
||||
1. Reads `/run/paperclip/runtime-command.json` (path overridable via `-spec`), a JSON file mounted by whatever schedules the run
|
||||
2. Parses `{ "command", "args" }`: the harness CLI and arguments
|
||||
3. Resolves the command on PATH and `syscall.Exec`s it, replacing itself
|
||||
4. SIGTERM from the kubelet propagates directly to the harness (no zombie processes)
|
||||
|
||||
**runtime-command.json Contract:**
|
||||
```json
|
||||
{
|
||||
"command": "claude-code",
|
||||
"args": ["--token", "xyz", "--workspace", "/workspace"]
|
||||
}
|
||||
```
|
||||
|
||||
The shim makes no assumptions about command structure; it is harness-agnostic. New harnesses swap the command/args; the base image stays the same.
|
||||
|
||||
## Security Model
|
||||
|
||||
- **Non-root execution**: user 1000:1000, no capability grants
|
||||
- **PSS Restricted compatible**: no privileged containers, no host mounts; works with a read-only root filesystem (writable `/workspace` + `/tmp` mounts)
|
||||
- **No secrets baked in**: API tokens and credentials come from per-run ephemeral Secrets mounted as env vars or files
|
||||
- **Image signing**: cosign keyless OIDC in the publish workflow
|
||||
|
||||
## Publishing
|
||||
|
||||
`.github/workflows/agent-runtime-images.yml` builds and pushes the default scope (base, opencode, pi, codex, gemini, claude) on `workflow_dispatch` (with an explicit version tag) or on pushes to `master` touching these paths, then signs each digest with cosign keyless OIDC.
|
||||
@@ -0,0 +1,91 @@
|
||||
group "default" {
|
||||
targets = ["base", "claude", "codex", "gemini", "opencode", "pi", "hermes"]
|
||||
}
|
||||
|
||||
variable "VERSION" { default = "dev" }
|
||||
variable "REGISTRY" { default = "ghcr.io/paperclipai" }
|
||||
|
||||
target "base" {
|
||||
context = "."
|
||||
dockerfile = "docker/agent-runtime/Dockerfile.base"
|
||||
platforms = ["linux/amd64"]
|
||||
tags = ["${REGISTRY}/agent-runtime-base:${VERSION}"]
|
||||
}
|
||||
|
||||
target "claude" {
|
||||
context = "."
|
||||
dockerfile = "docker/agent-runtime/Dockerfile.claude"
|
||||
platforms = ["linux/amd64"]
|
||||
tags = ["${REGISTRY}/agent-runtime-claude:${VERSION}"]
|
||||
args = {
|
||||
BASE_TAG = "${VERSION}"
|
||||
}
|
||||
contexts = {
|
||||
"paperclipai/agent-runtime-base:${VERSION}" = "target:base"
|
||||
}
|
||||
}
|
||||
|
||||
target "codex" {
|
||||
context = "."
|
||||
dockerfile = "docker/agent-runtime/Dockerfile.codex"
|
||||
platforms = ["linux/amd64"]
|
||||
tags = ["${REGISTRY}/agent-runtime-codex:${VERSION}"]
|
||||
args = {
|
||||
BASE_TAG = "${VERSION}"
|
||||
}
|
||||
contexts = {
|
||||
"paperclipai/agent-runtime-base:${VERSION}" = "target:base"
|
||||
}
|
||||
}
|
||||
|
||||
target "gemini" {
|
||||
context = "."
|
||||
dockerfile = "docker/agent-runtime/Dockerfile.gemini"
|
||||
platforms = ["linux/amd64"]
|
||||
tags = ["${REGISTRY}/agent-runtime-gemini:${VERSION}"]
|
||||
args = {
|
||||
BASE_TAG = "${VERSION}"
|
||||
}
|
||||
contexts = {
|
||||
"paperclipai/agent-runtime-base:${VERSION}" = "target:base"
|
||||
}
|
||||
}
|
||||
|
||||
target "opencode" {
|
||||
context = "."
|
||||
dockerfile = "docker/agent-runtime/Dockerfile.opencode"
|
||||
platforms = ["linux/amd64"]
|
||||
tags = ["${REGISTRY}/agent-runtime-opencode:${VERSION}"]
|
||||
args = {
|
||||
BASE_TAG = "${VERSION}"
|
||||
}
|
||||
contexts = {
|
||||
"paperclipai/agent-runtime-base:${VERSION}" = "target:base"
|
||||
}
|
||||
}
|
||||
|
||||
target "pi" {
|
||||
context = "."
|
||||
dockerfile = "docker/agent-runtime/Dockerfile.pi"
|
||||
platforms = ["linux/amd64"]
|
||||
tags = ["${REGISTRY}/agent-runtime-pi:${VERSION}"]
|
||||
args = {
|
||||
BASE_TAG = "${VERSION}"
|
||||
}
|
||||
contexts = {
|
||||
"paperclipai/agent-runtime-base:${VERSION}" = "target:base"
|
||||
}
|
||||
}
|
||||
|
||||
target "hermes" {
|
||||
context = "."
|
||||
dockerfile = "docker/agent-runtime/Dockerfile.hermes"
|
||||
platforms = ["linux/amd64"]
|
||||
tags = ["${REGISTRY}/agent-runtime-hermes:${VERSION}"]
|
||||
args = {
|
||||
BASE_TAG = "${VERSION}"
|
||||
}
|
||||
contexts = {
|
||||
"paperclipai/agent-runtime-base:${VERSION}" = "target:base"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user