Home / Cardano Foundation / cardano-node-antithesis
Apr 24, 9-10 AM (0)
Apr 24, 10-11 AM (0)
Apr 24, 11-12 PM (1)
Apr 24, 12-1 PM (0)
Apr 24, 1-2 PM (0)
Apr 24, 2-3 PM (0)
Apr 24, 3-4 PM (0)
Apr 24, 4-5 PM (0)
Apr 24, 5-6 PM (0)
Apr 24, 6-7 PM (0)
Apr 24, 7-8 PM (0)
Apr 24, 8-9 PM (0)
Apr 24, 9-10 PM (0)
Apr 24, 10-11 PM (0)
Apr 24, 11-12 AM (0)
Apr 25, 12-1 AM (0)
Apr 25, 1-2 AM (0)
Apr 25, 2-3 AM (0)
Apr 25, 3-4 AM (0)
Apr 25, 4-5 AM (0)
Apr 25, 5-6 AM (0)
Apr 25, 6-7 AM (0)
Apr 25, 7-8 AM (0)
Apr 25, 8-9 AM (0)
Apr 25, 9-10 AM (0)
Apr 25, 10-11 AM (0)
Apr 25, 11-12 PM (0)
Apr 25, 12-1 PM (0)
Apr 25, 1-2 PM (0)
Apr 25, 2-3 PM (0)
Apr 25, 3-4 PM (0)
Apr 25, 4-5 PM (8)
Apr 25, 5-6 PM (3)
Apr 25, 6-7 PM (0)
Apr 25, 7-8 PM (0)
Apr 25, 8-9 PM (0)
Apr 25, 9-10 PM (0)
Apr 25, 10-11 PM (0)
Apr 25, 11-12 AM (0)
Apr 26, 12-1 AM (0)
Apr 26, 1-2 AM (0)
Apr 26, 2-3 AM (0)
Apr 26, 3-4 AM (0)
Apr 26, 4-5 AM (0)
Apr 26, 5-6 AM (0)
Apr 26, 6-7 AM (0)
Apr 26, 7-8 AM (3)
Apr 26, 8-9 AM (0)
Apr 26, 9-10 AM (0)
Apr 26, 10-11 AM (0)
Apr 26, 11-12 PM (0)
Apr 26, 12-1 PM (0)
Apr 26, 1-2 PM (0)
Apr 26, 2-3 PM (6)
Apr 26, 3-4 PM (4)
Apr 26, 4-5 PM (0)
Apr 26, 5-6 PM (0)
Apr 26, 6-7 PM (0)
Apr 26, 7-8 PM (0)
Apr 26, 8-9 PM (0)
Apr 26, 9-10 PM (0)
Apr 26, 10-11 PM (0)
Apr 26, 11-12 AM (0)
Apr 27, 12-1 AM (0)
Apr 27, 1-2 AM (0)
Apr 27, 2-3 AM (0)
Apr 27, 3-4 AM (0)
Apr 27, 4-5 AM (0)
Apr 27, 5-6 AM (0)
Apr 27, 6-7 AM (0)
Apr 27, 7-8 AM (0)
Apr 27, 8-9 AM (0)
Apr 27, 9-10 AM (0)
Apr 27, 10-11 AM (0)
Apr 27, 11-12 PM (3)
Apr 27, 12-1 PM (2)
Apr 27, 1-2 PM (1)
Apr 27, 2-3 PM (6)
Apr 27, 3-4 PM (2)
Apr 27, 4-5 PM (0)
Apr 27, 5-6 PM (3)
Apr 27, 6-7 PM (0)
Apr 27, 7-8 PM (1)
Apr 27, 8-9 PM (1)
Apr 27, 9-10 PM (2)
Apr 27, 10-11 PM (0)
Apr 27, 11-12 AM (0)
Apr 28, 12-1 AM (0)
Apr 28, 1-2 AM (0)
Apr 28, 2-3 AM (0)
Apr 28, 3-4 AM (0)
Apr 28, 4-5 AM (0)
Apr 28, 5-6 AM (0)
Apr 28, 6-7 AM (0)
Apr 28, 7-8 AM (5)
Apr 28, 8-9 AM (2)
Apr 28, 9-10 AM (0)
Apr 28, 10-11 AM (2)
Apr 28, 11-12 PM (0)
Apr 28, 12-1 PM (0)
Apr 28, 1-2 PM (0)
Apr 28, 2-3 PM (3)
Apr 28, 3-4 PM (2)
Apr 28, 4-5 PM (0)
Apr 28, 5-6 PM (0)
Apr 28, 6-7 PM (1)
Apr 28, 7-8 PM (0)
Apr 28, 8-9 PM (0)
Apr 28, 9-10 PM (0)
Apr 28, 10-11 PM (0)
Apr 28, 11-12 AM (0)
Apr 29, 12-1 AM (0)
Apr 29, 1-2 AM (0)
Apr 29, 2-3 AM (0)
Apr 29, 3-4 AM (0)
Apr 29, 4-5 AM (0)
Apr 29, 5-6 AM (0)
Apr 29, 6-7 AM (0)
Apr 29, 7-8 AM (0)
Apr 29, 8-9 AM (2)
Apr 29, 9-10 AM (0)
Apr 29, 10-11 AM (5)
Apr 29, 11-12 PM (0)
Apr 29, 12-1 PM (0)
Apr 29, 1-2 PM (5)
Apr 29, 2-3 PM (3)
Apr 29, 3-4 PM (0)
Apr 29, 4-5 PM (0)
Apr 29, 5-6 PM (0)
Apr 29, 6-7 PM (0)
Apr 29, 7-8 PM (0)
Apr 29, 8-9 PM (0)
Apr 29, 9-10 PM (0)
Apr 29, 10-11 PM (0)
Apr 29, 11-12 AM (0)
Apr 30, 12-1 AM (0)
Apr 30, 1-2 AM (0)
Apr 30, 2-3 AM (0)
Apr 30, 3-4 AM (0)
Apr 30, 4-5 AM (0)
Apr 30, 5-6 AM (0)
Apr 30, 6-7 AM (0)
Apr 30, 7-8 AM (0)
Apr 30, 8-9 AM (0)
Apr 30, 9-10 AM (0)
Apr 30, 10-11 AM (4)
Apr 30, 11-12 PM (0)
Apr 30, 12-1 PM (8)
Apr 30, 1-2 PM (1)
Apr 30, 2-3 PM (2)
Apr 30, 3-4 PM (4)
Apr 30, 4-5 PM (7)
Apr 30, 5-6 PM (0)
Apr 30, 6-7 PM (0)
Apr 30, 7-8 PM (0)
Apr 30, 8-9 PM (0)
Apr 30, 9-10 PM (0)
Apr 30, 10-11 PM (0)
Apr 30, 11-12 AM (2)
May 01, 12-1 AM (0)
May 01, 1-2 AM (1)
May 01, 2-3 AM (1)
May 01, 3-4 AM (0)
May 01, 4-5 AM (1)
May 01, 5-6 AM (0)
May 01, 6-7 AM (2)
May 01, 7-8 AM (5)
May 01, 8-9 AM (5)
May 01, 9-10 AM (1)
120 commits this week Apr 24, 2026 - May 01, 2026
fix(smoke-test): gate tx-generator block on compose having the service
After PR #111 dropped tx-generator from cardano_node_master/, the
'Compose smoke test' job that runs scripts/smoke-test.sh against
master fails because the script unconditionally probes the
tx-generator control socket — which is only present on the new
cardano_node_tx_generator/ testnet now.

