Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ jobs:
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: |
package-lock.json
worker-sdk/package-lock.json
# --ignore-scripts blocks EVERY dependency's install/postinstall script
# (agent-authored deps land in these PR builds), then we explicitly
# rebuild only better-sqlite3 — the one native module the app needs — so
# its prebuilt binary is present for the test run. This is the
# single-package allowlist the SYD-209 spec calls for.
- run: npm ci --ignore-scripts
- run: npm ci --prefix worker-sdk --ignore-scripts
- run: npm rebuild better-sqlite3
- run: npm run lint
- run: npm run format:check
Expand Down
103 changes: 103 additions & 0 deletions .github/workflows/worker-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Worker images

on:
pull_request:
paths:
- "Dockerfile.worker*"
- "Dockerfile.egress-proxy"
- ".dockerignore"
- "scripts/container-entry*.sh"
- "scripts/egress-*"
- "scripts/worker-engine-runner.mjs"
- "scripts/worker-telemetry.mjs"
- "scripts/prime-workspace-trust.mjs"
- "scripts/install-guard.mjs"
- "scripts/attach.mjs"
- "tests/scripts/egress-proxy-contract.py"
- ".github/workflows/worker-images.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
smoke:
name: worker-image (${{ matrix.engine }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- engine: claude
dockerfile: Dockerfile.worker
binary: claude
- engine: codex
dockerfile: Dockerfile.worker.codex
binary: codex
- engine: gemini
dockerfile: Dockerfile.worker.gemini
binary: gemini
- engine: proxy
dockerfile: Dockerfile.egress-proxy
binary: mitmdump
env:
IMAGE: switchyard-smoke:${{ matrix.engine }}
DOCKERFILE: ${{ matrix.dockerfile }}
BINARY: ${{ matrix.binary }}
ENGINE: ${{ matrix.engine }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build image
run: docker build --pull -f "$DOCKERFILE" -t "$IMAGE" .
- name: Check binary and supported headless flags without network or credentials
shell: bash
run: |
set -euo pipefail
version=$(docker run --rm --network none --entrypoint "$BINARY" "$IMAGE" --version)
printf '%s\n' "$version"
if [ "$ENGINE" = proxy ]; then
expected=$(sed -n 's/^FROM mitmproxy\/mitmproxy://p' "$DOCKERFILE")
else
expected=$(sed -n 's/^ARG \(CLAUDE_CODE\|CODEX_CLI\|GEMINI_CLI\)_VERSION=//p' "$DOCKERFILE")
fi
test -n "$expected"
[[ "$version" == *"$expected"* ]]
case "$ENGINE" in
claude)
docker run --rm --network none --entrypoint claude "$IMAGE" --help > /tmp/worker-help
grep -F -- --output-format /tmp/worker-help
grep -F -- --model /tmp/worker-help
;;
codex)
docker run --rm --network none --entrypoint codex "$IMAGE" exec --help > /tmp/worker-help
grep -F -- --json /tmp/worker-help
grep -F -- --dangerously-bypass-approvals-and-sandbox /tmp/worker-help
;;
gemini)
docker run --rm --network none --entrypoint gemini "$IMAGE" --help > /tmp/worker-help
grep -F -- --output-format /tmp/worker-help
grep -F -- --approval-mode /tmp/worker-help
;;
proxy)
docker run --rm --network none --entrypoint python3 \
-e PYTHONDONTWRITEBYTECODE=1 -v "$PWD:/review:ro" "$IMAGE" \
/review/tests/scripts/egress-proxy-contract.py
# Load the actual addon and start the actual entrypoint. No ports
# are published and network=none prevents any provider request.
docker run -d --name proxy-smoke --network none \
-e ALLOWED_DOMAINS=registry.npmjs.org "$IMAGE"
trap 'docker logs proxy-smoke; docker rm -f proxy-smoke >/dev/null' EXIT
for attempt in {1..30}; do
test "$(docker inspect -f '{{.State.Running}}' proxy-smoke)" = true
if docker exec proxy-smoke python3 -c \
'import socket; socket.create_connection(("127.0.0.1", 8888), timeout=1).close()'; then
exit 0
fi
sleep 1
done
exit 1
;;
esac
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This repo tracks its own work in Switchyard itself (project key `SYD`) via the `
npm run dev # server on :3300 (tsx src/server.ts); SWITCHYARD_DB / PORT env override
npm test # vitest run (all tests)
npx vitest run tests/services/issues-update.test.ts # single test file
npm run typecheck # checks BOTH tsconfigs: app (tsc --noEmit) and ui (tsc -p ui)
npm run typecheck # checks app, ui, and isolated worker-sdk tsconfigs
npm run lint # eslint .
npm run format:check # prettier --check . (npm run format to fix)
npm run build:ui # vite build → dist/ui (server 404s SPA routes until this exists)
Expand Down Expand Up @@ -44,7 +44,7 @@ npx tsx scripts/syd.ts whoami --as SWITCHYARD_TOKEN # pick a credenti

Before any write it prints `acting as <name> (<type>) via <KEY>`. Read that line. Variable names describe the *actor*, not the tier — `SWITCHYARD_HUMAN_TOKEN` is a person, `SWITCHYARD_GITHUB_POLLER_TOKEN` and `SWITCHYARD_DELIVER_POLLER_TOKEN` are `service` actors, and `SWITCHYARD_TOKEN` is the dispatch worker's *agent* token, which the delivery and GitHub-ingestion endpoints refuse on purpose.

`worker-sdk/` has isolated dependencies (`npm install --prefix worker-sdk`) because the Claude Agent SDK needs zod@4 while the app is on zod@3.
`worker-sdk/` has isolated dependencies (`npm ci --prefix worker-sdk --ignore-scripts`) because the Claude Agent SDK needs zod@4 while the app is on zod@3. Install them before running the required checks; CI installs both lockfiles. `npm run typecheck:worker-sdk` checks the runner against its pinned SDK without adding the SDK to app dependencies.

## Constraints & conventions

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.egress-proxy
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# NOTE: confirm the mitmproxy tag exists on Docker Hub at build time and bump as
# releases land — pinned for reproducibility (matches the alpine pin the old
# image used).
FROM mitmproxy/mitmproxy:11.1.3
FROM mitmproxy/mitmproxy:12.2.3

# The base image runs as the non-root `mitmproxy` user (uid 1000, home
# /home/mitmproxy). Become root only to place files + prep the CA dir, then
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ RUN apt-get update \
# @anthropic-ai/claude-code` would silently pick up whatever is newest at build
# time. Bump via `--build-arg CLAUDE_CODE_VERSION=x.y.z` or by editing the
# default below; see README's containerized-mode section for the update steps.
ARG CLAUDE_CODE_VERSION=2.1.220
ARG CLAUDE_CODE_VERSION=2.1.283
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \
&& claude --version

