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.
The API is served at the web UI's same origin, under /api/ — the web
service's published port, 8081 by default, with the edge terminating TLS
living outside this repo. The examples below assume that origin:
BASE=http://localhost:8081/apiSessions are cookie-based. Everything except GET /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, not the protocol) — see AUTH.md. Treat any deployment's
API as public and rate-limit at the edge if you expose it.
A session is an HttpOnly cookie created on demand. Every request carrying a valid cookie refreshes the idle clock; the session idles out after one hour, and an expired session and everything it owns (drafts, submissions) is deleted.
# Create a session (keep the cookie jar)
curl -c jar.txt -X POST $BASE/session
# {"status":"active","idle_seconds":3600}
# Check it
curl -b jar.txt $BASE/session
# Validate without extending the idle clock (the frontend's inactivity
# watcher probes with this — watching must not keep an abandoned session alive)
curl -b jar.txt "$BASE/session?touch=0"GET /session returns {"status", "idle_seconds", "user"} where user is
null for a guest or {"username", "is_admin"} when signed in. A request
without a valid cookie gets 401 {"detail":"No active session"}.
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.
# Full list (single page — the editor needs the whole ordering)
curl -b jar.txt $BASE/problems
# Paginated slice for lists
curl -b jar.txt "$BASE/problems?page=2&page_size=50"Response page shape: {items, total, page, page_size, pages}; each item is
{id, slug, title, difficulty, tags, topics, type} (difficulty is one of
Easy, Medium, Hard, mirrored from the curated source). page_size=0
means all.
# One problem with statement, hints, invocation, limits, languages,
# starters, and public cases (inputs only — expected values are hidden)
curl -b jar.txt $BASE/problems/pair-sumGET /problems/topics→{"topics": [{"name", "count"}…]}— topic taxonomy with per-topic counts, busiest first.GET /problems/{slug}/figures/{figure}— an SVG from the bundle'sfigures/.GET /problems/{slug}/solutions— the solutions tab:titles, per-variantimplementationsby language, thecanonicalcode, the worst-to-bestorder, and which variant is thereference. 404 when the bundle publishes none.
invocation describes the judge contract: parameter names/types (the full
kind vocabulary — 25 kinds including nary_tree, quad_tree, nested,
graph, doubly_list, and json — is the table in
CODECS.md), the return type, and comparison — exact,
sorted, multiset, set, or close (floats compared per-scalar within
1e-9 relative tolerance; {"mode":"close","tolerance":…} customizes it in
the problem source). For type: "design" problems, cases carry
LeetCode-style actions/params sequences instead of a positional
argument list.
curl -b jar.txt $BASE/drafts/pair-sum
# [{"language":"python3","code":"…","updated_at":1786790973.5}]
curl -b jar.txt -X PUT $BASE/drafts/pair-sum/python3 \
-H 'content-type: application/json' -d '{"code":"class Solution:\n …"}'A draft is at most 256 KiB.
Shared implementation with the CLI (same runner/formatters.py, same
pinned toolchain). Always 200 when the runner is reachable — the payload
carries the state:
curl -b jar.txt -X POST $BASE/format \
-H 'content-type: application/json' \
-d '{"language":"python3","code":"…"}'| 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.
curl -b jar.txt -X POST $BASE/run \
-H 'content-type: application/json' \
-d '{"slug":"pair-sum","language":"python3","code":"…"}'Omit cases to run the problem's public cases (at most 20). Pass cases
(a list of {parameter: value} objects) to run custom inputs; custom cases
execute without an assertion and return the actual output. Response:
{
"status": "accepted",
"passed": 3, "total": 3, "runtime_ms": 128,
"results": [
{"name":"Case 1","status":"accepted","input":{…},"actual":[0,1]}
]
}curl -b jar.txt -X POST $BASE/submit \
-H 'content-type: application/json' \
-d '{"slug":"pair-sum","language":"python3","code":"…"}'A job judges at most CODERPUZZLE_MAX_JUDGED_CASES cases (default 200) —
every case runs in its own sandboxed process, so a corpus generated into the
tens of thousands of cases would otherwise take hours. The subset is chosen
deterministically from the bundle's own case list: all public examples, the
largest inputs, then an even sample of the rest. passed and total
describe the cases that ran.
Judges the hidden cases the job selects (bounded as above). Verdict
statuses: accepted, wrong_answer,
compile_error, runtime_error, time_limit_exceeded,
memory_limit_exceeded, system_error. The response adds submission_id
and reference_runtime_ms (the bundle's designated reference solution on
the same runner; the ratio is a hardware-independent speed signal).
# Viewer's submission history for a problem (guest submissions are
# session-scoped and purged with the session; signed-in submissions are
# user-scoped and survive idle expiry)
curl -b jar.txt "$BASE/submissions?slug=pair-sum&limit=30"
curl -b jar.txt $BASE/submissions/42GET /progress → {slug: "solved" | "attempted"} for the current viewer
(signed-in user or guest); solved when any submission in any language was
accepted, and absent slugs were never tried.
401 no/expired session · 400 unavailable language, malformed input,
unknown auth provider, or oversized draft · 403 registration closed
(default after the admin bootstrap; CODERPUZZLE_AUTH_REGISTRATION=open
reopens it) · 404 unknown problem/submission · 429 login or judge
throttle · 503 judge runner unavailable or busy. Error bodies are
{"detail": "…"}.
Per-problem limits (time_ms, memory_mb, output_kb) ship in the
problem payload and are enforced inside the isolated runner; the API returns
verdict statuses rather than killing the HTTP request. Bodies over 256 KiB
are rejected at the web proxy.
Run/submit responses include timing_mode (wall or cpu) and
resource_profile. runtime_ms is the sum of testcase wall times in the
shared profile and CPU times in the isolated profile. Separate wall_time_ms
and (when available) cpu_time_ms aggregates exclude queue_ms and
compile_ms. The latter includes language preparation, not just native
compilation. These fields also apply to failures when measurements exist.
Public testcase results may include cpu_time_ms, wall_time_ms,
memory_peak_bytes, cpu_throttled_ms, cpu_limit_ms, wall_limit_ms,
limit_mode, and timeout_reason (cpu or wall). timeout_ms follows
limit_mode, which may be wall time for threaded problems even when runtime
is displayed as CPU time. Hidden per-case measurements remain private.
Stored submissions and history include timing mode/profile; migrated records
are wall / shared-wall-v1. Reference ratios are omitted across different
profiles or timing modes. Clients must not combine historical wall timing
and isolated CPU timing into a single benchmark series.
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/.--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 this repo'sscripts/gen_starters.py. Requires this 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).coderpuzzle run <file>— 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
# 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/<shard>/<key>/my_draft.pyThe convenience wrapper scripts/format.sh does the mounting for the
format case; CODERPUZZLE_IMAGE overrides the tag.