One toolchain, two interfaces. The runner image (ghcr.io/coderpuzzle/coderpuzzle)
carries every pinned language tool and executor; the REST API drives the
web UI, and the coderpuzzle CLI (installed in the image from
runner/cli.py) drives authoring and CI. Formatting is the same code in
both — see the tri-state contract below.
Sessions are cookie-based. Everything except /health, GET /auth/status, and POST /auth/register requires a coderpuzzle_session
cookie from POST /session; authenticated callers additionally carry
the session of a signed-in
user. Auth is a provider catalog (password is one method) — see
AUTH.md. Errors are FastAPI payloads: {"detail": "<reason>"} with the
appropriate status (400 user error, 401 no/invalid session, 404 unknown
slug or submission, 429 judge rate limit, 503 runner unavailable or
busy).
GET /health→{"status": "ok"}— liveness; no session needed.POST /session— create a guest session; sets thecoderpuzzle_sessioncookie. →{"status": "active", "idle_seconds": 3600}.GET /session?touch=0|1— validate the session.touch=0validates without extending the idle clock (the frontend's inactivity watcher probes with it, so watching cannot keep an abandoned session alive). →{"status": "active", "idle_seconds", "user": null | {"username", "is_admin"}}; 401 when expired or absent.
Auth (pluggable providers; see AUTH.md)
GET /auth/status→{"needs_setup": bool, "providers": […]}— public; true until the first account exists.providersis the enabled catalog (id, flow, fields, can_login / can_register / can_bootstrap). Password is always present; OAuth/OIDC/email OTP appear when configured.POST /auth/start{provider, …}— begin a redirect or challenge flow. →{next: "redirect"|"challenge"|"complete", redirect_url?, challenge_id?}.POST /auth/complete{provider, …}— requires an active session; binds that session to the user. Password:{provider:"password", username, password}.POST /auth/register{provider, …}— public (no session, by necessity on a fresh install). The first account is admin; the password provider still requires usernameadmin. Afterwards registration is closed unlessCODERPUZZLE_AUTH_REGISTRATION=open.GET /auth/callback/{provider}— OAuth/OIDC return; 303 to/.POST /auth/logout— unbinds the user from the session.- Compatibility:
POST /auth/login {username, password}and register withoutproviderstill mean the password provider.
GET /problems?page=1&page_size=0— paged summaries (id, slug, title, difficulty, tags, topics, …).page_size=0means all.GET /problems/topics→{"topics": [{"name", "count"}…]}— topic taxonomy with per-topic counts, busiest first.GET /problems/{slug}— the full public problem: statement pieces, constraints, examples, starter per offered language, public cases.GET /problems/{slug}/solutions— the solution guide:titles, per-variantimplementationsby language, thecanonicalcode, the worst-to-bestorder, and which variant is thereference.GET /problems/{slug}/figures/{figure}— an SVG from the bundle'sfigures/.
GET /drafts/{slug}— the caller's saved per-language drafts for the problem.PUT /drafts/{slug}/{language}{code}— save one draft (≤ 256 KB).POST /format{language, code}— tri-state (see below).POST /run{slug, language, code, cases?}— run against the given cases (defaults to the bundle's public cases; ≤ 20). →JudgeResult:{status, passed, total, runtime_ms, results: [per-case verdicts]}.POST /submit{slug, language, code}— judged against all cases; the runtime is also measured against the designated reference solution (reference_runtime_ms); the attempt is stored.GET /submissions?slug=…&limit=30— the caller's stored attempts.GET /submissions/{id}— one stored attempt (404 if not the caller's).GET /progress→{slug: "solved" | "attempted"}— solved when any submission in any language was accepted; absent slugs are never-tried.
Shared implementation with the CLI (same runner/formatters.py, same
pinned toolchain). Always 200 when the runner is reachable — the
payload carries the state:
| status | meaning | extra field |
|---|---|---|
formatted |
already conforming | — |
unformatted |
parses; this is the formatted text | code |
error |
does not parse (the author's to fix) | diagnostics |
503 stays reserved for the runner being unreachable.
The image installs the CLI as coderpuzzle; locally
test an edited cli.py by bind-mounting it over /runner/cli.py.
coderpuzzle format <files|dirs…>— format in place to the CoderPuzzle standard; directories walk for formattable files, skippingnode_modulesand hidden trees.--check— report unformatted files, change nothing, exit 1 if any. This is what CI runs overproblems-adapt.--report json— non-mutating tri-state JSON per file (the same contract asPOST /format, withfileadded):formatted | unformatted (+ "code" text) | error (+ "diagnostics"). Exits 1 only when at least one file errored; "unformatted" is information, not a failure.
coderpuzzle gen-starters <problem.json> [--style modern|legacy]— emitstarter.<ext>for the languages the bundle already offers (the existing starter set is never widened), formatted by the pinned toolchain.--styledefaults tomodern; the provenance-aware choice (MAPPING.json-driven) lives in the problems repo'sscripts/gen_starters.py. Requires the problems repo bind-mounted at/tools(the loader shim is the schema contract).coderpuzzle judge <bundle-dir>— judge everysolution*.<ext>in the bundle through the real executors against all cases; all must pass every case. Assembles the bundle's ownprovided/sources exactly as a live judge job would. Compiles without the untrusted- submission sandbox (authoring on the author's own machine).
The authoring fast loop: judge one solution file without running the whole bundle matrix.
- Walks up from the file to the directory holding
problem.json(bundle discovery), so the file must live inside the bundle. - Language comes from the extension (
--langoverrides). - Assembles
provided/<lang>/exactly asjudgedoes. - Sandbox boundary: this is an authoring tool for the author's own
machine, so it compiles plainly (as
judgedoes); the untrusted- submission sandbox applies to the REST/runpath, not here. --publicjudges only the public cases (fast loop); default is all cases. Per-case verdicts print; exit 1 on any failure.
# authoring: format bundle files in place (checkout's cli.py wins)
docker run --rm --user 0:0 \
-v "$PWD/runner/cli.py:/runner/cli.py:ro" \
-v "$PWD/runner/formatters.py:/runner/formatters.py:ro" \
-v "$PWD:/work" -w /work \
ghcr.io/coderpuzzle/coderpuzzle:latest coderpuzzle format --check problems-adapt
# run one solution file against its bundle's cases
# (mount the repo root; the file is addressed from the working directory)
docker run --rm --user 0:0 \
-v "$PWD/runner/cli.py:/runner/cli.py:ro" \
-v "$PWD/runner/formatters.py:/runner/formatters.py:ro" \
-v "$PWD:/work" -w /work \
ghcr.io/coderpuzzle/coderpuzzle:latest coderpuzzle run problems-adapt/<shard>/<key>/my_draft.pyThe convenience wrapper scripts/format.sh (coderpuzzle repo) does the
mounting for the format case; CODERPUZZLE_IMAGE overrides the tag.