Skip to content
Open
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
9 changes: 6 additions & 3 deletions packages/agentenv-protocol/src/agentenv_protocol/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def granted_headers(url: str, grants: dict[str | None, dict[str, str]]) -> dict[
async def _rpc(base_url: str, method: str, params: dict, timeout: int, verify: bool,
headers: dict[str, str] | None) -> dict:
request = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
url = f"{base_url}{RPC_PATH}"
url = f"{base_url.rstrip('/')}{RPC_PATH}"
async with httpx.AsyncClient(verify=verify) as client:
response = await client.post(url, json=request, timeout=timeout, headers=request_headers(url, headers))
response.raise_for_status()
Expand Down Expand Up @@ -117,7 +117,7 @@ async def get_data(

async def get_card(base_url: str, timeout: int = 10, verify: bool = True) -> dict:
async with httpx.AsyncClient(verify=verify) as client:
response = await client.get(f"{base_url}{WELL_KNOWN_PATH}", timeout=timeout)
response = await client.get(f"{base_url.rstrip('/')}{WELL_KNOWN_PATH}", timeout=timeout)
response.raise_for_status()
return response.json()

Expand Down Expand Up @@ -341,7 +341,10 @@ async def invoke_extension(base_url: str, card: dict, uri: str, params: dict | N
raise ValueError(f"extension {uri} does not advertise method {method!r} (advertises: {offered})")
if not op["endpoint"]:
raise ValueError(f"extension {uri} has no endpoint in params")
url = f"{base_url}{op['endpoint']}"
endpoint = op["endpoint"]
if not endpoint.startswith("/"):
endpoint = f"/{endpoint}"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
url = f"{base_url.rstrip('/')}{endpoint}"
sent = request_headers(url, headers)
async with httpx.AsyncClient(verify=verify) as client:
if op["method"] == "GET":
Expand Down
51 changes: 51 additions & 0 deletions packages/agentenv-protocol/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ async def test_get_card_gets_well_known():
assert client.get.call_args.args[0] == f"{_BASE}/.well-known/agent-env.json"


@pytest.mark.asyncio
async def test_rpc_and_card_strip_trailing_slashes_from_base_url():
base_with_slash = f"{_BASE}/"
client = _client(_resp({"jsonrpc": "2.0", "id": 1, "result": {}}))
with _patch(client):
await protocol_v1.reset_data(base_with_slash)
assert client.post.call_args.args[0] == f"{_BASE}/agentenv"

client = _client(_resp({"name": "items"}))
with _patch(client):
await protocol_v1.get_card(base_with_slash)
assert client.get.call_args.args[0] == f"{_BASE}/.well-known/agent-env.json"


@pytest.mark.asyncio
async def test_supports_v1_true_on_card():
with _patch(_client(_resp({"name": "items"}))):
Expand Down Expand Up @@ -270,6 +284,36 @@ async def test_invoke_extension_posts_to_advertised_endpoint():
assert kwargs["json"] == {"tool_name": "list_items", "error_rate": 0.75}


@pytest.mark.asyncio
async def test_invoke_extension_strips_trailing_slashes_from_base_url():
base_with_slash = f"{_BASE}/"
client = _client(_resp({"tool_name": "list_items", "error_rate": 0.75}))
with _patch(client):
await protocol_v1.invoke_extension(
base_with_slash, _CARD_SET_ERRORS, "urn:agentenv:set-errors/v1",
{"tool_name": "list_items", "error_rate": 0.75},
)
args, _ = client.request.call_args
assert args[1] == f"{_BASE}/agentenv/ext/set_errors"


@pytest.mark.asyncio
async def test_invoke_extension_ensures_leading_slash_on_relative_endpoint():
card = {"capabilities": {"extensions": [{
"uri": "urn:agentenv:set-errors/v1",
"params": {"endpoint": "agentenv/ext/set_errors",
"methods": {"set_errors": {"method": "POST", "request": {}}}},
}]}}
client = _client(_resp({"tool_name": "list_items", "error_rate": 0.75}))
with _patch(client):
await protocol_v1.invoke_extension(
_BASE, card, "urn:agentenv:set-errors/v1",
{"tool_name": "list_items", "error_rate": 0.75},
)
args, _ = client.request.call_args
assert args[1] == f"{_BASE}/agentenv/ext/set_errors"


@pytest.mark.asyncio
async def test_invoke_extension_get_uses_query_params():
card = {"capabilities": {"extensions": [{
Expand Down Expand Up @@ -396,3 +440,10 @@ async def test_invoke_extension_unknown_method_raises_without_calling():
async def test_invoke_extension_named_method_on_a_methodless_extension_raises():
with pytest.raises(ValueError, match=re.escape("does not advertise method 'set_time' (advertises: none)")):
await protocol_v1.invoke_extension(_BASE, _CARD_WITH_EXT, "urn:agentenv:clock/v1", method="set_time")


@pytest.mark.asyncio
async def test_invoke_extension_missing_endpoint_raises_value_error():
card = {"capabilities": {"extensions": [{"uri": "urn:agentenv:test/v1", "params": {}}]}}
with pytest.raises(ValueError, match="has no endpoint in params"):
await protocol_v1.invoke_extension(_BASE, card, "urn:agentenv:test/v1")