PlatformTesting & Adversarial Analysis
Chio Arena
The arena runs ChioKernel through TOML scenarios and evolves adversary populations against the guard evaluator under a bounded budget.
Replay inputs and boundary
rng_seed, virtual_clock_start, and step order, so the listed outputs can be reproduced. Kani and LeanKani and Lean cover different decision-core properties; the arena executes the assembled kernel and generates adversarial scenarios.Two Halves
The crate has two functions that use the scenario witness.
- Deterministic scenario replay.
load_scenarioparses and fail-closed-validates thechio.arena.scenario/v1DSL into aScenario;DeterministicSchedulerorders the steps;ArenaRuntimedispatches each step to aChioKerneland collects signed receipts into anArenaRun. - Adversarial co-evolution. The
coevolvemodule lifts fixed adversary populations into a genetic-algorithm-shaped loop: mutation, two-parent crossover, elitism, and fitness-proportional selection scored against a fail-closed guard evaluator, all under a bounded budget gate.
Both halves are pure functions of the scenario witness. The same witness (rng_seed, virtual_clock_start, scheduler, locale, the agent list, and the step list) implies a byte-identical schedule, RNG snapshot, clock trace, and verdict trace across runs.
The Scenario DSL
A scenario is a single TOML file. The schema id is pinned at chio.arena.scenario/v1. The walking-skeleton reference scenario is the smallest complete example:
schema_version = "chio.arena.scenario/v1"
id = "walking_skeleton"
title = "Single-agent walking skeleton"
rng_seed = 42
virtual_clock_start = "2026-04-30T00:00:00.000Z"
[determinism]
rng_seed = 42
virtual_clock_start = "2026-04-30T00:00:00.000Z"
scheduler = "single-agent-v1"
locale = "C"
[[agents]]
id = "agent-a"
role = "operator"
model = "recorded:test-agent"
seed_prompt_ref = "prompts/walking-skeleton.txt"
[[budgets]]
agent = "agent-a"
server = "filesystem"
tool = "read_file"
max_invocations = 1
[[steps]]
id = "step-1"
agent = "agent-a"
server = "filesystem"
tool = "read_file"
arguments = { path = "/tmp/chio-arena.txt" }
expect_verdict = "allow"| Field | Type | Meaning |
|---|---|---|
schema_version | string | Must equal chio.arena.scenario/v1 exactly. |
id | string | Stable scenario id. ASCII letters, digits, _, ., -. |
rng_seed | u64 | Root seed for the deterministic RNG. Mirrored under [determinism] and validated to match. |
virtual_clock_start | RFC-3339 | Fixed UTC start instant. The virtual clock advances from here in fixed ticks. |
[determinism] | table | scheduler (single-agent-v1 or deterministic-multi-v1) and locale (must be C). |
[[agents]] | array | Offline actors: id, role, model (a string handle, not a provider SDK), seed_prompt_ref. |
[[steps]] | array | id, agent, server, tool, arguments, expect_verdict (allow/deny/rewrite). |
[[budgets]], [[guards]], [[adversaries]] | array | Optional. Budgets and guards are parsed and validated but not yet consumed by the runtime; adversaries drive the co-evolution loop. |
[ext] | table | Reserved for forward-compatible metadata. The only table where unknown keys are tolerated. |
validate_scenario fails closed before any kernel call is made. It rejects an unsupported schema version, scheduler, or locale; a witness copy that disagrees with a top-level field; duplicate agent, step, or guard ids; a step or budget that references an undeclared agent; a zero-invocation budget; any inline secret marker (bearer tokens, private-key blobs, api_key, sk-, and the rest of the marker list) anywhere in an agent, step, guard, or adversary field; and any provider-SDK dependency marker in an agent model string.
Replay Controls
Three types carry the reproducibility contract. Each is a pure function of the scenario witness.
VirtualClock· the arena's sole time authority. It parses the RFC-3339 start instant strictly and advances by a fixed tick (DEFAULT_TICK_NANOSis one millisecond) once per scheduled step. No arena code on the determinism path reads the system clock; the determinism gate grepsruntime.rsandclock.rsfor wall-clock readers and fails the build if it finds one.ArenaRng· a seededChaCha20Rngroot plus one sub-stream per agent, each derived by folding the agent id into the root seed with FNV-1a. ChaCha20 is platform-independent and free of hash randomization, so the byte sequence is stable across machines.DeterministicScheduler· totally orders the steps by(virtual_time, agent_id, intra_agent_step). It consumes no randomness; the RNG stays reserved for the adversary populations.
Scenario::determinism_witness projects the fields that must be stable into a DeterminismWitness (scenario id, schema version, seed, clock start, scheduler, locale, agent ids, step ids). That witness is what the bundle manifest records and what the determinism gate compares byte-for-byte.
Running a Scenario
chio arena run loads a scenario, prepares the bundle directory under target/arena/<scenario-id>/, and surfaces the determinism witness:
$ chio arena run arena/scenarios/walking_skeleton.toml --json
{"schema_version":"chio.arena.run/v1","scenario_id":"walking_skeleton","scenario_title":"Single-agent walking skeleton","bundle_dir":"target/arena/walking_skeleton","determinism":{"scenario_id":"walking_skeleton","schema_version":"chio.arena.scenario/v1","rng_seed":42,"virtual_clock_start":"2026-04-30T00:00:00.000Z","scheduler":"single-agent-v1","locale":"C","agents":["agent-a"],"steps":["step-1"]}}Under the hood, ArenaRuntime::run (single agent) or ArenaRuntime::run_multi_agent (routed through KernelMultiplexer / KernelLink for cross-kernel calls) dispatches each step's ToolCallRequest to the bound kernel via evaluate_tool_call. The kernel verdict is projected onto the scenario alphabet (a sanitized allow becomes Rewrite, a plain allow stays Allow, and Deny or PendingApproval both map to Deny), then compared against the step's expect_verdict. A disagreement stops the run with ArenaRuntimeError::UnexpectedVerdict.
use chio_arena::{load_scenario, write_arena_bundle, ArenaRuntime, KernelStepRequest};
let scenario = load_scenario("arena/scenarios/walking_skeleton.toml")?;
// kernel: Arc<ChioKernel>, tool servers and capability set up by the caller.
let runtime = ArenaRuntime::new(kernel);
let run = runtime
.run(&scenario, vec![KernelStepRequest { step_id: "step-1".into(), request }])
.await?;
write_arena_bundle("target/arena/walking_skeleton", &scenario, &run)?;The multi-agent runtime rejects a request count that does not equal the schedule length, a duplicate step_id, and a duplicate agent binding before it dispatches anything, so a malformed harness call points at the caller rather than corrupting the receipt trace.
Adversary Classes
An Adversary is a deterministic transformation: given a base ScenarioStep and a per-agent RNG sub-stream, it emits an AdversaryAction: a mutated step plus the verdict the guard pool is expected to return. The crate ships four canonical classes.
| Class id | Type | Attack | Canonical patterns |
|---|---|---|---|
prompt-injection | PromptInjectionAdversary | Mutates the seed prompt with an injection pattern. | ignore-previous-instructions, system-prompt-leak, tool-name-spoof, json-payload-smuggle, delimiter-confusion |
capability-overrequest | CapabilityOverrequestAdversary | Asks for a (server, tool) pair outside the issued scope. | OverrequestVariant (target_server, target_tool) |
replay-attempt | ReplayAttemptAdversary | Reuses a captured nonce or a revoked capability. | immediate-reuse, delayed-reuse, stale-nonce, concurrent-reuse |
scope-escape | ScopeEscapeAdversary | Delegates with a scope larger than the issuer's. | ScopeEscalation (broaden-tool, cross-server, wildcard-server) |
A scenario declares a population with an [[adversaries]] block; population_from_block resolves the class field and forwards the optional params table to the class constructor.
[[adversaries]]
class = "prompt-injection"
population = "default-injection"
seed_ref = "fuzz/artifacts/prompt-injection"
[adversaries.params]
patterns = ["ignore-previous-instructions", "system-prompt-leak", "tool-name-spoof"]AdversaryPopulation::new captures its class from the first member and rejects an empty population or one that mixes classes. Unit tests score actions against evaluate_against_guards, a toy fail-closed oracle that mirrors the kernel's decision tree (capability-scope check, nonce-replay check, scope-monotone delegation check) without booting a kernel. The toy oracle is a test referee only; a live arena run delegates to the real kernel and guard pipeline.
Co-Evolution
run_coevolution drives the genetic loop. Fitness is survival rate: the share of an adversary's actions the guard pool fails to deny. An action the guards correctly deny is a defeat and lowers fitness; an action that escapes (the guards allow when the adversary expected a deny) raises it. FitnessSample records the survivals, defeats, and per-action verdict tuples; FitnessReport aggregates one generation and ranks populations descending by survival rate, ties broken on population name for determinism.
Each generation runs elitism (the top elite_count populations carry over unchanged), a fitness-proportional selection wheel with a +1 bias so a zero-survival pool still reproduces, two-parent crossover on class-matched blueprints, and a single-field DSL-aware mutation. Every value is a pure function of the witness: the per-generation RNG is seeded by folding the generation index into the scenario's rng_seed with FNV-1a, so two runs of the same scenario produce a byte-identical generation trace.
CoevolutionConfig | Default | Role |
|---|---|---|
generations | 4 | Requested generation count; the budget clamps it. |
elite_count | 1 | Fittest populations carried over unmutated. |
fitness_rounds | 2 | next_action calls per population per generation. |
budget.max_generations | 200 | Generation cap. |
budget.max_wall_clock | 30 min | Wall-clock cap. CI PR lanes override to 5 minutes; nightly uses the default. |
Wall-clock only gates, never colors the trace
CoevolutionOutcome::Completed versus BudgetExceeded; it never changes the content of the generation trace, which stays byte-identical across runs of the same witness even when the budget varies.The initial population is seeded via load_seed_corpus, which reads fuzz crash artifacts under fuzz/artifacts/ plus the replay_attack and tampered_signature replay fixture families. A missing source directory is recorded in missing_sources rather than failing the load, and the corpus's SHA-256 fingerprint lands in the run record so corpus rotation surfaces instead of silently drifting the adversary set. The same fuzz artifacts feed the coverage-guided fuzz lane.
The fitness function borrows the oracle shape from the cross-provider verdict matrix at crates/tooling/chio-conformance/verdict_matrix/. Because that crate lives in its own workspace, the arena re-declares the MatrixVerdict (allow/deny/error) and VerdictTuple (verdict, reason_code, scope_set) shapes and documents the soft-coupling; a Rewrite counts as an effective allow at the matrix level, because the action was not blocked. The arena never invents a new comparator: the equality oracle stays the production referee, echoing the differential lane.
chio arena evolve exposes the loop behind the bounded-budget gate. It refuses a generation count above 200 or a wall budget above 1800 seconds:
$ chio arena evolve arena/scenarios/adversary/prompt_injection.toml \
--generations 40 --wall-seconds 300 --json
{"schema_version":"chio.arena.evolve/v1","scenario_id":"adversary_prompt_injection_reference","generations":40,"wall_seconds":300,"leaderboard_dir":"target/arena","budget_gate":"bounded:200_generations_or_30_minutes"}The Leaderboard
render_leaderboard ranks a FitnessReport into the chio.arena.leaderboard/v1 document: a canonical-JSON file and a Markdown table written side by side under the output root. The JSON is the contract the reputation layer reads against; rows are sorted descending by survival rate, ties broken on population name, and 1-indexed.
{
"schema_version": "chio.arena.leaderboard/v1",
"scenario_id": "adversary_prompt_injection_reference",
"generation": 0,
"rows": [
{
"rank": 1,
"population": "default-injection",
"class": "prompt-injection",
"survival_rate": 0.0,
"survivals": 0,
"defeats": 8,
"total_actions": 8
}
]
}A survival rate of zero is the healthy result: it means the guard pool denied every action the adversary threw at it. A population that climbs the board is a red flag the guard pipeline needs to answer.
Bundles and Promotion
write_arena_bundle converts the recorded receipts into chio-tee-frame::Frames, writes them through chio-replay-corpus::write_fixture as a chio.arena.bundle/v1 bundle, and drops an arena.json manifest carrying the determinism witness, the replay root, and one entry per step. It refuses a run whose scenario_id does not match the scenario, an empty run, and a run whose receipts reference a step id outside the scenario.
Two functions graduate the failures the arena finds into permanent corpora.
promote_to_fixtureswrites onlyDenyreceipts into the fixture corpus aschio.arena.fixture/v1descriptors, gated byArenaPromotionGate. The gate passes only whenCHIO_BLESSis exactly1,BLESS_REASONequalsarena:<scenario-id>exactly, andCIis unset or falsy. A per-run cap (defaultARENA_PROMOTE_CAP_DEFAULT, which is 5) clamps how many fixtures land in one call.promote_to_adversarial_suitewritesDenyandRewritereceipts into thechio-adversarial-suiteper-class case corpus aschio.arena.adversarial-case/v1cases. It carries noCHIO_BLESSgate of its own and falls back totarget/arena/promote-pending/when the livecrates/core/chio-adversarial-suite/casesdirectory is absent.
The Determinism Gate
The determinism test compares replay-relevant outputs from two runs:
$ cargo test -p chio-arenatests/determinism_gate.rs runs three reference scenarios ( walking_skeleton, two_agent_tool_exchange, and three_agent_triangular_delegation ) twice each and asserts byte-for-byte equality on the determinism witness, the schedule, the per-agent RNG snapshot, the clock trace, the verdict trace, and the canonical-JSON image of the manifest's deterministic subset. The one field held out is each step's signed receipt_id, whose Uuid::now_v7() key is intentionally non-replayable and is covered by the replay gate instead. CI runs the gate under LC_ALL=C and CARGO_INCREMENTAL=0 via the chio-arena-determinism.yml workflow, hardening against locale and incremental-codegen drift.
Where the Arena Sits
Proof lanes reason about specified decision logic. The arena executes the assembled kernel with generated scenarios. Its promoted failures become fixtures for later test runs; this does not prove the kernel correct outside the scenarios and runtime boundary described here.
- Formal Assurance Overview · where adversarial evaluation sits on the assurance pyramid.
- Fuzz Infrastructure · the crash artifacts the arena seeds its populations from.
- Differential Tests · the verdict-equality oracle the fitness function borrows its shape from.
- Fail-Closed Semantics · the decision tree the toy guard evaluator mirrors.
- Receipts · the signed evidence every scenario step records.