Repository navigation
v0.9.0
🚀 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 keyClassName#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.upsertandCoolhand.template_service.upsertcreate or update records by key. They need your private API key and raiseCoolhand::HttpErroron 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
endKeys 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-
v1entries 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 patchedlog line is gone. If anything matched on that line in your output, useCoolhand::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_captureandwithout_capturedon'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_captureshare one override helper. Error messages and strict/lenient behavior byRails.envare unchanged. - CI now runs the exhale duplication and complexity gate.
contract/is excluded from the gem package. - Dependency bumps:
ruby/setup-ruby1.329.0 andactions/upload-artifact7.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.