Conversation
ead61fa to
c83b0d1
Compare
|
Rebased onto current That surfaced one real break with no textual conflict: Verified both directions, since "optional" is easy to claim and easy to get wrong:
CI still hasn't run on this PR; its workflow runs remain at |
|
Currently we are trying to make rig-core a lot more minimal, so I believe that rig-core is the wrong place for this integration. I think rig-bedrock is a better place for the integration, or perhaps a separate rig-sigv4 crate. I don't want to add tokio or the various aws libraries as rig-core dependencies. What do you think? |
|
Agreed — Your objection to tokio in
I'll restructure and push to this branch rather than opening a new PR, unless you'd prefer a fresh one against |
|
Moved in 8041855: the integration now lives in |
…ait method
Lets the Anthropic provider talk to an Anthropic-compatible endpoint that sits
behind AWS SigV4, without changing anything for anyone who does not opt in.
Why not rig-bedrock
-------------------
rig-bedrock speaks the Bedrock Runtime InvokeModel API. This is the other
shape: an endpoint that accepts Anthropic's own /v1/messages request body, and
therefore carries Anthropic-shaped parameters, but authenticates with SigV4
instead of an x-api-key header. Different wire protocols against different
endpoints, so neither substitutes for the other.
What changed
------------
`AnthropicCompatibleProvider` gains one method with a default:
fn sigv4_region(&self) -> Option<&str> { None }
Returning `None` — the default — means no signing is attempted, so every
existing implementor is unaffected and none needs editing. `AnthropicExt`
overrides it; `AnthropicKey` gains a `SigV4 { region }` variant alongside
`ApiKey(String)`.
Going through the trait rather than a concrete field is deliberate: completions
run through `GenericCompletionModel<Ext, T>`, so a concrete field would not be
reachable from the generic path. This follows the provider-integration
checklist in CONTRIBUTING.md, which asks for wire-dialect differences to live
in the trait's hooks.
Signing happens in both request builders, immediately before the body is
attached and never earlier: the SigV4 payload hash covers the exact bytes
sent, so signing has to follow every mutation of the body. The SigV4 variant
emits no static header, because the signature covers the clock as well as the
body and so cannot be computed once at client construction.
Feature-gating
--------------
Behind a default-off `sigv4` feature. Default-off on purpose: the AWS
credential chain has no place in a wasm build, nor in builds that only ever
talk to api.anthropic.com.
Selecting SigV4 in a build without the feature is a hard error, not a silently
unsigned request — an unsigned request would 401 with a message about a
missing API key, which points at the wrong problem.
`#[cfg_attr(not(feature = "sigv4"), allow(unused_mut))]` on the request builder
keeps the no-feature build warning-free; without it the `mut` that only signing
needs would warn in every default build.
Two aws-config features are load-bearing, and both fail at RUNTIME rather than
at compile time, so `cargo check` cannot catch either. The workspace pins
aws-config with `default-features = false` (rig-bedrock supplies its own), while
`load_defaults` walks the full credential chain:
default-https-client — without it: "a http_client is required"
rt-tokio — without it: "An async sleep implementation is required
for retry to work"
aws-config was already a workspace dependency; aws-credential-types and
aws-sigv4 are added as bare-major floors, consistent with the documented policy
in the root manifest, since this code uses only long-stable API from both.
Verification
------------
cargo clippy -p rig-core --features sigv4 --all-targets 0 warnings
cargo test -p rig-core --features sigv4 --lib 1551 passed, 0 failed
cargo test -p rig-core --lib 1550 passed, 0 failed
The last line is the control that matters for existing users: it proves the
default-off path is unaffected. The +1 with the feature on is the new
`signed_headers_never_include_host` test, which asserts the signer does not
emit a `host` header — the HTTP client sets its own, and sending both breaks
the signature. That test carries its own anti-vacuity assertion, because
"no host header" passes trivially if nothing was produced at all.
`cargo fmt -- --check` is a required CI step and it was failing on three lines this branch added: one `for` header in completion.rs and two error closures in sigv4.rs. The same three diffs are present at the pre-merge tip of this branch, so they predate the merge; main itself is clean. Whitespace only, applied by `cargo fmt` on the toolchain rust-toolchain.toml pins (1.94.0). No behavior change.
CI runs `cargo check --package rig-core --all-features --target
wasm32-unknown-unknown` (ci.yaml, the rig-core leg of the wasm matrix).
`--all-features` means the new `sigv4` feature is on for that build, and
it failed to compile: 59 errors, from `compile_error!` in mio ("This wasm
target is unsupported by mio") and follow-on type errors in socket2.
The chain is sigv4 -> aws-config/default-https-client ->
aws-smithy-http-client -> hyper-rustls -> hyper-util -> tokio's `net`
feature -> mio/socket2. Present at the pre-merge tip of this branch too,
so it is not a merge artifact; main passes the same command.
Dropping `default-https-client` would only move the failure to runtime,
where `load_defaults` reports "a http_client is required". There is no
socket for the AWS credential chain to use on wasm either way, so the
dependencies are now declared in a `cfg(not(target_arch = "wasm32"))`
table and the module and its two call sites are gated to match. Native
targets are unchanged; a wasm caller that selects SigV4 gets the existing
hard error, whose message now names both ways of reaching it.
…-bedrock
Review feedback on this PR asked for exactly this: rig-core is being kept
minimal, and neither tokio nor the AWS libraries belong in it. rig-bedrock was
named as the better home over a new rig-sigv4 crate, because it needs no new
crate published and already carries the AWS credential chain.
An earlier attempt on this branch gated the dependencies instead, declaring them
optional and behind a `cfg(not(target_arch = "wasm32"))` table. That is not what
was asked for. The dependencies were still in rig-core's manifest, so
`cargo tree -p rig-core -e normal --all-features` still resolved 18 AWS crates
and tokio. They are gone now.
What moved
----------
Everything that needs an AWS dependency:
crates/rig-core/src/providers/anthropic/sigv4.rs
-> crates/rig-bedrock/src/anthropic/sigv4.rs
plus a new `crates/rig-bedrock/src/anthropic.rs` holding what used to be spread
through rig-core's Anthropic client: the `AnthropicKey` enum with its
`SigV4 { region }` variant and `sigv4()` constructor, the signing region on the
provider value, and the `Client`/`ClientBuilder` aliases. The provider is
`rig_bedrock::anthropic::AnthropicOnAws`. It implements rig-core's `Provider`,
`HasCompletion` and `AnthropicCompatibleProvider`, and builds its models from
`GenericCompletionModel`, so the Anthropic request and response mapping is
reused rather than duplicated.
`crates/rig-core/Cargo.toml`, `providers/anthropic/client.rs` and
`providers/anthropic/mod.rs` are byte-identical to main again.
Unlike before, there is no default endpoint. Signing a request to
api.anthropic.com is never what the caller meant, and a wrong regional default
would produce a 403 that reads like a credential problem, so the base URL comes
from `ClientBuilder::base_url` or `ANTHROPIC_BASE_URL` and building without one
fails immediately.
What stays in rig-core, and why
-------------------------------
One defaulted method on `AnthropicCompatibleProvider`, carrying no dependency:
fn signed_headers(&self, method: &str, uri: &str, body: &[u8])
-> impl Future<Output = Result<Vec<(String, String)>, CompletionError>>
+ WasmCompatSend
{ let _ = (method, uri, body); async { Ok(Vec::new()) } }
Signing has to happen inside rig-core's two Anthropic request builders, because
that is where the body becomes final and a SigV4 payload hash covers the exact
bytes sent. rig-bedrock cannot reach into those builders, so rig-core has to
ask. Handing back headers rather than taking a region is what keeps the AWS side
out: rig-core never learns that AWS exists, it just applies whatever the
provider returned. The default returns nothing, so the macro-generated
compatible providers (zai, minimax, moonshot, xiaomimimo) are untouched, and the
`--all-features` wasm32 build needs no gating because nothing is left to gate.
The previous hook, `sigv4_region() -> Option<&str>`, could not stay. A region is
only useful to code that can sign, and that code is no longer here.
`default_max_tokens_for_model` also becomes `pub`. The endpoint serves
Anthropic's own models, so the relocated provider needs Anthropic's own output
limits. Without it, `max_tokens` would become mandatory on every request through
the new client, or the provider would have to invent a flat limit that truncates
Opus.
Dependencies
------------
rig-bedrock gains `aws-credential-types` and `aws-sigv4`, both already in the
workspace table, plus `http`, and `bytes` as a dev-dependency. `aws-config`
gains `default-https-client` and `rt-tokio`: load-bearing rather than cosmetic,
and each one fails at runtime rather than at compile time, so the reasons now
sit in the manifest beside them. tokio was already a full dependency here, which
is why `sigv4.rs` keeps using `tokio::sync::OnceCell` to cache the resolved
`SdkConfig`. `std::sync::OnceLock` is not a substitute: initialization awaits
`load_defaults`, and `OnceLock` has no async initializer.
One dead item did not make the trip, a `static WARNED: OnceLock<()>` that was
set and never read. The message it claimed to produce once is an error return,
so it was produced on every failure regardless.
Verification
------------
`cargo tree -p rig-core -e normal`, AWS and tokio entries:
before after
default features 0 / 0 0 / 0
--all-features 63 / 15 0 / 0
The 63 AWS entries spanned 18 distinct crates, the 15 tokio entries 3.
`--all-features` bounds every feature combination, since features only add, so
zero there is zero everywhere. The whole `--all-features` tree drops from 554
lines to 297.
On the toolchain rust-toolchain.toml pins (rustc 1.94.0):
cargo check -p rig-core ok
cargo check -p rig-core --all-features ok
cargo check -p rig-core --all-features --target wasm32-... ok
cargo check -p rig-bedrock ok
cargo check -p rig-bedrock --all-features ok
cargo check -p rig --features bedrock,... --all-targets ok
cargo fmt -- --check ok
cargo clippy -p rig-core --all-features --all-targets 0 warnings
cargo clippy -p rig-bedrock --all-features --all-targets 0 warnings
Tests, `--lib`:
rig-core, default features 1638 -> 1638
rig-core, --all-features 1697 -> 1696 (the moved test)
rig-bedrock 113 -> 120 (+1 moved, +6 new)
`signed_headers_never_include_host` moved with the file and still passes.
Two of the six new tests drive a real completion and a real stream through
rig-core and assert the signature arrived on the request the transport saw.
Every piece here can be individually correct while the hook is simply never
called, and an unsigned request fails with a 403 that reads like a credential
problem rather than a wiring one. Both were checked against a negative control:
stubbing out either call site in rig-core fails the matching test, and only that
one.
rig-bedrock does not build for wasm32, before this change or after. The AWS SDK
reaches tokio's `net` feature and mio has a hard `compile_error!` there. That is
inherent to a crate whose job is calling AWS, and it is the other half of the
argument for putting the credential chain here rather than in a crate that has
to build for the browser.
…oint `VerifyClient::verify` is a blanket implementation over every rig-core `Client`. It sends a plain GET to `Provider::VERIFY_PATH` with the client's default headers, and a SigV4 client deliberately has none: the signature covers the body and the clock, so it cannot be precomputed at construction. Signing is applied at the two Anthropic request builders, which `verify` does not go through, so it returns 403 rather than a credential verdict. This is not new — the same was true when the integration lived in rig-core and `AnthropicKey::SigV4` produced no header there either. It was just easier to miss without a provider of its own to document it against. Closing it would mean a second signing hook in rig-core, this time covering arbitrary client requests rather than the two Anthropic request builders. That is more surface in the crate this change was about emptying than one verification convenience is worth, so the limitation is documented and a small completion is the suggested substitute. Docs only. `cargo fmt -- --check`, `cargo doc -p rig-bedrock --no-deps` (no warnings, so the intra-doc links resolve) and `cargo clippy -p rig-bedrock --all-features --all-targets` all clean.
Addresses five review findings on the SigV4 integration. They land together
because the trait signature, its implementation and the credential cache cross
the same two files, and no subset of them compiles on its own.
The signing hook took the request URI as a string, and the signer recovered the
host by splitting on "://" and "/". That re-parsed an authority the builder had
already parsed, and got two cases wrong:
- An explicit default port. base_url("https://host:443/anthropic") signed
"host:443", but the transport sends "Host: host". hyper_util's client omits
a port matching the scheme default, and rig-reqwest hands the URI to
reqwest as a string, so the url crate drops the port while re-parsing it.
- Userinfo. "https://user@host/" signed "user@host".
Either way the canonical request cannot be reproduced and every call fails 401
"the request signature we calculated does not match" -- the same opaque failure
already recorded here for the duplicated host header, and equally invisible to
a test that only asserts the Authorization header's shape.
The hook now takes &http::Uri, so the host is read off the parsed authority and
the re-parse is gone. This changes the public signature of
AnthropicCompatibleProvider::signed_headers, which has no implementors outside
this repository yet.
Also:
- The resolved AWS credential chain moved off a process-wide static and onto
the provider value. Two clients built under different profiles or roles
used to sign as whichever chain resolved first, which at best is a 403
naming an unexpected principal and at worst a call billed to the wrong
account. Resolution is still deferred to the first request, because
Provider::build is synchronous; that residual is documented where the cache
is defined.
- AnthropicKey no longer derives Debug, which printed the API key verbatim.
Nothing logged it, so this was latent.
- The module doc claimed signing had to live in rig-core's request builders
because rig-bedrock could not reach the final body. That is wrong: an
HttpClientExt decorator sees a fully built request, strictly later. The
actual obstacle is that Provider::from_env and from_val return
Client<Self, H> for the caller's H, so a decorator cannot be installed
inside them, and moving it to the caller makes a forgotten wrapper a
silently unsigned request. The doc now says that, states the cost of the
design chosen instead -- two call sites that must stay in step -- and stops
claiming a second rig-core hook would be needed to fix verify().
- default_max_tokens_for_model stays public, with the real reason. The old
one was falsifiable: rig-bedrock could reach the same value through
<Anthropic as AnthropicCompatibleProvider>::default_max_tokens.
Tests: the host derivation is pinned case by case against the rule read out of
hyper_util and the url crate, and the port's effect on the signature is
asserted end to end. That needed the signing instant to become a parameter,
since a signature covers the time. Both new signature tests failed before this
change and pass after; a control asserts a non-default port still does reach
the signature, so the equality test cannot pass by ignoring the host.
40351ea to
547ff27
Compare
Fixes #2386
Adds optional AWS SigV4 authentication to the Anthropic provider, so it can
reach an Anthropic-compatible endpoint that sits behind AWS. Behind a
default-off
sigv4feature; nothing changes for anyone who doesn't opt in.Implementation
AnthropicCompatibleProvidergains one method with a default:Nonemeans no signing is attempted, so every existing implementor isunaffected and none needed editing.
AnthropicExtoverrides it;AnthropicKeygains aSigV4 { region }variant alongsideApiKey(String).Going through the trait rather than a concrete field is deliberate — completions
run through
GenericCompletionModel<Ext, T>, so a concrete field isn'treachable from the generic path. This follows the provider-integration
checklist in
CONTRIBUTING.md: wire-dialect differences belong in the trait'shooks.
Signing happens in both request builders, immediately before the body is
attached and never earlier. The SigV4 payload hash covers the exact bytes sent,
so signing has to follow every mutation of the body — and the SigV4 variant
emits no static header, because the signature covers the clock as well as the
body and can't be computed once at client construction.
Selecting SigV4 in a build without the feature is a hard error, not a silently
unsigned request: an unsigned request 401s with a message about a missing API
key, which points at the wrong problem.
Two things worth a reviewer's attention
The
sigv4feature enables twoaws-configfeatures, and both areload-bearing. The workspace pins
aws-configwithdefault-features = false(rig-bedrock supplies its own), while
load_defaultswalks the full credentialchain:
default-https-clienta http_client is requiredrt-tokioAn async sleep implementation is required for retry to workBoth fail at runtime, not compile time, so
cargo checkcan't catch either— I only found them by running the tests. Note also that feature unification
means enabling
sigv4givesrig-bedrock'saws-configthose features too.That's additive, but it's a real consequence and I'd rather flag it than have it
discovered later.
aws-credential-typesandaws-sigv4are new workspace dependencies, addedas bare-major floors per the documented policy in the root manifest, since this
code uses only long-stable API from both.
aws-configwas already there.Verification
The last line is the control that matters for existing users — it's what proves
the default-off path is unaffected.
The +1 with the feature on is
signed_headers_never_include_host, which assertsthe signer doesn't emit a
hostheader: the HTTP client sets its own, andsending both breaks the signature. It carries an anti-vacuity assertion too,
because "no host header" passes trivially if nothing was produced at all.
Not included
No cassette-backed regression test. Signing is request-shaping rather than a
response-parsing behaviour, so the assertion that matters is on the emitted
header list, which is what the included test does. Happy to add one if you'd
prefer the coverage there instead.
Also in this diff:
output_configOutputConfig.formatbecomesOption<OutputFormat>, andOutputConfiggains asibling
effort: Option<String>carrying Anthropic's adaptive-thinking effortlevel. This is unrelated to the signing change; it lets a caller set an effort
level through
additional_paramswith or without a structured-output schema.Lifting a caller-supplied
output_configout ofadditional_paramsis whatmakes the two coexist: that field is
#[serde(flatten)], so leaving it in placewould emit a second
output_configkey next to the typed one, and a duplicateJSON key means one of the two is silently dropped.
The
sigv4feature is native-onlyThe AWS dependencies are declared under
[target.'cfg(not(target_arch = "wasm32"))'.dependencies], so--all-featuresstill compiles for wasm32. Selecting SigV4 on that target hits the same hard
error as a build with the feature off, because the credential chain has no
socket to use there.
Update: moved to
rig-bedrockPer your review: rig-core stays minimal, and neither tokio nor the AWS libraries
belong in it.
rig-bedrockover a newrig-sigv4crate, since it needs no newcrate published and already carries the AWS credential chain.
The first attempt at this only gated the dependencies, declaring them optional
under
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]. That missed thepoint — they were still in rig-core's manifest, so
cargo treestill resolvedthem. They are now gone outright.
What moved
providers/anthropic/sigv4.rsmoved tocrates/rig-bedrock/src/anthropic/sigv4.rs,and a new
crates/rig-bedrock/src/anthropic.rsholds what used to be spreadthrough rig-core's Anthropic client: the
AnthropicKeyenum with itsSigV4 { region }variant andsigv4()constructor, the signing region on theprovider value, and the
Client/ClientBuilderaliases.The provider is
rig_bedrock::anthropic::AnthropicOnAws. It implements rig-core'sProvider,HasCompletionandAnthropicCompatibleProvider, and builds itsmodels from
GenericCompletionModel, so the Anthropic request and responsemapping is reused rather than duplicated:
crates/rig-core/Cargo.toml,providers/anthropic/client.rsandproviders/anthropic/mod.rsare byte-identical tomainagain.There is no default endpoint any more. Signing a request to
api.anthropic.comis never what the caller meant, and a wrong regional default would produce a 403
that reads like a credential problem, so the base URL comes from
ClientBuilder::base_urlorANTHROPIC_BASE_URLand building without one failsimmediately.
What stays in rig-core
One defaulted method on
AnthropicCompatibleProvider, carrying no dependency:Handing back headers rather than taking a region is what keeps the AWS side out:
rig-core never learns that AWS exists, it just applies whatever the provider
returned.
Why a hook here at all, given that
HttpClientExtis also a place where the bodyis final — and a later one. A
SigV4HttpClient<H>decorator would see a fullybuilt
http::Request<T>, withT: Into<bytes::Bytes>making the payloadhashable, and would need no rig-core change, would sign at one site instead of
two, and would cover
verify().What rules it out is the transport type's position in
Provider::from_envandProvider::from_val. Both arefn(..., http: H) -> ProviderClientResult<Client<Self, H>>, so the transport thecaller passes in is the transport the returned client is typed on, and
Client<Self, SigV4HttpClient<H>>is not the declared return type. The decoratorcannot be installed inside them. Moving the wrapping out to the caller would make
AnthropicKey::sigv4(region)plus a forgotten wrapper a silently unsignedrequest, which is the one failure this design exists to prevent.
The cost of the hook, stated plainly: it has two call sites in rig-core that must
stay in step, and a third Anthropic request path added there later will not sign,
with nothing failing when that happens. If you would rather pay the other price —
one signing site,
verify()working, and the wrapper as the caller'sresponsibility — that is a reasonable call, and it is a change to
from_env/from_valrather than to this provider.The default returns nothing, so the macro-generated compatible providers (zai,
minimax, moonshot, xiaomimimo) are still untouched.
sigv4_region()could notstay — a region is only useful to code that can sign, and that code is no longer
there.
default_max_tokens_for_modelalso becomespub. The endpoint serves Anthropic'sown models, so the relocated provider needs Anthropic's own output limits.
To be accurate about the alternative: there is another path to the same value.
<Anthropic as AnthropicCompatibleProvider>::default_max_tokens(model)returnsexactly this and is already public, so rig-bedrock could reach it without
exporting anything new. The reason not to is that it makes a claim about models
— Anthropic's published output limits — reachable only through one particular
client type, so every Anthropic-dialect provider would have to depend on the
Anthropicclient to learn its own defaults. Happy to invert that if you preferthe smaller public surface; it is a two-line change on this side.
Dependency counts
cargo tree -p rig-core -e normal, AWS and tokio entries:--all-featuresThe 63 AWS entries spanned 18 distinct crates, the 15 tokio entries 3.
--all-featuresbounds every feature combination, since features only add, sozero there is zero everywhere. The whole
--all-featurestree drops from 554lines to 297.
rig-bedrock gains
aws-credential-typesandaws-sigv4(already in theworkspace table),
http, andbytesas a dev-dependency. It also enablesaws-config'sdefault-https-clientandrt-tokio— the same two load-bearingfeatures flagged above, now on the crate that was already going to pay for them,
so the feature-unification consequence I noted earlier no longer applies.
sigv4.rskeepstokio::sync::OnceCellfor the cachedSdkConfig;std::sync::OnceLockis not a substitute, because initialization awaitsload_defaultsandOnceLockhas no async initializer. tokio was already afull dependency of rig-bedrock, so this stops being a problem the moment the
file lands here.
One dead item did not make the trip: a
static WARNED: OnceLock<()>that was setand never read. The message it claimed to produce once is an error return, so it
was produced on every failure anyway.
Verification
On the toolchain
rust-toolchain.tomlpins (rustc 1.94.0):Tests,
--lib:--all-featuressigned_headers_never_include_hostmoved with the file and still passes; that isthe
-1above and one of the+13.Two of the new tests drive a real completion and a real stream through
rig-core and assert the signature arrived on the request the transport saw. Every
piece here can be individually correct while the hook is simply never called, and
an unsigned request fails with a 403 that reads like a credential problem rather
than a wiring one. Both were checked against a negative control: stubbing out
either call site in rig-core fails the matching test, and only that one.
Superseded above
sigv4feature enables twoaws-configfeatures" — there is nosigv4feature any more, and the feature-unification note no longer applies:the features are enabled directly on rig-bedrock's own
aws-config.aws-credential-typesandaws-sigv4are new workspace dependencies" —still true, but they are consumed by rig-bedrock now, not rig-core.
sigv4feature is native-only" — thecfg(not(target_arch = "wasm32"))table is gone. rig-core's--all-featureswasm32 build passes with nothing to gate. rig-bedrock does not build for
wasm32, before this change or after, because the AWS SDK reaches tokio's
netfeature and mio has a hard
compile_error!there — which is the other half ofthe argument for putting the credential chain in this crate.
output_config" — unchanged and still in the diff.rig-bedrock cannot reach into them" — wrong, and replaced above. The body is
also final at the
HttpClientExtboundary, strictly later. The real obstacle isthe transport type in
from_env/from_val's return position.verify()cannot be closed without a second signing hook in rig-core" —wrong for the same reason. A transport decorator would close it.
Update: review fixes
A self-review after the move found two bugs, both in the signing path.
The signed host did not match the host on the wire. The hook took the URI as
a
&strand the signer recovered the host by splitting on"://"and"/",re-parsing an authority the request builder had already parsed. Two cases came
out wrong:
base_url("https://host:443/anthropic")signedhost:443, but the transport sendsHost: host.hyper_util's client omits aport equal to the scheme default, and
rig-reqwestpasses the URI to reqwest asa string, so the
urlcrate drops the port while re-parsing.https://user@host/signeduser@host.Either way the canonical request cannot be reproduced, and every call fails 401
"the request signature we calculated does not match" — the same opaque failure
already documented in this diff for the duplicated host header, and equally
invisible to a test that only asserts the Authorization header's shape.
The hook now takes
&http::Uriand the host is read off the parsed authority.This changes the signature of a public trait method,
AnthropicCompatibleProvider::signed_headers. It has no implementors outside thisrepository yet, which seemed like the moment to get it right; say the word if you
would rather keep
&strand normalise inside rig-bedrock.Two clients could not use two AWS identities. The resolved credential chain
was cached in a process-wide
static, so the first client to sign fixed theidentity every later client signed with. A process building one client under one
profile or assumed role and a second expecting another would sign both as
whichever resolved first — at best a 403 naming an unexpected principal, at worst
a call authorized and billed against the wrong account. The cache is now a field
on the provider value, so it is per client, and clones share it. Resolution is
still deferred to the first request because
Provider::buildis synchronous;that residual is documented where the cache is defined.
Also:
AnthropicKeyno longer derivesDebug, which printed the API keyverbatim. Nothing logged it, so it was latent rather than a live leak.
Verification reran clean —
fmt, bothclippyinvocations at 0 warnings, allfour
checkvariants including--target wasm32-unknown-unknown, andcargo tree -p rig-core -e normal --all-featuresstill at zero AWS and zerotokio crates.
The two new signature tests were checked red before the fix and green after: the
host derivation reported
bedrock-mantle.us-east-1.api.aws:443where thetransport sends
bedrock-mantle.us-east-1.api.aws, and the two signaturesdiffered. A control asserts a non-default port does still reach the signature,
so the equality test cannot pass by ignoring the host. Pinning that needed the
signing instant to become a parameter, since a signature covers the time.