mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
security(deps): add upper bounds to 5 loose deps + document supply chain policy (#24226)
After the Mini Shai-Hulud supply chain campaign (May 2026) and the litellm compromise (March 2026), codify the dependency pinning policy that was established in PRs #2810 and #9801 but never written down for contributors. Changes: - pyproject.toml: Add tight upper bounds to the 5 deps that slipped through as review escapes from external contributor PRs: - hindsight-client>=0.4.22,<0.5 (was >=0.4.22) - aiosqlite>=0.20,<0.23 (was >=0.20) - asyncpg>=0.29,<0.32 (was >=0.29) - alibabacloud-dingtalk>=2.0.0,<3 (was >=2.0.0) - youtube-transcript-api>=1.2.0,<2 (was >=1.2.0) Pre-1.0 packages get <0.(current_minor+2) — tight enough to block hostile minor releases but loose enough to not require bumps every week. - CONTRIBUTING.md: Add 'Dependency pinning policy' section under Security with the full rationale, table of source types + treatments, and examples. - AGENTS.md: Add concise 'Dependency Pinning Policy' section for AI coding agents with the decision table and step-by-step checklist. - supply-chain-audit.yml: Add dep-bounds job that fails PRs introducing PyPI deps without <ceiling upper bounds. Fires on pyproject.toml changes. Posts a PR comment with the specific unbounded specs found. Refs: #2796 #2810 #9801 #24205
This commit is contained in:
parent
681778a0b7
commit
04b1fdaecf
3 changed files with 130 additions and 0 deletions
66
.github/workflows/supply-chain-audit.yml
vendored
66
.github/workflows/supply-chain-audit.yml
vendored
|
|
@ -11,6 +11,7 @@ on:
|
||||||
- '**/sitecustomize.py'
|
- '**/sitecustomize.py'
|
||||||
- '**/usercustomize.py'
|
- '**/usercustomize.py'
|
||||||
- '**/__init__.pth'
|
- '**/__init__.pth'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
@ -137,3 +138,68 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "::error::CRITICAL supply chain risk patterns detected in this PR. See the PR comment for details."
|
echo "::error::CRITICAL supply chain risk patterns detected in this PR. See the PR comment for details."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
dep-bounds:
|
||||||
|
name: Check PyPI dependency upper bounds
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: contains(github.event.pull_request.changed_files_url, 'pyproject.toml') || true
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check for unbounded PyPI deps
|
||||||
|
id: bounds
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE="${{ github.event.pull_request.base.sha }}"
|
||||||
|
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||||
|
|
||||||
|
# Only check added lines in pyproject.toml
|
||||||
|
ADDED=$(git diff "$BASE".."$HEAD" -- pyproject.toml | grep '^+' | grep -v '^+++' || true)
|
||||||
|
|
||||||
|
if [ -z "$ADDED" ]; then
|
||||||
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Match PyPI dep specs that have >= but no < ceiling.
|
||||||
|
# Pattern: "package>=version" without a following ",<" bound.
|
||||||
|
# Excludes git+ URLs (which use commit SHAs) and comments.
|
||||||
|
UNBOUNDED=$(echo "$ADDED" | grep -oE '"[a-zA-Z0-9_-]+(\[[^\]]*\])?>=[ 0-9.]+"' | grep -v ',<' || true)
|
||||||
|
|
||||||
|
if [ -n "$UNBOUNDED" ]; then
|
||||||
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "$UNBOUNDED" > /tmp/unbounded.txt
|
||||||
|
else
|
||||||
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post unbounded dep warning
|
||||||
|
if: steps.bounds.outputs.found == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
BODY="## ⚠️ Unbounded PyPI Dependency Detected
|
||||||
|
|
||||||
|
This PR adds PyPI dependencies without a \`<next_major\` upper bound. Per our [supply chain policy](../blob/main/CONTRIBUTING.md#dependency-pinning-policy-supply-chain-hardening), all PyPI deps must be pinned as \`>=floor,<next_major\`.
|
||||||
|
|
||||||
|
**Unbounded specs found:**
|
||||||
|
\`\`\`
|
||||||
|
$(cat /tmp/unbounded.txt)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Fix:** Add an upper bound, e.g. \`\"package>=1.2.0,<2\"\`
|
||||||
|
|
||||||
|
---
|
||||||
|
*See PR #2810 and CONTRIBUTING.md for the full policy rationale.*"
|
||||||
|
|
||||||
|
gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY" || echo "::warning::Could not post PR comment (expected for fork PRs)"
|
||||||
|
|
||||||
|
- name: Fail on unbounded deps
|
||||||
|
if: steps.bounds.outputs.found == 'true'
|
||||||
|
run: |
|
||||||
|
echo "::error::PyPI dependencies without upper bounds detected. Add <next_major ceiling per CONTRIBUTING.md policy."
|
||||||
|
exit 1
|
||||||
|
|
|
||||||
23
AGENTS.md
23
AGENTS.md
|
|
@ -308,6 +308,29 @@ The registry handles schema collection, dispatch, availability checking, and err
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Dependency Pinning Policy
|
||||||
|
|
||||||
|
All dependencies must have upper bounds to limit supply-chain attack surface.
|
||||||
|
This policy was established after the litellm compromise (PR #2796, #2810) and
|
||||||
|
reinforced after the Mini Shai-Hulud worm campaign (May 2026).
|
||||||
|
|
||||||
|
| Source type | Treatment | Example |
|
||||||
|
|---|---|---|
|
||||||
|
| PyPI package | `>=floor,<next_major` | `"httpx>=0.28.1,<1"` |
|
||||||
|
| Git URL | Commit SHA | `git+https://...@<40-char-sha>` |
|
||||||
|
| GitHub Actions | Commit SHA + comment | `uses: actions/checkout@<sha> # v4` |
|
||||||
|
| CI-only pip | `==exact` | `pyyaml==6.0.2` |
|
||||||
|
|
||||||
|
**When adding a new dependency to `pyproject.toml`:**
|
||||||
|
1. Pin to `>=current_version,<next_major` for post-1.0 (e.g. `>=1.5.0,<2`).
|
||||||
|
2. For pre-1.0 packages, use `<0.(current_minor + 2)` (e.g. `>=0.29,<0.32`).
|
||||||
|
3. Never commit a bare `>=X.Y.Z` without a ceiling — CI and reviewers will reject it.
|
||||||
|
4. Run `uv lock` to regenerate `uv.lock` with hashes.
|
||||||
|
|
||||||
|
Reference: #2810 (bounds pass), #9801 (SHA pinning + audit CI).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Adding Configuration
|
## Adding Configuration
|
||||||
|
|
||||||
### config.yaml options:
|
### config.yaml options:
|
||||||
|
|
|
||||||
|
|
@ -800,6 +800,47 @@ Hermes has terminal access. Security matters.
|
||||||
|
|
||||||
If your PR affects security, note it explicitly in the description.
|
If your PR affects security, note it explicitly in the description.
|
||||||
|
|
||||||
|
### Dependency pinning policy (supply chain hardening)
|
||||||
|
|
||||||
|
After the [litellm supply chain compromise](https://github.com/BerriAI/litellm/issues/24512) in March 2026 and the [Mini Shai-Hulud worm campaign](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack) in May 2026, all dependencies must follow these rules:
|
||||||
|
|
||||||
|
| Source type | Required treatment | Rationale |
|
||||||
|
|---|---|---|
|
||||||
|
| **PyPI package** | `>=floor,<next_major` | PyPI versions are immutable once published, but new versions can be pushed into your range. A `<next_major` ceiling stops a 1.x install from upgrading to a malicious 2.0.0. |
|
||||||
|
| **Git URL** (atroposlib, tinker, yc-bench, Baileys) | Full commit SHA | Branches and tags are mutable refs; SHA is content-addressed. |
|
||||||
|
| **GitHub Actions** | Full commit SHA + version comment | Action tags are mutable refs (e.g. tj-actions/changed-files March 2025). Pin as `uses: owner/action@<sha> # vX.Y.Z` |
|
||||||
|
| **CI-only pip installs** | `==exact` | Hermetic CI builds; churn is acceptable. |
|
||||||
|
|
||||||
|
**Every new PyPI dependency in a PR must have a `<next_major` upper bound.** PRs adding unbounded `>=X.Y.Z` specs will be rejected by reviewers. The `supply-chain-audit.yml` CI workflow also flags dependency manifest changes for manual review.
|
||||||
|
|
||||||
|
**How to determine the ceiling:**
|
||||||
|
- If the package is at version `1.x.y`, use `<2`.
|
||||||
|
- If the package is at version `0.x.y` (pre-1.0), use `<0.(current_minor + 2)` — e.g. if current is `0.29.x`, use `<0.32`. This gives ~2 minor versions of headroom while keeping the window small enough that a hostile takeover version is unlikely to land inside it.
|
||||||
|
- Exception: packages with very stable APIs (e.g. `aiohttp-socks`) can use `<1` at reviewer discretion.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```toml
|
||||||
|
# ✅ Correct — post-1.0
|
||||||
|
"openai>=2.21.0,<3"
|
||||||
|
"pydantic>=2.12.5,<3"
|
||||||
|
|
||||||
|
# ✅ Correct — pre-1.0 (tight minor window)
|
||||||
|
"asyncpg>=0.29,<0.32"
|
||||||
|
"aiosqlite>=0.20,<0.23"
|
||||||
|
"hindsight-client>=0.4.22,<0.5"
|
||||||
|
|
||||||
|
# ❌ Rejected — no upper bound
|
||||||
|
"some-package>=1.2.3"
|
||||||
|
|
||||||
|
# ❌ Rejected — too tight (blocks legitimate patches)
|
||||||
|
"some-package==1.2.3"
|
||||||
|
|
||||||
|
# ❌ Rejected — too loose for pre-1.0 (allows 80 minor versions)
|
||||||
|
"some-package>=0.20,<1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reference PRs:** #2796 (litellm removal), #2810 (upper bounds pass), #9801 (SHA pinning + supply-chain-audit CI).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pull Request Process
|
## Pull Request Process
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue