Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions crates/biorouter-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3052,20 +3052,24 @@ mod new_session_provider_binding_tests {
let working_dir = "/tmp/biorouter-new-chat-private-provider";
let mut overrides = provider_overrides(
"versa_azure",
biorouter::providers::versa_azure::VERSA_AZURE_DEPLOYMENT,
biorouter::providers::versa_azure::VERSA_AZURE_DEFAULT_MODEL,
None,
);
overrides.insert("VERSA_AZURE_API_KEY".into(), "test-api-key".into());
overrides.insert(
"AZURE_OPENAI_ENDPOINT".into(),
"VERSA_AZURE_ENDPOINT".into(),
biorouter::providers::versa_azure::VERSA_AZURE_ENDPOINT.into(),
);
overrides.insert(
"AZURE_OPENAI_DEPLOYMENT_NAME".into(),
biorouter::providers::versa_azure::VERSA_AZURE_DEPLOYMENT.into(),
"VERSA_AZURE_DEPLOYMENT_NAME".into(),
biorouter::providers::versa_azure::deployment_for_model(
biorouter::providers::versa_azure::VERSA_AZURE_DEFAULT_MODEL,
)
.expect("the default model has a deployment")
.into(),
);
overrides.insert(
"AZURE_OPENAI_API_VERSION".into(),
"VERSA_AZURE_API_VERSION".into(),
biorouter::providers::versa_azure::VERSA_AZURE_API_VERSION.into(),
);
let request = || StartAgentRequest {
Expand Down
2 changes: 1 addition & 1 deletion crates/biorouter/src/agents/subagent_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6045,7 +6045,7 @@ mod tests {
"not-a-real-key".to_string(),
),
(
"AZURE_OPENAI_ENDPOINT".to_string(),
"VERSA_AZURE_ENDPOINT".to_string(),
crate::providers::versa_azure::VERSA_AZURE_ENDPOINT.to_string(),
),
])
Expand Down
79 changes: 54 additions & 25 deletions crates/biorouter/src/privacy/config_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,36 +54,50 @@ pub const NOT_CAPABILITY_CONFIG_KEYS: &[(&str, &str)] = &[
("LLAMACPP_TIMEOUT", "transport timeout"),
("LLAMACPP_STARTUP_TIMEOUT", "sidecar readiness deadline"),
("LLAMACPP_CONTEXT_SIZE", "token budget"),
// ⚠ The four endpoint keys below MOVE where a Private-badged provider sends
// traffic, but they cannot RAISE a tier: Task 5 name-keys versa_azure and
// versa_bedrock Private regardless of endpoint, and azure.rs ships the
// UCSF gateway as a PUBLIC provider's default for the same reason.
// Pointing a private-badged provider off-site is a real and different
// problem — it belongs to Task 5's tier definition and to Open question 5,
// not to DR-16 — and it is recorded here rather than left unstated.
(
"AZURE_OPENAI_ENDPOINT",
"moves a Private provider's endpoint; does not raise a tier (see Task 5)",
),
("AZURE_OPENAI_DEPLOYMENT_NAME", "deployment selection"),
("AZURE_OPENAI_API_VERSION", "wire version"),
// Versa's own namespace for the same three overrides. They exist because
// onboarding used to write the `AZURE_OPENAI_*` keys above on Versa's
// behalf, which made the PUBLIC `azure_openai` card report itself
// Configured whenever a user connected UCSF's PRIVATE Versa. Same meaning,
// same classification as their legacy twins: they move a Private
// provider's endpoint, they do not raise a tier.
// ⚠ The two endpoint keys below MOVE where a Private-badged provider sends
// traffic, and since `e2e4eb9d` that moves its tier as well: `tier()`
// follows the endpoint an instance resolved (`ucsf_gateway_tier`), so an
// off-site value demotes it to Public, and deleting that value restores
// Private. These rows used to say the keys "cannot RAISE a tier" because
// Task 5 name-keyed versa_* Private regardless of endpoint, and that
// stopped being true. The classification rests on this instead: the only
// value that reads Private is the UCSF gateway's own host, so no write can
// make an off-site endpoint look Private, and a raise through one of these
// keys is always a return to the institution's gateway. Whether even that
// raise should be a user act, as it is for `OLLAMA_HOST`, is an open DR-16
// question, recorded here rather than left unstated.
//
// Versa Azure's three overrides, in its own namespace. It used to share the
// public `azure_openai` card's `AZURE_OPENAI_*` keys, which went wrong both
// ways: onboarding WROTE them on Versa's behalf, so connecting UCSF's
// PRIVATE Versa made that PUBLIC card report itself Configured (hence this
// namespace, 2026-09-03); and Versa went on READING them as a fallback, so
// whatever that card was set up with — a company resource's endpoint,
// deployment and API version — steered every Versa request (read removed
// 2026-09-11). No tier-input file reads the `AZURE_OPENAI_*` keys now, so
// they have no rows here; `azure.rs` still reads them and is not a
// tier-input file, because `azure_openai` is Public wherever it points.
(
"VERSA_AZURE_ENDPOINT",
"moves a Private provider's endpoint; does not raise a tier (see Task 5)",
"moves a Private provider's endpoint; only the UCSF gateway reads Private (see above)",
),
("VERSA_AZURE_DEPLOYMENT_NAME", "deployment selection"),
("VERSA_AZURE_API_VERSION", "wire version"),
// Versa Bedrock's two overrides, in its own namespace since 2026-09-11. It
// used to declare and read the public Amazon Bedrock card's `AWS_REGION` and
// an `AWS_ENDPOINT_URL_BEDROCK` key, then fall back to the process
// environment, so the public side's values steered Versa and a Versa setup
// configured the public card. No tier-input file reads an `AWS_*` key now,
// so none has a row; `bedrock.rs` still reads them and is not a tier-input
// file, because `aws_bedrock` is Public wherever it points.
(
"VERSA_BEDROCK_ENDPOINT",
"moves a Private provider's endpoint; only the UCSF gateway reads Private (see above)",
),
(
"AWS_ENDPOINT_URL_BEDROCK",
"moves a Private provider's endpoint; does not raise a tier (see Task 5)",
"VERSA_BEDROCK_REGION",
"SigV4 signing region; the endpoint, not the region, decides where a request goes",
),
("AWS_REGION", "region selection"),
("BEDROCK_MAX_RETRIES", "retry policy"),
("BEDROCK_INITIAL_RETRY_INTERVAL_MS", "retry policy"),
("BEDROCK_BACKOFF_MULTIPLIER", "retry policy"),
Expand Down Expand Up @@ -183,10 +197,10 @@ mod tests {
// of the two lists. Adding a config read to any of them fails this test
// until someone decides whether it determines capability. That is the
// checkable list: it does not depend on anyone remembering a rule.
let scanned = scan_get_param_keys(); // 26 today
let scanned = scan_get_param_keys(); // 23 today
assert_eq!(
scanned.len(),
26,
23,
"the tier-input files' config surface changed: {scanned:?}"
);
for key in &scanned {
Expand All @@ -205,6 +219,21 @@ mod tests {
// survives.
assert!(CAPABILITY_CONFIG_KEYS.contains(&"BIOROUTER_PROVIDER"));
assert_eq!(CAPABILITY_CONFIG_KEYS.len(), 5);

// …and the other way round: every classified key is still READ by a
// tier-input file. Without this, a read that goes away leaves its row
// behind — the count above moves, someone edits the number, and the
// lists quietly start classifying keys nothing reads.
let classified = CAPABILITY_CONFIG_KEYS
.iter()
.copied()
.chain(NOT_CAPABILITY_CONFIG_KEYS.iter().map(|(key, _why)| *key));
for key in classified.filter(|key| *key != "BIOROUTER_PROVIDER") {
assert!(
scanned.iter().any(|read| read == key),
"{key} is classified but no tier-input file reads it; delete its row"
);
}
}

#[test]
Expand Down
39 changes: 30 additions & 9 deletions crates/biorouter/src/providers/bedrock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,23 @@ impl BedrockProvider {
set_aws_env_vars(config.all_values());
set_aws_env_vars(config.all_secrets());

// Normalize AWS_ENDPOINT_URL_BEDROCK → AWS_ENDPOINT_URL_BEDROCK_RUNTIME.
// The AWS SDK for Rust reads the service-specific key AWS_ENDPOINT_URL_BEDROCK_RUNTIME,
// but users (and older configs) often set the shorter AWS_ENDPOINT_URL_BEDROCK.
// Accept either: if only the short form is set, promote it to the correct key.
if std::env::var("AWS_ENDPOINT_URL_BEDROCK_RUNTIME").is_err() {
if let Ok(url) = std::env::var("AWS_ENDPOINT_URL_BEDROCK") {
std::env::set_var("AWS_ENDPOINT_URL_BEDROCK_RUNTIME", url);
}
}
// ⚠ `AWS_ENDPOINT_URL_BEDROCK` is not an endpoint for this provider. The
// AWS SDK aims Bedrock Runtime at `AWS_ENDPOINT_URL_BEDROCK_RUNTIME`,
// from the environment or from `config.yaml` through the export above,
// or at an AWS profile's `services` section; that is how a VPC endpoint
// or a proxy is meant to be set. `AWS_ENDPOINT_URL_BEDROCK` is the name
// the SDK derives for a different service, the Bedrock control plane.
//
// This used to promote it to `AWS_ENDPOINT_URL_BEDROCK_RUNTIME`, added
// on 2026-04-12 for configs and setup scripts that used the short name,
// a month before Versa Bedrock existed. But every Biorouter surface that
// has written that key wrote it for Versa Bedrock: its onboarding card on
// every connect, its Settings form on every save, `biorouter configure`
// when asked to. So once Versa was set up, the promotion made UCSF's
// gateway THIS provider's endpoint, and the user's own AWS-signed
// requests went there and were refused. Versa reads its own
// `VERSA_BEDROCK_ENDPOINT` now, but installs keep the old key, so it has
// to be ignored here, not merely left unwritten (2026-09-11).

// Use load_defaults() which supports AWS SSO, profiles, and environment variables
let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest());
Expand Down Expand Up @@ -137,6 +145,19 @@ impl BedrockProvider {
})
}

/// The same client with only its HTTP transport replaced. Everything
/// `from_env` resolved — endpoint, region, credentials — is kept, so a
/// request captured through it is the request production would have sent.
#[cfg(test)]
pub(crate) fn with_http_client(
mut self,
http_client: impl aws_sdk_bedrockruntime::config::HttpClient + 'static,
) -> Self {
let config = self.client.config().to_builder().http_client(http_client);
self.client = Client::from_conf(config.build());
self
}

fn load_retry_config(config: &crate::config::Config) -> RetryConfig {
let max_retries = config
.get_param::<usize>("BEDROCK_MAX_RETRIES")
Expand Down
Loading
Loading