Skip to content

fix(agent-updates): fail-closed on GitHub Releases outage (Bonus 12 #5 / PR #135) - #139

Merged
Ghenghis merged 1 commit into
feat/hermes3d-7-complete-gui-repo-wiringfrom
claude/bonus12-ssrf-fix
May 9, 2026
Merged

Ghenghis merged 1 commit into
feat/hermes3d-7-complete-gui-repo-wiringfrom
claude/bonus12-ssrf-fix

Conversation

@Ghenghis

@Ghenghis Ghenghis commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

Closes Bonus 12 finding #5 from PR #135 / bonus12-bug-finder.md.

_remote_release_tags and _latest_release previously swallowed every Exception and returned [] — DNS poison, MITM TLS, GitHub 5xx, and rate-limit bans all collapsed silently into status=\"already_current\" downstream, masking a stale Hermes Agent checkout (CWE-918 SSRF / OWASP CICD-SEC-1 fail-open).

Patch detail

  • _remote_release_tags: tightens except to known modes — urllib.error.URLError (covers HTTPError 4xx/5xx), socket.timeout, TimeoutError, ConnectionError, json.JSONDecodeError. Real failures raise HTTPException(502) with a _redact()-cleaned detail. Non-list payload (200 + object) also raises 502 (upstream contract violation).
  • _latest_release: distinguishes 404 ("no /releases/latest yet" → soft warning) from real outages (5xx → 502). URLError on the latest endpoint preserves the soft-warning fast path because _remote_release_tags is the authoritative gate.

Behavior change

update_status / staged_update will now flip from outdated=False / already_current to HTTP 502 during GitHub blips. This is the correct fail-closed posture for a supply-chain update channel (CWE-918 integrity face). Reviewers should confirm UI tolerates 502.

Tests added (12, all green)

04_testing/pytest/unit/test_agent_updates_ssrf.py:

  • Happy paths: success + genuinely empty release list
  • socket.timeout, URLError, HTTP 5xx, malformed JSON, non-list payload all raise HTTPException(502)
  • Bearer-shaped strings in error detail are redacted
  • _latest_release: 404 keeps soft warning; 5xx raises 502; URLError keeps soft warning (downstream gate authoritative)

Test plan

  • python -m py_compile on touched files
  • Focused tests: 12/12 pass
  • All agent_updates-keyed unit tests: 37/37 pass
  • Pre-push hook: passed
  • CI on PR

Scope

Swarm provenance

Agent 6 of the 20-agent Blocker Elimination Swarm produced the unified diff this PR applies. Verified locally before apply; tests added by orchestrator.

Follow-up PRs

References

🤖 Generated with Claude Code

…/ PR #135)

Closes Bonus 12 finding #5 from PR #135 / bonus12-bug-finder.md.

Pre-fix
- _remote_release_tags swallowed every Exception and returned [].
  DNS poison, MITM TLS, GitHub 5xx, and rate-limit bans all collapsed
  silently into status="already_current" downstream, masking a stale
  Hermes Agent checkout (CWE-918 SSRF / OWASP CICD-SEC-1 fail-open).
- _latest_release had the same broad-catch shape on a parallel path.

Post-fix
- _remote_release_tags tightens except to the known network/parse modes:
  urllib.error.URLError (covers HTTPError 4xx/5xx, ContentTooShortError),
  socket.timeout, TimeoutError, ConnectionError, json.JSONDecodeError.
  Real failures raise HTTPException(502) with a _redact()-cleaned detail.
- non-list payload (200 + {object}) raises 502 (upstream contract violation).
- _latest_release distinguishes 404 (no /releases/latest yet -> soft warning)
  from real outages (5xx -> 502); URLError on latest endpoint preserves the
  soft-warning fast path because _remote_release_tags is the authoritative
  gate (if we got that far, tags came back successfully).

Behavior change
- update_status / staged_update will now flip from
  "outdated=False"/"already_current" to HTTP 502 during GitHub blips.
  This is the correct fail-closed posture for a supply-chain update channel
  (CWE-918 SSRF integrity face). Reviewers should confirm UI tolerates 502.

Tests added (12, all green)
- 04_testing/pytest/unit/test_agent_updates_ssrf.py
  * happy paths (success + genuinely empty release list)
  * socket.timeout, URLError, HTTP 5xx, malformed JSON, non-list payload
    all raise HTTPException(502)
  * Bearer-shaped strings in error detail are redacted
  * latest_release: 404 keeps soft warning; 5xx raises 502; URLError on
    latest keeps soft warning (downstream gate already authoritative)

Verification
- py_compile: OK
- Focused tests: 12/12 pass
- All agent_updates-keyed unit tests: 37/37 pass
- Pre-push hook: passed

Scope
- Bonus 12 finding #5 only - controlled-batch pattern continues.
- v0.13.0 update remains formally deferred.
- RC v2 commits 2-5 remain paused per user instruction.

Swarm provenance
- Agent 6 (general-purpose) of the 20-agent Blocker Elimination Swarm
  produced the unified diff this PR applies. Verified locally before
  apply; tests added by orchestrator.

Follow-ups (separate PRs)
- Bonus 12 #6 (config payload redaction asymmetry) - Agent 7 diff ready
- Bonus 12 #7 (apply_patch_proposal TOCTOU) - Agent 8 diff ready
- Bonus 12 #8 (_call_mcp_tool deadlock pattern) - Agent 9 diff ready
- Bonus 13 audit doc errata - Agent 10 diff ready
- GITHUB_TOKEN support to escape the 60-req/hr unauth rate limit
  (out-of-scope; new dominant cause of 502s post-fix)

References
- https://docs.python.org/3/library/urllib.error.html
- https://cwe.mitre.org/data/definitions/918.html (SSRF)
- https://owasp.org/www-project-top-10-ci-cd-security-risks/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7aec0eb-06fa-458b-a412-5069b332db11

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/bonus12-ssrf-fix

Comment @coderabbitai help to get the list of available commands and usage tips.

@Ghenghis
Ghenghis merged commit e0b719d into feat/hermes3d-7-complete-gui-repo-wiring May 9, 2026
1 check passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the security and robustness of the Hermes Agent update mechanism by replacing generic exception handling with specific checks for network and parsing errors when querying the GitHub Releases API. It implements a "fail-closed" approach for the primary release tags endpoint, raising a 502 Bad Gateway on failure to prevent the system from incorrectly assuming it is up-to-date during an outage. Review feedback suggests extending this "fail-closed" posture to the latest release endpoint for consistency and explicitly catching UnicodeDecodeError in both API interaction functions to handle malformed responses gracefully and avoid unhandled internal server errors.

Comment on lines +294 to 298
except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:
# Soft-warning preserved here ONLY because _remote_release_tags is the
# authoritative gate; if we got this far, tags came back successfully,
# so a transient hiccup on the latest endpoint is acceptable.
latest["api_warning"] = _redact(str(exc))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To maintain the "fail-closed" posture described in the PR summary, network outages (timeouts, DNS failures, etc.) on the /latest endpoint should raise an HTTPException(502) rather than falling back to a soft warning. Currently, if this endpoint fails with a URLError, latest["tag"] remains None, which causes downstream logic in update_status to report outdated=False. This effectively fails open by masking potential updates during an outage. Additionally, UnicodeDecodeError should be caught here as well to handle malformed responses gracefully. Note that this change will require updating the corresponding unit test test_latest_release_url_error_keeps_soft_warning.

    except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError, json.JSONDecodeError, UnicodeDecodeError) as exc:\n        raise HTTPException(\n            status_code=502,\n            detail=f"GitHub Releases latest endpoint unreachable: {_redact(str(exc))[:120]}",\n        ) from exc

payload = json.loads(response.read().decode("utf-8"))
except Exception:
return []
except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The decode("utf-8") call on line 244 can raise a UnicodeDecodeError if the upstream response contains invalid byte sequences. This exception is currently not caught by the specific tuple, which would result in an unhandled 500 Internal Server Error instead of the intended 502 Bad Gateway. Adding UnicodeDecodeError to the caught exceptions ensures consistent error handling for malformed upstream responses.

Suggested change
except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError, json.JSONDecodeError) as exc:
except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError, json.JSONDecodeError, UnicodeDecodeError) as exc:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant