Commit Graph

21619 Commits

Author SHA1 Message Date
ethernet 4c885c4902 fix(ci): the signing dlib skips the imds probe that hangs hosted runners 2026-08-10 02:34:58 -04:00
ethernet 657cc38805 fix(desktop): macos signs mach-o only; payload symlinks sanitized 2026-08-10 02:34:58 -04:00
ethernet d32e24731f fix(desktop): the payload ships no absolute symlink — codesign rejects it 2026-08-10 02:34:58 -04:00
ethernet e409de879d change: drop stale electronDist references from docstrings 2026-08-10 02:34:58 -04:00
ethernet 2983fbf11b change: electron-builder unpacks its own electron dist 2026-08-10 02:34:56 -04:00
ethernet f33e6feeb7 use payload python ver 2026-08-10 02:34:56 -04:00
ethernet df98acfa2f log level verbose 2026-08-10 02:34:56 -04:00
ethernet db82bf3dbd add timeout 2026-08-10 02:34:56 -04:00
ethernet 34090f39dd change: update ci action verisons 2026-08-10 02:34:56 -04:00
ethernet 0ea986db66 change: electron-builder 27 signs with signtool /dlib, not powershell 2026-08-10 02:34:56 -04:00
ethernet 101de26067 fix(ci): the signing tool needs a .net 8 runtime, not a preinstalled module 2026-08-10 02:34:56 -04:00
ethernet 8478b53adc fix(ci): the arm64 lane no longer stalls or duplicates work
Two time sinks found in the 90-minute win32-arm64 timeout:

* electron-builder's on-demand Install-Module TrustedSigning waited 59
  minutes on the NuGet provider prompt, which -NonInteractive cannot
  answer. A preinstall step now installs the module with prompts off.
* The workflow ran npm ci and then the build script ran npm ci again
  (~10 minutes each on Windows runners). The workflow copy is gone;
  the script's own install is the one that counts.
2026-08-10 02:34:56 -04:00
ethernet 847416ab3c change: pywinpty 3 — v2 sdist cannot link on windows arm64
The 2.0.15 sdist links against the winpty C library, which has no
arm64 build (LNK2019 on every winpty_* symbol from the Git-bundled
x64 winpty.lib). 3.0.5 ships native win_arm64 wheels, so the payload
step stops compiling it entirely. The 3.0.0 breaking change (PTY.read
lost num_bytes) does not affect Hermes: both consumers use the
high-level PtyProcess wrapper, whose read(size) survived.
2026-08-10 02:34:56 -04:00
ethernet 5f231f83f6 fix(desktop): electron-builder never publishes; the workflow uploads 2026-08-10 02:34:56 -04:00
ethernet 43e9795ab9 docs(desktop): how the bundled installers are built and tested 2026-08-10 02:34:55 -04:00
ethernet 3c6073de3c change(desktop): the windows installer is one-click per-user 2026-08-10 02:34:55 -04:00
ethernet 2a0bb64d18 fix: signing uses the azureSignOptions schema 2026-08-10 02:34:55 -04:00
ethernet 19c0f4e5e2 fix: windows signing config moves to environment variables 2026-08-10 02:34:55 -04:00
ethernet b926dcc3d9 fix: builder args with spaces survive the windows shell hop 2026-08-10 02:34:55 -04:00
ethernet d2777660b3 fix(desktop): write the stamp through uv, not a bare python3 2026-08-10 02:34:55 -04:00
ethernet 323df71ed3 fix(desktop): tag-to-commit resolution survives cmd.exe 2026-08-10 02:34:55 -04:00
ethernet 4d0df3882a fix: the uv triple gate accepts official windows banners 2026-08-10 02:34:55 -04:00
ethernet 12839c91f5 feat(desktop): About shows the install axes
The version details gain an Artifact row (embedded runtime with its
payload tag, or external) and a Runtime row (the tree the backend of
this session actually spawned from). Users copy this when they report
issues, so the two-axis state is visible without a terminal.
2026-08-10 02:34:55 -04:00
ethernet b073513d79 change: gate the build toolchain by engines and embed the gated versions
The rules come from one source, package.json engines, instead of
copies in the build script. The payload then embeds the EXACT host
versions the gates approved: the node dist is downloaded at the host
node version (and must be an official nodejs.org release), the staged
uv is the host binary, and npm ships inside the node dist. The
installer moves to Node 26 so source installs and embedded installs
run the same node major.
2026-08-10 02:34:55 -04:00
ethernet 62df64e853 change: the bundle build always runs every step
--no-install and --no-package are retired and rejected loudly. A
skipped step is a different artifact, and a different artifact is
not a reproduction. CI drops --no-install; its own npm ci remains
only as a cache warmer with retry protection. The payload stages
(uv python install, pip --target site-packages, node dist) already
ran unconditionally inside the desktop build.
2026-08-10 02:34:55 -04:00
ethernet fd6b0fc1bd feat(nix): a self-contained builder for the desktop bundle
nix run .#build-desktop-app-bundle -- --tag=vX.Y.Z --no-install

