From 8b30211aa959189b88b903490d7470997e351904 Mon Sep 17 00:00:00 2001 From: st-gr <38470677+st-gr@users.noreply.github.com> Date: Mon, 1 Jun 2026 05:02:31 -0700 Subject: [PATCH 1/2] feat(sandbox): allow AWS Bedrock InvokeModel paths through the L7 router MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two patterns to `default_patterns()` so the supervisor's L7 inference router recognizes the Bedrock InvokeModel URL shape and forwards matched requests to the registered upstream: - `POST /model/{modelId}/invoke` → aws_bedrock_invoke - `POST /model/{modelId}/invoke-with-response-stream` → aws_bedrock_invoke_stream The `{modelId}` segment is wildcarded by extending `detect_inference_pattern` to handle one middle `/*/` segment in addition to the existing trailing `/*`. The wildcard is constrained to a single non-empty path segment to avoid path-traversal liabilities — `/model//invoke` and `/model/a/b/invoke` both no-match. Without this, sandboxes running Claude Code in its native Bedrock mode (`CLAUDE_CODE_USE_BEDROCK=1`, `ANTHROPIC_BEDROCK_BASE_URL`, AWS-style auth) hit the supervisor with `403 connection not allowed by policy` because their URL doesn't match `/v1/*` shapes. The fix unblocks operators wanting to register direct AWS Bedrock, an in-cluster Bedrock-compatible bridge, or a Bedrock-emulating LiteLLM as `--type aws-bedrock` providers. Tests cover: positive matches for invoke + invoke-with-response-stream, query-string handling, GET rejection, empty-segment rejection, multi-segment rejection, and unknown-action rejection. Companion changes (provider discovery spec + YAML profile) follow in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --- crates/openshell-sandbox/src/l7/inference.rs | 98 +++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/openshell-sandbox/src/l7/inference.rs b/crates/openshell-sandbox/src/l7/inference.rs index acda0bb36..8dee88919 100644 --- a/crates/openshell-sandbox/src/l7/inference.rs +++ b/crates/openshell-sandbox/src/l7/inference.rs @@ -16,7 +16,7 @@ pub struct InferenceApiPattern { pub kind: String, } -/// Default patterns for known inference APIs (`OpenAI`, Anthropic). +/// Default patterns for known inference APIs (`OpenAI`, Anthropic, AWS Bedrock). pub fn default_patterns() -> Vec { vec![ InferenceApiPattern { @@ -55,10 +55,31 @@ pub fn default_patterns() -> Vec { protocol: "model_discovery".to_string(), kind: "models_get".to_string(), }, + // AWS Bedrock InvokeModel + InvokeModelWithResponseStream. The `*` + // segment is the Bedrock model id (e.g. `anthropic.claude-opus-4-7`). + InferenceApiPattern { + method: "POST".to_string(), + path_glob: "/model/*/invoke".to_string(), + protocol: "aws_bedrock_invoke".to_string(), + kind: "messages".to_string(), + }, + InferenceApiPattern { + method: "POST".to_string(), + path_glob: "/model/*/invoke-with-response-stream".to_string(), + protocol: "aws_bedrock_invoke_stream".to_string(), + kind: "messages".to_string(), + }, ] } /// Check if an HTTP request matches a known inference API pattern. +/// +/// Path globs support two wildcard shapes (one per pattern, not both): +/// - **Trailing `/*`**: `/v1/models/*` matches `/v1/models` and any +/// `/v1/models/` (one or many path segments). +/// - **Middle `/*/`**: `/model/*/invoke` matches `/model//invoke` +/// for a single non-empty segment that contains no `/`. Used for +/// AWS Bedrock's `/model/{modelId}/invoke[-with-response-stream]`. pub fn detect_inference_pattern<'a>( method: &str, path: &str, @@ -78,6 +99,21 @@ pub fn detect_inference_pattern<'a>( .is_some_and(|suffix| suffix.starts_with('/')); } + if let Some((before, after)) = p.path_glob.split_once("/*/") { + let Some(rest) = path_only.strip_prefix(before) else { + return false; + }; + let Some(rest) = rest.strip_prefix('/') else { + return false; + }; + // rest must look like `/` where is non-empty + // and contains no `/` (single path segment). + let Some(slash_at) = rest.find('/') else { + return false; + }; + return slash_at > 0 && rest[slash_at + 1..] == *after; + } + path_only == p.path_glob }) } @@ -445,6 +481,66 @@ mod tests { assert!(result.is_none()); } + #[test] + fn detect_aws_bedrock_invoke() { + let patterns = default_patterns(); + let result = + detect_inference_pattern("POST", "/model/anthropic.claude-opus-4-7/invoke", &patterns); + assert!(result.is_some()); + assert_eq!(result.unwrap().protocol, "aws_bedrock_invoke"); + assert_eq!(result.unwrap().kind, "messages"); + } + + #[test] + fn detect_aws_bedrock_invoke_stream() { + let patterns = default_patterns(); + let result = detect_inference_pattern( + "POST", + "/model/anthropic.claude-opus-4-7/invoke-with-response-stream", + &patterns, + ); + assert!(result.is_some()); + assert_eq!(result.unwrap().protocol, "aws_bedrock_invoke_stream"); + } + + #[test] + fn aws_bedrock_invoke_with_query_string() { + let patterns = default_patterns(); + let result = detect_inference_pattern("POST", "/model/foo.bar/invoke?trace=1", &patterns); + assert!(result.is_some()); + assert_eq!(result.unwrap().protocol, "aws_bedrock_invoke"); + } + + #[test] + fn aws_bedrock_rejects_empty_model_id() { + let patterns = default_patterns(); + // `/model//invoke` — empty wildcard segment is not a valid Bedrock id. + assert!(detect_inference_pattern("POST", "/model//invoke", &patterns).is_none()); + } + + #[test] + fn aws_bedrock_rejects_multi_segment_model_id() { + let patterns = default_patterns(); + // The `*` matches a single path segment only; multi-segment ids must + // not match (would be a path-traversal liability otherwise). + assert!(detect_inference_pattern("POST", "/model/foo/bar/invoke", &patterns).is_none()); + } + + #[test] + fn aws_bedrock_rejects_get() { + let patterns = default_patterns(); + assert!( + detect_inference_pattern("GET", "/model/anthropic.claude-opus-4-7/invoke", &patterns) + .is_none() + ); + } + + #[test] + fn aws_bedrock_rejects_unknown_action() { + let patterns = default_patterns(); + assert!(detect_inference_pattern("POST", "/model/foo/converse", &patterns).is_none()); + } + #[test] fn parse_simple_post_request() { let body = b"{\"hello\":true}"; From 6b51e1a63c10d90c014d099c047262113aacf466 Mon Sep 17 00:00:00 2001 From: st-gr <38470677+st-gr@users.noreply.github.com> Date: Mon, 1 Jun 2026 05:03:09 -0700 Subject: [PATCH 2/2] feat(providers): add aws-bedrock provider profile + discovery spec Adds `aws-bedrock` to the built-in provider catalog so operators can run `openshell provider create --type aws-bedrock --credential ...` and have the gateway treat it as a first-class inference provider alongside `anthropic`, `openai`, etc. - `providers/aws-bedrock.yaml`: YAML profile declaring four credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION). Default endpoint is `bedrock-runtime.us-east-1.amazonaws.com:443`; operators in other regions or running against a Bedrock-compatible proxy override via the operator-supplied `BEDROCK_BASE_URL` config-key (mirrors `ANTHROPIC_BASE_URL` for the `anthropic` provider). - `crates/openshell-providers/src/providers/aws_bedrock.rs`: the `ProviderDiscoverySpec` so `openshell provider create --auto-providers` picks up AWS_* env vars from local credentials. - `crates/openshell-providers/src/providers/mod.rs`: register the module. - `crates/openshell-providers/src/lib.rs`: register the SPEC in the default registry alongside the other providers. - `crates/openshell-providers/src/profiles.rs`: include the new YAML in `BUILT_IN_PROFILE_YAMLS`. What this PR explicitly does NOT add (intentionally separated for review-size reasons; will follow up): - A SigV4 signer in `openshell-router`. The current change simply declares the protocol; a follow-up PR adds outbound SigV4 signing using the `aws-sigv4` crate and a new `auth_style: sigv4` validator branch in profiles.rs. Operators who don't need SigV4 (e.g. an in-cluster bridge that ignores it and authenticates separately to the upstream) can use this PR today. - Body translation between Bedrock InvokeModel shape and other inference shapes. The router treats Bedrock requests as opaque pass-through; if the operator's upstream is real AWS Bedrock it speaks Bedrock natively, if it's a translating bridge the bridge does any conversion server-side. - `BEDROCK_BASE_URL` placeholder substitution in the YAML loader. Today the YAML's `host` is a literal default; operators override with the config-key the same way `ANTHROPIC_BASE_URL` works. Tested: `cargo test -p openshell-providers` (35 tests green) and `cargo test -p openshell-sandbox --lib l7::inference` (40 tests green including the seven new aws_bedrock cases from the previous commit). Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --- crates/openshell-providers/src/lib.rs | 1 + crates/openshell-providers/src/profiles.rs | 1 + .../src/providers/aws_bedrock.rs | 20 ++++++++++ .../openshell-providers/src/providers/mod.rs | 1 + providers/aws-bedrock.yaml | 38 +++++++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 crates/openshell-providers/src/providers/aws_bedrock.rs create mode 100644 providers/aws-bedrock.yaml diff --git a/crates/openshell-providers/src/lib.rs b/crates/openshell-providers/src/lib.rs index 21a1750ab..3f5d03b00 100644 --- a/crates/openshell-providers/src/lib.rs +++ b/crates/openshell-providers/src/lib.rs @@ -115,6 +115,7 @@ impl ProviderRegistry { registry.register(providers::generic::GenericProvider); registry.register(providers::openai::SPEC); registry.register(providers::anthropic::SPEC); + registry.register(providers::aws_bedrock::SPEC); registry.register(providers::nvidia::SPEC); registry.register(providers::gitlab::SPEC); registry.register(providers::github::SPEC); diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 68cc06260..8c51c4796 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -17,6 +17,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::OnceLock; const BUILT_IN_PROFILE_YAMLS: &[&str] = &[ + include_str!("../../../providers/aws-bedrock.yaml"), include_str!("../../../providers/claude-code.yaml"), include_str!("../../../providers/github.yaml"), include_str!("../../../providers/nvidia.yaml"), diff --git a/crates/openshell-providers/src/providers/aws_bedrock.rs b/crates/openshell-providers/src/providers/aws_bedrock.rs new file mode 100644 index 000000000..d696774f8 --- /dev/null +++ b/crates/openshell-providers/src/providers/aws_bedrock.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::ProviderDiscoverySpec; + +pub const SPEC: ProviderDiscoverySpec = ProviderDiscoverySpec { + id: "aws-bedrock", + credential_env_vars: &[ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + "AWS_REGION", + ], +}; + +test_discovers_env_credential!( + discovers_aws_bedrock_env_credentials, + "AWS_ACCESS_KEY_ID", + "AKIA-test-key" +); diff --git a/crates/openshell-providers/src/providers/mod.rs b/crates/openshell-providers/src/providers/mod.rs index dfe5935a1..57a8f2053 100644 --- a/crates/openshell-providers/src/providers/mod.rs +++ b/crates/openshell-providers/src/providers/mod.rs @@ -31,6 +31,7 @@ macro_rules! test_discovers_env_credential { }; } pub mod anthropic; +pub mod aws_bedrock; pub mod claude; pub mod codex; pub mod copilot; diff --git a/providers/aws-bedrock.yaml b/providers/aws-bedrock.yaml new file mode 100644 index 000000000..90bdcce1d --- /dev/null +++ b/providers/aws-bedrock.yaml @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +id: aws-bedrock +display_name: AWS Bedrock +description: Anthropic + Mistral + Llama models served via the AWS Bedrock InvokeModel API +category: inference +inference_capable: true +credentials: + - name: aws_access_key_id + description: AWS access key id used for SigV4 signing of outbound Bedrock requests + env_vars: [AWS_ACCESS_KEY_ID] + required: true + - name: aws_secret_access_key + description: AWS secret access key paired with aws_access_key_id + env_vars: [AWS_SECRET_ACCESS_KEY] + required: true + - name: aws_session_token + description: Optional session token for temporary credentials (STS, IAM Roles for Service Accounts) + env_vars: [AWS_SESSION_TOKEN] + required: false + - name: aws_region + description: AWS region the Bedrock endpoint resolves into (e.g. us-east-1) + env_vars: [AWS_REGION, AWS_DEFAULT_REGION] + required: true +discovery: + credentials: [aws_access_key_id, aws_secret_access_key, aws_region] +endpoints: + # Default endpoint targets us-east-1 since the YAML loader does not yet + # substitute the `{region}` placeholder. Operators in other regions + # override via the `BEDROCK_BASE_URL` config-key the same way the + # `anthropic` provider accepts `ANTHROPIC_BASE_URL`. + - host: bedrock-runtime.us-east-1.amazonaws.com + port: 443 + protocol: rest + access: read-write + enforcement: enforce +binaries: [/usr/bin/claude, /usr/local/bin/claude]