feat: replace 7 read tools with generic api_get tool #24
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "23-generic-api-get"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Replaces 7 hand-coded read tools with a single generic
api_gettool that makes GET-only requests to any basketball-api endpoint. Claude picks the endpoint path from system prompt examples. New endpoints (jerseys, schedules, billing) work automatically without code changes.Changes
app/ai.py— Replaced 7 read tool definitions with 1api_gettool (acceptspathparameter). Removed_execute_read_tooldispatcher, added_execute_api_getthat delegates toclient.get(path). UpdatedSYSTEM_PROMPTwith example endpoint list. Updated_handle_responseto extractpathfrom tool input. All 7 write tools + confirmation flow unchanged.app/basketball.py— Replaced 7 individual read methods with a singleget(path)method. HTTP method hardcoded toGETat code level. No request body ever sent. Prepends/if missing.tests/test_ai.py— Updated tool definition tests (8 tools: 1 read + 7 write). ReplacedTestReadToolExecutionwithTestApiGetExecution. Added integration tests forapi_getwith dynamic paths. Added GET-only enforcement test.tests/test_basketball.py— ReplacedTestReadOperationswithTestGenericGet. Added tests for novel endpoints, slash prepending, and GET-method-only enforcement. Updated token/error tests to useclient.get().Test Plan
pytest tests/— all 110 tests passpytest tests/ -k test_api_get— targeted api_get tests pass_execute_api_getandBasketballClient.get()levelsReview Checklist
BasketballClient.get()and_execute_api_get)Related Notes
PR #24 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / Anthropic SDK / httpx / pytest. Domain checklist applied: Python conventions, OWASP awareness, async patterns.
Architecture assessment: Clean refactor. 7 hand-coded read methods and their corresponding tool definitions are replaced by a single generic
api_gettool backed byBasketballClient.get(path). The design is sound -- the generic path approach means new basketball-api endpoints are immediately accessible without code changes.GET-only enforcement -- verified at two layers:
BasketballClient.get()(basketball.py:134-145) hardcodes"GET"in the call toself._request("GET", path). The method signature only acceptspath-- no parameter for HTTP method, nojsonbody parameter. Structurally impossible to override._execute_api_get()(ai.py:214-225) exclusively callsclient.get(path). No other client method is reachable from this code path._handle_response(ai.py:504-507) routesoperation == "read"to_execute_api_getandoperation == "write"to the confirmation flow. The only read tool isapi_get. Clean separation.Write tools and confirmation flow -- preserved exactly:
_execute_write_tool,_build_write_confirmation,_handle_confirmation,_ACTION_DESCRIPTIONSall unchangedconfirmation.pyuntouched by this PRTestWriteOperationsuntouchedSystem prompt updated with endpoint examples including new endpoints (/api/jerseys, /api/schedules, /api/billing/tiers) that demonstrate the extensibility benefit.
Test coverage -- thorough:
TestToolDefinitions: 8 tools (1 read + 7 write), schema validation, metadata lookupTestApiGetExecution: client.get() dispatch, multiple paths, error handling, None result, GET-only assertionTestGenericGet: HTTP-level verification that GET method is used, no json body, slash prepending, novel endpointsTestProcessMessage: Two integration tests for api_get through the full process_message pipelinetest_api_get_enforces_get_only) and basketball.py layer (test_get_always_uses_get_method)BLOCKERS
None.
NITS
SSRF hardening on path parameter (ai.py:506, basketball.py:142-144): The slash-prepend logic (
if not path.startswith("/"): path = f"/{path}") does not reject protocol-relative URLs. If Claude were to emitpath="//evil.com/foo", this starts with/and would pass through. In httpx with abase_url, RFC 3986 URL resolution of//evil.com/foorelative tohttp://basketball-api:8000resolves tohttp://evil.com/foo-- a potential SSRF vector. The risk is low here because (a) the path originates from Claude's tool_use output, not raw user input, and (b) the client runs on an internal network with Keycloak auth headers that would be sent to the wrong host. Still, a defensive check like rejecting paths containing://or starting with//would be a worthwhile hardening:Minor:
test_api_get_enforces_get_onlyassertion pattern (test_ai.py:236-239): Thenot hasattr(client, "post") or not client.post.calledassertions are weak becauseMagicMock()auto-creates attributes on access.hasattrwill always return True for a MagicMock. The assertions still pass but only because of theor not client.post.calledfallback. Consider usingspec=BasketballClienton the mock to make the hasattr check meaningful, or just assertclient.post.called is Falsedirectly.Minor: hardcoded default path (ai.py:506):
tool_input.get("path", "/")falls back to"/"if the path key is missing. Since"path"is"required"in the tool schema, Claude should always provide it. The fallback to root/is safe but could mask a bug. Consider logging a warning when the fallback triggers, or raising an error.SOP COMPLIANCE
23-generic-api-getreferences issue #23)Closes #23)PROCESS OBSERVATIONS
get()and_execute_api_get()clearly document the GET-only guarantee.VERDICT: APPROVED
Clean, well-tested refactor. GET-only enforcement is structurally sound at both layers. Write tools and confirmation flow are preserved exactly. The SSRF hardening nit (protocol-relative URL rejection) should be tracked as a follow-up issue but is not a blocker given the threat model (LLM-intermediated path, internal network, auth headers scoped to basketball-api).