Thanks for helping improve sandbox provider comparisons. Read the methodology for how a measurement is produced before extending the matrix.
This repo is a Bun workspace monorepo with a strict, enforced dependency DAG (see architecture) and a source-first, no-build layout.
- Fork, branch, and open a PR against
main. - Hosted CI (
ci.yml/ci-lint.yml) runs the command contract on your PR — no provider secrets. - Self-hosted Docker toolchain smoke and live provider benches do not run on fork PRs (by design: untrusted code must not execute on org runners or spend provider quota).
- Live benches, dataset publish, and GHCR toolchain releases are maintainer-only:
workflow_dispatchonmainbehind Environmentprivileged. See CI & secrets.
For local benches, copy .env.example to a gitignored .env. Never commit API
keys or paste them into issues/PRs — the repo-checks secret-hygiene gate fails CI if a credential
file or secret token is tracked (SECURITY.md).
The browser-free checks below are the shared local/CI baseline. CI runs the figures screenshot suite in a separate job that provisions pinned headless Chrome.
bun install # resolve the graph (frozen lockfile in CI)
bun run typecheck # tsc --noEmit per member
bun run test # browser-free bun test per member, incl. repo-checks invariants
bun run lint # biome check; warnings fail
bun run spell # typos (via mise)The Chrome-backed figures screenshot suite is intentionally separate from the normal local gate:
CI's figures job provisions its pinned headless Chrome on a hosted runner and runs
bun run test:figures explicitly.
PTS-catalog changes also have a drift gate:
bun run --filter @sandbox-benchmarks/schema generate-catalog # regenerate from vendored profiles
bun run check:catalog-drift # fail if the committed draft drifted
bun run check:provider-registry-drift # fail if provider metadata assembly drifted
bun run check:provider-wiring # fail if generated CI/docs/env wiring drifted- Identity + metadata — append the id to
PROVIDER_IDSinpackages/schema/src/provider-ids.ts, then add the one hand-authoredpackages/schema/src/provider-meta/<id>.tsmodule. Declare display/vendor identity, inputs, artifact lifecycle, isolation, vetted pricing, maturity, spec pinning, and transport there. Runbun run generate-provider-registryandbun run generate-provider-wiring, then review the generated correlated index plus managed workflow/docs/env regions. Filename, tuple key, and declared id disagreement is a compile error; malformed descriptor semantics fail the generator's Tier-3 arktype gate. Keep the independent hardcoded provider oracle inproviders.test.tscurrent. - Adapter — add a matching entry to the adapter map in
packages/providers: how tocreateCompute()and the create-timecreateOptions(the pinned target spec + toolchain image). The two registries are joined by id, so a one-sided provider is a compile error. - Artifact implementation — only when the descriptor's
artifact.kindrequires one, add the provider-specific bake/template implementation. Providers using a stock or shared image do not get no-op bakers. - Generated wiring — do not hand-edit provider choice/input regions. Provider metadata generates
the smoke dispatch options, three least-privilege workflow input blocks, runner routing,
.env.example, CI configuration docs, and the privileged-environment checklist. The drift gate rejects stale or hand-edited output. - Bring the provider up with a single-provider branch dispatch. Adding it to the default benchmark matrix remains a separate promotion decision after live validation.
- Register it in
SUITES: thedimensionsit measures, the cataloguedmetricsit emits, itscommands(mise tasks), and the timeouts. The suite↔dimension↔metric contract fails at load if a metric is uncatalogued or off-dimension, or a declared dimension has no metric. - Producer tasks — add the mise task(s) under
.mise/tasks/benchmark/**that thecommandsname, driving the benchmark via the helpers inlib/bench.sh(e.g.run_pts_benchmark). An orchestrator is a task file; its leaves live in a sibling directory (a task path can't be both) — unless the group's own task is spelled_default. mise loads.mise/tasks/a/b/_defaultas the taska:b, so a task can gain sibling leaves without being renamed (this is howbenchmark:system:providergrew:isolationand:egress). Two things follow from choosing it:task_result_namestrips the_defaultsegment so the artifact keeps its name — the normalizer matches those files by exact name — and a leaf that is a view rather than a producer must write no result at all, so it can never race the group task for that artifact. - No matrix job edit —
bench-matrix.ymlmatrices overplan.outputs.suites(fromSUITE_NAMESviaplan-suites), so a new suite is picked up automatically and nests as<suite> / <provider>in the Actions UI; a dispatch can still narrow to a subset with thesuitesinput. The workflow-registry-sync drift gate keeps that nesting wiring honest.bench-smoke.ymlruns the same plan →bench-suite.ymlpipeline, so it needs no edit either — except adding the name to itssuitedispatchoptions, which is what makes the suite selectable for a single-cell smoke.
PTS-backed metric (preferred — generated, not hand-written):
- Add the profile's exact
<name>-<ver>dir toPROFILESinfetch-profiles.tsand runbun run --filter @sandbox-benchmarks/schema fetch-profilesto vendor itstest-definition.xml/results-definition.xml. - Run
generate-catalogto regeneratepts-generated.ts. A single-result profile yields one description-less wildcard entry (no byte-match risk). A multi-result profile yields one entry per option combination — its synthesizedpts.descriptionmust byte-match real PTS output, so commit a recordedcomposite.xmlfixture underpackages/results/src/lib/__fixtures__/(the golden gate proves it). - Curate editorial fields in
pts-overrides.ts: a shortlabel, anydimensioncorrection, and the curatedheadline: truemetrics (one per dimension, except network's two WAN directions — ADR-0015). - Commit the regenerated
pts-generated.ts(the drift gate diffs it; overrides are excluded).
Non-PTS metric (harness-measured or derived): add the MetricDef to the relevant hand-authored
slice (harness-metrics.ts for timings, economics.ts for derived) and wire its producer — the
lifecycle driver for a timing, deriveEconomics for a derived metric. These carry no pts field and
don't trip the drift gate.
- Parse, don't validate: arktype schemas at every boundary; the TypeScript types are inferred from the runtime schema, never hand-written twice.
- Cross-registry invariants (id-uniqueness, the per-dimension headline count, the suite contract) are plain throws at module load over typed in-repo constants — fail fast at import.
- Keep packages within the dependency DAG;
@repo/repo-checksfails CI on a boundary violation.