Skip to content

Commit 279ce21

Browse files
committed
security(deps): add upper bounds to 5 loose deps + document supply chain policy (NousResearch#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 NousResearch#2810 and NousResearch#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: NousResearch#2796 NousResearch#2810 NousResearch#9801 NousResearch#24205
1 parent 764dea3 commit 279ce21

3 files changed

Lines changed: 130 additions & 0 deletions

File tree

.github/workflows/supply-chain-audit.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
- '**/sitecustomize.py'
1212
- '**/usercustomize.py'
1313
- '**/__init__.pth'
14+
- 'pyproject.toml'
1415

1516
permissions:
1617
pull-requests: write
@@ -137,3 +138,68 @@ jobs:
137138
run: |
138139
echo "::error::CRITICAL supply chain risk patterns detected in this PR. See the PR comment for details."
139140
exit 1
141+
142+
dep-bounds:
143+
name: Check PyPI dependency upper bounds
144+
runs-on: ubuntu-latest
145+
if: contains(github.event.pull_request.changed_files_url, 'pyproject.toml') || true
146+
steps:
147+
- name: Checkout
148+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
149+
with:
150+
fetch-depth: 0
151+
152+
- name: Check for unbounded PyPI deps
153+
id: bounds
154+
run: |
155+
set -euo pipefail
156+
157+
BASE="${{ github.event.pull_request.base.sha }}"
158+
HEAD="${{ github.event.pull_request.head.sha }}"
159+
160+
# Only check added lines in pyproject.toml
161+
ADDED=$(git diff "$BASE".."$HEAD" -- pyproject.toml | grep '^+' | grep -v '^+++' || true)
162+
163+
if [ -z "$ADDED" ]; then
164+
echo "found=false" >> "$GITHUB_OUTPUT"
165+
exit 0
166+
fi
167+
168+
# Match PyPI dep specs that have >= but no < ceiling.
169+
# Pattern: "package>=version" without a following ",<" bound.
170+
# Excludes git+ URLs (which use commit SHAs) and comments.
171+
UNBOUNDED=$(echo "$ADDED" | grep -oE '"[a-zA-Z0-9_-]+(\[[^\]]*\])?>=[ 0-9.]+"' | grep -v ',<' || true)
172+
173+
if [ -n "$UNBOUNDED" ]; then
174+
echo "found=true" >> "$GITHUB_OUTPUT"
175+
echo "$UNBOUNDED" > /tmp/unbounded.txt
176+
else
177+
echo "found=false" >> "$GITHUB_OUTPUT"
178+
fi
179+
180+
- name: Post unbounded dep warning
181+
if: steps.bounds.outputs.found == 'true'
182+
env:
183+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
184+
run: |
185+
BODY="## ⚠️ Unbounded PyPI Dependency Detected
186+
187+
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\`.
188+
189+
**Unbounded specs found:**
190+
\`\`\`
191+
$(cat /tmp/unbounded.txt)
192+
\`\`\`
193+
194+
**Fix:** Add an upper bound, e.g. \`\"package>=1.2.0,<2\"\`
195+
196+
---
197+
*See PR #2810 and CONTRIBUTING.md for the full policy rationale.*"
198+
199+
gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY" || echo "::warning::Could not post PR comment (expected for fork PRs)"
200+
201+
- name: Fail on unbounded deps
202+
if: steps.bounds.outputs.found == 'true'
203+
run: |
204+
echo "::error::PyPI dependencies without upper bounds detected. Add <next_major ceiling per CONTRIBUTING.md policy."
205+
exit 1

AGENTS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,29 @@ The registry handles schema collection, dispatch, availability checking, and err
308308

309309
---
310310

311+
## Dependency Pinning Policy
312+
313+
All dependencies must have upper bounds to limit supply-chain attack surface.
314+
This policy was established after the litellm compromise (PR #2796, #2810) and
315+
reinforced after the Mini Shai-Hulud worm campaign (May 2026).
316+
317+
| Source type | Treatment | Example |
318+
|---|---|---|
319+
| PyPI package | `>=floor,<next_major` | `"httpx>=0.28.1,<1"` |
320+
| Git URL | Commit SHA | `git+https://...@<40-char-sha>` |
321+
| GitHub Actions | Commit SHA + comment | `uses: actions/checkout@<sha> # v4` |
322+
| CI-only pip | `==exact` | `pyyaml==6.0.2` |
323+
324+
**When adding a new dependency to `pyproject.toml`:**
325+
1. Pin to `>=current_version,<next_major` for post-1.0 (e.g. `>=1.5.0,<2`).
326+
2. For pre-1.0 packages, use `<0.(current_minor + 2)` (e.g. `>=0.29,<0.32`).
327+
3. Never commit a bare `>=X.Y.Z` without a ceiling — CI and reviewers will reject it.
328+
4. Run `uv lock` to regenerate `uv.lock` with hashes.
329+
330+
Reference: #2810 (bounds pass), #9801 (SHA pinning + audit CI).
331+
332+
---
333+
311334
## Adding Configuration
312335

313336
### config.yaml options:

CONTRIBUTING.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,47 @@ Hermes has terminal access. Security matters.
800800

801801
If your PR affects security, note it explicitly in the description.
802802

803+
### Dependency pinning policy (supply chain hardening)
804+
805+
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:
806+
807+
| Source type | Required treatment | Rationale |
808+
|---|---|---|
809+
| **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. |
810+
| **Git URL** (atroposlib, tinker, yc-bench, Baileys) | Full commit SHA | Branches and tags are mutable refs; SHA is content-addressed. |
811+
| **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` |
812+
| **CI-only pip installs** | `==exact` | Hermetic CI builds; churn is acceptable. |
813+
814+
**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.
815+
816+
**How to determine the ceiling:**
817+
- If the package is at version `1.x.y`, use `<2`.
818+
- 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.
819+
- Exception: packages with very stable APIs (e.g. `aiohttp-socks`) can use `<1` at reviewer discretion.
820+
821+
**Examples:**
822+
```toml
823+
# ✅ Correct — post-1.0
824+
"openai>=2.21.0,<3"
825+
"pydantic>=2.12.5,<3"
826+
827+
# ✅ Correct — pre-1.0 (tight minor window)
828+
"asyncpg>=0.29,<0.32"
829+
"aiosqlite>=0.20,<0.23"
830+
"hindsight-client>=0.4.22,<0.5"
831+
832+
# ❌ Rejected — no upper bound
833+
"some-package>=1.2.3"
834+
835+
# ❌ Rejected — too tight (blocks legitimate patches)
836+
"some-package==1.2.3"
837+
838+
# ❌ Rejected — too loose for pre-1.0 (allows 80 minor versions)
839+
"some-package>=0.20,<1"
840+
```
841+
842+
**Reference PRs:** #2796 (litellm removal), #2810 (upper bounds pass), #9801 (SHA pinning + supply-chain-audit CI).
843+
803844
---
804845

805846
## Pull Request Process

0 commit comments

Comments
 (0)