Experiment Runs¶
The experiment run system captures data from each scenario execution for reproducibility and post-run analysis.
For a portable, third-party-verifiable evidence archive built from a run's verified artifact closure, see Portable Evidence Bundle Export (aptl runs export-bundle).
Clean State Between Runs¶
Persistent containers accumulate state across runs: service databases, indexes, logs, generated in-container credentials, and files written during a prior exercise. That state contaminates the next run and undermines reliable batch execution and benchmarking.
The clean-boot lifecycle mode guarantees a fresh environment. It tears down the project-scoped deployment, removes the Compose-managed volumes, and then boots the lab again through the standard start path, so certificates, seeds, and the SOC stack come up fresh:
# Ephemeral clean boot: destroy lab state, then start fresh.
aptl lab start --clean
# Skip the confirmation prompt (for scripted batch runs).
aptl lab start --clean --yes
A clean boot removes only Docker Compose state for the configured project. It does not delete .env, the keys/ directory, .mcp.json, checked-in configuration, or archived run directories. A failed cleanup is fatal: the lab does not start, because a contaminated environment must never be reused as clean.
The same capability backs aptl lab validate-live, which clean-boots the lab before snapshotting it (pass --skip-clean-boot to validate the running lab without destroying it).
Run Directory Structure¶
Each run is stored under <project_dir>/runs/<run_id>/ with:
<run_id>/
manifest.json # Terminal attempts: the sealed RAES experiment-run/v1 record
seal.json # Atomic seal marker (digest, inventory, completeness statement)
snapshot.json # Range snapshot (software, containers, rules, networks, config hashes)
flags.json # Captured flags
provenance/
startup-provenance.json # Provisional apparatus provenance, written at lab start
run-provenance.json # Canonical ready-to-seal provenance, written at the seal boundary
scenario/
definition.yaml # Scenario YAML copy
events.jsonl # Scenario events timeline
wazuh/
alerts.jsonl # Wazuh alerts from the run window
suricata/
eve.jsonl # Suricata IDS events (if available)
soc/
thehive-cases.json # TheHive cases (if available)
misp-correlations.json # MISP events (if available)
shuffle-executions.json # Shuffle SOAR executions (if available)
containers/
<name>.log # Container logs for each aptl- container
agents/
traces.jsonl # MCP agent traces (if available)
Run Provenance (provenance/)¶
Records which apparatus the run was actually realized on, so a later reader can tell exactly which scenario, backend, participant implementation, images, detector rules, collectors, and configuration produced the results.
There are two records, written at different lifecycle boundaries:
startup-provenance.jsonis provisional. Lab start knows the effective configuration, detection content, dependency locks, and realized images, but not the admitted trial plan, the realized participants, or any execution outcome.run-provenance.jsonis the canonical ready-to-seal record, written by the collector that owns the admitted and executed run context. Publishing it without that context is refused rather than written with the experiment and participant sources reported missing.
Both are written create-once: republishing identical content is a no-op, and republishing different content for the same run fails rather than overwriting. Separate paths are what keep the provisional startup artifact from foreclosing the later canonical write. Neither record is a seal; they carry no signature or attestation claim.
| Field | Description |
|---|---|
schema_version |
Version of the record shape |
run_id |
The run this provenance describes |
seal_state |
ready-to-seal for the canonical record, provisional for the startup record |
aggregate_identity |
Canonical identity over every section |
registry_declaration_digest |
Pins which provider declarations collected this run |
sections[] |
One entry per provenance source: status, declaration digest, content identity, and a bounded payload |
limitations[] |
Every source that did not fully collect, with a stable reason code |
Each source is collected by a code-owned provider under declared byte, entry,
and time limits. A source that is unavailable, denied, unsupported, truncated,
timed out, or failed appears in limitations[] with a stable reason code. Such
a source is never silently omitted and never given a fabricated digest. A plain
aptl lab start has no admitted experiment plan and no installed participants,
so its provisional record reports those two sources unavailable rather than
leaving them out.
Content identity is framed per artifact: every detector rule, decoder,
allowlist, image, and configuration is a leaf bound to its logical role, and
family identities fold the sorted leaves. Changing one rule therefore moves
that rule's leaf and the aggregate, and nothing else. Secret sources are
excluded by allowlist before collection: .env, rendered secret-bearing
config, credentials, and private keys are never read or hashed.
Sealed Terminal-Attempt Records (manifest.json, seal.json)¶
Every terminal execution attempt produces one conformant, sealed ACES run
record (ADR-050). The execution controller (src/aptl/core/execution/) drives
each admitted trial: it runs the trial workload under the evidence coordinator,
normalizes the terminal cause, and hands the archival coordinator an immutable
terminal-attempt context exactly once. The archival coordinator consumes the
admitted plan, the observed apparatus and participant provenance, the evidence
acquisition result, the evaluator-supplied result summaries, the clock context,
and the normalized terminal cause, and it composes the public RAES
ExperimentRunModel (schema_version = experiment-run/v1). It never recomputes
metrics, reconstructs captures, or infers a run from mutable state.
Each attempt always records its own terminal attestation as lifecycle evidence, so the run record's mandatory evidence, traceability, and result fields are satisfied even when a trial ends before any authored capture starts.
manifest.jsonis the canonical serialized RAES run record for a terminal attempt. It replaces the legacyaptl.run-record/v2shape as the single permanent run model; existing archives stay readable through one version-dispatching adapter.seal.jsonis the atomic seal marker. A run is discoverable as sealed only after this marker is durably committed. Before it exists the attempt is an unsealed recovery candidate even ifmanifest.jsonis present. The marker binds the canonical run-record digest, a bounded inventory (path, media type, size, and checksum for every sealed artifact), the applicable contract versions, and an explicit completeness and limitation statement.
Each execution attempt receives a distinct attempt_id, which is also its
portable run_id and its run-directory name. A retry reuses the same admitted
plan and planned_trial_id but gets a new attempt_id; retry and repair
lineage travel as RAES run references (used_refs, derived_from_refs,
generated_refs, and, where applicable, invalidation.superseded_by). The
terminal cause maps to the record's real status and is never overwritten by the
seal: completed execution records completed with the evaluator's outcome;
scenario or evaluator failure records failed; cancellation, policy stop, or
infrastructure interruption records aborted; capture loss or another validity
failure records invalidated with an invalidation reason; and a replaced record
version records superseded.
Sealing is atomic and immutable. The record and every referenced artifact are verified and checksummed through the run store's descriptor-relative, no-follow boundary before the marker is published create-once with durable, no-replace semantics. Once sealed, every overwrite-capable writer refuses; a correction is a new run version or attestation with explicit lineage, never an in-place edit. Export packages the already-sealed bytes and never writes back into the archive.
A small, append-safe discovery index under the run store records bounded routing facts (attempt identity, run version, manifest digest, seal state, terminal timestamp, and safe relative location) using a prepared and committed journal. Discovery replays that journal and verifies each entry against the on-disk seal marker, so restart discovery works without scanning or trusting arbitrary filesystem paths. The index is derived and rebuildable from sealed archives; its loss never changes the portable record or the seal.
Range Snapshot (snapshot.json)¶
Captured at the start of each run for reproducibility. Contains:
| Field | Description |
|---|---|
timestamp |
ISO 8601 capture time |
software.python_version |
Python interpreter version |
software.docker_version |
Docker Engine version |
software.compose_version |
Docker Compose version |
software.wazuh_manager_version |
Wazuh manager version (from /var/ossec/bin/wazuh-control) |
software.wazuh_indexer_version |
OpenSearch version on the Wazuh indexer |
software.aptl_version |
APTL package version |
containers[] |
Name, image, image ID, status, health, labels for each aptl-* container |
wazuh_rules.total_rules |
Total Wazuh rules loaded |
wazuh_rules.custom_rules |
Custom rules in /var/ossec/etc/rules/ |
wazuh_rules.custom_rule_files |
List of custom rule XML filenames |
wazuh_rules.total_decoders |
Total decoders loaded |
wazuh_rules.custom_decoders |
Custom decoders count |
networks[] |
Docker network name, subnet, gateway, connected containers |
config_hashes |
SHA-256 of aptl.json and docker-compose.yml. .env is deliberately excluded: it holds control-plane secrets whose values have a small guessable domain, so a digest of it would be a confirmation oracle rather than an opaque identity. |
Snapshots and trace exports must not contain live credentials, API keys, bearer tokens, cookies, JWTs, private key material, or default lab passwords. Redact at the common serialization/tracing boundary before writing JSON or OTel span attributes; do not rely on archive location, file permissions, or export controls as the primary protection.
CLI Commands¶
# List recent runs
aptl runs list
# Show run details
aptl runs show <run-id>
# Print run directory path
aptl runs path <run-id>
# Export run as tar.gz archive
aptl runs export <run-id>
# Export to S3 (requires pip install aptl-labs[s3])
aptl runs export <run-id> --s3-bucket my-bucket --s3-prefix runs/
Key Source Files¶
src/aptl/core/runstore.py: Storage backend protocol and local filesystem implementation, seal-marker commit, and sealed-state immutabilitysrc/aptl/core/archival/: Terminal-attempt archival, covering RAES run-record composition, terminal-cause status mapping, atomic seal marker, discovery index, and the legacy-manifest read adaptersrc/aptl/core/execution/: The execution controller that drives admitted trials to sealed terminal records, plus the owner-native RAES sub-model bridges (clock, apparatus context, parameters, stochastic controls, evidence artifacts, result summaries)src/aptl/core/run_assembler.py: Orchestrates data collection after scenario stopsrc/aptl/core/snapshot.py: Range snapshot dataclasses and capture logicsrc/aptl/core/exporter.py: Local tar.gz and S3 exportsrc/aptl/core/collectors.py: Individual data collectors (Wazuh, Suricata, TheHive, etc.)src/aptl/cli/runs.py: CLI commands (aptl runs list|show|path|export)