Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"attribution": {
"pr": ""
}
Comment thread
theseanything marked this conversation as resolved.
}
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Agent instructions

The Docker sandbox in `local/sbx/` tells Claude to follow these rules in every
GOV.UK Forms repo, so keep them general and keep this file at this path.

## Commits

- Split features into well-scoped commits. Each commit should leave the tests
green.
- Commit titles: 50 characters or fewer, imperative, naming only the thing
changed ("Add Brand model", not "Add Brand model backed by database table").
- Commit bodies: explain why the change is needed, and include only context
the diff doesn't show, such as external constraints or why an approach was
forced.
- Fold additional changes into the commit they belong to rather than adding
fixup commits.
- Don't push or update pull requests unless asked. Committing locally is fine.

## Pull requests

- Use `.github/PULL_REQUEST_TEMPLATE.md`.
- Keep descriptions to a few short paragraphs, with only context a reviewer
can't see in the diff.
- Don't mention AI tooling in pull request descriptions.
Comment thread
theseanything marked this conversation as resolved.
Comment thread
theseanything marked this conversation as resolved.
- Don't describe fixes as security or vulnerability issues in public pull
requests. Describe what the code now enforces.
3 changes: 3 additions & 0 deletions local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ The components which make up the service are:
- https://github.com/govuk-forms/forms-admin
- https://github.com/govuk-forms/forms-product-page

To work on the apps with Claude Code in an isolated Docker Sandbox instead,
see [sbx/README.md](sbx/README.md).

## How it works
Each of the components above has a Dockerfile in its repo which is used to build
a docker image. The file `compose.yaml` defines the configuration for using
Expand Down
211 changes: 211 additions & 0 deletions local/sbx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# GOV.UK Forms in a Docker Sandbox

Use this to work on GOV.UK Forms with Claude Code in a [Docker Sandbox][sbx].
A sandbox is an isolated Linux virtual machine. Claude can only see and change
what's inside it.

The sandbox has:

- the folder holding forms-admin, forms-runner, forms-product-page and
forms-deploy, shared with your machine so changes show on both sides
straight away. Claude starts in that folder, and can see anything else you
keep in it
- the Ruby and Node versions each app needs
- Postgres and Valkey
- Chrome and chromedriver, for the specs that run in a browser
- each app's gems, npm packages and databases, ready to use
- Claude Code, connected to the team's model gateway

The sandbox cannot see files outside that folder, so don't keep anything in it
you wouldn't want Claude to read. It also cannot see your gateway key or
GitHub token.

[sbx]: https://docs.docker.com/ai/sandboxes/

## Before you start

You need:

- [Docker Sandboxes][install] 0.48.0 or newer installed, and to be signed in
with `sbx login`. Check with `sbx version`, and upgrade with
`brew upgrade --cask sbx` if you installed it with Homebrew
- the GOV.UK Forms repos cloned side by side, as described in
[`../README.md`](../README.md)
- your model gateway key, which starts with `sk-`

Your repos should look like this:

```
top-level/
├── forms-admin
├── forms-deploy
├── forms-product-page
└── forms-runner
```

If you want Claude to push and open pull requests, you also need a
[fine-grained GitHub token][pat]. Give it read and write access to
**Contents** and **Pull requests** on the govuk-forms repos. Without a token,
Claude can commit in the sandbox, and you push from your machine as usual.

[install]: https://docs.docker.com/ai/sandboxes/install/
[pat]: https://github.com/settings/personal-access-tokens/new

## Set up the sandbox

You only need to do this once.

1. Run the setup script:

```bash
cd forms-deploy/local/sbx
./setup.sh
```

2. Choose whether to add a GitHub token.
3. Paste your model gateway key when asked.
4. Choose whether to add the status line.

The script:

- stores your gateway key and GitHub token as sbx secrets
- installs the [commit messages skill][skill]
- adds your git name and email to `~/.sbxenv.yaml`, so your commits in the
sandbox are in your name
- if you choose to, copies [`statusline.sh`](statusline.sh) into `.claude/` in
the folder holding the repos and sets it in `.claude/settings.local.json`
there. Claude then shows the model, folder, git state, context use and cost
under the prompt, in the sandbox and when you run Claude in that folder on
your machine

The sbx proxy adds your secrets to requests as they leave the sandbox, so the
sandbox never sees them.

You can safely run the script again. It skips anything already done. To
answer the questions up front, use `--github` or `--no-github`, and
`--statusline` or `--no-statusline`.

[skill]: https://gitlab.com/gitlab-org/ai/skills/-/blob/main/skills/commit-messages/SKILL.md

## Start the sandbox

From this directory, run:

```bash
sbx env run
```

Always run `sbx env` commands from this directory, without a path. Otherwise
sbx will not use your `~/.sbxenv.yaml`, and your commits will not have your
name and email. sbx also loses track of the sandbox if you mix the two.

The first run takes a few minutes to build and create the sandbox. After that,
it reconnects to the same sandbox and keeps your files, gems and databases.

Each time Claude starts, the sandbox installs anything missing and prepares
the databases in the background. This takes about a minute the first time and
a few seconds after that. Claude knows to wait for it before running specs.

## Run the apps

When you or Claude run an app in the sandbox, open it on your machine:

- forms-admin: http://localhost:3100
- forms-runner: http://localhost:3101
- forms-product-page: http://localhost:3102

These are not the apps' usual ports, so the sandbox can run alongside the
[Docker Compose setup](../README.md).

## Manage the sandbox

Run these commands from this directory.

| To | Run |
| ---------------------------- | --------------------------- |
| Run a command in the sandbox | `sbx env exec -- <command>` |
| Stop the sandbox | `sbx stop govuk-forms` |
| Delete the sandbox | `sbx env rm` |

