Skip to content

Commit 923278d

Browse files
committed
fix: add qualifier=DEFAULT back
1 parent b5ca362 commit 923278d

3 files changed

Lines changed: 57 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ image = { version = "0.25", default-features = false, features = ["jpeg", "png",
2727
unicode-width = "0.2"
2828
pulldown-cmark = { version = "0.13", default-features = false }
2929
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
30+
rustls = { version = "0.22", optional = true }
31+
tokio-rustls = { version = "0.25", optional = true }
32+
webpki-roots = { version = "0.26", optional = true }
3033
cron = "0.16.0"
3134
chrono = { version = "0.4.44", features = ["serde"] }
3235
chrono-tz = "0.10.4"
@@ -43,7 +46,7 @@ http = { version = "1", optional = true }
4346
[features]
4447
default = ["secrets-aws", "agentcore"]
4548
secrets-aws = ["dep:aws-sdk-secretsmanager", "dep:aws-config"]
46-
agentcore = ["dep:aws-config", "dep:aws-sigv4", "dep:aws-credential-types", "dep:urlencoding", "dep:hex", "dep:http"]
49+
agentcore = ["dep:aws-config", "dep:aws-sigv4", "dep:aws-credential-types", "dep:urlencoding", "dep:hex", "dep:http", "dep:rustls", "dep:tokio-rustls", "dep:webpki-roots"]
4750

4851
[target.'cfg(unix)'.dependencies]
4952
libc = "0.2"

src/acp/agentcore.rs

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ struct ShellHandle {
108108
}
109109

110110
type WsSink = futures_util::stream::SplitSink<
111-
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
111+
tokio_tungstenite::WebSocketStream<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>,
112112
Message,
113113
>;
114114

@@ -198,7 +198,7 @@ where
198198
.unwrap_or_default()
199199
.as_millis();
200200
let acp_sid = format!("agentcore-{ts}");
201-
let runtime_sid = format!("oab-session-{ts}");
201+
let runtime_sid = format!("oab-session-{ts:020}-{ts:013x}");
202202

203203
// Eagerly open shell + initialize the agent
204204
match self.open_shell(&runtime_sid).await {
@@ -376,6 +376,7 @@ where
376376
}
377377
}
378378

379+
#[allow(dead_code)]
379380
fn derive_runtime_session_id(&self, params: &Value) -> String {
380381
// Try to extract from sender_context in prompt blocks
381382
if let Some(blocks) = params.get("prompt").and_then(|p| p.as_array()) {
@@ -414,11 +415,38 @@ where
414415
}
415416

416417
async fn open_shell(&self, session_id: &str) -> Result<ShellHandle> {
417-
let request = build_signed_request(&self.runtime_arn, session_id, &self.region).await?;
418+
let (request, host) = build_signed_request(&self.runtime_arn, session_id, &self.region).await?;
419+
420+
// Manual TLS connection — gives us full control, avoids connect_async host override
421+
let tcp = tokio::net::TcpStream::connect(format!("{host}:443"))
422+
.await
423+
.map_err(|e| anyhow!("TCP connect to {host}:443 failed: {e}"))?;
424+
425+
let connector = tokio_tungstenite::Connector::Rustls(std::sync::Arc::new(
426+
rustls::ClientConfig::builder()
427+
.with_root_certificates(rustls::RootCertStore {
428+
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
429+
})
430+
.with_no_client_auth(),
431+
));
432+
433+
let tls_stream = match connector {
434+
tokio_tungstenite::Connector::Rustls(cfg) => {
435+
let domain = rustls::pki_types::ServerName::try_from(host.as_str())
436+
.map_err(|e| anyhow!("bad DNS: {e}"))?
437+
.to_owned();
438+
tokio_rustls::TlsConnector::from(cfg)
439+
.connect(domain, tcp)
440+
.await
441+
.map_err(|e| anyhow!("TLS failed: {e}"))?
442+
}
443+
_ => unreachable!(),
444+
};
418445

419-
let (ws_stream, _) = tokio_tungstenite::connect_async(request)
446+
// client_async performs the WebSocket upgrade using our exact request
447+
let (ws_stream, _) = tokio_tungstenite::client_async(request, tls_stream)
420448
.await
421-
.map_err(|e| anyhow!("WebSocket connect failed: {e}"))?;
449+
.map_err(|e| anyhow!("WebSocket upgrade failed: {e}"))?;
422450

423451
info!(session_id, "AgentCore shell connected");
424452

@@ -571,7 +599,7 @@ async fn build_signed_request(
571599
arn: &str,
572600
session_id: &str,
573601
region: &str,
574-
) -> Result<http::Request<()>> {
602+
) -> Result<(http::Request<()>, String)> {
575603
let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
576604
.region(aws_config::Region::new(region.to_string()))
577605
.load()
@@ -594,15 +622,18 @@ async fn build_signed_request(
594622
let hash = Sha256::digest(session_id.as_bytes());
595623
let shell_id = format!("oab-{}", hex::encode(&hash[..8]));
596624

625+
// Include session_id in query so it's covered by the presigned signature
597626
let query = format!(
598-
"shellId={shell_id}&X-Amzn-Bedrock-AgentCore-Runtime-Session-Id={}",
627+
"qualifier=DEFAULT&shellId={shell_id}&X-Amzn-Bedrock-AgentCore-Runtime-Session-Id={}",
599628
urlencoding::encode(session_id),
600629
);
601-
602630
let uri = format!("https://{host}{path}?{query}");
603631

632+
// Use presigned URL (auth in query string) — avoids tungstenite header issues
604633
let mut settings = SigningSettings::default();
605634
settings.expires_in = Some(std::time::Duration::from_secs(300));
635+
settings.uri_path_normalization_mode =
636+
aws_sigv4::http_request::UriPathNormalizationMode::Enabled;
606637

607638
let signing_params = v4::SigningParams::builder()
608639
.identity(&identity)
@@ -613,30 +644,28 @@ async fn build_signed_request(
613644
.build()?;
614645

615646
let headers = [("host", host.as_str())];
616-
617647
let signable = SignableRequest::new("GET", &uri, headers.into_iter(), SignableBody::empty())?;
618-
619648
let (instructions, _sig) = sign(signable, &signing_params.into())?.into_parts();
620649

621-
// Build presigned URL with auth in query params
622-
let mut signed_query = query.clone();
623-
let (_headers, params) = instructions.into_parts();
650+
// Append SigV4 query params to build the presigned URL
651+
let mut presigned_query = query.clone();
652+
let (_hdrs, params) = instructions.into_parts();
624653
for (name, value) in params {
625-
signed_query.push_str(&format!("&{name}={value}"));
654+
presigned_query.push_str(&format!("&{name}={value}"));
626655
}
627-
let wss_uri = format!("wss://{host}{path}?{signed_query}");
628656

629-
let builder = http::Request::builder()
657+
let wss_uri = format!("wss://{host}{path}?{presigned_query}");
658+
659+
// Build request with WebSocket upgrade headers (not signed, just protocol-required)
660+
let request = http::Request::builder()
630661
.method("GET")
631662
.uri(&wss_uri)
632663
.header("host", &host)
633664
.header("connection", "Upgrade")
634665
.header("upgrade", "websocket")
635666
.header("sec-websocket-version", "13")
636-
.header(
637-
"sec-websocket-key",
638-
tokio_tungstenite::tungstenite::handshake::client::generate_key(),
639-
);
667+
.header("sec-websocket-key", tokio_tungstenite::tungstenite::handshake::client::generate_key())
668+
.body(())?;
640669

641-
Ok(builder.body(())?)
670+
Ok((request, host))
642671
}

0 commit comments

Comments
 (0)