Part of #142.
Summary
Two compounding blind spots in the generic-HTTP path drop real outbound calls even when the destination is a literal in source.
plate-gallery backend/app/services/moderation/image_check.py:226 — await client.post("https://api.openai.com/v1/chat/completions", ...) where client is an httpx.AsyncClient context var. The literal OpenAI URL is never inspected.
image_check.py:140 (Gemini) and sapling services/storage_service.py:113 (httpx.put(url, ...)) — the URL is a variable / multi-line f-string; dropped.
Root cause (src/ast/ast-scanner.ts)
- The
7c generic-HTTP block (:731-763) only fires when HTTP_CLIENTS.has(rootIdentifier) — a fixed set {fetch, axios, got, ky, request, superagent, requests, httpx} (:151-152). A method call on a client/session variable (client.post, session.get) has root client, not in the set → never treated as HTTP, so the literal URL inside is never examined.
extractUrlFromArgs (:156-162) returns a URL only when arg0 is a bare string node; template_string → null, identifier/variable → null. So even recognized calls with a built URL yield no host.
Proposed fix
- Bind variables assigned from HTTP-client/session constructors (
httpx.Client / httpx.AsyncClient, requests.Session, JS axios.create(), undici new Client) to a synthetic http-client provider, so client.post / session.get enter the 7c path. Reuse the inner-scope + with ... as client var tracking from #A.
- Route the URL argument through the existing constant-folder (
src/scanner/constant-fold.ts, already used by the regex pass via tryFoldRegexMatchUrl) to resolve url = f"{BASE}/path", concatenations, and same-file consts before lookupHost.
- When the host is partially dynamic (env-var base) and can't resolve → emit outbound
unknown with the partial string as the capture (depends on #D).
Acceptance criteria
Part of #142.
Summary
Two compounding blind spots in the generic-HTTP path drop real outbound calls even when the destination is a literal in source.
plate-gallerybackend/app/services/moderation/image_check.py:226—await client.post("https://api.openai.com/v1/chat/completions", ...)whereclientis anhttpx.AsyncClientcontext var. The literal OpenAI URL is never inspected.image_check.py:140(Gemini) andsaplingservices/storage_service.py:113(httpx.put(url, ...)) — the URL is a variable / multi-line f-string; dropped.Root cause (
src/ast/ast-scanner.ts)7cgeneric-HTTP block (:731-763) only fires whenHTTP_CLIENTS.has(rootIdentifier)— a fixed set{fetch, axios, got, ky, request, superagent, requests, httpx}(:151-152). A method call on a client/session variable (client.post,session.get) has rootclient, not in the set → never treated as HTTP, so the literal URL inside is never examined.extractUrlFromArgs(:156-162) returns a URL only when arg0 is a barestringnode;template_string→ null, identifier/variable → null. So even recognized calls with a built URL yield no host.Proposed fix
httpx.Client/httpx.AsyncClient,requests.Session, JSaxios.create(), undicinew Client) to a synthetic http-client provider, soclient.post/session.getenter the7cpath. Reuse the inner-scope +with ... as clientvar tracking from #A.src/scanner/constant-fold.ts, already used by the regex pass viatryFoldRegexMatchUrl) to resolveurl = f"{BASE}/path", concatenations, and same-file consts beforelookupHost.unknownwith the partial string as the capture (depends on #D).Acceptance criteria
async with httpx.AsyncClient() as client: client.post("https://api.openai.com/...")→ provideropenai.url = f"{SUPABASE_URL}/storage/v1/object/..."; httpx.put(url)→ outbound (supabase if foldable, elseunknownwith capture).image_check.py:133-141resolves to gemini.