Skip to content

Commit 4b778e3

Browse files
fix(server): sanitize jsonvalue openapi variants
1 parent 31a2d02 commit 4b778e3

2 files changed

Lines changed: 34 additions & 9 deletions

File tree

server/src/agent_control_server/main.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,16 @@ async def attach_version_header(request, call_next): # type: ignore[no-untyped-
334334
)
335335

336336

337+
JSON_VALUE_SCHEMA_NAMES = (
338+
"JSONValue",
339+
"JSONValue-Input",
340+
"JSONValue-Output",
341+
"JsonValue",
342+
"JsonValue-Input",
343+
"JsonValue-Output",
344+
)
345+
346+
337347
# Override OpenAPI to avoid recursive JSONValue schema issues in TS generators.
338348
def custom_openapi() -> dict[str, Any]:
339349
if app.openapi_schema:
@@ -347,8 +357,9 @@ def custom_openapi() -> dict[str, Any]:
347357
)
348358

349359
schemas = openapi_schema.get("components", {}).get("schemas", {})
350-
if "JSONValue" in schemas:
351-
schemas["JSONValue"] = {"description": "Any JSON value"}
360+
for schema_name in JSON_VALUE_SCHEMA_NAMES:
361+
if schema_name in schemas:
362+
schemas[schema_name] = {"description": "Any JSON value"}
352363

353364
app.openapi_schema = openapi_schema
354365
return app.openapi_schema

server/tests/test_main_lifespan.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
from fastapi import FastAPI
4+
from fastapi.testclient import TestClient
5+
36
from agent_control_server import main as main_module
47
from agent_control_server.config import observability_settings, settings
58
from agent_control_server.main import lifespan
@@ -8,8 +11,6 @@
811
register_control_event_sink_factory,
912
unregister_control_event_sink_factory,
1013
)
11-
from fastapi import FastAPI
12-
from fastapi.testclient import TestClient
1314

1415

1516
def test_lifespan_initializes_observability_when_enabled(monkeypatch) -> None:
@@ -156,11 +157,22 @@ def test_lifespan_skips_observability_when_disabled(monkeypatch) -> None:
156157
assert not hasattr(app.state, "event_ingestor")
157158

158159

159-
def test_custom_openapi_replaces_jsonvalue(monkeypatch) -> None:
160-
# Given: a custom openapi generator that includes JSONValue
160+
def test_custom_openapi_replaces_jsonvalue_variants(monkeypatch) -> None:
161+
# Given: a custom openapi generator that includes Pydantic JSONValue schemas
162+
json_value_schema_names = (
163+
"JSONValue",
164+
"JSONValue-Input",
165+
"JSONValue-Output",
166+
"JsonValue",
167+
"JsonValue-Input",
168+
"JsonValue-Output",
169+
)
170+
161171
def fake_get_openapi(*, title, version, description, routes):
162172
return {
163-
"components": {"schemas": {"JSONValue": {"type": "object"}}},
173+
"components": {
174+
"schemas": {name: {"type": "object"} for name in json_value_schema_names}
175+
},
164176
"info": {"title": title, "version": version, "description": description},
165177
"paths": {},
166178
}
@@ -171,8 +183,10 @@ def fake_get_openapi(*, title, version, description, routes):
171183
# When: generating openapi
172184
schema = main_module.app.openapi()
173185

174-
# Then: JSONValue is replaced with safe description
175-
assert schema["components"]["schemas"]["JSONValue"]["description"] == "Any JSON value"
186+
# Then: JSONValue schemas are replaced with a non-recursive schema
187+
schemas = schema["components"]["schemas"]
188+
for schema_name in json_value_schema_names:
189+
assert schemas[schema_name] == {"description": "Any JSON value"}
176190

177191

178192
def test_custom_openapi_is_cached(monkeypatch) -> None:

0 commit comments

Comments
 (0)