Skip to content

Commit 3154c76

Browse files
authored
fix: refresh_context usa HttpClient.post invece di requests.post (P2 audit) (#55)
* fix: refresh_context usa HttpClient.post invece di requests.post Sostituisce requests.post() con HttpClient.post() per il dispatch del workflow GitHub Actions. headers personalizzati (Authorization, Accept) passati via kwargs. Gestione errori via HttpResult.is_ok invece di try/except requests.RequestException. import requests mantenuto per isinstance(e, requests.HTTPError) nei tool di fetch esistenti. * fix: mypy zero errors - github.py: _raise_on_bad_status typed, restituisce Response. Tutti i 6 caller usano la response restituita (response.json/response.text). - discussions.py: guard is_ok || response is None + err check. - mcp_server.py _fetch: guard is_ok || response is None + response locale.
1 parent 7c492cc commit 3154c76

4 files changed

Lines changed: 98 additions & 90 deletions

File tree

src/agent_context_builder/discussions.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,10 @@ def _get_repo_discussions(self, repo: str, first: int) -> list[Discussion]:
8787
headers={"Authorization": f"bearer {self.token}"},
8888
retries=0,
8989
)
90-
if result.is_error:
91-
raise result.err
90+
if not result.is_ok or result.response is None:
91+
raise result.err if result.err else RuntimeError("GraphQL request failed")
9292

93-
response = result.response
94-
payload = response.json()
93+
payload = result.response.json()
9594
if "errors" in payload:
9695
raise ValueError(f"GraphQL errors: {payload['errors']}")
9796

src/agent_context_builder/github.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from dataclasses import dataclass
66
from typing import Any, Optional
77

8-
from lab_connectors.http import HttpClient
8+
import requests
9+
from lab_connectors.http import HttpClient, HttpResult
910

1011

1112
@dataclass
@@ -111,13 +112,14 @@ def get_issues(self, repos: list[str], state: str = "open") -> list[Issue]:
111112
self.fetch_errors[f"{repo}:issues"] = str(e)
112113
return issues
113114

114-
def _raise_on_bad_status(self, result, url_desc: str) -> None:
115-
"""Raise RuntimeError if result is error or response status >= 400."""
116-
if not result.is_ok:
115+
def _raise_on_bad_status(self, result: HttpResult, url_desc: str) -> requests.Response:
116+
"""Raise RuntimeError if result is error or response status >= 400.
117+
Returns the response if OK."""
118+
if not result.is_ok or result.response is None:
117119
raise RuntimeError(f"{url_desc}: {result.err}")
118-
status = result.response.status_code
119-
if status >= 400:
120-
raise RuntimeError(f"{url_desc}: HTTP {status}")
120+
if result.response.status_code >= 400:
121+
raise RuntimeError(f"{url_desc}: HTTP {result.response.status_code}")
122+
return result.response
121123

122124
def _headers(self) -> dict[str, str]:
123125
"""Build Authorization headers if token is set."""
@@ -130,10 +132,10 @@ def _get_repo_prs(self, repo: str, state: str = "open") -> list[PR]:
130132
url = f"{self.base_url}/repos/{self.org}/{repo}/pulls"
131133
params = {"state": state, "per_page": 50}
132134
result = self._http.get(url, params=params, headers=self._headers())
133-
self._raise_on_bad_status(result, url)
135+
response = self._raise_on_bad_status(result, url)
134136