The derivation is pure: it wraps the pinned node/uv/git around
scripts/build-bundled-desktop.mjs. The wrapped script runs impurely
on the source tree (payload downloads, electron-builder, codesign).
This replaces the ad-hoc `nix shell nixpkgs#nodejs_22 ...` incantation
and pins the build toolchain to the repo flake.lock.
2026-08-10 02:34:55 -04:00
ethernet 01859bf72c test: the update suites opt into a managed root; the guard tolerates sentinels
The dev-tree guard fires in any non-managed checkout — including the
checkout the test runner itself sits in. The update-flow suites mark
their root managed (the guard has its own test file), and the guard
skips an unclassifiable PROJECT_ROOT instead of crashing on test
sentinels.
2026-08-10 02:34:55 -04:00
ethernet 3320b561e8 docs: record the sweep's tree-touching tradeoff 2026-08-10 02:34:54 -04:00
ethernet 140dfe7af3 feat: hermes doctor reports the unused legacy desktop checkout
The old external desktop app installed a git checkout at
$HERMES_HOME/hermes-agent. An embedded app never uses it. Doctor
suggests deletion only for a demonstrably untouched tree (clean
status, on main, no stashes; any probe failure counts as local
work). Doctor deletes nothing itself.
2026-08-10 02:34:53 -04:00
ethernet a28ecc88ff feat: hermes update asks before it touches a non-managed checkout
The update flow stashes local changes and moves the checkout to the
update branch. At a managed install root that is the purpose. In any
other checkout it yanks a working tree off its feature branch. The
guard asks first, refuses without a terminal, and honors --yes.
2026-08-10 02:34:53 -04:00
ethernet eb676fe66e change: derive install state from .git and the build stamp
hermes_cli/runtime_tree.py replaces install_manifest.py. A tree with
.git is a git checkout and `hermes update` owns it. A tree without
.git is sealed, and the distribution field of the build stamp names
the steward that replaces it (desktop-app, docker, nix). The refusal
message comes from a per-steward table.

.hermes-install.json dies: staging stops writing it into the payload,
the CLI never reads it, and the update channel lives in config.yaml
(update.channel; main is the default and keeps the current behavior).

Eject gates on Sealed(desktop-app) and is a full handoff: it tells
the user that Setup replaces the desktop app. --channel on a git
checkout writes config instead of a manifest.
2026-08-10 02:34:52 -04:00
ethernet 60372c1630 change: an embedded app always runs its embedded payload
Two decisions land together because the code cannot compile between
them:

- Staging has no per-item skip. A stage failure throws and the build
  fails. The payload manifest shrinks to a complete-payload sentinel
  (schemaVersion 3, tag, commit). External builds write an
  external:true stub.
- Backend selection is a constant of the artifact. resolvePayload
  requires every runtime item directory; when it resolves, the app
  spawns the embedded backend without a look at any checkout. A
  payload with no runnable interpreter is a damaged artifact and
  raises an error instead of a silent checkout fallback.

