Skip to content

Commit 0f4e29b

Browse files
committed
fix(web): enforce egress policy on a2a_discover agent card URL
1 parent 0e2e0c0 commit 0f4e29b

3 files changed

Lines changed: 104 additions & 3 deletions

File tree

crates/rexos-tools/src/ops/web/a2a/discover/url.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ pub(super) async fn agent_card_url(
77
security: &SecurityConfig,
88
) -> anyhow::Result<reqwest::Url> {
99
let mut url = reqwest::Url::parse(url).context("parse url")?;
10+
url.set_path("/.well-known/agent.json");
11+
url.set_query(None);
12+
url.set_fragment(None);
1013
super::super::super::ensure_remote_url_allowed(
1114
&url,
1215
allow_private,
@@ -15,8 +18,5 @@ pub(super) async fn agent_card_url(
1518
security,
1619
)
1720
.await?;
18-
url.set_path("/.well-known/agent.json");
19-
url.set_query(None);
20-
url.set_fragment(None);
2121
Ok(url)
2222
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"choices": [
4+
{
5+
"index": 0,
6+
"message": {
7+
"role": "assistant",
8+
"content": null,
9+
"tool_calls": [
10+
{
11+
"id": "call_1",
12+
"type": "function",
13+
"function": {
14+
"name": "a2a_discover",
15+
"arguments": "{\"url\":\"http://127.0.0.1/\",\"allow_private\":true}"
16+
}
17+
}
18+
]
19+
},
20+
"finish_reason": "tool_calls"
21+
}
22+
]
23+
}
24+
]
25+

crates/rexos/tests/session_replay.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,3 +846,79 @@ async fn replay_fixture_blocks_egress_policy_method_mismatches() {
846846

847847
server.abort();
848848
}
849+
850+
#[tokio::test]
851+
#[serial]
852+
async fn replay_fixture_blocks_a2a_discover_egress_path_mismatches() {
853+
let fixture = support::openai_compat_fixture::load_json_array(include_str!(
854+
"fixtures/replay/session_egress_policy_a2a_discover_path_block.json"
855+
));
856+
let server = support::openai_compat_fixture::FixtureServer::spawn(fixture).await;
857+
858+
let tmp = tempfile::tempdir().unwrap();
859+
let security = rexos::security::SecurityConfig {
860+
egress: EgressConfig {
861+
rules: vec![EgressRule {
862+
tool: "a2a_discover".to_string(),
863+
host: "127.0.0.1".to_string(),
864+
path_prefix: "/ok".to_string(),
865+
methods: vec!["GET".to_string()],
866+
}],
867+
},
868+
..Default::default()
869+
};
870+
871+
let (agent, _paths, workspace_root) = fixture_agent(&tmp, server.base_url.clone(), security);
872+
873+
let session_id = "s-replay-egress-a2a-discover";
874+
agent
875+
.set_session_allowed_tools(session_id, vec!["a2a_discover".to_string()])
876+
.unwrap();
877+
878+
let err = agent
879+
.run_session(
880+
workspace_root,
881+
session_id,
882+
None,
883+
"discover agent card",
884+
TaskKind::Coding,
885+
)
886+
.await
887+
.unwrap_err();
888+
let err_text = err.to_string();
889+
assert!(
890+
err_text.contains("egress path not allowed"),
891+
"expected egress path block, got: {err_text}"
892+
);
893+
assert!(
894+
err_text.contains("/.well-known/agent.json"),
895+
"expected agent card path in error, got: {err_text}"
896+
);
897+
assert!(
898+
err_text.contains("a2a_discover"),
899+
"expected tool name in error, got: {err_text}"
900+
);
901+
902+
let requests = server.requests.lock().unwrap().clone();
903+
assert_eq!(requests.len(), 1, "expected one chat completions call");
904+
assert_eq!(
905+
compact_request(&requests[0]),
906+
json!({
907+
"model": "fixture-model",
908+
"temperature": 0.0,
909+
"tools": [{
910+
"name": "a2a_discover",
911+
"type": "function",
912+
"param_type": "object",
913+
"required": ["url"],
914+
"properties": ["allow_private", "url"],
915+
"additional_properties": false,
916+
}],
917+
"message_roles": ["user"],
918+
"assistant_tool_calls": [],
919+
"tool_messages": [],
920+
})
921+
);
922+
923+
server.abort();
924+
}

0 commit comments

Comments
 (0)