Thanks for your interest in stunt! The fastest way to contribute is an
adapter — a local simulator for some public API. Adapters are just a
directory of YAML + Starlark + fixtures; no Go knowledge is required to write
one.
This guide covers the contribution workflow. For the adapter authoring
reference (the adapter.yaml schema, Starlark builtins, rules engine,
gRPC/WebSocket), see adapters/README.md.
stunt adapter new myapi-style # scaffold an adapter
cd myapi-style
# edit adapter.yaml, scripts/*.star, fixtures/*.jsonl
stunt adapter lint ./myapi-style # MUST pass: synthetic-data guard
stunt adapter test ./myapi-style # replay traces / conformance (optional)
# add a DISCLAIMER (required for branded APIs — see below)Then open a PR adding the directory under adapters/ and (optionally) an entry
in the catalog.
- An adapter for an API you test against (highest value).
- A bug fix or feature for the Go core (
internal/...). - Docs, examples, and test coverage.
- Reports — open an issue for bugs, missing API coverage, or rough edges.
Be kind and constructive. Assume good faith. "Unofficial, not affiliated" is the project's posture toward every provider whose API style an adapter mimics — please extend the same respect to people.
cmd/stunt/ # the CLI binary
internal/
adapter/ # adapter format + loader
rules/ # declarative rules engine (match/evaluate/faker/template)
starlark/ # sandboxed Starlark VM
engine/ # wires it all together; HTTP + gRPC + WebSocket servers
primitives/ # Collection / KV / Blob / Clock / Identity / Events / ...
netutil/ # portless.dev-style TLS proxy + CA + /etc/hosts
contrib/ # contributor tooling: new / import / lint / test / catalog
adapterdist/ # git adapter distribution
grpcsim/ # descriptor-driven gRPC server
manifest/ # stunt.yaml schema
adapters/ # first-party reference adapters (contributions land here)
docs/ # specs + plans
You need Go 1.27.1+. The repo pins it in .mise.toml, and CI pins the
same version in .github/workflows/*.yml — keep the two in step, or just ci
stops being the gate that ships.
git clone https://github.com/StuntAPI/stunt
cd stunt
go build ./... # builds everything
go test ./... # runs the suiteDev-machine note: if
gocommands fail with a stdlib-version mismatch, prefix them withenv -u GOROOT. (A localGOROOT/toolchain misconfiguration on some setups; not required in CI or for end users.)
-
Scaffold it:
stunt adapter new myapi-style
This creates
adapter.yaml, an exampleGET /helloendpoint, a sample template, a seed fixture, a Starlark handler, a JSON schema, and a README — all synthetic. -
Author it. See
adapters/README.mdfor the full reference: theadapter.yamlschema, the rules engine (templated responses, probabilistic faults, conditional expressions), the Starlark builtins (store_collection,store_kv_*,store_blob,identity_*,events_*), and the gRPC + WebSocket transports. -
Import from a spec (optional): jump-start an adapter from an existing API description:
stunt adapter import openapi path/to/openapi.json --dir ./myapi-style stunt adapter import har path/to traces.har --dir ./myapi-style stunt adapter import proto path/to api.proto --dir ./myapi-style
-
Test it locally:
# point a manifest at it and serve echo 'version: 1 services: myapi: adapter: ./adapters/myapi-style' > stunt.yaml stunt up curl http://127.0.0.1:8000/hello
Adapters that mimic a specific company's API must follow the neutral-naming
convention (see adapters/README.md
for the full rules):
-stylenaming for the id, directory, and description — e.g.stripe-style, neverstripe. Never claim to be the provider.- A
DISCLAIMERfile in the adapter dir (copyadapters/DISCLAIMER.template) stating it is not affiliated/endorsed, is for local testing only, and uses synthetic data only. - Synthetic data only — all fixtures/templates are faker-generated.
stunt adapter lintmust pass clean. This is the core safety property. - Structure, not documentation — reproduce the API shape needed for local testing; do not reproduce the provider's proprietary docs verbatim.
- No provider logos/trademarks.
Generic, non-branded adapters (e.g. echo-style) are exempt from -style
naming but still must pass stunt adapter lint.
stunt adapter lint ./adapters/<your-adapter> # no findingslint enforces the synthetic-data invariant: it scans fixtures, templates,
and handler scripts for real-looking data (emails, UUIDs, tokens, credit-card
numbers, PII field names). All adapter data must be synthetic. If you see a
finding, replace the value with a faker or a clearly-fake placeholder.
If you touch internal/... or cmd/..., the canonical gate is one command:
just ciThat runs build + test -race + vet + fmt-check + mod-tidy +
lint-adapters — the exact set a PR must pass. (A hosted CI job invokes the
same just ci, so there's one source of truth for "does this ship".) Granular
recipes exist too: just test, just vet, just fmt, just fmt-check,
just lint-adapters, just smoke. The equivalent raw commands are
go test -race ./..., go vet ./..., and gofmt -l . (must be empty).
All gates must pass. -race is non-negotiable for new concurrent code. Tests
are host-safe by convention: they use temp dirs, free high ports,
file:// git fixtures, and httptest sinks — they never touch ~/.stunt,
/etc/hosts, the system trust store, or the real network.
- Prefer table-driven tests.
- Integration tests build a temp adapter + start the engine via
ServeForTest(free ports) and use real clients (HTTP / gRPC / WebSocket) — mirror the existing*_test.goininternal/engine/. - Add a test for every new behavior; fix bugs with a failing test first (TDD).
CONFORMANCE.md and conformance/matrix.json are generated. Never edit
them by hand — run just conformance-matrix and commit the result. CI
regenerates and fails on drift.
just conformance-matrix needs conformance/node/node_modules to exist, so run
bun install --frozen-lockfile in conformance/node first. just ci-full does
both in order.
Run the Node suites through just conformance-node. It builds a fresh binary and
sets STUNT_BIN; the harness refuses to fall back to stunt on PATH, because
a stale install would silently become what the matrix attests to. If you see
STUNT_BIN is not set, you are running bun test directly — use the recipe.
What the matrix claims, and what it does not:
- A behavior is listed only if a section marker (
// ===== name =====) or aRecord(...)call exists. The matrix is derived from source text, not from test results. A skipped or assertion-free test still publishes its name. That gap is known, not closed. verification: VMmeans the adapter has published engine-level behavior names. It is not derived from a suite file merely existing.missing:anddeviations:bullets inconformance/matrix.yamlneed no justification. They are a coverage record, not a verification claim, and the ratchet below never guarded them.
SDK versions are load-bearing — they are published as the version each adapter
was verified against. Dependabot opens a weekly PR per ecosystem; a second
workflow regenerates the artifacts the bump invalidates; auto-merge lands it once
just ci (ubuntu-latest) is green. main is protected, so a bump cannot skip
the gate.
If you bump one by hand, regenerate in the same commit or the freshness gate will fail.
The regeneration workflow commits with GITHUB_TOKEN, and GitHub deliberately
starts no workflow from such a push — so it also approves the pull_request run
that the regenerated commit parks. Without that approval a bump regenerates its
artifacts and then sits red forever. Do not replace it with
gh workflow run ci.yml: a dispatched run reports the same check name, and
branch protection matches by name and takes the latest, so a green dispatch loses
to the stale cancelled one and the PR stays blocked with a passing gate.
Recorded so the absence is legible rather than looking like an oversight:
-
No .NET SDK conformance tier. Deferred, not abandoned. Designed and reviewed over two rounds by four seats (harness, conformance-integrity, security, adversarial), then closed at 5/6/4/6 — below the bar — after a stop rule fired on two counts: the open in-scope HIGH count did not fall between rounds (18 → ~17), and the same failure classes recurred in both.
The dominant finding, reached independently by three seats: the design derived its evidence from source text instead of attesting it at runtime. Most other clusters followed from that — tier conflation, junk behaviour names, and a harness that could boot the wrong adapter and still report green. Two blockers were independent of it: .NET SDKs auto-discover ambient credentials by default, so a harness would sign and send to real cloud endpoints silently, with no log line; and CPM pins versions rather than hashes with no lockfile, on a repo whose verified SDK surface is Go and Node. The review also conceded five errors of its own, among them a cited commit that does not exist and a causal inversion in its own motivating example.
The gap is real, not cosmetic. A read-validation defect surfaced through a .NET client while the Go SDK suite was green; a monolingual matrix cannot see that class. Today 36 of 99 adapters carry SDK-tier evidence, and the sources behind those rows are Go modules, Node packages, and vendored official API specs — 25 distinct client entries plus 19 vendored specs. Every one is Go, Node, or a published schema. There is no .NET project in the tree and no .NET client anywhere in the matrix; the only "Microsoft" in it is
graph.microsoft.cominside a deviation note.Two things must be settled before resuming. The first is a prerequisite the review named, needing no .NET suite at all: a provenance/attestation gate for the existing Go and Node rows, so the matrix's evidence is attested rather than derived. The second is an unverified premise — that roughly 40–46 of the 99 adapters have a first-party .NET client. That count was never produced. An earlier draft's "roughly 60" conflated "has no SDK row today" (63) with "no .NET client exists". If the real number is small, the sweep's premise does not hold and the question changes shape.
-
No GCS or Azure Cosmos adapters. Vyral's
Google.Cloud.Storage.V1andAzure.Cosmosdependencies have no simulator here. Straightforward work, not blocked. -
No coverage ratchet. The three matrix-integrity bugs it was meant to catch are fixed directly (VM tier derivation, the Discord assertion, the route-count floors), so a gate is not currently load-bearing.
- Fork & branch from
master. - Make your change; keep commits focused (one logical change per commit).
Conventional-ish commit subjects are appreciated (
feat:,fix:,test:,docs:,chore:) but not strictly enforced. - Run the gates above.
- If adding an adapter, include:
- the adapter directory under
adapters/(withDISCLAIMERif branded), - a one-line entry in the
adapters/README.mdtable, - confirmation that
stunt adapter lintis clean.
- the adapter directory under
- Open the PR with a clear description of what and why. Link any related issue.
- Adapters: lint clean; synthetic data; branded naming + disclaimer; useful coverage of the API's common paths.
- Core: tests added;
-race/vet/gofmtclean; host-safe tests; matches existing style; security-conscious (adapters run sandboxed Starlark — the sandbox is the trust boundary, so never add host I/O to the VM).
The defining property of stunt is that community adapters are safe to
install: adapter logic runs inside a sandboxed Starlark VM with no host
I/O (no filesystem, no network, no subprocess), bounded by an execution-step
limit. State is mediated through primitives (Collection/KV/Blob/Identity/
Events), not raw host access. When contributing core changes, preserve this
boundary — never expose host I/O to adapter scripts.
Adapters must contain only synthetic data (enforced by stunt adapter lint)
so installing one never ships real PII or provider secrets.
By contributing you agree your contributions are licensed under the project's license (see the repository root). Ensure you have the right to contribute any code or fixtures you submit, and that adapters contain no proprietary provider data or documentation.