mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-05 07:37:50 +00:00
* test(agents): cover backend card URL leak to non-admin callers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): assert backend URLs before card parity Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): scope unit card regressions to one agent Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): isolate card redaction from access policy loading Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): remove unused proxy fixture binding Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): normalize unit test imports Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): pass endpoint base URL to redaction helper Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): cover proxy card deep-copy behavior Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(agents): rewrite non-admin agent card URLs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(agents): simplify proxy card URL helper Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(agents): drop card mapping adapter import Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(agents): use typed card mapping helper Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): cover alternate card interfaces and real access checks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(agents): drop alternate backend interfaces from non-admin cards Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(agents): probe the real health check in card URL test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
495 lines
22 KiB
Python
495 lines
22 KiB
Python
import itertools
|
|
import json
|
|
import uuid
|
|
from collections.abc import Callable
|
|
from typing import Final, Literal
|
|
|
|
import pytest
|
|
from integration._support.client import Gateway, Scenario
|
|
from integration._support.database import read_rows, write_rows
|
|
from integration._support.wire import Reply, Request, wire_server
|
|
from pydantic import BaseModel, ConfigDict, JsonValue
|
|
|
|
|
|
class _JsonRpcResult(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
jsonrpc: str
|
|
id: str
|
|
result: dict[str, JsonValue]
|
|
|
|
|
|
class _JsonRpcErrorBody(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
code: int
|
|
message: str
|
|
data: dict[str, JsonValue]
|
|
|
|
|
|
class _JsonRpcError(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
jsonrpc: Literal["2.0"]
|
|
id: str | int | None
|
|
error: _JsonRpcErrorBody
|
|
|
|
|
|
_STREAM_EVENTS: Final = (
|
|
{"kind": "status-update", "taskId": "t1", "contextId": "c1", "status": {"state": "working"}, "final": False},
|
|
{
|
|
"kind": "artifact-update",
|
|
"taskId": "t1",
|
|
"contextId": "c1",
|
|
"artifact": {"artifactId": "a1", "parts": [{"kind": "text", "text": "chunk"}]},
|
|
},
|
|
{"kind": "status-update", "taskId": "t1", "contextId": "c1", "status": {"state": "completed"}, "final": True},
|
|
)
|
|
_STREAM_RESULTS: Final = (
|
|
{"contextId": "c1", "final": False, "kind": "status-update", "status": {"state": "working"}, "taskId": "t1"},
|
|
{
|
|
"append": False,
|
|
"artifact": {"artifactId": "a1", "parts": [{"kind": "text", "text": "chunk"}]},
|
|
"contextId": "c1",
|
|
"kind": "artifact-update",
|
|
"lastChunk": False,
|
|
"taskId": "t1",
|
|
},
|
|
{"contextId": "c1", "final": True, "kind": "status-update", "status": {"state": "completed"}, "taskId": "t1"},
|
|
)
|
|
|
|
|
|
def _peer_card(url: str, name: str) -> dict[str, JsonValue]:
|
|
return {
|
|
"protocolVersion": "0.3",
|
|
"name": name,
|
|
"description": "Synthetic peer",
|
|
"version": "1.0.0",
|
|
"url": url + "/",
|
|
"supportedInterfaces": [
|
|
{
|
|
"url": url + "/",
|
|
"protocolBinding": "JSONRPC",
|
|
"protocolVersion": "0.3",
|
|
}
|
|
],
|
|
"capabilities": {"streaming": True},
|
|
"defaultInputModes": ["text"],
|
|
"defaultOutputModes": ["text"],
|
|
"skills": [],
|
|
}
|
|
|
|
|
|
def _register_agent(
|
|
gateway: Gateway,
|
|
scenario: Scenario,
|
|
name: str,
|
|
url: str,
|
|
agent_card: dict[str, JsonValue] | None = None,
|
|
) -> str:
|
|
card: Final = _peer_card(url, name) if agent_card is None else agent_card
|
|
created: Final = gateway.request("POST", "/v1/agents", {"agent_name": name, "agent_card_params": card})
|
|
assert created.status_code == 200, created.text
|
|
identity: Final = created.json()["agent_id"]
|
|
|
|
def cleanup() -> None:
|
|
deleted: Final = gateway.request("DELETE", f"/v1/agents/{identity}")
|
|
assert deleted.status_code == 200, deleted.text
|
|
assert read_rows('SELECT agent_id FROM "LiteLLM_AgentsTable" WHERE agent_id=%s', (identity,)) == []
|
|
|
|
scenario.cleanups.callback(cleanup)
|
|
return identity
|
|
|
|
|
|
def test_agent_permissions_on_keys_teams_and_access_groups_gate_a2a_send_and_card_before_the_upstream(
|
|
gateway: Gateway,
|
|
) -> None:
|
|
marker: Final = "a2aacl" + uuid.uuid4().hex[:12]
|
|
|
|
def upstream(request: Request) -> Reply:
|
|
if request.method == "GET":
|
|
return Reply(body=json.dumps(_peer_card(wire.url, marker)).encode())
|
|
body: Final = json.loads(request.body)
|
|
if body["method"] == "message/stream":
|
|
frames: Final = tuple(
|
|
f"data: {json.dumps({'jsonrpc': '2.0', 'id': body['id'], 'result': event})}\n\n".encode()
|
|
for event in _STREAM_EVENTS
|
|
)
|
|
return Reply(content_type="text/event-stream", chunks=frames)
|
|
return Reply(
|
|
body=json.dumps(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": body["id"],
|
|
"result": {
|
|
"kind": "message",
|
|
"role": "agent",
|
|
"messageId": "peer-message",
|
|
"parts": [{"kind": "text", "text": "granted"}],
|
|
},
|
|
}
|
|
).encode()
|
|
)
|
|
|
|
with wire_server(upstream) as wire, gateway.scenario() as scenario:
|
|
allowed_name: Final = marker + "-allowed"
|
|
denied_name: Final = marker + "-denied"
|
|
allowed: Final = _register_agent(gateway, scenario, allowed_name, wire.url)
|
|
denied: Final = _register_agent(gateway, scenario, denied_name, wire.url)
|
|
group: Final = gateway.request(
|
|
"POST", "/v1/access_group", {"access_group_name": marker + "-group", "access_agent_ids": [allowed]}
|
|
)
|
|
assert group.status_code == 201, group.text
|
|
group_id: Final = group.json()["access_group_id"]
|
|
|
|
def delete_group() -> None:
|
|
deleted: Final = gateway.request("DELETE", f"/v1/access_group/{group_id}")
|
|
assert deleted.status_code == 204, deleted.text
|
|
assert (
|
|
read_rows(
|
|
'SELECT access_group_id FROM "LiteLLM_AccessGroupTable" WHERE access_group_id=%s', (group_id,)
|
|
)
|
|
== []
|
|
)
|
|
|
|
scenario.cleanups.callback(delete_group)
|
|
tag: Final = marker + "-tag"
|
|
write_rows(
|
|
'UPDATE "LiteLLM_AgentsTable" SET agent_access_groups = ARRAY[%s] WHERE agent_id = %s', (tag, allowed)
|
|
)
|
|
callers: Final = {
|
|
"key grant": scenario.key(object_permission={"agents": [allowed]}),
|
|
"team grant": scenario.key(team_id=scenario.team(object_permission={"agents": [allowed]})),
|
|
"team grant narrowed by key": scenario.key(
|
|
team_id=scenario.team(object_permission={"agents": [allowed, denied]}),
|
|
object_permission={"agents": [allowed]},
|
|
),
|
|
"access group": scenario.key(access_group_ids=[group_id]),
|
|
"declared agent access group": scenario.key(object_permission={"agent_access_groups": [tag]}),
|
|
}
|
|
message: Final = {
|
|
"kind": "message",
|
|
"role": "user",
|
|
"messageId": marker + "-in",
|
|
"parts": [{"kind": "text", "text": "ping"}],
|
|
}
|
|
send_payload: Final = {
|
|
"jsonrpc": "2.0",
|
|
"id": marker,
|
|
"method": "message/send",
|
|
"params": {"message": message},
|
|
}
|
|
stream_payload: Final = {
|
|
"jsonrpc": "2.0",
|
|
"id": marker,
|
|
"method": "message/stream",
|
|
"params": {"message": message},
|
|
}
|
|
|
|
def refusal(identity: str) -> str:
|
|
return json.dumps(
|
|
{"detail": f"Agent '{identity}' is not allowed for your key/team. Contact proxy admin for access."},
|
|
separators=(",", ":"),
|
|
)
|
|
|
|
def jsonrpc_refusal(text: str, identity: str) -> bool:
|
|
error: Final = _JsonRpcError.model_validate_json(text)
|
|
return (error.id, error.error.code, error.error.message) == (
|
|
marker,
|
|
-32000,
|
|
f"Agent '{identity}' is not allowed for your key/team. Contact proxy admin for access.",
|
|
)
|
|
|
|
spellings: Final = (
|
|
("send by id", lambda agent, name: f"/a2a/{agent}", send_payload, "message/send"),
|
|
("send by name", lambda agent, name: f"/a2a/{name}", send_payload, "message/send"),
|
|
("send v1 alias", lambda agent, name: f"/v1/a2a/{agent}/message/send", send_payload, "message/send"),
|
|
("send alias", lambda agent, name: f"/a2a/{agent}/message/send", send_payload, "message/send"),
|
|
("stream", lambda agent, name: f"/a2a/{agent}", stream_payload, "message/stream"),
|
|
)
|
|
denied_tasks: Final = (
|
|
(
|
|
"tasks/get",
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": marker,
|
|
"method": "tasks/get",
|
|
"params": {"id": "t1"},
|
|
},
|
|
),
|
|
(
|
|
"GetTask",
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": marker,
|
|
"method": "GetTask",
|
|
"params": {"id": "t1"},
|
|
},
|
|
),
|
|
(
|
|
"tasks/pushNotificationConfig/set",
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": marker,
|
|
"method": "tasks/pushNotificationConfig/set",
|
|
"params": {
|
|
"taskId": "t1",
|
|
"pushNotificationConfig": {"url": "https://callback.example.com/hook"},
|
|
},
|
|
},
|
|
),
|
|
)
|
|
|
|
def attempt(key: str, agent: str, name: str) -> tuple[tuple[str, int, str], ...]:
|
|
def call(
|
|
spelling: str, path_of: Callable[[str, str], str], payload: dict[str, JsonValue]
|
|
) -> tuple[str, int, str]:
|
|
headers: Final = {
|
|
"Authorization": f"Bearer {key}",
|
|
**({"Accept": "text/event-stream"} if payload is stream_payload else {}),
|
|
}
|
|
response = gateway.client.post(path_of(agent, name), headers=headers, json=payload)
|
|
return spelling, response.status_code, response.read().decode()
|
|
|
|
return tuple(call(label, path_of, payload) for label, path_of, payload, _ in spellings)
|
|
|
|
def card(key: str, identity: str) -> tuple[int, str]:
|
|
response = gateway.client.get(
|
|
f"/a2a/{identity}/.well-known/agent-card.json", headers={"Authorization": f"Bearer {key}"}
|
|
)
|
|
return response.status_code, response.text
|
|
|
|
for label, key in callers.items():
|
|
for spelling, status, text in attempt(key, denied, denied_name):
|
|
identity: Final = denied_name if "by name" in spelling else denied
|
|
assert status == 403 and jsonrpc_refusal(text, identity), f"{label} {spelling}: {status} {text}"
|
|
for spelling, payload in denied_tasks:
|
|
response: Final = gateway.client.post(
|
|
f"/a2a/{denied}", headers={"Authorization": f"Bearer {key}"}, json=payload
|
|
)
|
|
assert response.status_code == 403 and jsonrpc_refusal(response.text, denied), (
|
|
f"{label} {spelling}: {response.status_code} {response.text}"
|
|
)
|
|
assert card(key, denied) == (403, refusal(denied)), label
|
|
assert wire.drain() == (), label
|
|
for spelling, status, text in attempt(key, allowed, allowed_name):
|
|
assert status == 200, f"{label} {spelling}: {status} {text}"
|
|
if "stream" in spelling:
|
|
frames: Final = text.split("\n\n")
|
|
assert frames[-1] == "" and all(frame.startswith("data: ") for frame in frames[:-1]), (
|
|
f"{label} {spelling}: {text}"
|
|
)
|
|
parsed: Final = tuple(
|
|
_JsonRpcResult.model_validate_json(frame.removeprefix("data: ")) for frame in frames[:-1]
|
|
)
|
|
assert tuple(frame.result for frame in parsed) == _STREAM_RESULTS, f"{label} {spelling}: {text}"
|
|
assert {(frame.jsonrpc, frame.id) for frame in parsed} == {("2.0", marker)}, (
|
|
f"{label} {spelling}: {text}"
|
|
)
|
|
continue
|
|
assert _JsonRpcResult.model_validate_json(text) == _JsonRpcResult(
|
|
jsonrpc="2.0",
|
|
id=marker,
|
|
result={
|
|
"kind": "message",
|
|
"role": "agent",
|
|
"messageId": "peer-message",
|
|
"parts": [{"kind": "text", "text": "granted"}],
|
|
},
|
|
), f"{label} {spelling}: {text}"
|
|
status, text = card(key, allowed)
|
|
assert status == 200 and json.loads(text)["url"].endswith(f"/a2a/{allowed}"), f"{label}: {text}"
|
|
forwarded = tuple(json.loads(item.body) for item in wire.drain() if item.method == "POST")
|
|
wire_methods: Final = tuple(name for _, _, _, name in spellings)
|
|
assert forwarded == tuple(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": forwarded[index]["id"] if index < len(forwarded) else "<none>",
|
|
"method": wire_method,
|
|
"params": {"configuration": {"blocking": True}, "message": message},
|
|
}
|
|
for index, wire_method in enumerate(wire_methods)
|
|
), f"{label}: {forwarded}"
|
|
assert all(item["id"] != marker and str(uuid.UUID(item["id"])) == item["id"] for item in forwarded), label
|
|
|
|
emptied: Final = gateway.request("PUT", f"/v1/access_group/{group_id}", {"access_agent_ids": []})
|
|
assert emptied.status_code == 200, emptied.text
|
|
emptied_outcomes: Final = attempt(callers["access group"], allowed, allowed_name)
|
|
assert all(
|
|
status == 403 and jsonrpc_refusal(text, allowed_name if "by name" in spelling else allowed)
|
|
for spelling, status, text in emptied_outcomes
|
|
), emptied_outcomes
|
|
assert wire.drain() == (), emptied_outcomes
|
|
|
|
|
|
def test_a2a_refusals_answer_as_jsonrpc_errors_that_echo_the_callers_request_id(gateway: Gateway) -> None:
|
|
marker: Final = "a2arefuse" + uuid.uuid4().hex[:12]
|
|
|
|
def upstream(request: Request) -> Reply:
|
|
return Reply(body=json.dumps(_peer_card(wire.url, marker)).encode())
|
|
|
|
with wire_server(upstream) as wire, gateway.scenario() as scenario:
|
|
agent: Final = _register_agent(gateway, scenario, marker, wire.url)
|
|
refused_callers: Final = (
|
|
(
|
|
"over budget",
|
|
scenario.key(object_permission={"agents": [agent]}, max_budget=0.0001, spend=1.0),
|
|
422,
|
|
"Budget has been exceeded!",
|
|
"budget_exceeded",
|
|
),
|
|
(
|
|
"agent not granted",
|
|
scenario.key(object_permission={"agents": [marker + "-other"]}),
|
|
403,
|
|
f"Agent '{agent}' is not allowed for your key/team. Contact proxy admin for access.",
|
|
None,
|
|
),
|
|
("unknown key", "sk-" + uuid.uuid4().hex, 401, "Authentication Error", None),
|
|
)
|
|
message: Final = {
|
|
"kind": "message",
|
|
"role": "user",
|
|
"messageId": marker + "-in",
|
|
"parts": [{"kind": "text", "text": "ping"}],
|
|
}
|
|
requests: Final = tuple(
|
|
(path, method, request_id)
|
|
for path, (method, request_id) in itertools.product(
|
|
(f"/a2a/{agent}", f"/a2a/{agent}/message/send", f"/v1/a2a/{agent}/message/send"),
|
|
(("message/send", marker), ("message/stream", 7), ("SendMessage", "l10")),
|
|
)
|
|
)
|
|
for label, key, status, message_start, error_type in refused_callers:
|
|
for path, method, request_id in requests:
|
|
response = gateway.client.post(
|
|
path,
|
|
headers={"Authorization": f"Bearer {key}", "Accept": "application/json, text/event-stream"},
|
|
json={"jsonrpc": "2.0", "id": request_id, "method": method, "params": {"message": message}},
|
|
)
|
|
context = f"{label} {path} {method}: {response.status_code} {response.text}"
|
|
assert response.status_code == status, context
|
|
error = _JsonRpcError.model_validate_json(response.text)
|
|
assert (error.id, error.error.code) == (request_id, -32000), context
|
|
assert error.error.message.startswith(message_start), context
|
|
assert error_type is None or error.error.data["type"] == error_type, context
|
|
unparseable: Final = gateway.client.post(
|
|
f"/a2a/{agent}", headers={"Authorization": "Bearer sk-" + uuid.uuid4().hex}, content=b"not json"
|
|
)
|
|
assert unparseable.status_code == 400, unparseable.text
|
|
assert _JsonRpcError.model_validate_json(unparseable.text).id is None, unparseable.text
|
|
assert wire.drain() == ()
|
|
|
|
|
|
def test_legacy_agent_json_card_path_applies_agent_permissions_to_non_admin_keys(gateway: Gateway) -> None:
|
|
pytest.skip(
|
|
"BUG: GET /a2a/{agent_id}/.well-known/agent.json returns 401 'Only proxy admin can be used' for non-admin keys because agent_inference_routes lists only agent-card.json"
|
|
)
|
|
marker: Final = "a2alegacy" + uuid.uuid4().hex[:12]
|
|
|
|
def upstream(request: Request) -> Reply:
|
|
assert request.method == "GET", request.method
|
|
return Reply(body=json.dumps(_peer_card(wire.url, marker)).encode())
|
|
|
|
def refusal(identity: str) -> str:
|
|
return json.dumps(
|
|
{"detail": f"Agent '{identity}' is not allowed for your key/team. Contact proxy admin for access."},
|
|
separators=(",", ":"),
|
|
)
|
|
|
|
with wire_server(upstream) as wire, gateway.scenario() as scenario:
|
|
allowed_name: Final = marker + "-allowed"
|
|
denied_name: Final = marker + "-denied"
|
|
allowed: Final = _register_agent(gateway, scenario, allowed_name, wire.url)
|
|
denied: Final = _register_agent(gateway, scenario, denied_name, wire.url)
|
|
granted_key: Final = scenario.key(object_permission={"agents": [allowed]})
|
|
unrestricted_key: Final = scenario.key()
|
|
proxy_base: Final = str(gateway.client.base_url).rstrip("/")
|
|
|
|
granted_legacy: Final = gateway.client.get(
|
|
f"/a2a/{allowed}/.well-known/agent.json", headers={"Authorization": f"Bearer {granted_key}"}
|
|
)
|
|
granted_card: Final = gateway.client.get(
|
|
f"/a2a/{allowed}/.well-known/agent-card.json", headers={"Authorization": f"Bearer {granted_key}"}
|
|
)
|
|
assert granted_legacy.status_code == 200, granted_legacy.text
|
|
assert granted_legacy.json()["url"] == f"{proxy_base}/a2a/{allowed}", granted_legacy.text
|
|
assert granted_legacy.json() == granted_card.json(), granted_legacy.text
|
|
|
|
unrestricted_legacy: Final = gateway.client.get(
|
|
f"/a2a/{allowed}/.well-known/agent.json", headers={"Authorization": f"Bearer {unrestricted_key}"}
|
|
)
|
|
assert unrestricted_legacy.status_code == 200, unrestricted_legacy.text
|
|
assert unrestricted_legacy.json()["url"] == f"{proxy_base}/a2a/{allowed}", unrestricted_legacy.text
|
|
wire.drain()
|
|
|
|
denied_legacy: Final = gateway.client.get(
|
|
f"/a2a/{denied}/.well-known/agent.json", headers={"Authorization": f"Bearer {granted_key}"}
|
|
)
|
|
assert (denied_legacy.status_code, denied_legacy.text) == (403, refusal(denied)), denied_legacy.text
|
|
assert wire.drain() == ()
|
|
|
|
|
|
def test_agent_list_and_detail_cards_use_proxy_urls_for_scoped_keys(gateway: Gateway) -> None:
|
|
marker: Final = "a2acardurl" + uuid.uuid4().hex[:12]
|
|
|
|
def upstream(request: Request) -> Reply:
|
|
assert request.method == "GET", request.method
|
|
return Reply(body=json.dumps(_peer_card(wire.url, marker)).encode())
|
|
|
|
with wire_server(upstream) as wire, gateway.scenario() as scenario:
|
|
backend_url: Final = wire.url + "/"
|
|
peer_card: Final = _peer_card(wire.url, marker)
|
|
peer_interfaces: Final = peer_card["supportedInterfaces"]
|
|
assert isinstance(peer_interfaces, list)
|
|
agent_card: Final = {
|
|
**peer_card,
|
|
"additionalInterfaces": [{"url": backend_url, "transport": "GRPC"}],
|
|
"supportedInterfaces": [
|
|
*peer_interfaces,
|
|
{
|
|
"url": backend_url,
|
|
"protocolBinding": "GRPC",
|
|
"protocolVersion": "0.3",
|
|
},
|
|
],
|
|
}
|
|
agent_id: Final = _register_agent(gateway, scenario, marker, wire.url, agent_card)
|
|
user_id: Final = scenario.user(user_role="internal_user")
|
|
scoped_key: Final = scenario.key(user_id=user_id, object_permission={"agents": [agent_id]})
|
|
|
|
public_card: Final = gateway.request(
|
|
"GET",
|
|
f"/a2a/{agent_id}/.well-known/agent-card.json",
|
|
key=scoped_key,
|
|
)
|
|
listed: Final = gateway.request("GET", "/v1/agents", key=scoped_key)
|
|
detailed: Final = gateway.request("GET", f"/v1/agents/{agent_id}", key=scoped_key)
|
|
|
|
assert public_card.status_code == 200, public_card.text
|
|
assert listed.status_code == 200, listed.text
|
|
assert detailed.status_code == 200, detailed.text
|
|
assert wire.url not in listed.text
|
|
assert wire.url not in detailed.text
|
|
assert "additionalInterfaces" not in listed.text
|
|
assert "additionalInterfaces" not in detailed.text
|
|
public_urls: Final = (
|
|
public_card.json()["url"],
|
|
public_card.json()["supportedInterfaces"][0]["url"],
|
|
)
|
|
listed_agent: Final = next(item for item in listed.json() if item["agent_id"] == agent_id)
|
|
for body in (listed_agent, detailed.json()):
|
|
card: Final = body["agent_card_params"]
|
|
assert (card["url"], card["supportedInterfaces"][0]["url"]) == public_urls
|
|
assert card["supportedInterfaces"] == [
|
|
{
|
|
"url": public_urls[1],
|
|
"protocolBinding": "JSONRPC",
|
|
"protocolVersion": "0.3",
|
|
}
|
|
]
|
|
|
|
admin_listed: Final = gateway.request("GET", "/v1/agents")
|
|
assert admin_listed.status_code == 200, admin_listed.text
|
|
admin_agent: Final = next(item for item in admin_listed.json() if item["agent_id"] == agent_id)
|
|
assert admin_agent["agent_card_params"]["url"] == backend_url
|