TouchStone documentation
Regression tests for RL environments. You review the tests, TouchStone runs them on your machine or CI runner, and only approved tests can fail a build.
How it fits together
- Suite
- A folder with
suite.jsonand youradapter.py. - Adapter
- Python you own that drives your environment and returns what it observed.
- Case
- One test: an input, the requirement it checks, and assertions on the observations.
- Approval
- A reviewer's sign-off on the exact content of a case. Any edit clears it.
- Run
- One execution, with every observation and assertion result kept as evidence.
Quickstart
Needs Linux (or WSL) and Python 3.12+. No GPU, container engine or API key.
-
Install
uv tool install touchstone-us -
Start the workspace
touchstone serveOpens on loopback only, at port 4474. Change it with
--port. -
Open the example
Go to 127.0.0.1:4474/tests and choose Create the example suite.
Where files go, upgrading and source checkouts
- Suites, reviews and history live under
.local/in the folder you run from. Add it to.gitignore. - To open your own suite, set
TOUCHSTONE_SAFETY_SUITE=path/to/suitebeforetouchstone serve. touchstone doctorchecks whether this host can run the container-based tools. The test workflow does not need them.- After
uv tool upgrade touchstone-us, restart the server; a running process keeps the old version. - From a source checkout, run
uv sync --frozenandnpm start, and prefix commands withuv run. - The status page at
/statusshows whether the latest approved run passed, what failed, what changed, and run history by environment version, with the commands to reproduce each failure.
Starter walkthrough
The example checkout evaluator has a planted bug: it pays reward for a success marker even when nothing was bought. You catch it, fix it and keep the test.
Create the suite
Writes
suite.jsonandadapter.py. Nothing runs yet.Add starter tests
forged-successexpects reward 0.valid-checkoutexpects reward 1.Review and approve both
Your approval is tied to the exact content you read.
Run approved tests
forged-successfails with reward 1 and no purchase. The control passes, so the failure is real.Fix the evaluator
In
adapter.py, setREQUIRE_PURCHASE = True. No restart needed.Run again and compare
Both pass. Compared with the failing run, the forged case shows Fixed.
Connect your environment
Pick how your grader runs. init writes the adapter for the first three.
touchstone init ./touchstone-suite \
--requirement "Reward requires a paid backend order" \
--http http://grader.internal:8080/evaluate
Each case input is POSTed as JSON. The JSON reply is the observation.
touchstone init ./touchstone-suite \
--requirement "Reward requires a paid backend order" \
--command "python grade.py"
The input arrives as JSON on stdin. You can assert on exit_code, stdout, stderr and output (stdout parsed as JSON).
touchstone init ./touchstone-suite --from-harbor ./tasks/my-task
touchstone check ./touchstone-suite --experimental --timeout 300
Drafts five cases for a Harbor task: the reference solution earns 1, an empty submission earns 0, a forged reward.txt earns 0, and the agent phase cannot read /tests or /solution. Needs Podman or Docker.
def setup(case):
return your_environment.start()
def exercise(context, inputs):
context.perform(inputs)
def collect(context):
# Independent evidence, not the agent's claim of success.
return {"reward": context.grade(), "purchased": context.admin_order_exists()}
def cleanup(context):
if context is not None:
context.close()
Write adapter.py yourself. TouchStone calls these four functions in order, in a fresh subprocess per case.
Timeouts, interpreters and other adapter rules
- Each case times out after 30 seconds by default. Exceptions, timeouts, bad output and cleanup errors are inconclusive, never a pass.
- The subprocess is not a sandbox for hostile code, and forced termination can't guarantee cleanup. Use disposable resources.
- The adapter runs from a frozen copy. Install its dependencies in the runner and reference files by package or explicit path.
- Set
"python"insuite.json(for example"../.venv/bin/python") to run under your project's interpreter. A missing one exits 4. - Adapter changes are recorded by hash, so a fix is checked against the same approved test. Dependencies and services aren't frozen.
- Without a target flag,
initwrites a skeleton whose cases stay inconclusive until you fill it in.
Write tests
Cases are JSON assertions on what your adapter returns.
{
"cases": [{
"id": "forged-checkout",
"title": "A confirmation without an order earns no reward",
"requirement_id": "verified-purchase",
"input": {"action": "forge-marker"},
"assertions": [
{"path": "purchased", "operator": "eq", "expected": false},
{"path": "reward", "operator": "eq", "expected": 0}
],
"provenance": {"kind": "imported", "source": "Owner-authored regression"}
}]
}
touchstone generate DIR --from-json candidates.jsonOperators: eq ne lte gte contains not_contains. Paths use dots for nested fields. Missing fields and wrong types never pass, and there is no eval.
Reusing an ID edits that case and clears its approval.
Propose tests with a model
Nothing calls a hosted model by default. Use your own signed-in Codex CLI or any trusted command:
touchstone generate .local/my-suite --codex
touchstone generate .local/my-suite --command python my_author.py
Or run touchstone suggest DIR to rank twenty reward-hacking patterns against your requirements, with no model at all. Twelve come from published taxonomies and eight from game and robotics field reports.
What the model sees
The suite, requirements, existing cases and adapter source. Never run history or network captures. Codex runs read-only in a temporary directory for up to five minutes. A custom command gets JSON on stdin and must return a cases object within 120 seconds and 1 MiB.
Gate CI
Run touchstone check and branch on the exit code.
jobs:
regression:
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
- run: uv tool install touchstone-us
- run: touchstone check touchstone-suite --evidence-dir /var/lib/touchstone/evidence
| 0 | Every approved assertion passed. |
|---|---|
| 2 | An approved assertion failed. |
| 3 | Inconclusive: no approved cases, a timeout, a crash or bad adapter output. |
| 4 | Bad configuration, the suite is locked by another run, or storage failed. |
| 130 | Interrupted. Partial evidence is kept as inconclusive. |
What to commit
Commit suite.json, adapter.py, approvals.json and .touchstone-suite.lock. Keep .touchstone/ out of Git.
approvals.json holds each reviewer's decision and the hash it was made on, with no test content. A fresh checkout gates on the same approved cases; an approval whose hash no longer matches is ignored. When the file and local history disagree, the newer decision wins.
If you mount a suite read-only, create the lock file in the writable source first and keep it. --evidence-dir on persistent storage keeps history across jobs. An optional runner image is described in RUNNER.md.
Share with a team
Optional and always explicit. The control plane is a separate app your organization runs; no hosted instance is open to customers yet.
touchstone sync .local/my-suite <run-id> \
--url http://127.0.0.1:4480 --suite checkout
| Default | IDs, timestamps, hashes and statuses only. |
|---|---|
--include-evidence | Also requirements, inputs, adapter source, observations and logs. An owner must enable it first. |
--pull-reviews | Applies team decisions locally where hashes still match. |
Tokens, metrics and retention
Sign in with GitHub, create an organization API token and set TOUCHSTONE_API_TOKEN. Tokens can upload but never approve; owners can revoke them. Uploaded evidence is customer-reported, not server attestation.
The shared workspace shows runs, approved and failing tests, verified fixes and how long each failure stayed open (elapsed time from run timestamps, not effort), with CSV export. touchstone train ./suite -- python train.py adds GPU-hours spent training on versions that fail an approved test. Owners can set a retention window.
Commands
Every browser action has a CLI equivalent, and output is JSON.
init DIR | Create a suite. Add --requirement with --http, --command or --from-harbor for your own environment. |
|---|---|
generate DIR | Add starter candidates, or import with --from-json, --codex or --command. |
suggest DIR | Draft cases from twenty reward-hacking patterns. Imports nothing; generate --from-json accepts the output once each kept draft has an input. |
review DIR CASE | Approve or reject at the hash you read. In scripts, pass --sha256 from report. |
check DIR | Run approved cases and exit with a CI code. --timeout sets seconds per case, up to 300. |
report DIR [RUN] | Print the suite with hashes, or one run's evidence. --csv for spreadsheets. |
sync DIR RUN | Upload a run to your control plane. |
All commands accept --evidence-dir PATH; use the same path for review, check and report. From Python, use SafetyStore in touchstone.safety.store and check in touchstone.safety.engine.
Other tools
Two earlier workflows ship in the same server. Both need rootless Podman.
What a pass means
Reviews are self-reported local decisions, not authenticated release approvals. Treat inconclusive runs as unfinished work.
What it does not detect
Tests check what the grader returns for a known input. These failures happen while the grader behaves exactly as approved.
Objective-level hacking
Spurious signals from token-level credit misalignment, not an exploitable verifier (Dong et al., arXiv 2602.01103). Watch: training-inference discrepancy and loss dynamics in your trainer's logs.
Training-inference numerical mismatch
Small numerical disagreements between rollout engine and trainer can cause collapse on their own (Zhong et al., arXiv 2605.14220). Watch: generation-time log probabilities against the trainer's recomputed values.
Capability regressions under a sound verifier
The policy can get worse while every test passes, because tests exercise the grader. Watch: held-out evaluations and reward curves per environment version.
Verifier-aware attacks
Cases assume the verifier is hidden. If the agent can read it, it can target it. Watch: that the verifier stays outside the agent's filesystem and network on every version.
Anything without a deterministic oracle
Rubric or model-graded quality and open-ended tasks have no expected value to approve. Watch: your judge calibration and human spot checks.
Usage reports
Each finished check, grader audit or stack comparison sends one small record to the TouchStone control plane. A failed report never affects the run.
Sent
Product, outcome, test counts, duration, package and Python version, OS, architecture, whether it ran in CI, a random install ID, and the run's IDs and hashes.
Never sent
Test content, inputs, observations, adapter source, paths or error text.
export TOUCHSTONE_TELEMETRY=offDO_NOT_TRACK=1, TOUCHSTONE_LOCAL_ONLY=1 or check --local-only also send nothing. TOUCHSTONE_TELEMETRY=log prints each record instead.
How reports are attributed
Connected safety suites report to their saved control plane with the saved organization token, which takes precedence over telemetry settings. Other runs send TOUCHSTONE_API_TOKEN only to TOUCHSTONE_TELEMETRY_URL when you set it (the server that issued the token), attributing reports to your organization and the member who created the token; redirects are never followed. Otherwise the default host gets a one-way fingerprint of the token, which groups your runs but can't be used as the token. Without a token a report is anonymous, unless your organization uploads the same run's summary, which links the two.
Licensing
TouchStone is proprietary. You may run it on infrastructure you control for a thirty-day evaluation; continued use needs a written agreement such as a pilot or subscription. No redistribution, modification or use in a competing product. The full text ships as LICENSE. The control plane is licensed separately under the Functional Source License.