Skip to content

fix(stella-core): take retry jitter and hook stamps through ports, and guard the engine against I/O - #6482

Merged
macanderson merged 1 commit into
mainfrom
fix/core-no-io-audit
Sep 10, 2026
Merged

fix(stella-core): take retry jitter and hook stamps through ports, and guard the engine against I/O#6482
macanderson merged 1 commit into
mainfrom
fix/core-no-io-audit

Conversation

@macanderson

@macanderson macanderson commented Sep 10, 2026

Copy link
Copy Markdown
Owner

What this is

An adversarial audit of stella-core against AGENTS.md rule 2, No I/O in the engine. The sweep covered every filesystem, process, network, environment, standard-stream, clock, sleep and entropy surface in the crate's shipping source, plus its manifest. It found no file, process or network I/O. It found two ambient reads the crate's own prose said were not there, and no guard that would have caught either.

Two fixes

1. The retry ladder drew its jitter from the OS entropy pool. retry.rs called rand::rng() at four sites in the live path, while its header said "Nothing here reads a clock or a budget directly" and pointed at the Clock port as the seam it follows. A retry ladder that rolls its own dice cannot be replayed, and the crate linked rand for one draw.

The draw is now a port: Sleeper gains fn jitter(&self, upper: u64) -> u64, a uniform draw in 0..=upper. compute_backoff_delay_ms and server_hint_delay_ms take &dyn Sleeper in place of &mut impl Rng; the caller clamps the draw to the span, so a careless host widens nothing. The method has no default on purpose: a default of zero would let a host forget the draw and ship a fleet whose workers all wake on the same millisecond after a shared 429. Production impls in stella-cli::runtime::TokioSleeper and stella-serve::remote::TokioSleeper draw from rand::rng(); every test double answers the floor. rand moves from stella-core's [dependencies] to [dev-dependencies] (the tests' seeded double still uses StdRng); stella-cli gains rand as a dependency, which is where the entropy now lives.

Exemplar: tower::retry::backoff::ExponentialBackoffMaker takes its RNG as a constructor parameter rather than seeding one inside the policy. Here the port is the existing Sleeper, because the two questions it now answers — how long the wait is, and how it is waited out — are one decision about the backoff, and the trait's doc says so.

2. The hook bus stamped every event from SystemTime::now(). bus.rs's seal and both failure-log sites read the wall clock directly, while ports::Clock's doc said the crate "never carries a concrete time source of its own". HookBus::new now takes impl Clock + 'static, counting from the Unix epoch — the stamp crosses a process boundary to hook scripts and the observatory, so the host's wall clock is the right one and the host owns it. ports::FixedClock(u64) is the pinned clock a test or a replay hands in. Hosts: stella-cli::rules passes WallClock; stella-serve::extensions passes a new remote::WallClock (the same shape as stella-runtime's HostClock and the CLI's, with the same doc). Every test site passes FixedClock(0).

The guard: core-no-io