Skip the tx-generator-specific block when the compose under test
doesn't declare a 'tx-generator:' service. Same script keeps
working for both master (no tx-generator) and the new feature
testnet (still drives the daemon end-to-end).
chore(testnet): split tx-generator into its own feature testnet
Mirrors 43bf739 (chore(testnet): remove adversary service from
cardano_node_master) — same shape, different component.

The master testnet is the canonical reference cluster: cron-fired
Antithesis runs against it produce the baseline-finding signal that
everyone reads. With tx-generator in active iteration (reconnect
supervisor #105, freshness gate #110, composer-assertion shape work
in #98), keeping it on master pollutes that signal with experiments
in flight.

This commit:

* Creates testnets/cardano_node_tx_generator/ — same topology as
  master (3 producers + 2 relays + tracer + sidecars + log-tailer)
  plus the tx-generator service. Asteria-stub is deliberately
  absent (different feature, lives on master).

* Removes tx-generator from testnets/cardano_node_master/ along
  with its 'utxo-keys' volume mount on configurator and the volume
  declaration. Nothing else on master uses utxo-keys.

* README in the new testnet documents why and how to dispatch.

Workflow dispatch from this point:
  gh workflow run cardano-node.yaml \
    --ref <feature-branch> \
    -f test=cardano_node_tx_generator \
    -f duration=1

publish-images currently only scrapes cardano_node_master's compose;
the new testnet's tx-generator image is pushed manually until the
publish-images script is generalised to all testnets (follow-up).
asteria-game: split into a dedicated testnet (testnets/asteria_game/)
Adds the asteria-game workload as an isolated testnet so iteration
on the real asteria game can land on `main` without disturbing the
canonical scheduled run on `cardano_node_master`.

testnets/asteria_game/ (copied from cardano_node_master, then edited):
  - same producer/relay/tracer/sidecar/log-tailer/tx-generator
    topology.
  - asteria-game service replaces asteria-stub: same indexer-driven
    composer harness plus /utxo-keys mount so bootstrap can read
    the genesis wallet skey, and CARDANO_NODE_SOCKET_PATH +
    NETWORK_MAGIC env vars so Asteria.Provider.settingsFromEnv
    resolves to relay1's N2C socket.
  - asteria-game image tag pinned to 3042c0a (the prior commit on
    this branch — last commit to touch components/asteria-game/).
  - testnets/cardano_node_master/ untouched — its scheduled
    Antithesis run is unaffected.

Pipeline (additive, no edits to cardano_node_master jobs):
  - scripts/push-asteria_game_images.sh — sibling of
    push-cardano_node_master_images.sh, scans testnets/asteria_game
    for image tags and resolves each via the same nix build path.
  - .github/workflows/publish-images.yaml — new
    smoke-test-asteria-game job runs scripts/smoke-test.sh against
    testnets/asteria_game; the existing cardano_node_master jobs
    are unchanged.

Locally validated: 3-run idempotence (1 cold deploy, 2 short-circuit
via Asteria.Bootstrap.isAlreadyDeployed) on the asteria_game compose.

Antithesis dispatch wiring for this testnet is a follow-up PR.
asteria-game: lift PR #67 source + idempotent bootstrap
Renames components/asteria-player/ → components/asteria-game/ and
upgrades the lifted PR #67 sources so the bootstrap is safe to
re-run on container restart.

  - cabal package + executable renamed asteria-player → asteria-game.
  - cabal.project SRP pinned to cardano-node-clients PR #98 head
    5707836b (utxo-indexer supervisor + N2C reconnect).
  - flake.nix pulls cardano-node-clients utxo-indexer as a flake
    input so dockerTools bundles the prebuilt binary.
  - nix/docker-image.nix bundles utxo-indexer + asteria-bootstrap +
    asteria-game (player) execs + composer/stub scripts; entrypoint
    is the indexer, the bootstrap runs as a serial driver.
  - composer/stub/ shape replaces composer/asteria/: green-baseline
    heartbeat / eventually_alive / finally_alive plus a new
    serial_driver_asteria_bootstrap that execs /bin/asteria-bootstrap.
  - app/BootstrapMain.hs gains Asteria.Bootstrap.isAlreadyDeployed:
    queries Provider for UTxOs at the asteria spend address and
    short-circuits if any UTxO carries the @"asteriaAdmin"@ token.
    Antithesis can restart the asteria-game container at will and
    bootstrap exits 0 quickly on subsequent invocations.

The asteria_game testnet that wires this image into Antithesis is
added in the next commit.

KNOWN GAP — admin_mint validator is the always-true placeholder
PR #67 ships. The Haskell-side detection plus Antithesis's
@serial_driver_@ scheduling are the contract until a follow-up PR
replaces admin_mint with a one-shot policy parameterised on a seed
@OutputReference@.

Tracks: #67 (asteria-spawn-v2), #98 (utxo-indexer supervisor).
Closes companion: #108 (idempotent bootstrap, content folded here).
chore(testnet): remove adversary service from cardano_node_master
Roll back the adversary service from the master testnet's compose
until we can verify findings_new ≤ baseline on a feature branch
with workflow_dispatch --ref before re-merging.

PR #99 added the daemon (findings_new went 9 → 8 — strictly improved
vs baseline). PR #104 added must_hit:true SDK reachable assertions
that Antithesis didn't observe firing in the run, creating 5 new
findings (13 total, +4 vs baseline).