decideResidentRuntime, the adoption-era checkout examination, and the
installMode parameter of shouldUseAppUpdater are deleted. The app
self-update gate is now: embedded stamp AND packaged. The update
channel moves to config.yaml (update.channel); Electron mirrors it
with a narrow parser for the version pill. The resident vocabulary is
renamed to embedded; thin builds are now called external.
2026-08-10 02:34:50 -04:00
ethernet 18478723f1 feat: the nix build stamps its steward
Docker already stamps distribution:docker in CI, and .dockerignore
already excludes .git from the image. Only the nix stamp lacked the
field. nix/desktop.nix already had it.
2026-08-10 02:34:50 -04:00
ethernet 28451113c0 feat: stamp the steward into build info
The distribution field names who replaces a gitless tree. The desktop
payload writes desktop-app. The CLI reads this value to give the
correct update instruction.
2026-08-10 02:34:50 -04:00
ethernet 0f752ec5bc fix(nix): keep Linux-only tools off the macOS devshell
The sandbox (bubblewrap) and the Wayland E2E stack exist on Linux
only. Gate them so the macOS devshell evaluates. Add actionlint for
workflow validation on both platforms.
2026-08-10 02:34:50 -04:00
ethernet 5ff391af5b feat(desktop): channel-aware update vocabulary and version details
The renderer speaks in releases on the stable channel and in commits
on the main channel. The statusbar pill shows "(update)" and names the
release tag in its tooltip; a commits-behind count reads as an
alarming +N on a channel where a release is one step. The updates
overlay names the release ("Hermes v0.21.0 is ready to install")
instead of the no-changelog copy, because a release feed carries no
commit rows by design.

The new VersionDetails panel shows version, branch, commit, source,
and distribution from the build stamp on the About page and in the
updates overlay, so support screenshots carry full provenance. The
statusbar tooltip stacks the same details in one panel; TooltipContent
changes from per-line marker chips to a single column panel, and a new
TooltipDetails helper renders muted secondary rows.

gen-share-codes.ts only picks up lint fixes (import order, blank
lines).
2026-08-10 02:34:50 -04:00
ethernet 33a30fbc80 feat(desktop): run the bundled backend from app resources
A complete payload makes the launch "resident": the backend spawns
directly from the payload in resources, with no materialized checkout
and no bootstrap. The payload CPython resolves imports through its own
hermes-bundle.pth, so the spawn needs no PYTHONPATH and survives
renames, Gatekeeper translocation, and read-only mounts. Writable
state (pycache, lazy installs) goes under HERMES_HOME.

decideResidentRuntime keeps existing users safe: a checkout whose
manifest says source-managed wins, and a pre-manifest checkout with no
desktop marker (the CLI-first cohort) wins too. Desktop-managed trees
go resident; an eject reverses the preference on the next launch.

Bundled installs update through electron-updater and the GitHub
Releases feed instead of git. checkUpdates() maps feed failures to the
same structured error shape as the git path, so an offline check never
surfaces as a raw IPC rejection. The download progress listener comes
off the singleton after each attempt, so a retry cannot stack ghost
listeners. Source installs on the stable channel compare against the
newest release tag, not commits behind main.
2026-08-10 02:34:50 -04:00
ethernet 08f13942f4 feat(desktop): stage offline agent payloads into the bundled artifact
stage-agent-payloads.mjs assembles the resources-resident runtime that
ships inside the bundled installer: the repo tree at the release tag
(no .git, with the prebuilt TUI and dashboard JS), a static uv, a
uv-managed CPython, the full site-packages tree from uv.lock, and a
node dist. A hermes-bundle.pth with relative paths makes the payload
interpreter resolve repo/ and site-packages/ wherever the app bundle
sits — no venv, no PYTHONPATH, no absolute paths.

Each CI runner stages natively for its own (os, arch), so there are no
cross-platform wheel-tag tables. Banner probes verify that every staged
binary was built FOR the target: uv prints its build triple, python
reports platform.machine(), node reports process.arch. A wrong-arch
payload fails the build instead of shipping. Packages with no
win_arm64 wheel build from sdist on the arm64 Windows runner; user
machines never compile.

The script stays dormant unless HERMES_DESKTOP_BUNDLED=1, and writes a
thin stub manifest otherwise, so dev builds are unchanged.

scripts/build-bundled-desktop.mjs runs the same sequence locally on
any platform. The desktop-bundled-release workflow builds each
(os, arch) target on a tag push, signs through Azure OIDC (Windows)
and the Apple secrets (macOS) when they exist, and attaches the
artifacts plus the latest*.yml feed files to the GitHub release.
2026-08-10 02:34:50 -04:00
ethernet 694bd9ab2e feat(update): install manifest, release channels, and eject
.hermes-install.json marks a checkout as source-managed or
desktop-bundled and records its update channel. A missing file means
source mode on the main channel, so no existing install changes.