135137
prs = []
136-
for item in result.response.json():
138+
for item in response.json():
137139
prs.append(
138140
PR(
139141
number=item["number"],
@@ -170,8 +172,8 @@ def list_directory(self, repo: str, path: str, ref: str = "main") -> list[str] |
170172
params: dict[str, str] = {"ref": ref}
171173
try:
172174
result = self._http.get(url, params=params, headers=self._headers())
173-
self._raise_on_bad_status(result, url)
174-
items = result.response.json()
175+
response = self._raise_on_bad_status(result, url)
176+
items = response.json()
175177
if not isinstance(items, list):
176178
# GitHub returns a single object if path is a file, not a directory
177179
raise RuntimeError(f"{url}: path is not a directory")
@@ -203,8 +205,8 @@ def get_raw_file(self, repo: str, path: str, ref: str = "main") -> str | None:
203205
url = f"https://raw.githubusercontent.com/{self.org}/{repo}/{ref}/{path}"
204206
result = self._http.get(url, headers=self._headers())
205207
try:
206-
self._raise_on_bad_status(result, url)
207-
return result.response.text
208+
response = self._raise_on_bad_status(result, url)
209+
return response.text
208210
except Exception as exc:
209211
self.fetch_errors[f"{repo}:{path}"] = str(exc)
210212
return None
@@ -219,8 +221,8 @@ def get_repos_info(self, repos: list[str]) -> dict[str, RepoInfo]:
219221
try:
220222
url = f"{self.base_url}/repos/{self.org}/{repo}"
221223
result = self._http.get(url, headers=self._headers())
222-
self._raise_on_bad_status(result, url)
223-
data = result.response.json()
224+
response = self._raise_on_bad_status(result, url)
225+
data = response.json()
224226
result_map[repo] = RepoInfo(
225227
name=repo,
226228
description=data.get("description") or "",
@@ -265,8 +267,8 @@ def get_latest_workflow_run(
265267
}
266268
try:
267269
result = self._http.get(url, params=params, headers=self._headers())
268-
self._raise_on_bad_status(result, url)
269-
data = result.response.json()
270+
response = self._raise_on_bad_status(result, url)
271+
data = response.json()
270272
runs = data.get("workflow_runs", [])
271273
if not runs:
272274
return None
@@ -293,10 +295,10 @@ def _get_repo_issues(self, repo: str, state: str = "open") -> list[Issue]:
293295
url = f"{self.base_url}/repos/{self.org}/{repo}/issues"
294296
params = {"state": state, "per_page": 50}
295297
result = self._http.get(url, params=params, headers=self._headers())
296-
self._raise_on_bad_status(result, url)
298+
response = self._raise_on_bad_status(result, url)
297299

298300
issues = []
299-
for item in result.response.json():
301+
for item in response.json():
300302
# Skip pull requests: they have a pull_request field
301303
if "pull_request" in item:
302304
continue

src/agent_context_builder/mcp_server.py

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,14 @@ def _fetch(path: str, retries: int = 1, backoff: float = 1.0) -> str:
156156
client = HttpClient(max_retries=retries, retry_backoff=backoff, timeout=10)
157157
result = client.get(url, headers=headers)
158158

159-
if result.is_error:
159+
if not result.is_ok or result.response is None:
160160
_log.error("fetch", "failed", path=path, error=str(result.err))
161-
raise result.err
161+
raise result.err if result.err else RuntimeError(f"Failed to fetch {path}")
162162

163-
result.response.raise_for_status()
164-
_log.info("fetch", "success", path=path, status=result.response.status_code)
165-
return result.response.text
163+
response = result.response
164+
response.raise_for_status()
165+
_log.info("fetch", "success", path=path, status=response.status_code)
166+
return response.text
166167

167168

168169
@mcp.tool()
@@ -361,60 +362,62 @@ def refresh_context() -> str:
361362
)
362363

363364
_last_refresh_attempt = now
364-
try:
365-
response = requests.post(
366-
f"{_API_BASE}/actions/workflows/build-context.yml/dispatches",
367-
json={"ref": "main"},
368-
headers={
369-
"Authorization": f"token {token}",
370-
"Accept": "application/vnd.github+json",
371-
},
372-
timeout=10,
365+
client = HttpClient(timeout=10)
366+
result = client.post(
367+
f"{_API_BASE}/actions/workflows/build-context.yml/dispatches",
368+
json={"ref": "main"},
369+
headers={
370+
"Authorization": f"token {token}",
371+
"Accept": "application/vnd.github+json",
372+
},
373+
)
374+
375+
if not result.is_ok or result.response is None:
376+
_log.error("refresh_context", "network_error", error=str(result.err))
377+
return json.dumps(
378+
{
379+
"ok": False,
380+
"tool": "refresh_context",
381+
"error": f"Errore di rete: {result.err}",
382+
"ts": datetime.now(timezone.utc).isoformat(),
383+
}
373384
)
374-
if response.status_code == 204:
375-
_log.info("refresh_context", "triggered", ref="main")
376-
return json.dumps(
377-
{
378-
"ok": True,
379-
"tool": "refresh_context",
380-
"message": "Build triggerato. Artifact aggiornati entro ~1 minuto.",
381-
"ts": datetime.now(timezone.utc).isoformat(),
382-
}
383-
)
384-
elif response.status_code == 422:
385-
_log.error(
386-
"refresh_context",
387-
"rejected",
388-
status=response.status_code,
389-
body=response.text,
390-
)
391-
return json.dumps(
392-
{
393-
"ok": False,
394-
"tool": "refresh_context",
395-
"error": "Build rifiutato (422). Verifica che il workflow sia su main.",
396-
"status_code": 422,
397-
"ts": datetime.now(timezone.utc).isoformat(),
398-
}
399-
)
400-
else:
401-
_log.error("refresh_context", "failed", status=response.status_code, body=response.text)
402-
return json.dumps(
403-
{
404-
"ok": False,
405-
"tool": "refresh_context",
406-
"error": f"Errore {response.status_code}: {response.text}",
407-
"status_code": response.status_code,
408-
"ts": datetime.now(timezone.utc).isoformat(),
409-
}
410-
)
411-
except requests.RequestException as e:
412-
_log.error("refresh_context", "network_error", error=str(e))
385+
386+
response = result.response
387+
if response.status_code == 204:
388+
_log.info("refresh_context", "triggered", ref="main")
389+
return json.dumps(
390+
{
391+
"ok": True,
392+
"tool": "refresh_context",
393+
"message": "Build triggerato. Artifact aggiornati entro ~1 minuto.",
394+
"ts": datetime.now(timezone.utc).isoformat(),
395+
}
396+
)
397+
elif response.status_code == 422:
398+
_log.error(
399+
"refresh_context",
400+
"rejected",
401+
status=response.status_code,
402+
body=response.text,
403+
)
404+
return json.dumps(
405+
{
406+
"ok": False,
407+
"tool": "refresh_context",
408+
"error": "Build rifiutato (422). Verifica che il workflow sia su main.",
409+
"status_code": 422,
410+
"ts": datetime.now(timezone.utc).isoformat(),
411+
}
412+
)
413+
else:
414+
_log.error("refresh_context", "failed", status=response.status_code, body=response.text)
413415
return json.dumps(
414416
{
415417
"ok": False,
416418
"tool": "refresh_context",
417-
"error": f"Errore di rete: {e}",
419+
"error": f"Errore {response.status_code}: {response.text}",
420+
"status_code": response.status_code,
418421
"ts": datetime.now(timezone.utc).isoformat(),
419422
}
420423
)

tests/test_mcp_server.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def test_topic_index_resolve_not_found():
242242

243243

244244
# ---------------------------------------------------------------------------
245-
# refresh_context — still uses raw requests.post (separate concern)
245+
# refresh_context — via HttpClient.post
246246
# ---------------------------------------------------------------------------
247247

248248

@@ -256,10 +256,14 @@ def _reset_refresh_state(monkeypatch) -> None:
256256
monkeypatch.setattr(mcp_server, "_ENV_LOADED", False)
257257

258258

259-
def _mock_post_response(status: int = 204):
259+
def _mock_http_result(status: int = 204):
260+
"""Build an HttpResult-like object for mocking HttpClient.post."""
260261
resp = MagicMock()
261262
resp.status_code = status
262-
return resp
263+
resp.text = "mock response"
264+
from lab_connectors.http import HttpResult
265+
266+
return HttpResult(response=resp, err=None, ssl_fallback_used=None)
263267

264268

265269
@pytest.mark.adapter
@@ -283,8 +287,8 @@ def test_refresh_context_loads_token_from_env_file(monkeypatch, tmp_path):
283287
monkeypatch.setenv("ACB_ENV_FILE", str(env_file))
284288
_reset_refresh_state(monkeypatch)
285289

286-
with patch("agent_context_builder.mcp_server.requests.post") as mock_post:
287-
mock_post.return_value = _mock_post_response(204)
290+
with patch("agent_context_builder.mcp_server.HttpClient.post") as mock_post:
291+
mock_post.return_value = _mock_http_result(204)
288292
result = mcp_server.refresh_context()
289293

290294
data = json.loads(result)
@@ -302,8 +306,8 @@ def test_refresh_context_loads_token_when_env_is_empty(monkeypatch, tmp_path):
302306
monkeypatch.setenv("ACB_ENV_FILE", str(env_file))
303307
_reset_refresh_state(monkeypatch)
304308

305-
with patch("agent_context_builder.mcp_server.requests.post") as mock_post:
306-
mock_post.return_value = _mock_post_response(204)
309+
with patch("agent_context_builder.mcp_server.HttpClient.post") as mock_post:
310+
mock_post.return_value = _mock_http_result(204)
307311
result = mcp_server.refresh_context()
308312

309313
data = json.loads(result)
@@ -324,8 +328,8 @@ def test_refresh_context_continues_after_partial_env(monkeypatch, tmp_path):
324328
monkeypatch.setenv("ACB_ENV_FILE", str(explicit_env))
325329
_reset_refresh_state(monkeypatch)
326330

327-
with patch("agent_context_builder.mcp_server.requests.post") as mock_post:
328-
mock_post.return_value = _mock_post_response(204)
331+
with patch("agent_context_builder.mcp_server.HttpClient.post") as mock_post:
332+
mock_post.return_value = _mock_http_result(204)
329333
result = mcp_server.refresh_context()
330334

331335
data = json.loads(result)
@@ -339,8 +343,8 @@ def test_refresh_context_success(monkeypatch):
339343
monkeypatch.setenv("GITHUB_TOKEN", "fake-token")
340344
_reset_refresh_state(monkeypatch)
341345

342-
with patch("agent_context_builder.mcp_server.requests.post") as mock_post:
343-
mock_post.return_value = _mock_post_response(204)
346+
with patch("agent_context_builder.mcp_server.HttpClient.post") as mock_post:
347+
mock_post.return_value = _mock_http_result(204)
344348
result = mcp_server.refresh_context()
345349

346350
data = json.loads(result)
@@ -353,8 +357,8 @@ def test_refresh_context_api_error(monkeypatch):
353357
monkeypatch.setenv("GITHUB_TOKEN", "fake-token")
354358
_reset_refresh_state(monkeypatch)
355359

356-
with patch("agent_context_builder.mcp_server.requests.post") as mock_post:
357-
mock_post.return_value = _mock_post_response(403)
360+
with patch("agent_context_builder.mcp_server.HttpClient.post") as mock_post:
361+
mock_post.return_value = _mock_http_result(403)
358362
result = mcp_server.refresh_context()
359363

360364
data = json.loads(result)

0 commit comments

Comments
 (0)