Skip to content

v0.4.0 — architecture lockdown, money-path coverage, instrumentation

Choose a tag to compare

@danielstpaul danielstpaul released this 25 Jul 18:02
63905b5

The architecture-and-hardening release. Chaos-gated at 225 kill/deploy cycles — zero duplicate side effects, byte-identical replay, on SQLite and Postgres.

Fixed

The email approval channel had never worked. The approval email template and the confirmation page both called approval_url/approval_path, but the route is declared inside namespace :channels, so the real helpers are channels_approval_url/_path. Rendering raised — meaning ChannelMailer#approval blew up and the "your agent needs approval" email was never delivered, and the confirmation page 500'd. If you relied on email approvals, you were silently never told that a money-moving call was parked. Found by writing the first specs for these surfaces.

Changed (breaking, pre-1.0)

  • The inference seam is Adapters::, not Engines:: — and config.adapter, not config.engine. "Engine" meant two unrelated things in one namespace: the Rails engine at Silas::Engine, and the pluggable inference backend. Every comparable seam disambiguates — ActiveJob has QueueAdapters::, ActiveStorage Service::, RubyLLM Provider. Changed now because 1.0 freezes the public API and host apps subclass this seam. Nothing breaks today: Silas::Engines::Base, config.engine, and Silas.resolved_engine still resolve, warn through the new deprecator, and are removed in 2.0.
  • Notification names follow the Rails convention <event>.silas (like sql.active_record). The two existing events were backwards: silas.stepstep.silas, silas.deltadelta.silas. subscribe(/\.silas\z/) now catches everything.

Added

  • Instrumentation for the durable loop. It was silent before — a turn could start, park for a human, be rescued after a kill -9, breach a budget and finish without a single line. Ten documented events now. tool.silas times the tool's own execution and reports how it settled; resume.silas carries parked_for — how long the human actually took. Silas::LogSubscriber renders them at operator-filterable levels and stays quiet when the rescuer did nothing.
  • Silas.deprecator, registered in app.deprecators[:silas], so hosts control Silas deprecations exactly as they control Rails'.
  • 36 specs on the four money-path surfaces that had zero coverage — Slack webhook signature verification end to end (unsigned, wrong-secret and replayed requests refused), the signed email approval links (tampered/expired/wrong-purpose tokens refused; GET never mutates, so a link preview cannot approve a refund), inbound-mail threading, and the approval mailer.
  • Quality tooling in CI: rubocop-rails-omakase (zero offenses), SimpleCov with a 90% floor that fails the build (actual 92.5%), Brakeman and bundler-audit, and a rake zeitwerk:check job.
  • Ten dependency contract specs pinning the Solid Queue and RubyLLM internals Silas couples to, where a rename would break recovery silently — plus an allowed-to-fail canary against ruby_llm edge, which on its first run already flagged that RubyLLM 2.0 drops Tool::Halt and changes the model-registry price accessors.
  • docs/conventions.md recording the deliberate deviations from Rails defaults and the audited posture: nothing mass-assigned, no raw/html_safe anywhere, Time.current throughout.

No migration. 336 specs on SQLite and Postgres.