Expand Down Expand Up @@ -43,6 +43,7 @@ RUN npm install -g pnpm@${PNPM_VERSION} \
COPY scripts/container-entry.sh /entry.sh
COPY scripts/prime-workspace-trust.mjs /prime-workspace-trust.mjs
COPY scripts/install-guard.mjs /install-guard.mjs
COPY scripts/worker-engine-runner.mjs scripts/worker-telemetry.mjs /
RUN chmod +x /entry.sh

# Out-of-band attachment uploader (SYD-182): baked in so any dispatched worker
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile.worker.codex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ RUN apt-get update \
# @openai/codex` would silently pick up whatever is newest at build
# time. Bump via `--build-arg CODEX_CLI_VERSION=x.y.z` or by editing the
# default below; see README's containerized-mode section for the update steps.
ARG CODEX_CLI_VERSION=0.145.0
ARG CODEX_CLI_VERSION=0.157.1
RUN npm install -g @openai/codex@${CODEX_CLI_VERSION} \
&& codex --version

Expand All @@ -36,6 +36,7 @@ RUN npm install -g pnpm@${PNPM_VERSION} \
COPY scripts/container-entry.codex.sh /entry.sh
COPY scripts/prime-workspace-trust.mjs /prime-workspace-trust.mjs
COPY scripts/install-guard.mjs /install-guard.mjs
COPY scripts/worker-engine-runner.mjs scripts/worker-telemetry.mjs /
RUN chmod +x /entry.sh

# Out-of-band attachment uploader (SYD-182): baked in so any dispatched worker
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile.worker.gemini
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates python3 make g++ \
&& rm -rf /var/lib/apt/lists/*

RUN npm install -g @google/gemini-cli
# Match the other worker engines: upgrades are explicit and the installed
# version is recorded in build output, never selected by npm's latest tag.
ARG GEMINI_CLI_VERSION=0.61.0
RUN npm install -g @google/gemini-cli@${GEMINI_CLI_VERSION} \
&& gemini --version

# SYD-253: pnpm for target repos that commit pnpm-lock.yaml (yarn is bundled
# in the base image; the --version call asserts it) -- see the matching block
Expand All @@ -32,6 +36,7 @@ RUN npm install -g pnpm@${PNPM_VERSION} \
COPY scripts/container-entry.gemini.sh /entry.sh
COPY scripts/prime-workspace-trust.mjs /prime-workspace-trust.mjs
COPY scripts/install-guard.mjs /install-guard.mjs
COPY scripts/worker-engine-runner.mjs scripts/worker-telemetry.mjs /
RUN chmod +x /entry.sh

# Out-of-band attachment uploader (SYD-182): baked in so any dispatched worker
Expand Down
Loading
Loading