fix(credentials): make workload_identity.rb survive bootsnap's iseq precompile on Ruby 4#192
Conversation
…ile on Ruby 4 The pattern `(Integer | String) => expires_in` parses fine in Ruby 4 (`ruby -c` and `ruby -e` both accept it after anthropics#189) but `RubyVM::InstructionSequence.compile_file` still rejects it with `alternative pattern after variable capture (SyntaxError)`. Bootsnap (Rails' default) iseq-precompiles every required file via `compile_file`, so requiring `anthropic` blows up Rails boot for every user on Ruby 4 + bootsnap. Splitting into two `in` clauses (Integer and String) sidesteps the alternative-pattern shape entirely and is behaviorally identical (both branches `.to_i`). Verified on ruby 4.0.1: `ruby -c`, `ruby file.rb`, and `RubyVM::InstructionSequence.compile_file(file)` all succeed.
|
For anyone landing here from the boot crash: updating Bootsnap to ≥ 1.24.2 works around this without any change to this gem. Bootsnap 1.24.2 added a workaround for the underlying CRuby bug (ruby-lang #22023) — on That said, I think this PR is still worth merging. The Bootsnap workaround is conditional and lossy:
The gem is shipping source that |
Summary
Rewrites the
case datablock inlib/anthropic/credentials/workload_identity.rb#perform_exchangeto avoid an alternative-pattern-with-variable-capture ((Integer | String) => expires_in). That shape parses on Ruby 4 but is rejected byRubyVM::InstructionSequence.compile_file, which Bootsnap (Rails' default) calls on every required file during iseq precompile — sorequire "anthropic"currently aborts Rails boot on Ruby 4 + Bootsnap.The fix splits the offending
inclause into two clauses (Integer / String). Behaviorally identical — both branches.to_i.Why the prior fix (#189 / #188) wasn't enough
#188 reported the original PRISM parse error on
Integer | String => expires_in; #189 added parens —(Integer | String) => expires_in. That maderuby -cand a plainrequirehappy, so the issue was closed.But
RubyVM::InstructionSequence.compile_fileuses a different path inside CRuby than the source-level parser and still rejects the parenthesized form:So #189 silenced the parser error but didn't touch the iseq-compiler error. Rails apps surface the iseq error because Bootsnap (
require "bootsnap/setup"inconfig/boot.rb) routes everyrequirethroughRubyVM::InstructionSequence.compile_fileto populatetmp/cache/bootsnap. Stack trace from a stockrails runneron Ruby 4.0.1 withanthropic1.41.0:This is ultimately a CRuby parser/iseq divergence — both paths should agree. The workaround here is purely defensive: avoid the shape that triggers it. Worth a separate report at https://bugs.ruby-lang.org/ so the SDK can drop this workaround once Ruby fixes the divergence.
The diff
Comment above the block notes why the seemingly redundant split is intentional, so a future codegen pass / cleanup doesn't fold it back into
(Integer | String) => expires_in.Verification
On Ruby 4.0.1:
ruby -c file.rbruby file.rbRubyVM::InstructionSequence.compile_file("file.rb")Same Ruby version, same gem otherwise, just the patched block. End-to-end I'm able to
require "anthropic"from a Rails app withbootsnapenabled after applying this.Generated-code note
CONTRIBUTING.mdsays modifications to generated files persist across regenerations. If this file is regenerated from a Stainless template, the template likely needs the same rewrite so the next regen doesn't undo it. Happy to take a follow-up suggestion on which template to touch — I don't have visibility into the generator config.Out of scope
expect { RubyVM::InstructionSequence.compile_file(workload_identity_path) }.not_to raise_errorif you want it; let me know where it'd fit best (didn't want to guess the suite layout).Related: #188, #189.