Skip to content
Open
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
92 changes: 92 additions & 0 deletions axe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# axe

A mixin kit that installs a Deque [axe-core](https://github.com/dequelabs/axe-core)
accessibility CLI (**axe-core 4.13.0** via `@axe-core/playwright`) plus
**Chromium** (same Playwright browser channel as the `playwright` kit) so
an agent can audit pages served inside the sandbox against WCAG —
headless, sandbox-local.

## Usage

```console
sbx run claude --kit "docker.io/sbx/axe-kit:latest" .
```

Or straight from this repository over git:

```console
sbx run --kit "git+https://github.com/docker/sbx-kits-contrib.git#dir=axe" claude
```

Or with a local clone of this repo:

```console
sbx run claude --kit ./axe/ .
```

Prerequisites:

- A base image with Node.js ≥ 18 and npm — all standard agent templates
ship it. The install fails loudly if npm is missing.

Inside the sandbox:

```console
axe http://localhost:3000
axe http://localhost:3000 --save ./axe-report.json
chromium --version
```

Compose with `playwright` when the agent also needs to drive the browser:

```console
sbx run claude --kit "docker.io/sbx/playwright-kit:latest" --kit "docker.io/sbx/axe-kit:latest" .
```

Both kits set `PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`, so Chromium
is downloaded once and reused.

## How it works

### Why not `@axe-core/cli`

`@axe-core/cli@4.13.0` depends on `chromedriver@latest` (an unpinned
postinstall download) and Selenium. That driver will not match the
Playwright Chromium this repo already installs, and `latest` is the
supply-chain pattern kits here refuse. This kit installs
`@axe-core/playwright@4.13.0` + `playwright@1.61.1` and ships a thin
`axe <url> [--save file.json]` wrapper that launches that Chromium and
prints the axe-core JSON report. Exit 0 if there are no violations.

### Why Chromium comes from Playwright, not apt

Ubuntu's `chromium` packages are often snap stubs that do not run in the
sandbox. Chromium is installed with
`npx playwright@1.61.1 install --with-deps chromium` into
`/opt/ms-playwright`, then symlinked to `/usr/local/bin/chromium`. If
the tree is already present (composed `playwright` / `lighthouse` kit),
the hook skips the download.

### Why these domains

`permissions.network.allow` is the kit's complete outbound contract — CI
runs e2e under a `deny-all` policy.

| Domain | Why |
| --- | --- |
| `registry.npmjs.org` | npm tarballs for playwright + `@axe-core/playwright` (install time) |
| `cdn.playwright.dev` | Playwright's primary browser-binary CDN |
| `playwright.download.prss.microsoft.com` | Documented fallback CDN |
| `storage.googleapis.com` | Chrome-for-Testing zip on amd64 after the CDN 302 |
| `archive.ubuntu.com` | Ubuntu apt archive, amd64 — `--with-deps` |
| `security.ubuntu.com` | Ubuntu security pocket, amd64 |
| `ports.ubuntu.com` | Ubuntu archive/security for arm64 |
| `download.docker.com` | Docker's apt repo, pre-added by `*-docker` templates |

**Runtime reminder:** the allowlist covers installing the toolchain, not
the sites an audit visits. `localhost` always works; external sites need
a per-sandbox allow rule.

## Cleanup

Everything is sandbox-local and disappears with `sbx rm <name>`.
47 changes: 47 additions & 0 deletions axe/files/home/.local/bin/axe
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env node
"use strict";

// Thin CLI over @axe-core/playwright. We do not ship @axe-core/cli:
// that package depends on chromedriver@latest (unpinned postinstall
// download) and Selenium, which will not match Playwright Chromium.
const url = process.argv[2];
if (!url || url.startsWith("-")) {
console.error("usage: axe <url> [--save <file.json>]");
process.exit(2);
}

let save = null;
for (let i = 3; i < process.argv.length; i++) {
if (process.argv[i] === "--save") {
save = process.argv[i + 1];
i++;
}
}

(async () => {
const { chromium } = require("playwright");
const axePlaywright = require("@axe-core/playwright");
const AxeBuilder = axePlaywright.AxeBuilder || axePlaywright.default;
const browser = await chromium.launch({
executablePath: process.env.CHROME_PATH || "/usr/local/bin/chromium",
headless: true,
args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
});
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle" });
const results = await new AxeBuilder({ page }).analyze();
const json = JSON.stringify(results, null, 2);
if (save) {
require("fs").writeFileSync(save, json);
} else {
console.log(json);
}
process.exit(results.violations.length ? 1 : 0);
} finally {
await browser.close();
}
})().catch((err) => {
console.error(err);
process.exit(2);
});
97 changes: 97 additions & 0 deletions axe/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
schemaVersion: "2"
kind: mixin
name: axe
displayName: axe-core
description: Deque axe-core accessibility CLI — audits a URL against WCAG with headless Chromium, so agents can score pages served inside the sandbox.
agentInstructions:
content: |
## axe-core