Deleting the sandbox does not affect your repos.

## Change the sandbox

Changes to `env:` in `sbxenv.yaml` apply the next time you run `sbx env run`.

Changes to kits, bindings or secrets only apply when the sandbox is created.
To pick them up, delete the sandbox with `sbx env rm`, then run `sbx env run`.

## How it works

- [`sbxenv.yaml`](sbxenv.yaml) defines the sandbox: its kits, repos,
environment variables and ports, and which hosts can receive each secret.
- `~/.sbxenv.yaml` holds your personal settings. sbx merges it with
`sbxenv.yaml`.
- The agent is Docker's [Claude Code kit][claude-kit]. The 2 [kits][kits] in
this directory add to it. Each has a descriptor (`<kit>.yaml`), and
forms-dev also has a Dockerfile for the files it adds.
- [`ai-gateway/`](ai-gateway) connects Claude Code to the model gateway.
Claude only sees a placeholder in `ANTHROPIC_AUTH_TOKEN`. The sbx proxy
replaces it with your key on requests to the gateway, and nowhere else.
- [`forms-dev/`](forms-dev) installs build tools, mise and Chrome, allows the
domains the setup needs, and declares the optional `github` secret. Each time the
sandbox starts, it runs:
- `forms-dev-up`, which starts Postgres and Valkey and gives each app a
separate `node_modules` in the sandbox. This keeps Linux builds of
packages like esbuild and rolldown out of your machine's `node_modules`.
- `forms-app-setup`, which installs everything each app needs.
- forms-dev also gives Claude
[notes about the sandbox](forms-dev/forms-dev-context.md), and tells it to
follow the commit and pull request rules in this repo's
[`AGENTS.md`](../../AGENTS.md) in every repo. Change the rules there.
- forms-dev turns off the Claude Code footer on pull requests and turns on
fullscreen rendering in the sandbox user's Claude settings, so both apply
in every repo.
- The sandbox cannot use SSH, so inside the sandbox git rewrites GitHub
remotes to HTTPS. The proxy adds your GitHub token to those requests.

[claude-kit]: https://hub.docker.com/r/docker/sbx-kit-claude
[kits]: https://docs.docker.com/ai/sandboxes/customize/use-kits/

## Troubleshooting

### Specs fail before they run

The app setup may not have finished, or it may have failed. In the sandbox,
check `/tmp/forms-app-setup.status` and `/tmp/forms-app-setup.log`. If there is
no status file, the setup has not started. To run the setup again, run
`forms-app-setup`. It stops straight away if the setup is still running.

### Claude gets a 401 error from the gateway

Your gateway key is missing or wrong. Run `sbx secret ls` to check there's a
global `ai-gateway` secret. To replace it, run
`sbx secret rm ai-gateway -f && ./setup.sh`, then delete and recreate the
sandbox.

### Building a kit fails with "failed to connect to the docker API"

If the `docker` command is installed, sbx uses Docker to build the kits in
this directory. Start Docker Desktop and try again.

### A download or request is blocked

Run `sbx policy log govuk-forms` to see which domains were blocked. Add them
to the network policy's `runtime` list in
[`forms-dev/forms-dev.yaml`](forms-dev/forms-dev.yaml).

### `git push` or `gh` fails in the sandbox

The sandbox needs a GitHub token to push. Push from your machine instead, or
run `sbx secret set github` and then delete and recreate the sandbox.

### Vite fails on your machine after using the sandbox

Linux packages have been written to your machine's `node_modules`. Run
`npm ci` in that app on your machine.

### Claude Code's full screen view will not scroll in tmux

Add `set -g mouse on` to `~/.tmux.conf` to turn on mouse mode, or use the
Page Up and Page Down keys.
55 changes: 55 additions & 0 deletions local/sbx/ai-gateway/ai-gateway.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# syntax=docker/sandbox-kit:3
#
# Points Claude Code at the team's model gateway, a LiteLLM proxy, and has
# the sbx proxy supply each person's gateway key. The key stays in the host's
# secret store under the `ai-gateway` service (setup.sh stores it). The
# sandbox only sees a placeholder in ANTHROPIC_AUTH_TOKEN, and the proxy
# replaces the Authorization header on requests to the gateway.
schemaVersion: "3"
kind: mixin
displayName: AI Engineering Lab model gateway
description: >-
Routes Claude Code through the AI Engineering Lab LiteLLM gateway, with
the gateway key supplied by the sandbox proxy.
version: "1.0.0"

# Hooks run after the kits they require, so the hook below runs after the
# claude kit writes settings.json
requires: ["claude"]

build: |
FROM scratch
ENV ANTHROPIC_BASE_URL=https://licenseportal.aiengineeringlab.co.uk

capabilities:
- type: com.docker.sandbox/network-policy@1
config:
runtime:
allow:
- licenseportal.aiengineeringlab.co.uk

- type: com.docker.sandbox/credential@1
description: Model gateway key
config:
service: ai-gateway
phase: runtime
apiKey:
name: ANTHROPIC_AUTH_TOKEN
proxyManaged: true
inject:
- domain: licenseportal.aiengineeringlab.co.uk
header: Authorization
format: "Bearer %s"

# The claude kit adds an apiKeyHelper when the person has an anthropic
# secret, and Claude Code warns when that and ANTHROPIC_AUTH_TOKEN are both
# set. jq comes with the claude kit's image.
- type: com.docker.sandbox/lifecycle@1
config:
install:
- description: Remove the claude kit's apiKeyHelper
user: agent
command: >
f=~/.claude/settings.json;
[ ! -s "$f" ] ||
{ new=$(jq 'del(.apiKeyHelper)' "$f") && printf '%s\n' "$new" > "$f"; }
Loading
Loading