`hermes update` reads the manifest:
- On a bundled install it refuses and points at the in-app updater.
- On the stable channel it fast-forwards the checkout to the newest
  final release tag (vX.Y.Z, three-digit major cap so legacy CalVer
  tags never match) instead of origin/main. The ZIP fallback resolves
  the tag through the GitHub API because that path runs when git file
  I/O is broken.
- `update.channel` in config.yaml overrides the channel for source
  installs. "auto" defers to the manifest.

`hermes update --eject` is the exit from desktop management. On a
bundled install it downloads Hermes Setup and launches it pinned to
the exact commit the bundle was built from; the installer creates a
normal source checkout at ~/.hermes/hermes-agent. Hermes Setup accepts
the new `--pin-commit <sha>` argument for this flow. On a
source-managed install, --eject with --channel only switches the
channel. The "ejected" manageStyle is the permanent opt-out that stops
future auto-adoption.
2026-08-10 02:34:50 -04:00
ethernet 01aba5cf16 feat(version): one install stamp carries build provenance
All packagers (Docker, Nix, desktop) write the same install-stamp.json
with scripts/write_install_stamp.py or with equivalent inline data. The
new hermes_cli/version_info.py reads the stamp first, falls back to
live git for source installs, and reports "unknown" when neither
exists. It caches the result per process.

The stamp replaces three separate provenance paths:
- the HERMES_REVISION env var from the Nix wrapper,
- the .hermes_build_sha file from the Docker build arg,
- live git probes in banner.py and dump.py.
hermes_cli/build_info.py and the desktop's write-build-stamp.mjs are
deleted with them. The desktop build calls the shared Python script.

The banner, `hermes --version`, `hermes dump`, the TUI session panel,
and the desktop About panel now show the same derived version: the
release version, plus "+N" when the build is N commits past the
release tag, or "+?" for a dirty tree with no countable tag. The
release_date field is gone from every surface.

The dirty probe uses `git status --porcelain -uno`: it runs on the
startup-banner path, and an untracked-file scan costs real time on
large checkouts.
2026-08-10 02:34:47 -04:00
ethernet c1af64ee99 feat(release): create SemVer tags for new releases
release.py now tags each release vX.Y.Z from the package version. The
old CalVer date tags stay readable as history. get_last_tag() prefers
the newest SemVer tag and falls back to the legacy CalVer tags for the
first SemVer release.