The right pre-merge process — dispatch on the PR branch via
'gh workflow run "Antithesis on cardano-node testnet" --ref <branch>
-f duration=1' and compare findings_new before merging — was
available the whole time and was not used. Both PRs landed on main
on the strength of the Compose smoke test alone, which only proves
"containers come up", not Antithesis behaviour.

Removing the service is the cheapest restore-baseline step. The
adversary image and the daemon code in cardano-node-clients stay
intact; we re-add the compose service in a follow-up PR after a
branch-dispatched Antithesis run shows findings_new ≤ baseline.

components/adversary/ remains so the wrapper image keeps building
and publishing; only the consumer-side service entry is removed.

Tracks: https://github.com/cardano-foundation/cardano-node-antithesis/issues/89 (epic).
chore(testnet): remove adversary service from cardano_node_master
Roll back the adversary service from the master testnet's compose
until we can verify findings_new ≤ baseline on a feature branch
with workflow_dispatch --ref before re-merging.

PR #99 added the daemon (findings_new went 9 → 8 — strictly improved
vs baseline). PR #104 added must_hit:true SDK reachable assertions
that Antithesis didn't observe firing in the run, creating 5 new
findings (13 total, +4 vs baseline).

The right pre-merge process — dispatch on the PR branch via
'gh workflow run "Antithesis on cardano-node testnet" --ref <branch>
-f duration=1' and compare findings_new before merging — was
available the whole time and was not used. Both PRs landed on main
on the strength of the Compose smoke test alone, which only proves
"containers come up", not Antithesis behaviour.

Removing the service is the cheapest restore-baseline step. The
adversary image and the daemon code in cardano-node-clients stay
intact; we re-add the compose service in a follow-up PR after a
branch-dispatched Antithesis run shows findings_new ≤ baseline.

components/adversary/ remains so the wrapper image keeps building
and publishing; only the consumer-side service entry is removed.

Tracks: https://github.com/cardano-foundation/cardano-node-antithesis/issues/89 (epic).
fix(composer): always exit 0 + use Reachability for not-applicable
Three composer-side fixes for findings carried over on
the previous Antithesis run (329a599 — see issues #105,
#106, #107):

1. (B / #105) The not-applicable case in
   parallel_driver_transact.sh and parallel_driver_refill.sh
   was emitting `sdk_sometimes false ...`. Antithesis grades
   a Sometimes assertion as PASSING when at least one sample
   has condition=true; we always emitted condition=false, so
   this assertion could never satisfy and was always failed.
   Switch to `sdk_reachable` — accumulates samples without a
   pass/fail grade, the right type for "documented
   not-applicable response".

2. (C / #106) The composer scripts intentionally exited 1 on
   not-applicable to mark the tick as "skipped". Antithesis's
   built-in 'Always: Commands finish with zero exit code'
   property has no opt-out and grades every non-zero exit as
   a failure. Switch to the asteria-stub convention: always
   exit 0, encode tick state purely via SDK assertions
   (parallel_driver_heartbeat.sh and eventually_alive.sh in
   components/asteria-stub/composer/stub/ do this). Same in
   eventually_population_grew.sh — fire the unreachability
   on did-not-grow and exit 0.

3. (D-adjacent / #107) Add 'index-not-ready' to the refill
   driver's not-applicable case set. After
   cardano-node-clients#110 the daemon's freshness gate
   returns IndexNotReady for refills during the post-
   reconnect stale-UTxO window; the composer should treat
   this as a documented not-applicable, not an unknown
   failure.

The submit-rejected paths keep their `sdk_unreachable`
(strict) framing — the daemon-side freshness gate (#110)
is the actual fix, and we want any leftover
submit-rejecteds to surface as findings, not be silenced.

Verification gate: a fresh 1h Antithesis run on this
branch should show 0 failures from these three findings,
plus the supervisor still triggering its full reconnect
load.
chore(tx-generator): bump pin to upstream main d0928a6 (post-#110)
Picks up the full reconnect-resilience stack on upstream
main:
  * #105 — N2C reconnect supervisor + BlockedIndefinitely catch
  * #110 — post-reconnect indexer freshness gate (rsIndexFresh)

The freshness gate closes the stale-UTxO window between
bearer reconnect and chain-sync re-sync that produced
the residual tx_generator_*_submit_rejected and
tx_generator_population_did_not_grow Always-assertion
failures on the previous Antithesis run (329a599).
asteria-bootstrap: idempotent deploy via Provider UTxO query
PR 2: make rerunning bootstrap safe. Antithesis can restart the
asteria-game container at any time; serial_driver_asteria_bootstrap
will re-fire on each restart. The Haskell binary now skips the
mint+lock if the asteria spend address already carries a UTxO with
the asteriaAdmin token.

Defence layers in place after this PR:
  1. Haskell-side detection (this PR): Provider.queryUTxOs at the
     asteria spend address; presence of (admin_mint_hash,
     "asteriaAdmin") = already deployed → exit 0 with
     sdk_sometimes("asteria_bootstrap_already_deployed").
  2. Antithesis serial_driver scheduling: exclusive access while
     bootstrap runs → no concurrent invocations on the same timeline.

KNOWN GAP — defence layer 3 (Plutus one-shot) is still on the
todo list. PR #67's admin_mint validator is the always-true
placeholder, so there is no chain-level uniqueness guarantee yet.
A future PR replaces admin_mint with a parameterised one-shot
that consumes a seed OutputReference; until then the indexer
race window between detection and submission, while small under
serial_driver scheduling, is non-zero. See spec FR-010 / User
Story 4.

New: composer/stub/serial_driver_asteria_bootstrap.sh — exec
/bin/asteria-bootstrap. Antithesis discovers it under
/opt/antithesis/test/v1/stub/ (already mounted via the same
docker-image build).
asteria-game: rename component + lift PR #67 source for bootstrap+player
PR 1 of 2 toward the real asteria workload. Scope-limited to the
rename + Haskell source lift + nix scaffolding so the binaries
build alongside the existing utxo-indexer. Bootstrap is iteration
5b — *not safe for repeat invocation* — that gap is explicitly the
subject of PR 2.

Rename:
  components/asteria-stub/ → components/asteria-game/
  service asteria-stub      → asteria-game
  volume  asteria-stub-db   → asteria-game-db

Lift from cardano-foundation/cardano-node-antithesis#67 (asteria-spawn-v2):
  components/asteria-game/aiken/         (validators + apply-params)
  components/asteria-game/src/Asteria/   (game state, datums, validators, wallet, providers, RNG, SDK)
  components/asteria-game/app/{BootstrapMain.hs, PlayerMain.hs}
  components/asteria-game/asteria-game.cabal      (renamed package)
  components/asteria-game/cabal.project           (cardano-node-clients SRP bumped to PR #98 head 5707836b)

Build infra:
  flake.nix — extends prior asteria-stub flake with haskell.nix /
    iohk-nix overlays so local Haskell packages compile, while
    keeping the upstream cardano-node-clients flake input that
    supplies the prebuilt utxo-indexer (PR #98 supervisor).
  nix/project.nix — lifted from PR #67, package renamed.
  nix/docker-image.nix — bundles utxo-indexer + asteria-bootstrap
    + asteria-game (player) execs + composer scripts + bash/jq/
    socat; entrypoint stays utxo-indexer.

KNOWN GAP — admin_mint validator is the always-true placeholder
PR #67 ships. Without an on-chain one-shot guarantee, every
container restart could mint another admin NFT. Bootstrap is *not*
wired into compose in this PR for that reason. PR 2 patches the
Aiken admin_mint to take an OutputReference parameter, runs
apply-params at bootstrap-time, and adds defence-in-depth detection
in Bootstrap.hs.

Composer scripts (composer/stub/{parallel_driver_heartbeat,
eventually_alive, finally_alive, helper_sdk}.sh) unchanged from
the green stub baseline so this PR stays bisect-safe and in
property terms identical to commit 3b6fb0e (PR #74's merged head).
tools: antithesis-overview — publish skill + docs in repo (#103)
Adds tools/antithesis-overview/, a Claude Code skill that lists
recent Antithesis runs for a given GitHub repo and publishes a
Markdown digest as a secret gh gist with clickable commit and
report links. The skill is parametric — tenant, repo, hours-back,
requester are all flags — so anyone with access to this repo can
clone, symlink the tool into ~/.claude/skills, and run it against
their own SUT.

Layout mirrors tools/query-logs/: SKILL.md + README.md +
assets/. The runtime depends on antithesis-triage.js which we
vendor at v3.0.0 from antithesishq/antithesis-skills.

Adds docs/overview.md as the user-facing guide and threads it
through the mkdocs nav and the existing triage.md cross-references.

Why publish it here rather than keep it in a personal dotfiles
repo: this is a project-specific workaround for the lack of an
Antithesis runs API, and discovery is easier when the tool sits
next to the existing query-logs tool the team already uses. The
skill is interview-driven so every operator drives it the same way.

Future scriptable replacement is tracked in #97.
feat(adversary): close SDK instrumentation gap (#102)
The adversary container was the only post-PR-D composer template
without SDK assertions. Constitution rule II requires every
non-trivial composer command to emit at least one Antithesis SDK
assertion per meaningful control-flow moment.

Adds three composer files alongside the existing parallel driver:

- components/adversary/composer/chain-sync-client/helper_sdk_lib.sh
  — copy of components/tx-generator/composer/tx-generator/helper_sdk_lib.sh.
  Provides sdk_reachable, sdk_sometimes, sdk_always, sdk_unreachable
  shell helpers writing to $ANTITHESIS_OUTPUT_DIR/sdk.jsonl.

- components/adversary/composer/chain-sync-client/eventually_adversary_active.sh
  — short single-shot validator (sleep 15, one bounded {"ready":null}
  probe, ≤1 min total). Emits sdk_sometimes true "adversary_ready" on
  success, sdk_sometimes false "adversary_not_ready_after_settle" on
  failure.

- components/adversary/composer/chain-sync-client/finally_adversary_summary.sh
  — diagnostic dump: one snapshot, sdk_reachable "adversary_summary"
  with the JSON response embedded as details. Always exits 0.

Rewrites parallel_driver_flaky_chain_sync.sh to source the helper
and emit:

- sdk_reachable "adversary_chain_sync_flap_started" on entry
- sdk_sometimes true "adversary_chain_sync_flap_completed" on ok=true
- sdk_sometimes false "adversary_no_chain_points" on no-chain-points-yet
- sdk_unreachable "adversary_misconfigured_no_points_file" on no-chain-points-file
- sdk_unreachable "adversary_misconfigured_no_producers" on no-producers
- sdk_unreachable "adversary_endpoint_not_implemented" on not-implemented
- sdk_unreachable "adversary_unexpected_response" on parsing failure

Drive-by fix in the same script: replace `nc -U -q 1` with
`nc -U -N`. The image ships netcat-openbsd; `-q` is GNU-only and
silently failed under the old image (always returned empty body).
The fix makes the daemon's response actually reachable from the
composer.

components/adversary/nix/docker-image.nix:

- chmod 0755 also on eventually_*.sh / finally_*.sh (mirrors tx-generator).
- writeShellScriptBin wrappers `eventually_adversary_active` and
  `finally_adversary_summary` so the antithesis composer can invoke
  them by short name.

End-to-end smoke verified locally: docker exec adv parallel_driver_flaky_chain_sync /
eventually_adversary_active / finally_adversary_summary all exit 0
and write five distinct SDK assertions to sdk.jsonl.
tools: antithesis-overview — publish skill + docs in repo
Adds tools/antithesis-overview/, a Claude Code skill that lists
recent Antithesis runs for a given GitHub repo and publishes a
Markdown digest as a secret gh gist with clickable commit and
report links. The skill is parametric — tenant, repo, hours-back,
requester are all flags — so anyone with access to this repo can
clone, symlink the tool into ~/.claude/skills, and run it against
their own SUT.

Layout mirrors tools/query-logs/: SKILL.md + README.md +
assets/. The runtime depends on antithesis-triage.js which we
vendor at v3.0.0 from antithesishq/antithesis-skills.

Adds docs/overview.md as the user-facing guide and threads it
through the mkdocs nav and the existing triage.md cross-references.

Why publish it here rather than keep it in a personal dotfiles
repo: this is a project-specific workaround for the lack of an
Antithesis runs API, and discovery is easier when the tool sits
next to the existing query-logs tool the team already uses. The
skill is interview-driven so every operator drives it the same way.

Future scriptable replacement is tracked in #97.
feat(adversary): close SDK instrumentation gap (#102)
The adversary container was the only post-PR-D composer template
without SDK assertions. Constitution rule II requires every
non-trivial composer command to emit at least one Antithesis SDK
assertion per meaningful control-flow moment.

Adds three composer files alongside the existing parallel driver:

- components/adversary/composer/chain-sync-client/helper_sdk_lib.sh
  — copy of components/tx-generator/composer/tx-generator/helper_sdk_lib.sh.
  Provides sdk_reachable, sdk_sometimes, sdk_always, sdk_unreachable
  shell helpers writing to $ANTITHESIS_OUTPUT_DIR/sdk.jsonl.

- components/adversary/composer/chain-sync-client/eventually_adversary_active.sh
  — short single-shot validator (sleep 15, one bounded {"ready":null}
  probe, ≤1 min total). Emits sdk_sometimes true "adversary_ready" on
  success, sdk_sometimes false "adversary_not_ready_after_settle" on
  failure.

- components/adversary/composer/chain-sync-client/finally_adversary_summary.sh
  — diagnostic dump: one snapshot, sdk_reachable "adversary_summary"
  with the JSON response embedded as details. Always exits 0.

Rewrites parallel_driver_flaky_chain_sync.sh to source the helper
and emit:

- sdk_reachable "adversary_chain_sync_flap_started" on entry
- sdk_sometimes true "adversary_chain_sync_flap_completed" on ok=true
- sdk_sometimes false "adversary_no_chain_points" on no-chain-points-yet
- sdk_unreachable "adversary_misconfigured_no_points_file" on no-chain-points-file
- sdk_unreachable "adversary_misconfigured_no_producers" on no-producers
- sdk_unreachable "adversary_endpoint_not_implemented" on not-implemented
- sdk_unreachable "adversary_unexpected_response" on parsing failure

Drive-by fix in the same script: replace `nc -U -q 1` with
`nc -U -N`. The image ships netcat-openbsd; `-q` is GNU-only and
silently failed under the old image (always returned empty body).
The fix makes the daemon's response actually reachable from the
composer.

components/adversary/nix/docker-image.nix:

- chmod 0755 also on eventually_*.sh / finally_*.sh (mirrors tx-generator).
- writeShellScriptBin wrappers `eventually_adversary_active` and
  `finally_adversary_summary` so the antithesis composer can invoke
  them by short name.

End-to-end smoke verified locally: docker exec adv parallel_driver_flaky_chain_sync /
eventually_adversary_active / finally_adversary_summary all exit 0
and write five distinct SDK assertions to sdk.jsonl.