Skip to content

v0.9.0

Choose a tag to compare

@mikecarroll mikecarroll released this 18 Oct 14:31
· 17 commits to main since this release
23e6609

🚀 What's New

v0.9.0 lets you declare which workload and template each captured LLM call belongs to, with Coolhand.with_context, a class-level Declarable mixin, and opt-in Rails auto-declare. It also makes webhook validation work through secret rotation, sanitizes the warnings Coolhand's server sends back, and adds a bin/live-examples runner for smoke-testing against real provider keys.


✨ New: Declare the Workload and Template of Captured Calls

Passive capture now tells Coolhand which workload and template an LLM call belongs to. Declarations are sent as llm_request_log.declared, and the context is snapshotted on the calling thread, so it survives async_logging.

  • Coolhand.with_context(reporter:, workload:, template:) { ... } stamps every call captured inside the block. Nested blocks override per field, and the innermost wins.
  • Coolhand::Declarable (coolhand_workload, coolhand_declare, coolhand_declare_all) declares it once on a class. It is inherited by subclasses, and each wrapped method defaults to the workload and template key ClassName#method.
  • config.auto_declare = [:active_job, :controller] does the same for Rails jobs and controller actions. It is off by default, and the gem still has no Rails dependency.
  • Coolhand.workload_service.upsert and Coolhand.template_service.upsert create or update records by key. They need your private API key and raise Coolhand::HttpError on failure.
  • Warnings that come back with a logged request (for example, the public-key declaration cap) are now logged.
Coolhand.with_context(reporter: 'my-app', workload: { key: 'Parser#call' }) do
  client.chat(...) # captured with the declared workload
end

class ApplicationService
  include Coolhand::Declarable
  coolhand_workload reporter: 'my-app'
  coolhand_declare_all
end

Coolhand.configure do |config|
  config.auto_declare = [:active_job, :controller] # Rails only
end

Keys longer than 255 characters, with surrounding whitespace or a leading, trailing or doubled /, and a template key with no workload raise ArgumentError up front. The server would otherwise reject and drop the whole log.

See Declared Workloads.


🛠️ Fixed: Webhooks During Secret Rotation

Coolhand::OpenAi::WebhookValidator now accepts a webhook-signature header with several space-separated signatures (v1,sigA v1,sigB) and passes if any v1, entry matches. Previously the whole remainder was compared as one value, so a legitimate webhook sent while a secret was being rotated got a 401.

  • Every candidate is compared in constant time, and non-v1 entries are ignored.
  • A header that isn't valid UTF-8 is rejected rather than raising.
  • The timestamp and replay-id checks still run only after a signature matches.
  • Single-signature headers behave exactly as before.

See OpenAI Batch Webhook Handler.


🧪 New: bin/live-examples

bin/live-examples (also bundle exec rake examples:live) runs every examples/*.rb against real provider keys and prints one PASS / SKIP / FAIL line per script. It writes a log per script to tmp/live-examples/, exits non-zero only on FAIL, and warns when every script skipped, since that run validated nothing. Development tooling only: bin/ and examples/ are not packaged in the gem. See Examples.


⚠️ Heads-up When Upgrading

  • The 🔗 Net::HTTP interceptor patched log line is gone. If anything matched on that line in your output, use Coolhand::NetHttpInterceptor.patched? instead.
  • Declared workloads and templates need a recent Coolhand server. This follows the server contract in Coolhand-Labs/coolhand#1768. Older servers ignore declared, so nothing breaks, but nothing is attributed either.
  • Context is fiber-local. with_context, with_capture and without_capture don't follow into a new Thread, Fiber or Async task started inside the block. Open the block inside that Fiber instead. See Threads and background logging.

🔒 Security Hardening

  • Server-supplied warnings are sanitized before they are printed. Entries are coerced with to_s, invalid UTF-8 is scrubbed, control characters and line separators become a space, invisible Unicode format characters (bidi overrides, zero-width) are removed, entries over 500 characters are truncated, and at most 5 are printed per response. A misbehaving or self-hosted backend can no longer forge log lines or inject terminal escape sequences into your stdout.

🧹 Other Changes

  • Internal de-duplication with no behavior change. List reads share one helper, webhook rejection and the webhook validator's enforce-or-allow paths are consolidated, and with_capture / without_capture share one override helper. Error messages and strict/lenient behavior by Rails.env are unchanged.
  • CI now runs the exhale duplication and complexity gate. contract/ is excluded from the gem package.
  • Dependency bumps: ruby/setup-ruby 1.329.0 and actions/upload-artifact 7.0.2.

📌 Known Follow-ups

Still open from earlier release reviews: #141 response-body size cap, #142 response-body redaction, #144 shared replay store, #145 absolute-URL req.path matching, #146 remaining test gaps, #120 body-credential redaction beyond Azure, #121 path normalization, #122–#123 webhook behavior (the signature-rotation part of #123 is fixed in this release), #124 forward_webhook headers, #125 streaming capture, #127 redaction gaps.


🔗 Resources