__release_rev_count__ records the commit count of the release-bump
commit. Immutable Nix builds carry no git history, so they derive the
"+N commits since release" display from this number and the flake's
revCount.
2026-08-10 02:34:40 -04:00
teknium1 ddd21abfa8 chore(evals): track results/.gitignore (its own * rule excluded it from the original add) 2026-08-09 23:30:02 -07:00
Teknium 0e63ed1feb feat(tools): stat-based special-file guard for read_file + readtool eval harness
read_file on a workspace FIFO/socket blocked until the exec timeout —
the existing device guard is name-based (/dev/*, /proc/*) and cannot
see an arbitrary special file. Add _special_file_kind(): one os.stat
on the resolved path, refusing FIFO/socket/char/block devices with a
plain note ('no read was attempted') instead of hanging. Host-visible
filesystems only; regular files, dirs, and missing paths unchanged.

Also adds evals/readtool/: an A/B harness that runs the real AIAgent
against hostile-file fixtures (huge lockfile, one-line bundle, FIFO,
NFD filenames, lying extensions) and measures accuracy, turns, tool
calls, and tokens. Measured for this guard (3 reps, file-only arm):
qwen3.8-max fifo task tokens 122k -> 26k (-79%), turns 9.3 -> 5.0;
opus-4.8 tokens 40k -> 23k; accuracy held 1.00 both arms.
2026-08-09 23:30:02 -07:00
Teknium 58bd286273 docs(sessions): document repair-routing and the continuity guarantees
User-visible surface from the #82616 session-continuity campaign:
- sessions.md: 'Repair Stranded Gateway Sessions' (evidence rules,
  dry-run-first, why adoption is never automatic) and 'Continuity After
  Crashes and Restarts' (atomic identity, self-heal, recency resolution,
  reset-boundary fence)
- cli-commands.md: repair-routing row in the hermes sessions table

Docs build verified (en + zh-Hans).
2026-08-09 23:29:16 -07:00
kshitij e09ef9ebd8 fix(transport): use getattr for supports_prompt_cache_key on stale profiles
After a partial update (stash restore overwriting providers/base.py with
an older version), the NousProfile singleton was instantiated from a
ProviderProfile class that predates the supports_prompt_cache_key field
(added in f4fb23f3d). Accessing profile.supports_prompt_cache_key raised
AttributeError, crashing every API call with:
  'NousProfile' object has no attribute 'supports_prompt_cache_key'

Use getattr(profile, 'supports_prompt_cache_key', False) so a stale
profile degrades to 'no prompt cache key' instead of crashing.
2026-08-09 23:19:39 -07:00
kshitij f45a3fb2b0 fix(update): force-reload config modules before migration check
hermes update runs in the PRE-pull Python process. After git pull
updates the source files on disk, sys.modules still holds the OLD
hermes_cli.config and hermes_cli.config_migrations. Function-level
imports return the cached module, so DEFAULT_CONFIG["_config_version"]
is the OLD value and check_config_version() reports (33, 33) —
"up to date" — even though the freshly-pulled code has v34 with a
migration to run.

The personality reset migration (#81946) was silently skipped this
way: display.personality: kawaii stayed active after updates that
should have reset it. Every user who updated from a pre-v34 codebase
to a post-v34 codebase was affected.

Fix: _run_config_check_fresh and _run_migrate_config_fresh call
importlib.reload() on hermes_cli.config_defaults, hermes_cli.config,
and hermes_cli.config_migrations before calling check_config_version
and migrate_config. This forces the modules to be re-read from the
updated source files on disk.
2026-08-09 23:19:39 -07:00
Teknium 4227336677 feat(skills-hub): fall back to live repo for optional skills missing from local checkout
Optional skills merged to main after a user's install was cut were
invisible to 'hermes skills install official/...' until they ran
'hermes update' — the OptionalSkillSource only scanned the local
optional-skills/ checkout.

Now, when an official/<category>/<skill> identifier is not found
locally, OptionalSkillSource resolves it against the live default
branch of NousResearch/hermes-agent: one Trees API call enumerates
optional-skills/*/SKILL.md dirs (cached on disk via the shared index
cache, 1h TTL), then the full skill directory is downloaded byte-exact
(including root-level install scripts, LICENSE, tests/ — files the
generic GitHubSource.fetch path drops). search() and inspect() also
surface remote-only skills so discovery works pre-update too.

Local checkout always wins when present; offline degrades to the old
local-only behavior; traversal and ambiguous bare names are refused;
provenance stays official/builtin.
2026-08-09 23:14:18 -07:00
notkisk 481ccdafb7 fix(desktop): keep react-router in one runtime chunk 2026-08-10 02:04:47 -04:00
hermes-seaeye[bot] e400dca96e
fmt(js): `npm run fix` on merge (#82962)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 05:40:43 +00:00
kshitij 327f7efab8 fix: close sibling display_kind drops and ui-tui parity for #82756
Review follow-ups on the composite salvage (whole-bug-class sweep):

- session.branch and _persist_branch_seed copied parent history without
  display_kind/display_metadata, so a tagged timeline marker (personality
  pivot, model switch, auto-continue) re-entered the branched session as a
  bare role=user row after a restart — re-planting the phantom-ordinal
  class this PR fixes. Both projection dicts now carry the tags; regression
  asserts added to both branch tests (mutation-checked: fail without the
  fix).
- ui-tui renderer learns display_kind=personality_switch (was falling
  through to an opaque user bubble; desktop got the case in commit 1).
- programmatic-integration docs: document the two new 4004 refusals
  (boolean ordinal, bare confirm_truncate).
- hermes_state comment: archived rows are searchable only with
  include_inactive=True, not by default search — align comment with the
  actual FTS filter.
- strip stray trailing blank line in test_tui_gateway_server.py
2026-08-10 11:01:15 +05:30