`axe` is on PATH. It runs axe-core 4.13.0 via @axe-core/playwright
against Playwright Chromium. CHROME_PATH points at
/usr/local/bin/chromium; don't unset it or PLAYWRIGHT_BROWSERS_PATH.

Headless only — the sandbox has no display. Chrome is launched with
--no-sandbox --disable-dev-shm-usage --disable-gpu.

- Audit a local server: `axe http://localhost:3000`
- Write JSON: `axe http://localhost:3000 --save ./axe-report.json`
- Confirm: `axe` with no args prints usage; `chromium --version`

Exit 0 if there are no violations, 1 if any, 2 on usage or launch
errors. External sites fail with a proxy error unless their hosts
are in the sandbox network policy; localhost always works.
permissions:
network:
allow:
# npm packages (install time): @axe-core/playwright, axe-core,
# playwright. puppeteer is not used.
- registry.npmjs.org:443
# Chromium via `playwright install --with-deps chromium` — same
# channel as the playwright / lighthouse kits.
- cdn.playwright.dev:443
- playwright.download.prss.microsoft.com:443
- storage.googleapis.com:443
# `--with-deps` apt-installs Chromium system libraries.
# `apt-get update` refreshes every configured source.
- archive.ubuntu.com:80
- security.ubuntu.com:80
- ports.ubuntu.com:80
- download.docker.com:443
environment:
variables:
CHROME_PATH: /usr/local/bin/chromium
PLAYWRIGHT_BROWSERS_PATH: /opt/ms-playwright
NODE_PATH: /usr/local/share/npm-global/lib/node_modules
setup:
install:
# @axe-core/cli@4.13.0 depends on chromedriver@latest (unpinned
# postinstall fetch) + Selenium, which will not match this
# Chromium. We install @axe-core/playwright@4.13.0 + playwright
# 1.61.1 (same pin as the playwright kit) and ship a thin `axe`
# CLI in files/home/.local/bin/axe. To bump: change both npm
# versions here and in agentInstructions / README.
- command: |
set -euo pipefail
command -v npm >/dev/null || { echo "npm not found: this mixin needs a base image with Node.js >= 18 (all standard agent templates ship it)" >&2; exit 1; }
if [ -n "${HTTP_PROXY:-}" ]; then
npm config set proxy "$HTTP_PROXY"
npm config set https-proxy "$HTTP_PROXY"
fi
AXE_VERSION=4.13.0
PLAYWRIGHT_VERSION=1.61.1
npm install -g "playwright@${PLAYWRIGHT_VERSION}" "@axe-core/playwright@${AXE_VERSION}"
node -e "require('playwright'); require('@axe-core/playwright'); console.log('axe-core playwright ok')"
# files/home/.local/bin/axe is the CLI. Link it onto PATH for every
# user; the target is injected with the files/ tree (dangling until
# then is fine). chmod in startup covers the after-files case.
mkdir -p /home/agent/.local/bin
ln -sfn /home/agent/.local/bin/axe /usr/local/bin/axe
user: "0"
description: Install playwright 1.61.1 + @axe-core/playwright v4.13.0 and the axe CLI wrapper
- command: |
set -euo pipefail
export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-/opt/ms-playwright}"
mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"
find_chrome() {
find "$PLAYWRIGHT_BROWSERS_PATH" -type f -name chrome ! -path '*headless*' 2>/dev/null | head -1
}
CHROME_BIN="$(find_chrome || true)"
if [ -z "$CHROME_BIN" ]; then
npx -y playwright@1.61.1 install --with-deps chromium
rm -rf /var/lib/apt/lists/*
CHROME_BIN="$(find_chrome || true)"
fi
if [ -z "$CHROME_BIN" ] || [ ! -x "$CHROME_BIN" ]; then
echo "failed to locate Chromium chrome binary under $PLAYWRIGHT_BROWSERS_PATH" >&2
exit 1
fi
ln -sfn "$CHROME_BIN" /usr/local/bin/chromium
chmod -R a+rX "$PLAYWRIGHT_BROWSERS_PATH"
chromium --version
user: "0"
description: Install Playwright Chromium into /opt/ms-playwright and symlink /usr/local/bin/chromium
startup:
- command: ["sh", "-c", "chmod 0755 /home/agent/.local/bin/axe 2>/dev/null || true"]
user: "0"
description: Ensure the axe CLI wrapper is executable