From 4ee65b961f1263528296346d6abd2bdd702f5470 Mon Sep 17 00:00:00 2001 From: LobsterQBA <101247207+LobsterQBA@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:07:15 -0700 Subject: [PATCH 1/2] fix: validate agent request messages --- agent_system/server.py | 10 +++++++++- tests/test_server.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/agent_system/server.py b/agent_system/server.py index 2aab02f..e868c92 100644 --- a/agent_system/server.py +++ b/agent_system/server.py @@ -20,6 +20,7 @@ STATIC_ROOT = Path(__file__).parent / "static" MAX_BODY_BYTES = 16_384 +MAX_MESSAGE_CHARS = 4_000 def load_dotenv(path: Path = Path(".env")) -> None: @@ -68,6 +69,11 @@ def live(self) -> AgentSystem: def run(self, message: str, mode: str) -> dict: if mode not in {"demo", "live"}: raise ValueError("mode must be demo or live") + message = message.strip() + if not message: + raise ValueError("message must not be empty") + if len(message) > MAX_MESSAGE_CHARS: + raise ValueError(f"message must be at most {MAX_MESSAGE_CHARS} characters") agent = self.demo if mode == "demo" else self.live() return agent.run(message).to_dict() @@ -155,7 +161,9 @@ def do_POST(self) -> None: return try: payload = self._read_json() - message = str(payload.get("message", "")) + message = payload.get("message", "") + if not isinstance(message, str): + raise TypeError("message must be a string") mode = str(payload.get("mode", "demo")) self._json(self.app.run(message, mode)) except (TypeError, ValueError) as exc: diff --git a/tests/test_server.py b/tests/test_server.py index bfe1c4b..959d119 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -63,3 +63,30 @@ def test_live_mode_requires_configuration(tmp_path, monkeypatch): server.shutdown() server.server_close() thread.join(timeout=3) + + +def test_run_rejects_empty_non_string_and_oversized_messages(tmp_path): + server = create_server(port=0, home=tmp_path / "agent-home") + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + base = f"http://127.0.0.1:{server.server_port}" + try: + for message, expected in [ + (" ", "message must not be empty"), + (["not", "a", "string"], "message must be a string"), + ("x" * 4_001, "message must be at most 4000 characters"), + ]: + try: + request_json( + f"{base}/api/run", + payload={"message": message, "mode": "demo"}, + ) + except urllib.error.HTTPError as exc: + assert exc.code == 400 + assert expected in json.loads(exc.read())["error"] + else: + raise AssertionError("invalid message should return HTTP 400") + finally: + server.shutdown() + server.server_close() + thread.join(timeout=3) From b297c234eec7006f301043c2ecb3629ecc17e919 Mon Sep 17 00:00:00 2001 From: LobsterQBA <101247207+LobsterQBA@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:49:17 -0700 Subject: [PATCH 2/2] docs: document local API input contract --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 836976a..655b36d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,20 @@ Then restart the server and ask: The answer survives because `.agent-mini/state.db` is the source of truth. +## Local API contract + +The cockpit calls a local JSON API, which is also useful when trying the agent from a script: + +```bash +curl -X POST http://127.0.0.1:8787/api/run \ + -H 'Content-Type: application/json' \ + -d '{"message":"Calculate 8 * 9","mode":"demo"}' +``` + +`message` must be a non-empty string of at most 4,000 characters. Invalid requests return +HTTP 400 before an agent turn, model call, or trace entry is created. The only supported modes +are `demo` and `live`; `live` additionally requires `AGENT_API_KEY` and `AGENT_MODEL`. + ## The four pieces ```mermaid