Commit Graph

654 Commits

Author SHA1 Message Date
Teknium
3f524a2459 ci: version the required check so CI greens from before a new required job stop counting
The protect-main ruleset requires a check by NAME, and a green check run on a
PR head never expires. After the code-health ratchet joined the required set,
32 PRs whose last CI run predated it merged on their old "All required checks
pass" green without the ratchet ever running; 20 of them would have been
blocked (62 findings, incl. 15 new blind `except Exception`).

Renaming the gate to "All required checks pass (v2)" and pointing the ruleset
at the new name voids every older green: each open PR needs one fresh CI run,
which now includes the ratchet, before it can merge, whoever merges it.
The ci.yaml comment documents the bump as the standard step whenever the
required set gains a job every PR must pass.

live_comment.py keeps both names in its infra list (old runs still render),
and a test pins that the gate's current display name never shows as a job.
2026-10-06 03:57:34 -07:00
Teknium
93c9360a8d ci: markdown under pm/ no longer builds six PM bundles
pm-bundle, install-e2e and termux-verify trigger on pm/**, so adding pm/AGENTS.md
(and every later edit to it) queued darwin-x64 / win32-arm64 bundle builds for a doc
change. Nothing reads .md files under pm/, so exclude them from the path filters.
2026-10-04 23:05:42 -07:00
Teknium
e1ec584b57 feat(ci): cap every AGENTS.md chain below the context budget it is truncated at
scripts/ci/check_agents_md_size.py (in scripts/check and lint.yml): root <= 12k, every
AGENTS.md plus its ancestors <= 30k (the joined root->cwd block on a 128k window, minus
the per-file labels), every area file <= _MAX_HINT_CHARS read from the source so the two
cannot drift. On origin/main it reports the root and all 11 root-to-area chains over.
2026-10-04 23:05:42 -07:00
teknium1
6f0d25c1a1 feat(code-health): ENFORCEMENT switch and a pre-push hook
- `ENFORCEMENT` in scripts/code_health/config.py (blocking / advisory / off)
  turns the ratchet down or off with a one-line commit to main. It is read
  from the BASE revision, so it applies to fork PRs (repo variables are not
  passed to fork-triggered workflows), CI and local hooks alike, and a PR
  cannot relax its own check.
- `scripts/check --install-hook pre-push` judges each pushed commit the way
  CI judges the PR (base = that commit's merge-base); `--commit REV` is the
  underlying mode, `--uninstall-hook` removes our hooks and leaves foreign
  ones. Pre-commit and pre-push share one tree snapshot that never touches
  the user's worktree or index.
- A failing report names the local commands; CONTRIBUTING, AGENTS.md and the
  PR template point at `python scripts/check`.
2026-10-04 21:29:39 -07:00
teknium1
cf0755f947 fix(code-health): occurrence provenance, alias-aware rules, strict file cap (round 4)
Fourth review round (F11-F18, N1-N4, kvnloo, Adolanium):

- Hits are old only where the base occurrence survives: a line the comment- and
  whitespace-blind line diff keeps (blame semantics), or anything inside a unit whose
  body is unchanged. An identical violation moved past unrelated code is new; removing
  an allow comment or re-indenting is not. Hit fingerprints exclude comments (N4).
- Inline allows cover their own line; only a comment-only line covers the next (N3).
- Rules read a copy of the AST with import aliases resolved, so `sp.run`,
  `from subprocess import run as execute`, aliased threading/asyncio/load_config are
  seen (F12); deadline helpers must resolve to the real asyncio API, and
  communicate()/wait() are judged only on receivers holding a spawned process (F13);
  infinite deadlines are unbounded (F14).
- HERMES_* writes introduce a name too (N1); bool(os.environ[...]) is HX010 (N2);
  HX009 needs the isinstance subject to come from gather(return_exceptions=True).
- Recursive self-references are normalised out of body hashes, so a recursive rename
  keeps its cap, py and ts (F15). Decorators belong to their function's scope and
  classes are matchable units, so their hits follow proven moves (F16).
- An over-cap file may not grow (F17, Teknium: strict improvement-only).
- A file the checker cannot parse or measure (ast/ruff/TS parse error) fails.
- scripts/check: --staged always lints an index snapshot (ignored files are invisible
  to git status, not to guards) (F18); create the --prefix dir for Git for Windows
  (F11); --only builds just the selected jobs, so it needs no ruff for non-ruff checks.
- CODEOWNERS covers scripts/check + scripts/code_health; frontend-only PRs skip the
  Python-only lint jobs again and run only the ratchet.
2026-10-04 21:29:39 -07:00
teknium1
ca682666b8 ci: one command runs every blocking lint check, locally and in CI
`python scripts/check` runs ruff, the Windows-footgun/shebang/no-tmp/
config-writer/OS-marker guards and the code-health ratchet in parallel
with CI's pinned ruff (read from pyproject's dev group; a different local
ruff is never used silently, `uvx ruff@<pin>` is the fallback).
`--install-hook` makes it the pre-commit hook; the hook skips branches
that predate scripts/check, since hooks are shared by every worktree.

lint.yml gains the blocking `code health ratchet` job: it compares the PR
merge commit with its first parent (the base branch tip), so a PR is held
to main as it is now. The lint lane also runs for frontend-only changes
because the ratchet covers TypeScript, and the existing ruff installs read
the same pin.
2026-10-04 21:29:39 -07:00
teknium1
1a69f42275 ci(plugin-catalog): full blobless checkout so Hermes resolves its version
pinned-source-validate checked out depth 2. Hermes resolves its own
version from release tags merged into HEAD, so the shallow checkout
reported 'unknown' and plugins that gate on the host version refused to
load (omh, #122578: requires Hermes >=0.21.1). A blobless full clone
carries the commit graph and tags for about the cost of the shallow one.
2026-10-04 12:33:42 -07:00
ethernet
1af98f58ba Merge pull request #131726 from NousResearch/ethie/codeowners-ci-review
ci: replace the ci-reviewed label gate with CODEOWNERS
2026-10-02 18:49:46 -04:00
ethernet
de2c9c2ec9 ci: widen CODEOWNERS to manifests, installers, scripts/ci, SECURITY.md, plugin catalog
Adds package.json and pyproject.toml at any depth, flake.lock, every
*package-lock.json (incl. nix/node-gyp-11-4-0-package-lock.json), .npmrc
(the min-release-age quarantine), pm/artifact-mirror.json, the shell
installers and their pin generator, scripts/ci/, SECURITY.md and
plugin-catalog/. Drops the header claim that the ruleset's
required-reviewers rule already covers the manifests.
2026-10-02 18:23:54 -04:00
ethernet
82a62cd58d ci: cover every Dockerfile in CODEOWNERS
*Dockerfile also matches docker/sandbox-desktop.Dockerfile and
scripts/termux/termux-builder.Dockerfile, not just the root one.
2026-10-02 18:21:56 -04:00
ethernet
4022f1da56 ci: add lockfiles and the Dockerfile to CODEOWNERS
uv.lock, package-lock.json (any depth, incl. pm/uv.lock), pm/lock.json
and the root Dockerfile now request core review directly instead of
relying only on the ruleset's required-reviewers rule.
2026-10-02 18:20:42 -04:00
ethernet
059efe8b29 ci: replace the ci-reviewed label gate with CODEOWNERS
Path-based review (workflows, composite actions, eslint/prettier config,
the MCP catalog, install-hook files) moves to .github/CODEOWNERS. GitHub
requests and enforces that review, so CI no longer classifies those paths,
checks a label, or reruns failed jobs when the label is added.

Removes review-labels.yml, emit_review_status.py, the rerun-review-labels
job, and the ci_review / ci_review_files / mcp_catalog classifier outputs.
The run-e2e rerun job in label-rerun.yml stays.

The supply-chain scan keeps its content patterns (base64 decode + exec,
obfuscated subprocess), which CODEOWNERS cannot express. It fails on its
own again, as it did before #68833 routed it through the label gate. Its
.pth and install-hook path checks are dropped in favour of CODEOWNERS.
2026-10-02 15:22:49 -04:00
ethernet
8c34d2ecc5 fix(release): let the win32-bundle candidate call assemble the universal bundle
stable-release.yml runs `jobs: win32-bundle` as its own call after the
per-arch candidate calls. In that call validate selects only win32-bundle, so
assemble-win32-bundle's gate (win32-x64/win32-arm64 selected AND their build
jobs succeeded) was always false and the job skipped, which failed the stable
bundle phase with `assemble-win32-bundle=skipped`.

A build selected in the same call must still succeed; an unselected one is
fetched from the R2 handoff, as the assemble steps already do.
2026-10-02 15:10:43 -04:00
unsupportedpastels
009afb3ac5 docs: route agents and contributors to SECURITY.md for security reports 2026-10-02 12:10:51 -04:00
ethernet
0217a2a324 feat(icons): custom drive artwork as the volume icon of both installer DMGs
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the Icon Composer package, and
dmgbuild's badge option can only paste onto the stock removable-drive icon.
Ship our own drive-with-the-girl artwork instead: assets/dmg-volume.png
(hand-made, one artwork since volume icons have no appearance variants) is
emitted as apps/desktop/packaging/dmg-volume.icns, beside the DMG background
so the `files` whitelist keeps it out of the app bundle, and dmg.icon points
at it.

Tauri always uses the app icns as the volume icon and offers no override, so
the bootstrap workflow runs scripts/bundles/set_dmg_volume_icon.sh on the
built DMG before signing (the same .VolumeIcon.icns + custom-icon flag
dmgbuild sets), and the verify step checks the icon is on the mounted image.
Verified on macOS 26.6.2: both DMGs render the identical volume icon.
2026-10-02 10:33:44 -04:00
ethernet
383916b845 feat(icons): layered macOS 26 icon for the bootstrap installer and the DMGs
Tauri's bundler (cli 2.11.4) compiles an Icon Composer package listed in
bundle.icon into Assets.car with actool >= 26 and sets CFBundleIconName, so
the bootstrap installer gets an unbranded twin of the desktop package under
src-tauri/icons/icon.icon (generator target; the bundle stays the plain
brand because flavours only apply under apps/desktop/). Tauri only logs when
actool is too old, so the macOS job selects Xcode 26 and the verify step
asserts Assets.car and the plist key are in the built .app.

The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the .icon package; point
dmg.icon at the full-resolution assets/icon.icns. Tauri's DMG already takes
--volicon from the bundle's .icns, which the regenerated outputs cover.
2026-10-02 10:33:44 -04:00
ethernet
26960b1afb build(desktop): compile the layered icon to Assets.car on macOS 26 hosts
electron-builder (app-builder-lib 27.0.0-alpha.6) compiles a `.icon` given as
`mac.icon` into Contents/Resources/Assets.car and sets CFBundleIconName, but
only with actool >= 26 (Xcode 26); on any other host it throws and the build
fails. scripts/mac-icon.cjs picks the Icon Composer package when that actool
is present and the legacy .icns otherwise, so a dev Mac on Xcode 16 keeps
building the app it always built.

The release runners keep Xcode 16.4 selected although 26.x is installed, so
the darwin job now selects the newest Xcode 26 via DEVELOPER_DIR and asserts
the actool version before packaging; a silent fallback cannot ship from CI.

electron-builder's `.icon` path also replaces the bundled icon.icns with
actool's 256px fallback. macOS <= 15 shows that file, so after-pack restores
our 16..1024 assets/icon.icns.
2026-10-02 10:33:43 -04:00
teknium1
093f8d4480 ci: run the Python suite and the macOS lane as two slices each
The full suite was the last job to finish in 33 of 43 CI runs since it
moved from a 96-core to one 32-core runner (p50 666 s). That runner is
~90% packed, so only more machines help, and one bigger box is poor
value: on 96 cores the same files took 25-50% longer each and the suite
ran ~1.8x faster for 3x the price. Two 32-core slices scale linearly
(simulated from per-file CI timings: 262 s / 252 s vs 514 s on one box).

macOS (free 3-core runner, ~7.5 min) would become the slowest required
job once Linux splits, so it runs as two slices on the same free runner.

--slice now partitions from the checkout alone (sorted round-robin):
each slice job restores its own duration cache, and a main push saving
a newer cache between the two restores made timed LPT compute different
partitions per job, silently dropping or double-running files.
Round-robin lands within 5 s of timed LPT at this suite size. Each slice
saves its own cache key on main; the runner keeps the restored entries
for files outside its slice, so the cache stays complete.
2026-10-01 23:49:20 -07:00
teknium1
013b776ab0 ci(label-rerun): never rerun a CI run whose PR head has moved
Adding ci-reviewed starts a job that waits for the PR's CI run, then reruns
its failed jobs. A push during the wait cancels that run, the waiter sees
"completed", reruns it, and the rerun rejoins the per-PR concurrency group
and cancels the new head's run. The PR is left testing a stale commit (seen
on #131236 and #131246). Skip the rerun when the head moved: the new run's
own event already carries the label. Same guard on the run-e2e job.
2026-10-01 23:49:01 -07:00
teknium1
b909c01d07 fix(ci): scope the catalog data-only guard to entry PRs; diff the merge commit shallowly
- Run the data-only guard once, inside the changed-entries step of the
  pinned-source job, and only when an entry YAML changed. README, policy,
  docs and CI PRs that touch plugin-catalog/ without an entry no longer go
  red.
- Allow contributors/emails/** beside plugin-catalog/**: the attribution
  fix we push onto catalog PR branches is data the gate never executes
  (29 of the last 134 merged catalog PRs carried one).
- Diff GitHub's merge commit against its first parent with fetch-depth: 2
  instead of a full-history clone plus merge-base. The structural job,
  which only validates the whole directory, goes back to a shallow
  checkout with no diff at all.
- `subdir:` with no value means the repo root in both the parse step and
  the structural validator, matching how the runtime loader has always
  read it.
2026-10-01 10:46:45 -07:00
beardthelion
7e52bd0fa9 fix(ci): close remaining pin-gate escapes found in review
- Scan the whole clone for escaping symlinks, not just PLUGIN_DIR:
  find does not descend into a symlinked directory, so an allowed
  in-clone link (plugin/ext -> ../lib) hid a nested escape.
- Require catalog admission PRs to be data-only in both jobs; the
  gate's enforcement code resolves from the PR checkout, so a diff
  that also touches tooling could neuter the gate vetting it.
- persist-credentials: false on both checkouts: `hermes plugins
  validate` runs pinned-repo code and must not see the job token.
- Broaden the self-updater patterns: releases/download and codeload
  fetches, and the Node fs write sinks (writeFileSync, renameSync,
  copyFileSync, cpSync, createWriteStream).
2026-10-01 10:46:45 -07:00
teknium1
eb9cb6c8e1 fix(ci): make the -I shadow guard load-bearing; widen clone timeout to 600s
Follow-up to the admission-gate confinement:

- tests/ci: test_parser_imports_cannot_be_shadowed_by_checkout_files planted
  `re.py`, but `re` is imported during interpreter startup, so a cwd copy never
  shadows it and the test passed even with `-I` stripped from the workflow.
  Plant `shlex.py` instead (the parser is its first importer): with a shadow
  whose quote() swallows the rejection, the gate returns rc 0 / PASS without
  `-I` and rc 1 with it, so the test now actually guards the fix.
- workflow: `timeout 120 git clone` -> 600. The clone is deliberately full
  (the pin may not be the tip) and listed repos exceed 700 MB
  (hermes-jackal-verified 1.43 GB, hindsight 741 MB); 120s would false-fail a
  legitimate re-pin on a slow runner while 600s still bounds a hostile URL
  well under the 30-minute job cap.

Attribution: beardthelion@users.noreply.github.com is already in the legacy
AUTHOR_MAP, so no mapping file is needed.
2026-10-01 10:46:45 -07:00
beardthelion
8444e9eb18 fix(ci): confine plugin-catalog admission gate to the pinned clone
The pinned-source job joined a PR-controlled subdir onto the clone root
with no validation, accepted file:// repos and ref names as the "pin",
and followed committed symlinks out of the clone, so the gate could
validate attacker-planted files instead of the declared commit's
contents. Harden the step end to end:

- validate repo (https://, whitespace-free), sha (40-hex, commit object),
  and subdir (plain relative path) in the parser before any use
- run the parser under python3 -I against installed ruamel.yaml so the
  PR checkout cannot shadow its imports, capture parser output, and
  reset shell variables per entry so a dead parser cannot replay the
  previous entry's fields
- confine PLUGIN_DIR and every symlink beneath it to the clone root
- include renamed and typechanged entry files in the changed set
- feed entries over fd 3 and keep plugin-executed commands off stdin so
  validate cannot drain the remaining list
- NUL-delimit both self-updater grep stages and sanitize the echoed
  filename list so crafted names cannot forge workflow commands
- mirror the same repo/subdir rules in the runtime catalog loader and
  the site extractor, and use \Z anchors so a trailing newline cannot
  split the verdict between gates
2026-10-01 10:46:45 -07:00
Maciej Lisiewski
7239625ae1 ci: reduce HERMES_TEST_WORKERS to 16 for Windows x64/arm64, default for macos 2026-09-30 16:34:52 -04:00
Maciej Lisiewski
eb46d8bf35 ci: bump HERMES_TEST_WORKERS to match earlier runner size change 2026-09-30 16:34:52 -04:00
Maciej Lisiewski
1188c9c397 ci: Update Github actions versions, use same versions everywhere 2026-09-30 16:34:52 -04:00
Siddharth Balyan
09681e7959 fix(release): --branding stable works before stable has a channel record (#129541)
`--branding stable` copies the identity from the published stable channel
record on R2, and refused when there was none. Production has no
releases/channels/stable.json yet: the channel pipeline merged after the
last stable release (v2026.9.24), and no stable release has published
through it since. So every stable-branded channel build failed at
allocation ("none is published"), including the first real dispatch.

When the stable record does not exist, use product_identity(), the
identity channel_publish already holds every stable release to. A stable
record without a head still refuses, so once the first stable release
publishes, its record is the only authority again and this path stops
running by itself.

The allocation step needs node for product_identity(), so the workflow's
identity-reader setup also runs for branding == 'stable'.
2026-10-01 01:30:24 +05:30
emozilla
5c08ad68f7 ci: run the slow lanes on a PR only when it touches their paths
The E2E suites, the Windows E2E jobs, the Docker image build and the Nix
flake check took about 120 of the ~140 larger-runner minutes each PR push
spent, and fired on nearly every PR. The 32-core pool backed up behind
them.

On a pull request each slow lane now runs only when the diff touches the
suite, its harness, or the code it guards (classify_changes.py
_E2E_LANES, _DOCKER_PATHS, _NIX_LANE_PATHS), or when the PR carries the
run-e2e label. Every push to main still runs all of them, and a release
run still forces every lane on.

Adding run-e2e reruns the head commit's CI, Docker and Nix runs; detect
reads the live labels because a rerun replays the original event.

Replayed over the last 200 merged PRs, the modelled 32-core cost of one
push drops from about 120 to about 10 runner-minutes.
2026-09-30 14:37:52 -04:00
Jeffrey Quesnelle
acc673c0c8 Merge pull request #129319 from NousResearch/feat/channel-stable-branding
release.py --branding stable builds a channel commit as the regular Hermes app
2026-09-30 13:16:53 -04:00
Maciej Lisiewski
fcdf5f7e7f ci: Switch from 96 to 32 core runner for test 2026-09-30 16:33:03 +02:00
ethernet
13dbe260aa feat(release): --branding stable builds a channel commit as the regular app
A preview channel always minted its own side-by-side identity (Hermes NAME,
ai.hermes.channel.h<token>), and a plain --build-commit carries
Hermes Agent <sha7> with a red icon, so there was no way to ship an exact
commit under the official name, icon and package ID.

--branding stable (with --channel) makes channel creation copy the identity
from the published stable channel record instead of reserving a new token.
Branding is fixed at creation like the rest of the identity: re-dispatching
with the other branding is refused, as is stable branding before a stable is
published or in a disposable namespace. --branding without --channel is
refused rather than silently falling back to a sha-branded one-off build.

The workflow gains a `branding` input, forwarded to the allocation step and
the dispatch log so the printed release.py replay matches what was sent.
2026-09-30 09:19:35 -04:00
JoaoMarcos44
16b214e1a1 fix(config): make destructive replacements explicit
atomic_config_write() now refuses deletion-by-omission at any mapping depth:
atomic_roundtrip_yaml_save treats keys absent from the payload as deleted
recursively, while the old docstring described the call as a merge, so a
partial dict could silently erase unrelated config (#125107). A new
atomic_config_replace() names the deliberate full-state replacement contract
explicitly, and the owners that intentionally remove state (save_config,
provider reset/switch, credential mirror scrub, doctor key migration, profile
channel stripping, TUI full-state saves) route through it. Additive writers
stay on the guarded atomic_config_write.

Salvages #125152 (identical patch, cherry-picked; conflict in
_write_user_config resolved to pass atomic_config_replace through the new
telemetry wrapper) and carries the executable bit main expects on
scripts/check_config_yaml_writers.py.

Fixes #125107

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-29 20:24:19 -04:00
ethernet
ee5f49b943 fix(ci): give the stable-release reusable workflows their environment secrets
stable-release.yml called desktop-bundled-release.yml, docker.yml and
termux-verify.yml without `secrets: inherit`, so every job inside them that
declares `environment: release-signing` (or container-publish) attached its
deployment and read the environment's `vars`, but resolved `secrets.*` to the
empty string. The signed-candidate legs died at "Archive every pinned input"
with `missing env CLOUDFLARE_R2_ACCESS_KEY_ID`; the docker publish legs and
the termux packaging checks would have failed the same way.

A called workflow receives no secrets by default, and a job-level
`environment:` inside it is required but not sufficient: a caller job that
`uses:` a reusable workflow cannot declare `environment:` at all, so
`secrets: inherit` is the only mechanism that can carry them
(actions/runner#1490).

`secrets: inherit` is added only where the callee reads a protected
environment's secrets. ci.yaml, nix, pm-bundle, windows-venv-e2e and the
install-e2e chain reference none, and ci.yaml must stay secret-free by its own
header (it runs PR-controlled code).
2026-09-29 09:40:09 -04:00
Teknium
a5bd246865 Old pre-decomposition import paths are gone: plugin compat layer removed on schedule (#126164)
* refactor(plugins): remove the Sep 2026 decomposition compat layer on schedule

The PLUGIN-COMPAT layer (2776813df3 + d63e380324 + 0a5164cebe) kept pre-#102117 import paths
alive for external plugins until 2026-09-14. That window closed two weeks ago; since then the loader
has already been skipping plugins that use the old paths. This removes the layer itself:

- 328 appended `PLUGIN-COMPAT` blocks (lazy `__getattr__` pointer tables, re-exported third-party
  names, restored dead definitions) and the three re-export stub modules
  (gateway/startup_watchdog, hermes_cli/observability/relay_runtime, tools/environments/modal_utils)
- COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py and its lint step
- the reporting surfaces: CLI banner notice, `hermes plugins compat`, the `hermes doctor` section,
  the post-update notice, the Desktop one-time dialog, the loader's pre-import skip and the
  `plugins.allow_deprecated_imports` escape hatch

An external plugin that still imports an old path now fails to load with its ImportError as the
reason in `hermes plugins list`, the same path as any broken plugin.

hermes_cli/plugin_compat.py stays as three inert stubs (compat_report, removal_in_effect,
summary_lines): an already-running pre-removal `hermes update` lazy-imports them after the checkout
swap (tests/compat/old_updater_surface.json).

In-tree fallout, both already dead: hermes_cli/setup.py::_check_espeak_ng (no callers; its
`shutil` came from a compat block) and gateway/config.py::SessionResetPolicy ("retained solely for
the scheduled plugin-compat window"). Two test_run_agent patches targeted the removed
`run_agent.handle_function_call` pointer; they now patch `model_tools.handle_function_call`, the
seam production reads, like every sibling test in that file.

* chore: retrigger CI (zero-job startup_failure phantom)

* test: drop resolution allowlist rows for the two deleted which() sites

hermes_cli/setup.py::_check_espeak_ng (dead) and tools/skillevaluator_scan.py::scanner_available
(a restored definition inside a PLUGIN-COMPAT block) no longer exist; the stale-row gate requires
their allowlist entries go with them.
2026-09-28 10:21:41 -07:00
Teknium
188a1a5efd Publish nousresearch/hermes-sandbox:desktop, the terminal-backend sandbox image with a display stack (#121110)
Some checks failed
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Skills Index Freshness Check / check-freshness (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
* feat(docker): publish nousresearch/hermes-sandbox:desktop for terminal backends

The terminal backends (docker, modal, daytona, singularity) all default to
nikolaik/python-nodejs:python3.11-nodejs20, a bare Python+Node base. For Bot
Screen, computer_use and the browser to run INSIDE that sandbox instead of on
the gateway host, the sandbox image needs the display stack.

docker/sandbox-desktop.Dockerfile is that base plus:
  - the everyday tools it lacked (jq, ripgrep, fd, tmux, less, nano, vim,
    zip, rsync, tree, procps, htop, sudo for the base's uid-1000 `pn`)
  - the exact package set the Hermes -desktop image installs (TigerVNC,
    Xfce components, dbus, xauth, fonts)
  - Playwright's headed Chromium (same build as the -desktop image)
  - cua-driver 0.28.2 from its pinned release tarball

No Hermes inside; the default user stays root like the base so nothing
changes for people who just switch docker_image. Desktop processes run as
`pn`. 4.27 GB on amd64.

docker.yml gains a `sandbox` variant with its own cache scope and repository
(nousresearch/hermes-sandbox:desktop, :main-desktop, :<release>-desktop);
the docker-integration suite is skipped for it (no Hermes to test) and
docker/sandbox-desktop-smoke.sh runs instead: as `pn`, every launcher and
cua-driver binary resolves, the real launcher.sh publishes :20, the RFB
socket completes the 3.8 handshake relayed over `docker exec -i` stdio, and
a headed Chromium maps a window on that display. hadolint lints the new
Dockerfile in docker-lint.yml.

* ci(docker): build the sandbox image on release/dispatch, not every main push

Leaves docker.yml exactly as on main. The sandbox image carries no Hermes code,
so two 4 GB multi-arch builds per merge bought nothing. sandbox-image.yml builds
and smokes on a PR that edits its own Dockerfile/smoke, and publishes only on a
release or a manual dispatch with publish=true. Stable tag stays :desktop.

* feat(docker): sandbox desktop base on python3.13-nodejs26

Matches the Hermes image (Python 3.13 / Node 26) and the top of requires-python;
the default docker_image tag it inherited was Python 3.11 / Node 20. Same pn
uid 1000, Debian 13; smoke (launcher, RFB relay, headed Chromium) passes.

* feat(docker): sandbox desktop image bakes agent-browser and Pillow

The runtime PR wraps the browser tools' agent-browser invocation in the sandbox
and grabs the Screen pane thumbnail inside it (the X socket and its cookie live
there). Both need to be in the image the default tag publishes, or the runtime
finds a tag-compatible image without them. agent-browser is pinned to the range
the gateway resolves (tools/browser_tool.py AGENT_BROWSER_NPX_SPEC), scripts off,
so the first browser_navigate in a fresh sandbox never waits on npm. The smoke
checks both as pn.

* docker(sandbox-desktop): login sessions find the browser (PLAYWRIGHT_BROWSERS_PATH via /etc/environment)

Dockerfile ENV reaches docker exec only. As the target of the ssh backend
(sshd on top of this image) a login session gets its environment from PAM,
so agent-browser reported 'Chrome not found' over ssh while working under
docker exec. Write the browser location to /etc/environment as well; the
smoke test asserts a clean login environment resolves it.
2026-09-28 01:13:26 -07:00
teknium1
3859e5c5b2 test(e2e/windows): a stuck install.ps1 fails with its process table and Python stacks
install.ps1 now runs under the same watchdog as `hermes update` (40 min),
and the watchdog adds a py-spy stack for every Python process of the leg
before it stops them. The workflow installs py-spy beside pywinpty.
2026-09-27 04:15:09 -07:00
teknium1
840f0fc9cc test(e2e/windows): one PM tool store per leg
`windows: desktop-installer@latest -> hermes-update (HEAD -> NEXT)` and
`installer-script+desktop -> hermes-update (HEAD -> NEXT)` failed with
"source launcher publication failed". install.ps1 and `hermes update` ran
with setup-pm's HERMES_RUNTIME_DIR, while the desktop smoke launched the app
without it (as a user's app runs) and settled the checkout onto a second
store at <HERMES_HOME>\tools. The update then re-pointed its own running,
locked hermes.exe.

windows-e2e.ps1 now drops HERMES_RUNTIME_DIR, HERMES_PYTHON and VIRTUAL_ENV
on entry, so every product step of a leg (installer, app, update, chat)
resolves the one store a user's machine has. The driver keeps setup-pm only
through $DriverPython (and PATH, which older installers need for uv and
ripgrep).

`hermes update` also runs under a 45-minute watchdog. Past it, the driver
writes every process (pid, parent, start time, command line) to
logs\update-hang-processes.txt, stops this leg's processes and fails with
that table, instead of being cancelled blind at the job cap.
2026-09-27 04:15:09 -07:00
teknium1
bb1e5680d6 ci(install-e2e): give Windows legs a 90-minute job budget
windows: desktop-installer@latest -> hermes-update (v2026.9.24 -> HEAD)
was cancelled at the 60-minute cap (job 108542725043) with nothing wrong:
its hermes update ran the historical venv->PM takeover, npm ci and a
Desktop rebuild in 24 minutes (one npm ci stalled 11 of them), exited 0 and
passed every post-update check, and the desktop smoke was cut off. Green
Windows legs already take up to ~40 minutes and the app-update driver may
wait 30. Real hangs stay bounded by pty-run.py and the per-step budgets.
2026-09-27 04:15:09 -07:00
teknium1
03ffc307ee ci(install-e2e): a rate-limited result chart no longer turns the run red
The Result chart job lists the run's jobs and artifacts through the repo's
GITHUB_TOKEN, which shares one hourly budget with every other workflow. In
a busy hour it answered "API rate limit exceeded for installation" and
failed a run whose legs were all green (36283843710, 36285499760). Retry
after 30/60/90 s. If the API still refuses, write a warning and a summary
line instead of failing. Each leg's own conclusion still stands.
2026-09-27 04:15:09 -07:00
teknium1
5afba1e8d9 ci(install-e2e): keep one install-e2e-red issue in step with the scheduled matrix
A red scheduled run blocked nothing and told nobody. install-e2e-red.yml
runs after every scheduled "Install & Update E2E" run: red opens one issue
labelled install-e2e-red (or rewrites the open one's body in place) with the
red legs grouped by failure class and linked; the first green run closes
it. No per-run comment, never a second issue.

It is its own workflow_run workflow because install-e2e.yml is also called
by stable-release.yml with read-only permissions, and a nested job asking
for issues: write would fail that call at startup. workflow_dispatch with a
dry-run default previews the change for any run id.
2026-09-27 04:15:09 -07:00
teknium1
7605349f25 ci(install-e2e): run a path-filtered four-leg subset on pull requests
install-e2e.yml only ran on the clock, so nothing in front of a merge
installed a release and updated it on a real OS. A pull_request trigger,
path-filtered to the install/update surface (derived from 60 days of
update/install/pm commits), runs the new `pr` route of
generate-e2e-matrix.mjs with only the newest release tag sampled:

  linux   installer-script -> hermes-update (newest release -> PR)
  linux   installer-script -> hermes-update (PR -> NEXT)
  windows installer-script -> hermes-update (PR -> NEXT)
  macos   installer-script -> hermes-update (newest release -> PR)

The bundle-manifest validation job is skipped on PRs (bundled legs need
dispatch-only manifests). The full matrix stays on schedule and release.
2026-09-27 04:15:09 -07:00
teknium1
7b4211eee2 test(e2e/desktop): stop the installer's nested lazy-fetch chain on CI
Trail from CI: 78+ nested 'git fetch origin --filter=blob:none --stdin'
lazy fetches, each with its own upload-pack on the seeded origin, until
the runner is OOM-killed. The origin serves full clones, so nothing is
legitimately missing; GIT_NO_LAZY_FETCH=1 for the installer run only.
2026-09-27 00:42:43 -07:00
teknium1
9bc96b8502 ci(e2e-desktop-update): trail the upload-pack requester, not the oldest git 2026-09-27 00:42:43 -07:00
teknium1
78084696a4 ci(e2e-desktop-update): trail git transfer ancestry
The second CI run showed the OOM cause: hundreds of concurrent
upload-pack/pack-objects processes on the seeded origin.git, reaching
105 GB RSS about 90 s into seeding. Log the client-side git command and
its parent chain to find what drives them.
2026-09-27 00:42:43 -07:00
teknium1
3b1da278d1 ci: Desktop update E2E is advisory via continue-on-error, not by leaving all-checks-pass
tests/ci/test_stable_release_graph.py requires every reusable-workflow
job in all-checks-pass; keep it there and let a red shard show without
failing the gate. Log a resource trail during the run: every shard's
runner died ~2 minutes into seeding on the first CI run with only
'The runner has received a shutdown signal'.
2026-09-27 00:42:43 -07:00
teknium1
e4c4d95d29 ci: keep Desktop update E2E out of all-checks-pass until it is stable on CI 2026-09-27 00:42:43 -07:00
Teknium
183e0d4adc test(e2e/desktop): PR-time Desktop install/update suite on a real install
Drive the packaged app a real scripts/install.sh + hermes desktop install
built, against a local bare origin, a local api.github.com stand-in and a
scripted model: first run, Update now hand-off/relaunch/chat, a failed
Desktop build inside that update, and a second update while one runs.
Open bugs are message-gated (gated on #122991).
2026-09-27 00:42:43 -07:00
teknium1
81f873b334 test(e2e/pm): PM environment lifecycle across real hermes updates
tests/e2e/core/upgrade/pm/: 22 cells in 7 files, one per failure class, each
driving the real entry points (install.sh, hermes update, hermes pm
repair/status/install, hermes doctor, hermes gateway run/install --force,
hermes kanban dispatch, hermes -z through the loopback provider) inside the
existing bwrap sandbox, seeded with an N-1 install and a dependency-changing
release:

- configured features survive update, repair and legacy-venv migration
- SIGKILL at the stage/publish boundary of a dependency update recovers
- stamps, receipts and doctor tell the truth (healthy, drifted, failed update)
- no stray in-tree venv / repo-local 3.11 .venv reaches any interpreter;
  execute_code and Kanban workers spawned after an update boot and import deps
- repeated updates/repairs/restarts don't accumulate generations
- gateway install --force keeps the install bootable

Every non-gated cell has a recorded sabotage proof; open bugs are
message-gated with known_failure (gated on #124228, #122627, #122425,
#124214, #124075, #124049, #124668).

Harness: make_origin clones --single-branch and allows filters, so a
blobless developer checkout can serve the installer's clone.

CI: e2e-upgrade is sharded per suite directory (an e2e-upgrade-plan job
lists "core" plus every subdirectory holding test_*.py); a shard that
collects no files fails; HERMES_TEST_WORKERS=6.
2026-09-27 00:42:13 -07:00
teknium1
15ae3f94b2 ci(windows-install-update-e2e): headroom for slow Windows runners 2026-09-27 00:41:41 -07:00
teknium1
16da7f1b38 test(e2e/windows): real C:\Users profiles, serialized gateway phases, Git-for-Windows machines 2026-09-27 00:41:41 -07:00