scripts/check-core-no-io.py, a new make gate step in the guards-fast rung, wired into ci.yml, guard-self-tests.yml, check-gate-parity.sh, AGENTS.md's gate block and rule 2, CONTRIBUTING.md, and the crate README. Three questions per run:

  • Floor — no shipping line (with #[cfg(test)], tests/ and tests.rs stripped) names std::fs/process/net/env, a standard stream, tokio's I/O drivers, SystemTime::now, a thread or tokio sleep, an entropy source, or a print macro. No baseline: the tree has none.
  • Manifest[dependencies] names no I/O or entropy crate, and tokio takes only sync, time, macros, rt.
  • RatchetInstant::now() reads per file, recorded in scripts/core-no-io-baseline.txt (19 reads in 10 files today), down-only. --update refuses to add a file or raise a count. The monotonic clock is not I/O, but a turn that reads it cannot be replayed from its record, which is what ports::Clock exists for.

Run against main it fails on both fixes above (retry.rs: an entropy source, bus.rs: SystemTime::now); run here it passes. scripts/test-core-no-io.sh (make core-no-io-test) drives 24 fixture cases: each floor pattern, the three things that are not shipping code, both manifest directions, and every ratchet direction including --update refusing to grow and refusing to write over a red floor.

Witness tests

  • crates/stella-core/tests/hook_bus_stamps_from_the_clock.rs — a bus given FixedClock(1_700_000_000_123) stamps 2023-11-14T22:13:20.123Z. Could not be written on the old code, where the stamp was whatever the wall clock said.
  • retry::tests::the_jitter_is_the_ports_draw_and_a_wide_draw_is_clamped — a sleeper answering a fixed number lands the delay at floor + that number, and a draw past the span is clamped to the cap.

No test was deleted.

What was not fixed, and why

The 19 Instant::now() reads in the driver/step/budget path are recorded, not removed. Routing them through ports::Clock means changing BudgetGuard's Instant-typed deadline API and every host that arms it, and driver.rs is a god file closed to growth. That is larger than this session; the ratchet holds the count where it is and each removal lowers it.

Verified

  • cargo check -p stella-core --all-targets, -p stella-engine -p stella-tools -p stella-serve --all-targets, -p stella-cli --all-targets: clean.
  • cargo test -p stella-core: all pass (retry 38, bus 55, and the full crate).
  • make guards-fast: green, core-no-io included.
  • ./scripts/test-core-no-io.sh: 24 passed. shellcheck clean.

Closes nothing by design (closes-nothing): the audit was asked for directly, and the fix rides the PR rather than an issue.

Summary by Sourcery

Enforce stella-core's no-I/O architecture by injecting retry jitter and event clocks through ports and adding automated guards for ambient I/O and clock reads.

New Features:

  • Route retry jitter through the Sleeper port so production hosts provide entropy while tests and replays can control it.
  • Route hook-event timestamps through an injected Clock, with fixed clocks available for deterministic tests and replay.

Bug Fixes:

  • Remove direct entropy and wall-clock reads from shipping stella-core code, preserving the engine's no-I/O boundary.

Enhancements:

  • Clamp injected jitter to the configured retry window and move the entropy dependency from stella-core to host crates.
  • Add a down-only guard for Instant::now() usage and enforce the core dependency feature policy.

CI:

  • Add the core no-I/O and clock-ratchet guard to fast gates, CI, guard self-tests, and gate-parity checks.

Documentation:

  • Document the core no-I/O guard, port ownership, and clock-read ratchet in project and crate guidance.

Tests:

  • Add deterministic witness coverage for injected hook timestamps and retry jitter bounds.
  • Add hermetic fixture tests covering the no-I/O source floor, manifest checks, and ratchet behavior.

…d guard the engine against I/O

The retry ladder drew its jitter from the OS entropy pool through
`rand::rng()`, and the hook bus stamped every event from
`SystemTime::now()`, while both files' headers said the crate reads
nothing directly. `Sleeper` gains a required `jitter` draw, `HookBus::new`
takes a `Clock` counting from the Unix epoch, and `rand` leaves
stella-core's shipping dependencies for its dev-dependencies.

`make core-no-io` (scripts/check-core-no-io.py) now reads the crate's
shipping source and manifest on every gate run: a floor over every I/O,
wall-clock, sleep and entropy surface, and a down-only count of the
`Instant::now()` reads the deadline arithmetic still makes.
@macanderson macanderson added the closes-nothing Substantial change that closes no issue by design (SCR-003) label Sep 10, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 19 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR audits and hardens stella-core’s no-I/O boundary by injecting retry jitter and hook timestamps through ports, moving entropy and wall-clock implementations to host crates, and enforcing the contract with source/manifest checks plus a down-only Instant::now() ratchet wired into CI and repository gates.

Sequence diagram for ported retry jitter and backoff

sequenceDiagram
    participant Engine as stella-core retry
    participant Sleeper as Sleeper port
    participant Host as Host TokioSleeper
    participant Entropy as OS entropy
    participant Timer as Tokio timer

    Engine->>Sleeper: jitter(span)
    Sleeper->>Host: jitter(upper)
    Host->>Entropy: random draw
    Entropy-->>Host: value
    Host-->>Sleeper: value
    Sleeper-->>Engine: clamped jitter
    Engine->>Sleeper: sleep(delay_ms)
    Sleeper->>Host: sleep(duration_ms)
    Host->>Timer: tokio::time::sleep(duration_ms)
    Timer-->>Engine: wait complete
Loading

Sequence diagram for clock-injected hook event stamps

sequenceDiagram
    participant Host as Host runtime
    participant Bus as HookBus
    participant Clock as Clock port
    participant Event as Hook event
    participant Observer as Hook script or observatory

    Host->>Bus: new(session_id, clock)
    Host->>Bus: emit(event draft)
    Bus->>Clock: now_ms()
    Clock-->>Bus: Unix epoch milliseconds
    Bus->>Bus: stamp()
    Bus-->>Event: ISO 8601 timestamp
    Event-->>Observer: event crosses process boundary
Loading

Flow diagram for the core-no-io gate

flowchart TD
    Start[Run core-no-io] --> Strip[Strip test code and comments]
    Strip --> Floor[Check shipping source for I/O surfaces]
    Floor --> Manifest[Check production dependencies and Tokio features]
    Manifest --> Ratchet[Count Instant.now per file]
    Ratchet --> Compare{At or below baseline?}
    Compare -->|Yes| Pass[Gate passes]
    Compare -->|No| Fail[Gate fails]
    Floor -->|Hit found| Fail
    Manifest -->|Denied dependency or feature| Fail
    Update[--update] --> Compare
    Compare -->|Lower counts only| Baseline[Write reduced baseline]
Loading

File-Level Changes

Change Details Files
Moved retry jitter and hook-event timestamps behind host-owned ports so stella-core no longer creates entropy or reads wall-clock time directly.
  • Added required Sleeper::jitter and routed both backoff paths through it, clamping host-provided draws to the intended span.
  • Moved production entropy draws into CLI and serve sleeper implementations and moved rand out of stella-core shipping dependencies.
  • Changed HookBus constructors to require a Clock, stamped events from injected Unix-epoch milliseconds, and added FixedClock for deterministic tests.
  • Added CLI and serve wall-clock adapters and updated all bus/sleeper implementations and call sites.
crates/stella-core/src/retry.rs
crates/stella-core/src/bus.rs
crates/stella-core/src/ports.rs
crates/stella-core/Cargo.toml
crates/stella-cli/src/runtime.rs
crates/stella-cli/src/rules.rs
crates/stella-cli/Cargo.toml
crates/stella-serve/src/remote.rs
crates/stella-serve/src/extensions.rs
crates/stella-core/tests/hook_bus_stamps_from_the_clock.rs
crates/stella-core/src/**/tests*.rs
crates/stella-engine/src/tests.rs
crates/stella-engine/tests/embedding.rs
crates/stella-tools/src/**/*.rs
crates/stella-tools/tests/*.rs
Added an automated no-I/O invariant and down-only monotonic-clock ratchet for stella-core.
  • Scans shipping Rust source for filesystem, process, network, environment, stream, sleep, wall-clock, entropy, and print APIs.
  • Checks the core manifest for denied I/O/entropy dependencies and restricts Tokio features.
  • Tracks Instant::now() reads per file with a baseline that can only shrink and refuses unsafe updates.
  • Added 24 fixture-based guard self-tests covering source filtering, manifest parsing, and ratchet behavior.
scripts/check-core-no-io.py
scripts/core-no-io-baseline.txt
scripts/test-core-no-io.sh
Integrated the invariant into repository gates and contributor documentation.
  • Added core-no-io to the fast guard rung, CI, guard self-tests, gate-parity mapping, and contributor checks.
  • Documented the enforced rule, port ownership, baseline workflow, and crate-level no-I/O contract.
Makefile
.github/workflows/ci.yml
.github/workflows/guard-self-tests.yml
scripts/check-gate-parity.sh
AGENTS.md
CONTRIBUTING.md
crates/stella-core/README.md
Cargo.lock
Expanded deterministic coverage for the two audited ambient reads.
  • Added a witness test proving injected clock values produce exact ISO-8601 event stamps.
  • Added retry coverage proving jitter comes from the sleeper port and oversized draws cannot exceed the backoff cap.
  • Updated existing doubles to provide explicit zero jitter and existing bus tests to use fixed clocks.
crates/stella-core/tests/hook_bus_stamps_from_the_clock.rs
crates/stella-core/src/retry.rs
crates/stella-core/src/**/*.rs
crates/stella-engine/src/tests.rs
crates/stella-engine/tests/embedding.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@macanderson
macanderson merged commit d0ee2d6 into main Sep 10, 2026
29 of 31 checks passed
@macanderson
macanderson deleted the fix/core-no-io-audit branch September 10, 2026 05:08
macanderson added a commit that referenced this pull request Sep 10, 2026
…eeper port (#6486)

## What this is

The follow-up #6482 said it was leaving behind. That PR fixed the two
ambient reads that were also I/O-adjacent (OS entropy, wall clock) and
recorded 19 `Instant::now()` reads as ratchet debt. This one removes
every ambient read of time from `stella-core`'s shipping code — the 19
explicit reads, the 9 hidden ones (`.elapsed()` is an `Instant::now()`
in disguise), and the 4 `tokio::time::timeout`s, which are the runtime's
timer rather than the port — and drops tokio's `time` feature from the
crate. The ratchet baseline is empty.

## The port

`retry::Sleeper` is now the engine's whole time port: `now() -> Instant`
joins `sleep` and `jitter`. One port rather than a second `Clock`
because a double cannot answer them apart — a sleeper that suspends
virtually has moved its own `now`, and a timeout is a sleep racing a
call. That is tokio's own shape: a paused runtime puts one virtual clock
behind `sleep` and `Instant::now`. `ports::Clock` (`now_ms`) stays what
it was — the millisecond clock hosts hand to things that stamp records
(fleet ledger, runtime stamps, the hook bus) — and is untouched.

`retry::bounded(sleeper, limit, call)` is `tokio::time::timeout` written
against the port: `futures_util::future::select` over the call and
`sleeper.sleep(limit)`, call polled first so a ready call never loses to
a ready sleep, `Option` rather than `Result` because the limit passing
is an answer, not an error.

## What moved, in shipping code

- `Instant::now()` → `self.sleeper.now()` / `sleeper.now()`: driver
(deadline notice, step timing, settlement, speculation pool), dispatch,
completion, rate-limit park allowance, sub-agent deadline and tick,
accounted call, retry attempt timing.
- `.elapsed()` → `sleeper.now().duration_since(started)` at each of the
nine sites; `CancelUsageGuard` carries `&dyn Sleeper` because a `Drop`
has no caller to hand it `now`.
- `tokio::time::timeout` → `retry::bounded`: the tool dispatch ceiling,
the idle generation bound, the task-deadline bound, the accounted call's
idle bound. `stella-core`'s tokio features are `["sync"]`.
- `TurnState::new` / `TurnState::from_checkpoint` /
`BorrowedTurn::adopt` take `now: Instant`; `bounded_generation` /
`deadline_bounded_generation` take the sleeper. `from_checkpoint` is
public, so `stella-cli`'s resume path and `stella-serve`'s checkpoint
test pass `Instant::now()` — hosts are where that read belongs.
- The hook bus times observer dispatch off the `Clock` it already holds
(wall milliseconds); the quarantine rule needs three consecutive
overruns, so one wall-clock step cannot quarantine anything. Its test
advances a hand-stepped clock instead of `thread::sleep(100ms)`.

## The guard

`check-core-no-io.py`'s floor gains `.elapsed()` and any `tokio::time`
use; the tokio feature allowlist loses `time`; the baseline is empty and
`--update` refuses to add to it, so any `Instant::now()` now fails.
Harness: 27 cases (+3: `.elapsed()`, a tokio timer, the `time` feature).
AGENTS.md rule 2, the crate README and the `ci.yml` comment say the
count is zero.

## Test doubles — the part worth reading

An instant-returning `Sleeper` double was tolerable while timeouts
bypassed the port and is a lie once they go through it:
`EngineConfig::default()` arms a 816 s model timeout and a 15 min tool
timeout, and an instant sleep makes both fire the moment a provider
future waits on another task. Twelve tests failed that way on the first
run. The faithful double sleeps on tokio's clock and reads `now` from
`tokio::time::Instant::now().into_std()`, and the files that use it run
`#[tokio::test(start_paused = true)]`: virtual time is free while the
runtime is idle and correct when it is not. `driver/tests.rs`'s and
`subagent/tests.rs`'s shared doubles (now named `TokioSleeper`) and 17 +
7 test files changed that way; `hard_drop_write_back` and the streaming
accounted-call test got the same double. Files with their own instant
`NoSleep` that passed were left alone.

A side effect worth stating: the `stella-core` lib suite went from 46 s
to 2.5 s, because several tests had been paying real backoff sleeps
through `tokio::time::timeout` that the paused clock now skips.

## Witness

- `retry::tests::bounded_races_the_ports_own_sleep`: with a sleeper
whose sleep returns at once, `bounded(1h, pending)` is `None`
immediately and `bounded(1h, ready(5))` is `Some(5)`, and the port
recorded exactly one 3 600 000 ms sleep. On `tokio::time::timeout` this
waits the real hour.
- `check-core-no-io.py` on `main` fails on 19 `Instant::now()` reads, 9
`.elapsed()` calls, 4 `tokio::time` uses and the `time` feature; here it
passes with an empty baseline.

No test was deleted.

## Verified

- `cargo check -p stella-core -p stella-engine -p stella-tools -p
stella-serve -p stella-cli --all-targets`: clean.
- `cargo test -p stella-core`: 893 lib tests + every integration test
pass (2.5 s lib). `cargo test -p stella-engine`: pass.
- `make guards-fast`: green; `./scripts/test-core-no-io.sh`: 27 passed;
shellcheck clean.

Closes nothing by design (`closes-nothing`): this is the remaining half
of the audit the maintainer asked for directly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

closes-nothing Substantial change that closes no issue by design (SCR-003)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant