Skip to content

Releases: dmarzzz/shade-tree-node

v0.7.5

Choose a tag to compare

@github-actions github-actions released this 21 Oct 02:47
2eaf922

0.7.5 — register-member funds itself

shadenet register-member with no key creates its own funding wallet, quotes one amount, waits for it and stakes (zkAPI's deposit step). Also: the proxy as a scheduler with --preopen session books, CLI output that names the budget, record RPC changes, monitoring fixes and dependency security bumps.

register-member funds itself (feature request: mirror zkAPI's funding path)

  • shadenet register-member --identity … with no --key-file and no SHADENET_REGISTER_KEY on
    a remote RPC used to fail asking for a funded key. It now does what zkapi-clientd config
    does: creates an owner-only funding wallet (funding.key next to the identity file;
    --funding-key <path> to move it), prints the wallet's address and the one amount to send
    (the tier's bond from bondFor plus a gas reserve: the smallest of 0.002, 0.004, 0.008 … ETH
    that covers 1 800 000 gas at the current EIP-1559 fee; the lowest rung covers up to 1.11 gwei,
    since Sepolia idles at a few wei and then spends a week near 1 gwei), polls the balance every
    15 s, and stakes once the balance covers it. The reserve moves in those coarse steps rather
    than tracking the fee to the wei because the amount is public next to the stake: on the
    bundled Sepolia set every quiet-fee registration sends the same 0.012 ETH, so the amount does
    not single out its sender. Top-ups after a partial payment are rounded up to 0.0001 ETH.
  • With the first amount it prints the Layer-0 advice from docs/PAYMENTS.md: the address, amount
    and stake are public and link the sending wallet to the membership (not to its use), so send
    from a fresh address funded through a large shared pool (Railgun, Privacy Pools, a CEX
    withdrawal) and send exactly the quoted amount. After the stake it prints the leftover gas
    money still in the funding wallet and says it is tied to the stake: keep it for a later
    top-up or send it on to a fresh address, not back to the wallet that funded it. The same command resumes after Ctrl-C and reuses the key; an already-staked
    identity is reported before anyone is asked for money. --wait-timeout <seconds> (default
    3600; 0 quotes once; SHADENET_FUNDING_POLL_MS sets the poll) bounds the wait; an unfunded
    wallet at the deadline exits 4 naming what is still missing. The quote signs nothing and never calls eth_estimateGas (an
    unfunded sender cannot be estimated against), which is why the reserve is a ceiling; what
    the registration does not spend stays in the wallet. --key-file, the environment key and
    the loopback Anvil default are unchanged.
  • Proven live on 2026-10-11 against the bundled Sepolia set 0x789967F0…680b: a fresh identity,
    shadenet register-member --identity … with no key, funding wallet
    0x33ec7E53E143e2065887Bd5D5d7EB47159D72982 quoted 0.0116 ETH (0.01 bond + 1 600 000 gas at the
    1 gwei floor, the ceiling at the time); the deployer sent exactly that
    (0x80dd0656d783f4279fa06e4448e1d3f40672b0f9369401a88bb1456d2cbe67ef, block 11897250); the
    waiting command saw it on its next poll and staked
    (0xd0ebc6fbc3ca69fbf95a490b0205d4f70c89ccad88f500f139f3e9faf1ea8a17, block 11897251,
    1 471 267 gas at 0.001 gwei, member-status → active, index 14), leaving 0.0016 ETH in the
    wallet. Two earlier one-hour waits also ran to the deadline and exited 4, and retried two
    transient RPC errors (eth_call: expected 0x-hex data, a 15 s timeout) without losing the
    wait. The stake used more gas than the first live registration (1 317 674), and the signer
    prices at estimate + 20 % (about 1.77M), so the ceiling moved from 1 600 000 to 1 800 000 after
    this run, and the reserve ladder above replaced the per-fee figure: the quote is now 0.012 ETH
    at quiet fees.
  • shadenet init prints the keyless register-member command and says where the funding
    wallet will be created; --key-file is mentioned as the alternative.
  • The JavaScript shade-tree register-member does the same with no SHADE_TREE_REGISTER_KEY on a
    remote RPC: --funding-key <path> / SHADE_TREE_FUNDING_KEY (default ~/.config/shade-tree/funding.key),
    --wait-timeout, SHADE_TREE_FUNDING_POLL_MS, exit 4, same quote and the same printed
    lines. The arithmetic, rounding, key file and wait loop live in
    packages/node/lib/funding-wallet.mjs with a selftest (quote and reserve ladder, privacy text, rounding, owner-only
    creation and reuse, refusal of a world-readable or malformed file, the wait loop against an
    injected clock: one quote, progress on partial payments, a retried RPC error, exit 4 at the
    deadline, no quote for a staked leaf).
  • Library: Wallet::generate(), member::fee_terms (factored out of the signer, same
    numbers), member::funding_quote / FundingQuote / REGISTER_GAS_CEILING / QUOTE_MIN_FEE_PER_GAS; the bond
    checks register performs moved into tier_bond, shared with the quote. Tests cover the
    quote arithmetic and the staked / wrong-chain refusals against the mock RPC, the owner-only
    creation and reuse of funding.key (and the chmod 600 refusal), the default key location,
    the parser, and the rounding.

Monitoring

  • The compose monitoring bundle scraped the heartbeat on 9102 and the Elder on 9103; the defaults
    are heartbeat 9103 and Elder 9100, so HeartbeatDown and BootnodeDown fired on a healthy host.
    RpcEndpointFailing and CanopyPinDrift used severity: warn and had no component; they now
    use warning with component: rpc and component: fleet. monitoring/alerts.selftest.mjs
    checks both (labels on every rule, compose ports against bootstrap.sh).

  • CanopyPinDrift compares release versions, not commits. The published node image is built at the
    release tag's commit while systemd hosts run the record's commit; both report the record's
    version, so the orbital-one container (0.7.4 at 6709328) no longer fires it next to the fleet
    (0.7.4 at 114065d). A role left on another release still fires; an off-pin commit on one host is
    CanopyOffRecordPin's job. Checked with promtool test rules: same version and two commits stays
    quiet, while two versions fire after 30 minutes.

  • The uptime probe accepts a comma-separated SHADE_TREE_DIR_SIGNER, which is what the record sets
    for a canopy with two Elder Trees. It passed the joined string to verifyDirectory as one key, so
    every check failed with directory:signer-not-pinned and the lab vantage point reported the Elder
    unreachable while it served a fresh, signed directory.

  • The Sepolia record pins services.*.commit to 114065d, the commit the fleet runs (written with
    scripts/record-canopy.mjs, as agent-devops' copy already was). It still named 954216e from 0.7.2,
    so the probe reported every Elder off pin.

Dependencies

  • quinn-proto 0.11.16 → 0.11.19 (cargo update -p quinn-proto, lock only; it arrives through
    arti/reqwest). RustSec published five advisories against 0.11.16 on 2026-10-12
    (RUSTSEC-2026-0339 to -0343, stream-reassembly and connection-ID memory exhaustion; fix

    = 0.11.17), which turned the supply-chain audit red on every PR, including #353. cargo audit
    and cargo deny check advisories are clean on the new lock; the workspace builds --locked.

  • docker/setup-qemu-action v3 → v4.4.0 in node-image (Dependabot #256).
    • Without a push, node-image now also builds the arm64 image through QEMU (neither loaded nor
      pushed), so a QEMU or base-image break shows up on a dispatch instead of during a release.
    • Checked with a dispatch on the branch (run 37545890531, push=false): the amd64 build and
      smoke test passed, and the arm64 build through QEMU v4 passed.
  • toml 0.8 → 1.1 for config.toml (Dependabot #263). toml 1.x gates from_str behind its serde
    feature, so the dependency now enables ["parse", "serde"]; Dependabot's version alone did not
    compile.
    • A new corpus test pins every config people have, field by field: what shadenet init writes,
      what agent-devops renders for Hermes, and every key at once with inline comments, a literal
      string, a multi-line array with a trailing comma, and CRLF line endings.
    • It also checks that an unknown key (named in the error), a wrong type, a duplicate key and
      broken syntax stay errors.
    • It passes unchanged on 0.8 and 1.1.
  • snarkjs 0.7.5 → 0.7.6 in packages/sdk, which proves exit/withdraw actions in the browser
    (proveAction; Dependabot #257). The repo's hoisted snarkjs, used by the nodes and the JS client
    for RLN proofs, stays 0.7.5.
    • Checked across versions:
      • a fresh SDK exit proof (0.7.6) verifies under the hoisted 0.7.5; this is now a permanent
        check in sdk.selftest;
      • the on-chain WithdrawGroth16Verifier accepts it on Anvil and refuses it with a tampered
        address;
      • the pre-bump testdata/withdraw-proof.json fixture still verifies.
  • @noble/curves 1.2.0 → 2.4.0, the browser backend's ed25519 (Dependabot #259). The import is
    @noble/curves/ed25519.js; sign/verify (RFC 8032, zip215: false) are unchanged.
    • ethers, @semaphore-protocol/proof and @zk-kit/lean-imt keep their own nested 1.x.
    • test/crypto-backends.selftest.mjs (32 checks), directory forward-compat under both backends
      and the SDK conformance vectors pass.
    • The stake page bundle is byte-identical.
  • @noble/hashes 1.3.2 → 2.4.0, the browser crypto backend's sha256 and sha3-256
    (packages/node/lib/crypto-browser.mjs; Dependabot #260, merged as #330). v2 exports explicit .js
    subpaths, so the imports are @noble/hashes/sha3.js and @noble/hashes/sha2.js.
    • New test/crypto-backends.selftest.mjs (32 checks) pins the browser and Node (OpenSSL)
      backends to each other: FIPS known answers, the RFC 8032 ed25519 vector, cross-verification,
      and refusal of a corrupted or non-canonical-S signature.
    • The stake page bundle is byte-identical.
  • chacha20poly1305 0.10 → 0.11, the XChaCha20-Poly1305 seal on pa...
Read more

v0.7.4

Choose a tag to compare

@github-actions github-actions released this 05 Oct 23:59
6709328

0.7.4 — forward-compatible capability verification

Clients verify a signed directory over the bytes as signed and tolerate capability fields they do not recognize, instead of rejecting the directory. A future caps.* field no longer bricks an installed client the way caps.sets did to v0.7.0; interpretation still ignores unknown fields. Symmetric in the Rust client and the JS node, with a previous-release conformance vector. No new signed capability field ships until this release is the floor.

v0.7.3

Choose a tag to compare

@github-actions github-actions released this 05 Oct 22:02
d90f7ea

0.7.3 — first-run polish

The one-tier launch set plus: init shortens the leaf it prints so it cannot be pasted where the identity commitment belongs, and doctor reports a pruned backup RPC as a note for clients instead of a failure.

Nothing yet.

v0.7.1

Choose a tag to compare

@github-actions github-actions released this 01 Oct 07:37
5156257

0.7.1 — the node container

  • ghcr.io/dmarzzz/shadenet-node:<version>: Tor + the JS node + its heartbeat in one image, run from a deployment record alone (SHADENET_RECORD). shadenet-node run | check [--probe] | identity | authorize | status | retire; ten knobs as SHADENET_* or /state/node.toml, including sets (one node admitting several staked sets); any explicit SHADE_TREE_* still wins. Published multi-arch and attested by the release workflow.
  • bootstrap.sh and the v4 role take SHADENET_SETS / shade_tree_extra_sets for the same thing.
  • The operator page, OPERATOR.md, JOIN.md, CONFIG.md and docker/README.md lead with the container; the stale "blocked by #6" note is gone.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 00:50
56ae0a6

0.7.0 — ShadeNet research preview on Sepolia

The launch release. The fleet, the site, both SDKs and the shadenet binary read one
production record, network/sepolia/deployment.json, deployed 2026-09-30 from the final
economics (H2) with the PSE trusted-setup verifier (H3). Signed and hashed v4 wire strings are
unchanged (test/wire-freeze.selftest.mjs). docs/LAUNCH-REPORT.md is the launch gate line by
line with evidence.

Launch

  • Production contracts on Sepolia: StakedReputationSet 0xDEB294E6e9ad6A3FcBDeFfD1F67aC9678AC94bBC
    with the ceremony WithdrawVerifier, all four contracts Sourcify exact_match; the earlier set
    0xEB67…4275 is retired (CHAIN-1). The record lists the RPC failover order (ADR 0012).
  • Economics final (H2, #212, docs/ECONOMICS.md): tier 1 = 0.01 ETH, tier 8 = 0.08 ETH, 24 h
    unbonding, slash bounty 1/10, 24 sponsor seats, session tickets on.
  • Trusted setup: PSE's finalized RLN ceremony adopted for both circuits after an independent
    re-verification from public inputs (H3, #214, docs/ceremony/PSE-VERIFICATION.md, two archive
    mirrors). The dual-VK window closed with the production record: only
    rln-ae43614cd02ebe95 is accepted, security.proofArtifacts is trusted-ceremony,
    circuits/rln/previous/ is gone, and deploy/v4/preflight.mjs reads the lock's CEREMONY
    trust value.
  • The Get access page and its status API build from the production record; install lines pin
    v0.7.0 (#216, #218, #222).

Protocol and SDKs

  • Session tickets (#103, ADR 0011, #205): with the record's sessionTickets switch on, one RLN
    proof per epoch buys a gateway-bound ticket good for several tunnels inside the epoch's payload
    budget; shared vectors, Rust client, JS node and @shadenet/sdk all agree. Off by default in
    code; on in the launch record. Units render the switch (#221, #225).
  • Every Elder hears every node and the Lab reads the record's elders[] (ADR 0012, #213, #224).
  • Member-set replay verified against the contract's counters, fail closed on an RPC that returns
    an empty log page (#207); v4 preflight retries a missing receipt (#209) and checks fallback RPCs
    for current state (#217).

Ops and release

  • The JS node lives in packages/node/ (#202) and the compatibility shims are gone (#208).
  • Staging rehearsal report (M7, docs/STAGING-REHEARSAL.md, #209, #219, #223, #224): every
    launch-gate line run on the staging canopy, including a browser stake, Hermes and SearXNG
    through the canopy, Elder failover, an RPC outage, an alert round trip and a release from a tag.
  • Release: Intel macOS live binary (#198), --version names the commit (#199), packageable crates
    and a tag gate (#191), prerelease handling and a Homebrew tap step with a deploy key (#194, #211),
    the publish dry-run passes a dist-tag for prereleases (#206); install.sh glibc and aarch64
    loader fixes (#220).

Dependencies

v0.7.0-rc.1

v0.7.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Sep 20:34
db56701

0.7.0-rc.1 — ShadeNet research preview, release candidate

First release cut from a tag under the ShadeNet name; a prerelease, so it never becomes "Latest"
and the fleet rolls to it on staging only. Work toward the ShadeNet research preview on Sepolia.
Signed and hashed v4 wire strings are unchanged (test/wire-freeze.selftest.mjs). The contracts
change and take effect with a fresh deployment: staging is live (network/sepolia-staging/);
production waits for the economics and the trusted setup.

Contracts

  • Burn 90% of each slashed member bond instead of paying it all to the slasher (#115, audit 2.1.1).
  • The set derives each leaf from the identity commitment and the tier it proves; zero and
    non-canonical commitments are rejected; exit and withdraw proofs bind chain, contract and leaf
    index (#136, audit 2.1.2 to 2.2.1).
  • The paid set burns a slashed identity at every tier and refuses to re-insert it; the registrar
    refuses before charging (#181, audit 2.3.3). The audit's reproductions are regression tests.
  • Economics as config: network/<net>/economics.json in, a read-back-verified deployment record
    out; production refuses placeholder economics (#177). Staging deployed and source-verified on
    Sourcify's v2 API (#187).
  • The internal audit report and a finding-by-finding index (#151).

SDKs and clients

  • Session tickets behind the sessionTickets switch (#103, ADR 0011): one RLN proof buys a
    research-v1 book of six single-use tunnel tickets at one node, spent with proof-less
    envelopes on the v4 port; shared byte ceiling, shaping, lifetime and idle limits per book; the
    onion-signed session capability; the same vectors in the node, the JS client, @shadenet/sdk
    and the Rust SDK. Off by default; H2 turns it on in economics.json.
  • shadenet, the Rust SDK crate, with an async proxy and the shadenet CLI (#155): concurrent
    CONNECTs, structured errors, a status endpoint, shadenet mcp for agents.
  • @shadenet/sdk, the JavaScript SDK for browsers and Node (#135).
  • Passphrase-protected identity files (#171); no panics on untrusted input (#164); Ethereum
    primitives in shadenet::eth replace ethers-core (#174).
  • Several Elder Trees: deployment record schemaVersion 2 with elders[]; both SDKs use every
    Elder (#182).
  • Agent docs, examples, llms.txt, a local API spec and an installer (#161).
  • Rust CLI copy says ShadeNet and canopy (#184). The JS CLI binary is shade-tree-node (#134).

Node and operations

  • One-command join from the deployment record; secrets as systemd credentials; pinned Node;
    journald caps (#165). Bootstrap fixes (#167).
  • RPC failover across up to five endpoints (#156); the spent-nullifier set persists across
    restarts (#159); the running commit is in build_info and /health (#158).
  • Onion DoS defenses: a per-circuit stream cap and Arti proof-of-work support (#162).
  • Federation dials the configured Tor SOCKS port (#168); alert rules load in Prometheus (#150);
    uptime probe hardening (#160).
  • npm run dev:offline runs a real node and Proxy on loopback without Tor (#175).
  • Every server environment variable is documented and checked (#172); SLOs, on-call and key
    backup (#166); fleet hot key rotated and split by role (#148).

Site and docs

  • ShadeNet and canopy across the site and current docs; Grove and Canopy merged into canopy.
    /canopy serves the network page; /grove and the v1/v2 Data API paths keep working (#132, #152).
  • Get access page built from the deployment record: stake, status, exit and withdraw with proofs
    made in the tab (#147, #169). /pricing redirects to /stake/.
  • Docs reorganized into use, run, reference, security, design and history, with a link check (#170).
  • The launch runbook for the owner's gates (#185); ADR-0010, two SDKs and one spec (#130).
  • Ceremony kit (#128); PSE's RLN setup passes the adoption check for both circuits (#163); the
    launch circuit set is frozen (#186).

Build, CI and release

  • Rust workspace moved to crates/ with shadenet-* crates (#131).
  • The JS node moved to packages/node/ with shims at the old paths for one minor release (#202).
  • Required checks: real-Tor e2e, bootstrap e2e, cargo deny, cargo audit; one Node lane per PR
    and the full matrix nightly (#153, #179); main CI is never cancelled (#183).
  • Release assets for shadenet, Intel Mac live build, signing, Homebrew formula, GHCR image and
    publish dry-runs (#157).
  • The Smithers harness is removed (#154).

Dependencies

  • arti-client and tor-rtcompat 0.46 (#127); rustls 0.23.45 (RUSTSEC-2026-0285); underscore
    1.13.8 in the ceremony kit (#133); arkworks bumps grouped (#149); routine npm, cargo and
    Actions bumps.

Ceremony archive: PSE RLN trusted setup (adopted 2026-09-30)

Choose a tag to compare

@dmarzzz dmarzzz released this 30 Sep 21:06
5f7e691

Mirror 1 of the trusted-setup artifacts ShadeNet adopted under launch decision D3: PSE's RLN Trusted Setup Ceremony (rln-20: 60 contributions + beacon; rln-withdraw: 62 + beacon; Hermez phase 1, circom 2.1.5 --O2, circom-rln 17f0fed).

The tarball holds PSE's published zkeys/R1CS/WASM/vkeys/verifiers, the ptau, Firestore snapshots, both verifyFromInit transcripts and the check log; SHA256SUMS covers every file. Individual final zkeys, verification keys and the joined transcript are attached as well. Verification report: docs/ceremony/PSE-VERIFICATION.md on main after the adoption PR.

This is an archive release, not a software release.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 04:43
5bed894

0.6.0 — Private staking and recovery

Official Sepolia research preview. This release still uses unaudited,
untrusted-testnet proof artifacts and testnet ETH. It is not a production anonymity
or security boundary and must not be used with real funds or sensitive traffic.

The public tier-1 Grove now has a friendly local-first staking page for humans and a
complete Node-free identity, staking, status, exit, and withdrawal path for agents.
The on-chain profile is unchanged: 0.1 Sepolia ETH admits one CONNECT tunnel per
fixed 60-second epoch with a 40 MiB combined payload ceiling.

Added

  • Added a static privacy-first staking page that creates a Semaphore-v3-compatible
    identity locally, validates imports, requires a recovery download, supports
    injected-wallet registration, and offers a public-leaf-only sponsor mode.
  • Added register-member --identity so the Rust client recomputes and validates the
    secret, leaf, and exact tier before its first wallet or RPC interaction.
  • Added native Rust member-status, exit-member, and withdraw-member commands.
    Exit and recipient-bound withdrawal proofs are built and self-verified locally,
    then the exact EIP-1559 call is simulated and signed locally with a separable gas
    wallet.
  • Embedded the deployed withdrawal circuit artifacts in the live Rust binary and
    verified Rust-generated proof calldata against the live Sepolia verifier.

Hardened

  • Pin the browser flow to the current Sepolia chain, contract bytecode, tier, and
    exact bond; reject active leaves, insufficient balances, changed parameters, and
    reverted simulations before requesting a staking transaction.
  • Reject malformed, oversized, mismatched, extra-field, wrong-tier, and duplicate
    identity/CLI inputs without printing bearer material.
  • Document the precise privacy ledger: the static host can see a page load, the
    wallet/RPC sees the public registration, and the public commitment necessarily
    links the pseudonymous register/exit/withdraw lifecycle.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 06:34

0.5.0 — Public Sepolia staking

Official research preview. This release uses unaudited, untrusted-testnet
proof artifacts and Sepolia ETH. It is not a production anonymity or security
boundary and must not be used with real funds or sensitive traffic.

The bundled Grove now admits any member who registers a tier-1 commitment with
exactly 0.1 Sepolia ETH. Tier 1 permits one CONNECT tunnel per fixed 60-second
epoch and caps that tunnel at 40 MiB of combined payload. Cross-gateway replay
suppression is authenticated but best-effort and fail-open, not an atomic global
reservation system.

Added

  • Added a fresh immutable public staking profile with tier 1 at 0.1 Sepolia ETH,
    compatibility tier 8 at 0.8 Sepolia ETH, a 24-hour unbonding window, and the
    real in-repo testnet exit verifier.
  • Added native Rust member registration with owner-only key-file or environment
    key input, local EIP-1559 signing, exact on-chain bond discovery, receipt
    confirmation, and duplicate protection.
  • Added bundled zero-configuration JS and Rust defaults for the live Elder,
    Canopy signer, staking contract, RPC, deployment block, tier, and rate policy.
  • Added onion-signed node rate capabilities and fail-closed client matching for
    the 60-second epoch, 60-second root lifetime, and 40 MiB payload ceiling.
  • Added a pinned runtime-bytecode manifest for the staking set, commitment
    hasher, exit wrapper, Groth16 verifier, and linked Poseidon libraries; both
    local release tests and live deployment preflight reject executable drift.

Fixed

  • Expire superseded membership roots and last-known-good RPC snapshots by wall
    clock even when no later membership event occurs.
  • Reset node and light-client root history after a stale observation gap, and
    retain only the current root during RPC fallback, so recovery cannot make an
    old withdrawn-member root fresh again.
  • Keep identity enrollment, registration, and egress on the same explicit tier
    in both implementations, including RPC-only overrides.
  • Sort contract tier tables globally so the new limit-1 tier precedes limit 8.

v0.4.1

Choose a tag to compare

@github-actions github-actions released this 02 Sep 05:00
b3293aa

0.4.1 — Accepted-tunnel close handling

Official research preview. This patch supersedes v0.4.0 for agents, but it
still uses the unaudited, untrusted-testnet proving setup and is not a production
anonymity or security boundary. Grove access remains invite-only.

Install the matching checksummed Rust -live binary with:

curl -q -fsSL --proto '=https' --proto-redir '=https' \
  https://raw.githubusercontent.com/dmarzzz/shade-tree-node/v0.4.1/scripts/install.sh \
  | SHADE_TREE_VERSION=v0.4.1 sh

Added

  • Added a hardened POSIX installer that detects the supported target, downloads
    the pinned binary and checksum over HTTPS, verifies the digest and filename,
    and installs without sudo.

Fixed

  • Fixed Rust CONNECT Proxy completion after proof acceptance: once the Proxy has
    sent 200 Connection Established, a later peer-close relay error is logged as
    the end of that accepted tunnel instead of being treated as a pre-accept setup
    failure. Errors before the 200 response remain fail-closed and nonzero.
  • Hardened the real-Hermes/embedded-Arti E2E so each successful agent request is
    corroborated by a new gateway acceptance while bounded retries remain within
    the eight-slot research epoch budget.