Skip to content

fix(credentials): make workload_identity.rb survive bootsnap's iseq precompile on Ruby 4#192

Open
nbrustein wants to merge 1 commit into
anthropics:mainfrom
nbrustein:fix-workload-identity-iseq-compile
Open

fix(credentials): make workload_identity.rb survive bootsnap's iseq precompile on Ruby 4#192
nbrustein wants to merge 1 commit into
anthropics:mainfrom
nbrustein:fix-workload-identity-iseq-compile

Conversation

@nbrustein

Copy link
Copy Markdown

Summary

Rewrites the case data block in lib/anthropic/credentials/workload_identity.rb#perform_exchange to avoid an alternative-pattern-with-variable-capture ((Integer | String) => expires_in). That shape parses on Ruby 4 but is rejected by RubyVM::InstructionSequence.compile_file, which Bootsnap (Rails' default) calls on every required file during iseq precompile — so require "anthropic" currently aborts Rails boot on Ruby 4 + Bootsnap.

The fix splits the offending in clause 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 made ruby -c and a plain require happy, so the issue was closed.

But RubyVM::InstructionSequence.compile_file uses a different path inside CRuby than the source-level parser and still rejects the parenthesized form:

$ ruby -v
ruby 4.0.1 ...
$ ruby -c lib/anthropic/credentials/workload_identity.rb
Syntax OK
$ ruby -e 'require "./lib/anthropic"' # works (no bootsnap)
$ ruby -e 'RubyVM::InstructionSequence.compile_file("lib/anthropic/credentials/workload_identity.rb")'
.../workload_identity.rb:148: alternative pattern after variable capture (SyntaxError)
...> token, expires_in: (Integer | String) => expires_in}
...                              ^

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" in config/boot.rb) routes every require through RubyVM::InstructionSequence.compile_file to populate tmp/cache/bootsnap. Stack trace from a stock rails runner on Ruby 4.0.1 with anthropic 1.41.0:

SyntaxError:
/usr/local/.../anthropic-1.41.0/lib/anthropic/credentials/workload_identity.rb:161:
  alternative pattern after variable capture (SyntaxError)
  from .../bootsnap-1.24.1/lib/bootsnap/compile_cache/iseq.rb:51:in 'RubyVM::InstructionSequence.compile_file'
  from .../bootsnap-1.24.1/lib/bootsnap/compile_cache/iseq.rb:51:in 'Bootsnap::CompileCache::ISeq::Compiler#input_to_storage'
  ...
  from /usr/local/.../anthropic-1.41.0/lib/anthropic.rb:60:in 'Kernel#require_relative'

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

# Before (post-#189, current main)
case data
in {access_token: String => token, expires_in: (Integer | String) => expires_in}
  [token, expires_in.to_i]
in {access_token: String => token}
  [token, 3600]
else
  raise WorkloadIdentityError, "Token endpoint response missing access_token field"
end

# After
case data
in {access_token: String => token, expires_in: Integer => expires_in}
  [token, expires_in.to_i]
in {access_token: String => token, expires_in: String => expires_in}
  [token, expires_in.to_i]
in {access_token: String => token}
  [token, 3600]
else
  raise WorkloadIdentityError, "Token endpoint response missing access_token field"
end

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:

check before (parens) after (split)
ruby -c file.rb OK OK
ruby file.rb OK OK
RubyVM::InstructionSequence.compile_file("file.rb") SyntaxError OK

Same Ruby version, same gem otherwise, just the patched block. End-to-end I'm able to require "anthropic" from a Rails app with bootsnap enabled after applying this.

Generated-code note

CONTRIBUTING.md says 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

  • No formatting / unrelated changes.
  • No version bump or changelog edit; assume that's handled by the release tooling.
  • No regression test added — happy to add one along the lines of expect { RubyVM::InstructionSequence.compile_file(workload_identity_path) }.not_to raise_error if you want it; let me know where it'd fit best (didn't want to guess the suite layout).

Related: #188, #189.

…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.
@nbrustein nbrustein requested a review from a team as a code owner May 15, 2026 13:28
@nbrustein

Copy link
Copy Markdown
Author

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 +PRISM builds it reroutes RubyVM::InstructionSequence.compile_file through compile_file_prism (which accepts (Integer | String) => var), and otherwise rescues the SyntaxError and skips iseq-caching that file. I confirmed this on Ruby 4.0.1 +PRISM: boot crashes on Bootsnap 1.24.1 and is fine on 1.24.2+.

That said, I think this PR is still worth merging. The Bootsnap workaround is conditional and lossy:

  • On non-PRISM Ruby 4.0 builds, Bootsnap falls back to marking the file UNCOMPILABLE — so workload_identity.rb silently never gets iseq-cached.
  • Anyone iseq-precompiling the gem without Bootsnap (AOT/preload tooling, custom caches) still hits the hard SyntaxError.

The gem is shipping source that compile_file rejects; this 9-line, behaviorally-identical change fixes that at the source regardless of consumer. The Bootsnap update is the right immediate unblock; this is the right real fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant