Catches the failure mode that produced #25045: a contributor PR whose
branch had been disconnected from main's history (likely an accidental
'git checkout --orphan' or '.git/' re-init). GitHub's merge UI does
not refuse merges of unrelated histories, so the PR landed cleanly
with its intended one-file change but its parent-less root commit
(413990c94) got grafted into main as a second root. The merge
resolution itself was correct — main's content won for every
conflicting file — but ~1500 files' worth of git blame collapsed
onto that single commit.
Implementation: 'git merge-base origin/main HEAD' exits non-zero and
prints nothing when the two commits share no ancestor. Check both
conditions and fail with a clear message + recovery steps.
Verified: against the historic state of PR #25045 (base 5d90386ba,
head 1149e75db), 'git merge-base' returns empty with exit 1, so the
new check would have rejected it.
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
Previously :latest tracked the tip of main, which meant pulling :latest
got you whatever was last merged — fine for development, surprising for
users who expect :latest to mean 'the most recent stable release'.
Reshape the publish flow so the floating tags carry their conventional
meaning:
- :sha-<sha> every main commit (unchanged, immutable)
- :main tip of main (NEW; what :latest used to do)
- :<release_tag> every published release, e.g. :v1.2.3 (unchanged)
- :latest most recent release (CHANGED; release-only now)
Implementation:
- Rename the move-latest job to move-main; it still gates on push to
main, still ancestor-checks the existing :main label before
retagging, still uses cancel-in-progress: false so queued moves run
serially.
- Add a new move-latest job gated on release: published. Reads the
OCI revision label off the existing :latest and only advances if
the release commit is a strict descendant. This keeps backport
releases on older branches (e.g. patching v1.1.5 after v1.2.3 has
already shipped) from dragging :latest backwards.
- merge job exposes pushed_release_tag and release_tag outputs so
move-latest knows when to fire and what to retag from.
Let electron-builder's desktop package config provide platform-specific artifact extensions while the workflow injects the release version/channel metadata.
Adds `pull_request` trigger to docker-publish.yml so PRs that touch
Dockerfile / docker/ / pyproject.toml / uv.lock / the workflow itself
verify the image builds cleanly before merge. Previously, Dockerfile
regressions (e.g. a stale uv.lock, a typo'd dep) would only surface
after merge when the docker-publish workflow ran on main.
Build-verify-only on PRs: the per-arch jobs run their `load: true`
build + smoke test, but the push-by-digest + artifact upload steps
remain gated on push-to-main or release. The `merge` and
`move-latest` jobs stay excluded from PRs by their existing `if:`
gates, so :latest and SHA tags are never touched from PR runs.
Concurrency: PR runs use a PR-scoped group (`docker-<pr_number>`)
with `cancel-in-progress: true` so rapid pushes to the same PR
collapse to the latest commit. Push/release runs keep
`cancel-in-progress: false` — every merge still gets its own
SHA-tagged image.
Also adds arm64 smoke tests (previously amd64-only): the image is
now built with `load: true` on arm64 too, then `docker run --help` +
`dashboard --help` smoke tests run identically on both arches. Both
smoke test blocks were extracted into a new composite action at
`.github/actions/hermes-smoke-test` to keep the two jobs DRY.
New files:
- .github/actions/hermes-smoke-test/action.yml
Modified:
- .github/workflows/docker-publish.yml
Runs `uv lock --check` on every PR and on push to main that touches
pyproject.toml, uv.lock, or this workflow itself. Exits non-zero if
the lockfile is out of sync with pyproject.toml, blocking the PR
before it can break the Docker build on main.
Rationale: the new Dockerfile layout uses `uv sync --frozen --extra all`,
which rejects stale lockfiles. Without this guard, a PR that changes
pyproject.toml dependencies but forgets to regenerate uv.lock would
merge fine and then break docker-publish on main (visible only after
~15 min of build time, producing no image).
On failure, the step adds a GitHub annotation and a workflow summary
block with the exact commands to run locally (`uv lock`,
`git add uv.lock`, `git commit`).
Verified locally that:
- Clean tree: `uv lock --check` succeeds (resolves in ~2ms, no work).
- Stale lockfile (added cowsay to pyproject.toml, not in lock): exits 1
with message 'The lockfile at `uv.lock` needs to be updated'.
Build amd64 and arm64 natively on their own GitHub runners in
parallel, then stitch the per-arch digests into a tagged multi-arch
manifest. Replaces the previous single-runner pattern which rebuilt
arm64 from scratch on every run because QEMU emulation + unscoped GHA
cache meant no layer reuse across invocations.
Jobs:
build-amd64 — ubuntu-latest, native, runs smoke tests, pushes by
digest
build-arm64 — ubuntu-24.04-arm, native (no QEMU), pushes by digest
merge — stitches both digests into :sha-<sha> (main) or
:<release>
move-latest — unchanged ancestor-check logic, now needs: merge
Preserved:
- per-commit sha-<sha> tags on main (immutable, race-free)
- org.opencontainers.image.revision label on each per-arch image
- dashboard subcommand smoke test (#9153 guard)
- race-safe :latest advancement via move-latest
- top-level cancel-in-progress: false
Changed behavior:
- move-latest flipped to cancel-in-progress: false for
defense-in-depth.
Top-level concurrency already serializes runs for the ref, so the
old
cancel=true on move-latest was dead code. Flipping to false
prevents
any starvation mode if top-level is ever loosened.
Cache scopes separated per-arch (scope=docker-amd64 /
scope=docker-arm64)
so the two runners don't clobber each other in the gha cache backend.
Paired with commit e0c03defd (enabled PLW1514 in pyproject.toml) and
commit 3dfb35700 (added scripts/check-windows-footguns.py). Both
commits noted that the corresponding workflow edits were held back
because the authoring token lacked the `workflow` OAuth scope.
New jobs, both separate from `lint-diff` so the advisory diff
comment still posts when enforcement fails:
- ruff-blocking: runs `ruff check .` against the explicit select
list in pyproject.toml (currently PLW1514, which catches bare
open() that defaults to locale encoding — cp1252 on Windows).
No --exit-zero, no `|| true`; exit code propagates to the
required-check gate.
- windows-footguns: runs scripts/check-windows-footguns.py --all
(380 files, stdlib-only, <2s). Covers 11 Windows-unsafe
primitives — os.kill(pid, 0) bpo-14484 footgun, os.killpg,
os.setsid/setpgrp, signal.SIGKILL/SIGHUP/SIGUSR* without
getattr fallback, shebang scripts via subprocess, wmic without
shutil.which guard, hardcoded ~/Desktop OneDrive trap, bare
open() without encoding=, etc.
Both jobs pin actions by SHA to match repo convention.
tests/test_lint_config.py::test_workflow_has_blocking_ruff_step
now finds the blocking step and passes.
- Add hermes dashboard examples to the CLI help epilogue so users can
discover the web UI command from 'hermes --help' output
- Add an independent 'Test dashboard subcommand' CI step that verifies
'hermes dashboard --help' works in the Docker image, with its own
mkdir/chown setup to remain independent of the prior smoke test step
- Prevents regressions like #9153 where the dashboard subcommand was
present in source but missing from the published Docker image
Closes#9153
Switch top-level concurrency to cancel-in-progress=false so every push
to main gets its own SHA-tagged image published — no more discarded
builds when commits land back-to-back.
Guard the :latest tag with a second job that has its own concurrency
group with cancel-in-progress=true plus a git-ancestor check against
the revision label on the current :latest. Together these guarantee
:latest only ever moves forward in history: a slower run whose commit
isn't a descendant of the current :latest refuses to clobber it, and
a newer push mid-way through the move-latest job preempts the older
one before it can retag.
- Every main push publishes nousresearch/hermes-agent:sha-<commit>
with an org.opencontainers.image.revision label embedded.
- move-latest job reads that label off :latest, runs merge-base
--is-ancestor, and only retags (via buildx imagetools create,
registry-side, no rebuild) if our commit strictly descends.
- fetch-depth bumped to 1000 so merge-base has the history it needs.
- Release tag flow unchanged (unique tag, no race).
Adds two supply-chain controls that complement our existing pinning
strategy (full-SHA action pins, exact-version source dep pins via
uv.lock / package-lock.json) without undermining it.
.github/workflows/osv-scanner.yml
Detection-only scan of uv.lock and the ui-tui/website package-locks
against the OSV vulnerability database. Runs on PRs that touch
lockfiles, on push to main, and weekly against main so CVEs
published after merge still surface. Uses Google's officially-
recommended reusable workflow pinned by full SHA (v2.3.5).
Findings upload to the Security tab; fail-on-vuln is disabled so
pre-existing vulns in pinned deps do not block merges — we move
pins deliberately, not under CI pressure.
.github/dependabot.yml
Scoped to github-actions only. Action pins must be moved when
upstream publishes patches (often themselves security fixes);
Dependabot opens a PR with the new SHA + release notes for normal
review. Source-dependency ecosystems (pip, npm) are deliberately
NOT enabled — automatic version-bump PRs against uv.lock /
package-lock.json would fight our pinning strategy. CVE-driven
security updates for source deps are enabled separately via the
repo's Dependabot security updates setting (GitHub UI), which
fires only when a pinned version becomes known-vulnerable.
Two machine-readable entry points to the Hermes Agent docs:
/llms.txt curated index of every doc page, one link per page
with short descriptions. ~17 KB, safe to load into
an LLM context window.
/llms-full.txt every page under website/docs/ concatenated as markdown.
~1.8 MB. For one-shot ingestion by coding agents and
RAG pipelines.
Both files are also served from /docs/llms.txt and /docs/llms-full.txt
(Docusaurus serves website/static/ under baseUrl=/docs/). Some agents and
IDE plugins probe the classic site-root path; the deploy workflow now copies
both files to _site root so either URL works.
Conforms to the emerging llmstxt.org spec: H1 project name, blockquote
summary, short install command, GitHub link, then curated sections
mirroring the docs-site navigation (Getting Started, Using Hermes,
Features, Messaging, Integrations, Guides, Developer Guide, Reference).
Generated by website/scripts/generate-llms-txt.py. Wired into prebuild.mjs
so every 'npm run build' and 'npm run start' refreshes the files alongside
the existing skills.json extraction. Both outputs are gitignored (same
precedent as src/data/skills.json).
Descriptions in llms.txt are pulled from each page's frontmatter, so they
stay current automatically. All ~80 section slugs are validated against
the filesystem at generation time; an invalid slug would fail the prebuild.
* change(nix): dedupe nix lockfile checking scripts in ci
* feat(nix): make .#fix-lockfiles run --apply if no args passed
* fix(nix): use same nodejs version everywhere & small lints
- prevent lockfile thrashing while using nix :3
- use lib.getExe instead of raw /bin/ paths
- use inputs'.self instead of passing system in manually
* fix(nix): update lock files yet again (hopefully for the last time)
* fix(nix): align indentation of collision check echo
---------
Co-authored-by: Hermes Agent <hermes@nousresearch.com>
* fix(nix): replace magic-nix-cache with Cachix
magic-nix-cache caused recurring CI failures (TwirpErrorResponse
ResourceExhausted) by hitting GitHub Actions Cache's 10 GB limit and
200 req/min rate limit. This was flagged as 'unfixable infra flake' in
#17836 but is actually a fixable architecture choice.
Switch to Cachix (dedicated binary cache, no GHA quota dependency):
- Replace DeterminateSystems/magic-nix-cache-action with cachix/cachix-action
- Add cachix-auth-token input to nix-setup composite action
- Pass CACHIX_AUTH_TOKEN secret through all three nix workflows
- continue-on-error: true so cache failures never block CI
Cache 'hermes-agent' is public at hermes-agent.cachix.org.
Devs can pull locally with: cachix use hermes-agent
* fix: correct cachix-action commit SHA pin
---------
Co-authored-by: Hermes Agent <hermes@nousresearch.com>
* ci(nix): auto-fix stale npm hashes on push to main
When a PR merges to main with updated package-lock.json or package.json
in ui-tui/ or web/, the new auto-fix-main job detects stale npmDepsHash
values and pushes a fix commit directly to main.
This eliminates the recurring manual hash-bump PRs (#15420, #15314,
#15272, #15244) by reusing the existing fix-lockfiles --apply pipeline.
The fix commit only touches nix/*.nix files, which are outside the push
path filter (package-lock.json / package.json), so it cannot re-trigger
itself.
Closes#15314
* fix(ci): use GitHub App token for auto-fix-main push
GITHUB_TOKEN commits are invisible to workflow triggers (GitHub's
infinite-loop prevention). The auto-fix-main job pushes directly to
main, so the fix commit never triggered downstream nix.yml verification.
Mint a short-lived token via the repo's GitHub App (daimon-nous, APP_ID
+ APP_PRIVATE_KEY secrets) so the push is treated as a real event and
nix.yml fires to verify the corrected hashes.
Tested via workflow_dispatch dry-run: app token minted successfully,
checkout with app token succeeded, fix job correctly gated.
Resolves review feedback from Bugbot (r3144569551).
* ci(nix): rename lockfile check job for required status check
Rename 'check' → 'nix-lockfile-check' so the status check name is
unambiguous when added as a required check on main.
* fix(ci): harden auto-fix-main against races, loops, and silent failures
Address adversarial review findings:
1. Race condition (#1): Job-level concurrency with cancel-in-progress
collapses back-to-back pushes; ref: main checkout always gets latest
branch state; explicit push target (origin HEAD:main).
2. Loop prevention (#2): File-whitelist check before commit aborts if
any file outside nix/{tui,web}.nix was modified, preventing
accidental self-triggering.
3. Silent infra failures (#8): nix-lockfile-check now fails explicitly
when fix-lockfiles exits without reporting stale status (catches nix
setup failures, network errors, script bugs that bypass continue-on-error).
4. Commit traceability (#11): Auto-fix commits include source SHA and
workflow run URL in the commit body.
5. Explicit push target (#12): git push origin HEAD:main instead of
bare git push.
---------
Co-authored-by: alt-glitch <alt-glitch@users.noreply.github.com>
Generates a full dedicated Docusaurus page for every one of the 132 skills
(73 bundled + 59 optional) under website/docs/user-guide/skills/{bundled,optional}/<category>/.
Each page carries the skill's description, metadata (version, author, license,
dependencies, platform gating, tags, related skills cross-linked to their own
pages), and the complete SKILL.md body that Hermes loads at runtime.
Previously the two catalog pages just listed skills with a one-line blurb and
no way to see what the skill actually did — users had to go read the source
repo. Now every skill has a browsable, searchable, cross-linked reference in
the docs.
- website/scripts/generate-skill-docs.py — generator that reads skills/ and
optional-skills/, writes per-skill pages, regenerates both catalog indexes,
and rewrites the Skills section of sidebars.ts. Handles MDX escaping
(outside fenced code blocks: curly braces, unsafe HTML-ish tags) and
rewrites relative references/*.md links to point at the GitHub source.
- website/docs/reference/skills-catalog.md — regenerated; each row links to
the new dedicated page.
- website/docs/reference/optional-skills-catalog.md — same.
- website/sidebars.ts — Skills section now has Bundled / Optional subtrees
with one nested category per skill folder.
- .github/workflows/{docs-site-checks,deploy-site}.yml — run the generator
before docusaurus build so CI stays in sync with the source SKILL.md files.
Build verified locally with `npx docusaurus build`. Only remaining warnings
are pre-existing broken link/anchor issues in unrelated pages.
Recent main runs have been hitting the 10-minute cap repeatedly — the
full non-integration suite no longer fits in that window on
ubuntu-latest. Cancelled runs leave main without a green signal, which
masks real regressions.
Bumps only the test job. The e2e job still finishes in ~25s, so its
10-minute cap stays as-is.
PR #12681 removed the audit entirely because it fired on nearly every PR
(Dockerfile edits, dependency bumps, Actions version strings, plain
base64 usage, etc.) — reviewers were ignoring it like cancer warnings.
Restore it with aggressive scope reduction:
Kept (real attack signatures):
- .pth file additions (litellm-attack mechanism)
- base64 decode + exec/eval on the same line
- subprocess with base64/hex/chr-encoded command argument
- install-hook files (setup.py, sitecustomize.py, usercustomize.py,
__init__.pth)
Removed (low-signal noise that fired constantly):
- plain base64 encode/decode
- plain exec/eval
- outbound requests.post / httpx.post / urllib
- CI/CD workflow file edits
- Dockerfile / compose edits
- pyproject.toml / requirements.txt edits
- GitHub Actions version-tag unpinning
- marshal / pickle / compile usage
Also gates the workflow itself on path filters so it only runs on PRs
touching Python or install-hook files — no more firing on docs/CI PRs.
The workflow still fails the check and posts a PR comment on
critical findings, but by design those findings are now rare and
worth inspecting when they occur.
- Docker build only triggers on main push (code/config changes) and
releases, no longer on every PR
- Tests skip markdown-only and docs-only changes
- Remove supply-chain-audit workflow