Two protocol-correctness gaps from review:
1. Stage-Node used [void](Test-Node) which discarded Test-Node's return
value, so the JSON frame always reported ok=true even when Node
install fully failed. A GUI driver consuming the manifest couldn't
tell 'node ready' from 'node missing'. Wire a soft-skip channel
($script:_StageSkippedReason) that workers can populate to surface
'ran, but the thing it was supposed to set up is not available' as
skipped=true with a reason in the JSON, without aborting the install
(Node is optional -- browser tools degrade gracefully, matches
Write-Completion's existing 'Note: Node.js could not be installed'
behavior). Reset before each stage so a prior reason can't leak.
2. The -Stage dispatch used 'if ($Stage)' which is falsy for empty
string, so 'install.ps1 -Stage ""' fell through to Main and silently
kicked off a full destructive install. Switch to
PSBoundParameters.ContainsKey('Stage') so an explicit empty value
surfaces as unknown-stage exit 2 with a structured JSON frame, the
way every other bad stage name does.
Address the two cosmetic items from review:
- Completion banner middle line was 62 chars vs 59-char top/bottom borders
(replacing the 1-char checkmark with [OK] added width that wasn't
reflected in the trailing whitespace). Drop 3 trailing spaces.
- Smoke test file had a single em-dash in a comment -- the only
non-ASCII byte across both files. Replace with -- for consistency
with install.ps1's pure-ASCII goal.
Three issues flagged by the Copilot review on this PR:
1. Double JSON emit on stage failure (Copilot #1, #2). When -Stage <name>
ran a worker that threw, Invoke-Stage's finally emitted a JSON result
frame AND the entry-point catch emitted a second error frame --
producing two concatenated JSON objects on stdout and breaking the
one-line-per-invocation contract that drivers parse against. Same
issue applied to -Json mode on a full install (every stage's finally
plus a final error frame missing duration_ms/skipped).
Fix: Invoke-Stage's finally now sets $script:_StageEmittedErrorFrame
when it emits a failure frame; the entry-point catch checks the flag
and skips its own emit, still exit 1.
2. $prevEAP uninitialized on early try-block throw (Copilot #3). In
Install-Uv, Test-Python, Test-Node's winget fallback,
_Run-NpmInstall, and the playwright block, '$prevEAP =
$ErrorActionPreference' lived as the first statement INSIDE the
try. If anything between 'try {' and that line threw (Write-Info on
an unusual host, the npx-finding loop, etc.), the catch's
'if ($prevEAP) { ... }' restore was a no-op and EAP could remain
relaxed.
Fix: hoist '$prevEAP = $ErrorActionPreference' to the line
immediately before 'try {' in all five sites. Catch's restore is
now always meaningful regardless of where in the try the throw
originated.
No change to Invoke-Stage's success path or to the four lint-clean EAP
sites (Test-Node was the only winget-related catch). All 19 metadata
smoke tests still pass.
Adds an opt-in stage protocol that lets programmatic drivers (the
desktop GUI's onboarding wizard, CI, future install.sh parity) drive
install.ps1 one step at a time with structured JSON results. Default
invocation (`irm | iex` one-liner) behaves unchanged.
Entry points:
install.ps1 Today's interactive install (unchanged)
install.ps1 -ProtocolVersion Emit protocol version integer
install.ps1 -Manifest Emit JSON manifest of available stages
install.ps1 -Stage <name> Run one stage, emit JSON result
install.ps1 -NonInteractive Suppress Read-Host prompts (skips the
setup wizard and gateway autostart)
install.ps1 -Json Machine-readable completion frame
Manifest exposes 14 stages across prereqs/install/finalize/post-install
categories, with 2 (configure, gateway) flagged needs_user_input=true
so GUI drivers can skip them and handle the equivalent UX themselves.
Along the way, clean-VM testing on stock Windows 10/11 surfaced a
series of latent install.ps1 bugs that were never exercised by
developer machines. Fixed in the same commit:
* Encoding: file is now pure ASCII with no BOM. Windows PowerShell
5.1 reads BOM-less files as Windows-1252 and chokes on em-dashes
(and other UTF-8 sequences), while iex chokes on a leading U+FEFF.
Pure-ASCII satisfies both invocation paths.
* EAP=Stop + native `2>&1` captures: PowerShell wraps stderr lines
from native commands as ErrorRecord objects under EAP=Stop and
throws even when the command exits 0. Relaxed to EAP=Continue
around the astral.sh uv installer, `uv python install`, `npm
install`, `npx playwright install`, the venv import probes, and
the Node winget fallback. Check $LASTEXITCODE for the real signal.
* Cross-process state: each `-Stage <name>` invocation spawns a
fresh powershell child. $script:UvCmd set by Stage-Uv was invisible
to Stage-Python; PATH updated by Stage-Git/Stage-Node was invisible
to subsequent stages spawned by the driver shell. Added Resolve-UvCmd
helper called at the top of every stage that needs uv, and a
Sync-EnvPath helper called at the top of Invoke-Stage to refresh
PATH from the registry.
* UAC avoidance: `winget install OpenJS.NodeJS.LTS` triggers a UAC
prompt that often appears minimized in the taskbar -- looks like a
hang. Switched Test-Node to prefer the official portable Node zip
dropped into %LOCALAPPDATA%\hermes\node\ (mirrors the PortableGit
pattern Install-Git already uses). winget kept as fallback.
* npx hangs on confirmation: `npx playwright install chromium` blocks
on stdin waiting for "Need to install playwright@X.Y.Z (y/N)" when
playwright isn't in local node_modules. Tee-Object pipelines
disconnect stdin from the user's TTY so the install hangs forever.
Pass `--yes` to auto-accept.
* Silent long-running installs: `*> $logPath` redirected every stream
to disk and left the user staring at a frozen "Installing..." line
for the 5-10 minutes Playwright Chromium takes to download. Switched
to `2>&1 | ForEach-Object { "$_" } | Tee-Object -FilePath $log` so
output streams live to the console AND captures to log for failure
diagnostics. ForEach-Object coercion strips PowerShell's red
NativeCommandError formatter from stderr items.
* Console encoding: forced [Console]::OutputEncoding to UTF-8 so
playwright/git/npm progress bars, box-drawing, and check marks render
correctly instead of as IBM437/Windows-1252 mojibake.
* Performance: set $ProgressPreference = "SilentlyContinue" so
Invoke-WebRequest doesn't paint its per-chunk progress bar. The
PS 5.1 progress UI throttles downloads by 10-100x (a 57MB PortableGit
grab takes 5 minutes with the bar on vs ~20 seconds with it off,
same network). Affects PortableGit, Node portable zip, and the
Hermes repo zip fallback.
Tests: scripts/tests/test-install-ps1-stage-protocol.ps1 provides 19
metadata-only assertions covering -ProtocolVersion, -Manifest schema,
and unknown -Stage error frame. No install side effects.
End-to-end validated on a clean Windows 10 VM via:
1. `irm <branch>/scripts/install.ps1 | iex` (canonical CLI path)
2. `powershell -File install.ps1 -Stage X` iterated through every
stage (GUI driver path, exercises cross-process fixes)
The install_open_webui function correctly resolved the python interpreter into the $py variable, but hardcoded 'python' in subsequent pip install commands. This caused 'command not found' or 'externally-managed-environment' errors on systems where 'python' is not implicitly aliased to 'python3'.
Two unit tests for run_hermes_oauth_login_pure():
1. test_authorization_url_state_is_not_pkce_verifier — asserts state in the
auth URL is independent from the PKCE code_verifier sent in the token
exchange, and that the verifier never appears in the URL.
2. test_callback_state_mismatch_aborts — asserts the flow returns None
(no token exchange) when the callback state does not match the value
we generated.
Negative control verified: reintroducing the b17e5c10 vulnerable pattern
(state = verifier, no callback validation) makes both tests fail.
Also adds AUTHOR_MAP entry for shaun0927 (contributor of the fix).
The cherry-picked PR #15251 from @tw2818 correctly identified the
DeepSeek 400 root cause but placed the fix in the legacy fallback path
of `build_kwargs`, which DeepSeek never reaches — DeepSeek has a
registered ProviderProfile and goes through `_build_kwargs_from_profile`
instead. The legacy-path block was therefore dead code.
This commit pivots the fix to where it actually fires:
- New `DeepSeekProfile` in `plugins/model-providers/deepseek/__init__.py`
overrides `build_api_kwargs_extras` to emit DeepSeek's expected wire
format (mirrors `KimiProfile`):
{"reasoning_effort": "<low|medium|high|max>",
"extra_body": {"thinking": {"type": "enabled" | "disabled"}}}
- Model gating: only `deepseek-v4-*` and `deepseek-reasoner` emit
thinking control. `deepseek-chat` (V3) is untouched — current behavior.
- Effort mapping: low/medium/high passthrough, xhigh/max → max, unset →
omitted (DeepSeek server applies its own default).
- Revert the legacy-path additions from PR #15251 — they were dead code,
and the `_copy_reasoning_content_for_api` strip block specifically
would have nullified the existing reasoning_content padding machinery
(`_needs_deepseek_tool_reasoning` → space-pad on replay) that the
active provider already relies on for replay correctness.
- Unit tests pin the wire-shape contract and the model gating rules
(26 tests, all passing). Existing transport + provider profile suites
(321 tests) continue to pass.
- AUTHOR_MAP: map twebefy@gmail.com → tw2818 for release notes credit.
Closes#15700, #17212, #17825.
Co-authored-by: tw2818 <twebefy@gmail.com>
Fresh Windows installs were failing on first run with:
⚠ uv python install error: Downloading cpython-3.11.15-windows-x86_64-none (24.5MiB)
✗ Installation failed: Python was not found; run without arguments
to install from the Microsoft Store...
Two bugs compounding:
1) EAP=Stop swallows uv's stderr progress as an exception. uv writes
download progress ("Downloading cpython-3.11.15-windows-x86_64-none
(24.5MiB)") to stderr. With $ErrorActionPreference = "Stop" set at
the top of the script plus 2>&1 capture, PowerShell wraps each stderr
line as an ErrorRecord and throws on the first one — even though uv
exits 0 and Python was installed successfully. This was previously
fixed in commit ec1714e71 (May 8) but lost in the May 12 release
squash (413990c94). Reapply the EAP=Continue + verify-via
'uv python find' pattern.
2) System-python fallback invokes the Microsoft Store stub. When the uv
paths fall through, the legacy 'python --version' check invokes
%LOCALAPPDATA%\\Microsoft\\WindowsApps\\python.exe, a 0-byte
reparse-point stub that prints 'Python was not found...' to stdout
and exits non-zero. Get-Command matches it. The resulting error
message is what the user sees as the final installer crash. Detect
and skip the stub by checking for the \\WindowsApps\\ path
component or a 0-byte file size before invoking python.
Also save/restore EAP defensively in the catch blocks so a throw before
the assignment can't leave EAP in 'Continue'.
Wraps every sync->async coroutine-scheduling site in the codebase with a
new agent.async_utils.safe_schedule_threadsafe() helper that closes the
coroutine on scheduling failure (closed loop, shutdown race, etc.)
instead of leaking it as 'coroutine was never awaited' RuntimeWarnings
plus reference leaks.
22 production call sites migrated across the codebase:
- acp_adapter/events.py, acp_adapter/permissions.py
- agent/lsp/manager.py
- cron/scheduler.py (media + text delivery paths)
- gateway/platforms/feishu.py (5 sites, via existing _submit_on_loop helper
which now delegates to safe_schedule_threadsafe)
- gateway/run.py (10 sites: telegram rename, agent:step hook, status
callback, interim+bg-review, clarify send, exec-approval button+text,
temp-bubble cleanup, channel-directory refresh)
- plugins/memory/hindsight, plugins/platforms/google_chat
- tools/browser_supervisor.py (3), browser_cdp_tool.py,
computer_use/cua_backend.py, slash_confirm.py
- tools/environments/modal.py (_AsyncWorker)
- tools/mcp_tool.py (2 + 8 _run_on_mcp_loop callers converted to
factory-style so the coroutine is never constructed on a dead loop)
- tui_gateway/ws.py
Tests: new tests/agent/test_async_utils.py covers helper behavior under
live loop, dead loop, None loop, and scheduling exceptions. Regression
tests added at three PR-original sites (acp events, acp permissions,
mcp loop runner) mirroring contributor's intent.
Live-tested end-to-end:
- Helper stress test: 1500 schedules across live/dead/race scenarios,
zero leaked coroutines
- Race exercised: 5000 schedules with loop killed mid-flight, 100 ok /
4900 None returns, zero leaks
- hermes chat -q with terminal tool call (exercises step_callback bridge)
- MCP probe against failing subprocess servers + factory path
- Real gateway daemon boot + SIGINT shutdown across multiple platform
adapter inits
- WSTransport 100 live + 50 dead-loop writes
- Cron delivery path live + dead loop
Salvages PR #2657 — adopts contributor's intent over a much wider site
list and a single centralized helper instead of inline try/except at
each site. 3 of the original PR's 6 sites no longer exist on main
(environments/patches.py deleted, DingTalk refactored to native async);
the equivalent fix lives in tools/environments/modal.py instead.
Co-authored-by: JithendraNara <jithendranaidunara@gmail.com>
The contributor's commit author email is the legacy GitHub noreply
form (no leading numeric "id+"), so it doesn't match the
check-attribution workflow's auto-resolve regex
(\+.*@users\.noreply\.github\.com). Register it explicitly in
AUTHOR_MAP so the PR #26457 attribution check passes.
PR #22345 by @btorresgil authors commits as 'Brian Conklin
<brian@dralth.com>' (git config carries a different name/email than the
GitHub account). GitHub's commit-author mapping correctly attributes these
commits to @btorresgil based on the public-key registration, but Hermes'
release attribution audit reads the raw commit email, not the GitHub
mapping. Without this AUTHOR_MAP entry, salvaging #22345 would fail
`scripts/contributor_audit.py` strict mode at release time.
Prerequisite for the langfuse trace fix salvage that cherry-picks
@btorresgil's commits onto current main.
When a user sends a Slack message like '/hermes ' (trailing whitespace
after the slash) the legacy subcommand router hit `text.split()[0]` with
a truthy-but-whitespace-only `text`. `' '.split()` returns `[]` →
IndexError, blowing up the slash handler before fallthrough to `/help`.
Switch to a two-step guard that materializes the parts list first and
indexes only if non-empty.
Salvaged from PR #2752 by @nidhi-singh02. The PR's other two hunks
(`tools/file_operations.py`, `agent/anthropic_adapter.py`) are
unreachable in current code — `LINTERS` is a hardcoded constant dict
with no empty values, and the anthropic version-detection site is
already guarded by a `result.stdout.strip()` truthy check — so only the
slack hunk is taken.
Closes#2745
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Some catalog endpoints (OpenCode Zen, etc.) sit behind a WAF that
returns 403 for the default Python-urllib/<ver> User-Agent. The
generic profile-based live fetch in providers/base.py was silently
failing for any such provider — falling through to the static catalog
and missing newly-launched models.
Set a generic 'hermes-cli/<version>' UA on the catalog probe so every
api_key provider profile benefits. Verified live against opencode-zen:
before this change, profile.fetch_models() raised HTTP 403; after, it
returns 42 models including gpt-5.5, gpt-5.5-pro, kimi-k2.6, glm-5.1
and the *-free variants the static catalog doesn't list.
Also strip the now-stale comment in validate_requested_model() claiming
opencode-zen's /models returns 404 against the HTML marketing site —
the API endpoint at /zen/v1/models returns 200 with valid JSON.
Surfaced by #2651 (@aashizpoudel) — fixes the same user-facing gap
their PR targeted, applied at the right layer so all api_key provider
profiles get live catalogs through the same code path.
Co-authored-by: Aashish Poudel <mr.aashiz@gmail.com>
- Adds plugins/platforms/simplex docs page to the messaging sidebar
between LINE and Open WebUI.
- Maps louismichalot@hotmail.com -> Mibayy in scripts/release.py so the
attribution check on the salvage PR passes.
Baileys' sock.sendMessage() can hang indefinitely while uploading
media to WhatsApp servers (and, less often, on text sends), pinning
the bridge's Express handler until the gateway's aiohttp timeout
fires — surfacing to the user as a 120s wait followed by an empty
error from the TTS/voice path.
Wrap every sock.sendMessage() call inside the bridge in a
sendWithTimeout() helper that rejects after WHATSAPP_SEND_TIMEOUT_MS
(default 60s) via Promise.race. The four call sites are /send,
/edit, and /send-media's primary send. Express handlers catch the
rejection in their existing try/catch and return a real 500 to the
gateway, which can then surface a retryable error.
Salvaged from #2608 — wysie diagnosed the hang and the
Promise.race shape; the other two parts of that PR (gateway HTTP
session pooling, base.py metadata kwarg removal) already landed on
main via separate routes and are no longer needed.
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
When a non-Anthropic provider (e.g. Morpheus proxy) returns a 429 with
`{"error": "Too Many Requests"}` instead of the expected
`{"error": {"type": ...}}` dict, _err_body.json().get("error", {})
returns the raw string and the next .get("type") line crashes with
AttributeError, taking down the message handler.
Guard with isinstance(_err_json, dict) so non-dict error bodies fall
through to the generic rate-limit hint.
Salvaged from PR #2587 by @KiraKatana. The PR's fallback-config
`base_url`/`api_key_env` fix was already implemented independently
on main (run_agent.py:8759-8780) with additional aliases and Ollama
Cloud host handling, so only the gateway guard is cherry-picked.
Co-authored-by: KiraKatana <kira.ops@proton.me>
Discord's CDN serves attachments with Content-Encoding: br. aiohttp's
compression_utils tries 'import brotlicffi as brotli' first and falls back
to google's Brotli, but Brotli<1.2.0's Decompressor.process() is 1-arg
while aiohttp calls it with 2 args (data, max_length). Result: every
.txt/.md/.doc uploaded to a Discord-gateway session fails to decode at
att.read() with 'Can not decode content-encoding: br' / 'TypeError:
process() takes exactly 1 argument (2 given)', the agent never sees the
bytes, and falls back to filesystem guessing.
Pin brotlicffi==1.2.0.1 in both surfaces:
- tools/lazy_deps.py 'platform.discord' tuple: Discord users on the
lazy-install path get it on first discord.py import.
- pyproject.toml [messaging] extra: users who explicitly install
hermes-agent[messaging] (skipping the lazy path) get it eagerly.
brotlicffi wins aiohttp's import race regardless of what else is
installed (try brotlicffi / except: import brotli), so existing setups
that already pulled google's Brotli transitively don't change behavior
beyond the bug fix. ~1.5 MB wheel, manylinux/macOS/Windows coverage.
E2E verified: round-trip decode of Brotli-compressed payload via
aiohttp.compression_utils.brotli succeeds with brotlicffi pinned; same
test against Brotli==1.1.0 alone reproduces the reported TypeError.
Credit to @Korkyzer for the original diagnosis and fix shape in #15744;
the lazy-deps gating layer was added on top to keep brotlicffi out of
the install path for users who don't run a Discord gateway.
Fixes#12511.
Closes#15744.
Co-authored-by: Korky <korkyzer@gmail.com>
The ACP Registry schema supports uvx as a first-class distribution method
alongside npx and binary. Pointing the registry directly at the existing
hermes-agent PyPI release removes:
- the @nousresearch npm scope (we don't own it)
- a separate npm publish step on every weekly release
- 90 lines of Node launcher + tests in packages/hermes-agent-acp/
The Zed registry now installs Hermes via:
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
This is the same command the npm launcher was shelling out to anyway, so
end-user behavior is unchanged. Registry CI validates the PyPI URL +
version-pin exact match automatically.
Changes:
- acp_registry/agent.json: distribution.npx -> distribution.uvx
- delete packages/hermes-agent-acp/ entirely
- scripts/release.py: drop npm-launcher bump paths, keep manifest lockstep
- tests/acp/test_registry_manifest.py: assert uvx shape + version pin
- tests/scripts/test_release_acp_registry.py: rewrite for uvx-only shape
- docs (user-guide + dev-guide): drop all npm-launcher references
- delete docs/plans/acp-registry-zed-integration.md (stale, npm-shaped)
Validated against agentclientprotocol/registry agent.schema.json via
jsonschema. hermes-agent==0.13.0 is already live on PyPI.
The ACP Registry manifest (acp_registry/agent.json), the npm launcher
package.json, and the launcher's HERMES_AGENT_VERSION constant must all
match pyproject.toml exactly — tests/acp/test_registry_manifest.py
enforces this lockstep.
Without a release-script hook, the next weekly version bump fails that
test until someone hand-edits four files. Extend update_version_files()
to drive the ACP bump alongside __init__.py and pyproject.toml, and
add tests covering the lockstep and the missing-files no-op path.
Also map adam.manning@gmail.com -> am423 for the salvage commit.
Follow-up to the sandbox-bypass env-var fix:
- Update the opt-out gate so a user-provided AGENT_BROWSER_ARGS is also
respected, not just the legacy AGENT_BROWSER_CHROME_FLAGS. Previously
the gate only checked the broken legacy var, so a user who pre-set
AGENT_BROWSER_ARGS would still get clobbered by Hermes's auto-injection.
- Document AGENT_BROWSER_ARGS in .env.example, the browser feature page,
and the env var reference, with notes about the auto-injection on
AppArmor-restricted systems (Ubuntu 23.10+, DGX Spark, containers).
- Add Anadi Jaggia to AUTHOR_MAP.
When the terminal shrinks, already-printed box-drawing rules (response,
reasoning, streaming TTS, background-task Panels) reflow into multiple
narrower rows — visible as duplicated horizontal separators / ghost
lines in scrollback. Similarly, prompt_toolkit redraws a fresh status
bar on SIGWINCH on top of one the terminal just reflowed, producing
double-bar artifacts on column shrink.
Two surgical changes:
1. Decorative scrollback boxes now use a new
`HermesCLI._scrollback_box_width()` helper that clamps to
`max(32, min(width, 56))`. The live TUI footer is unaffected and still
uses the full width. Covers: streaming response box (open + close),
reasoning box (open + close, both streaming and post-stream paths),
streaming-TTS box close, final-response Rich Panel, and the
background-task Rich Panel.
2. `_recover_after_resize()` now also sets a new
`_status_bar_suppressed_after_resize` flag so the dynamic status bar
and both input separator rules stay hidden until the next user input.
The flag is cleared in the process loop the moment the user submits
their next prompt, restoring chrome cleanly.
Tests:
- New `test_input_rules_hide_after_resize_until_next_input` covers the
flag's effect on rule heights.
- New `test_scrollback_box_width_caps_to_resize_safe_value` covers the
helper at floor / cap / mid-range / overflow.
- Existing resize-recovery test extended to assert the flag flips.
Refs: #18449#19280#22976
Salvage of #24403.
Co-authored-by: Szymonclawd <szymonclawd@mac.home>
Adds references/template-integrity.md covering safe conversion of the
official comfyui-workflow-templates package from editor format to API
format — Reroute bypass via link tracing, dotted dynamic-input keys
(values.a, resize_type.width) that must NOT be flattened, server-error
"patch don't rebuild" loop, Cloud quirks (302 redirect to signed GCS
URL, free-tier 1 concurrent job, 1920x1080 OOM on RTX 5090), and a
Discord-compatible ffmpeg stitch recipe (yuv420p + xfade/acrossfade).
SKILL.md lists the new reference so the agent loads it when starting
from an official template. purzbeats added to author list and to
scripts/release.py AUTHOR_MAP.
Co-authored-by: purzbeats <97489706+purzbeats@users.noreply.github.com>
The Debian/Ubuntu branch of install_node_deps() ran 'npx playwright install
--with-deps chromium' unconditionally. Playwright invokes sudo interactively
to apt-install Chromium's system libraries, which blocks the installer for
non-sudo users (systemd service accounts, unprivileged operator users) on
an unsatisfiable password prompt.
Changes:
- install.sh: gate --with-deps behind a sudo capability check on the apt
branch (matches the existing Arch/pacman branch pattern). Non-sudo users
fall back to 'npx playwright install chromium' alone and the installer
prints the exact 'sudo npx playwright install-deps chromium' command an
administrator can run separately.
- install.sh: add --skip-browser (alias --no-playwright) to skip the
Playwright step entirely for headless installs that don't need browser
automation. Mirrors the existing --no-venv / --skip-setup shape.
- installation.md: add a 'Non-Sudo / System Service User Installs' section
covering the admin/service-user split, the --skip-browser flag, and the
~/.local/bin PATH gotcha (the root cause of the 'No module named dotenv'
error users hit when running the repo source 'hermes' script with system
Python instead of the venv launcher).
- test_install_sh_browser_install.py: regression coverage for the
--skip-browser flag and the sudo-gate on the apt branch.
Reported by @ssilver in Discord.
Set file mode 0600 on ~/.hermes/.env after creation in the installer and
after every write via memory_setup._write_env_vars(). This ensures only
the file owner can read/write API keys and tokens, matching standard
practice for credential files (.netrc, .aws/credentials, .ssh/config).
Fixes#25477
Pre-stages AUTHOR_MAP for 7 new contributors in the upcoming batch:
- HxT9 (#25760)
- evgyur (#25651)
- AsoTora (#25624)
- oxngon (#25603)
- yifengingit (#25589)
- vanthinh6886 (#25562)
- Arkmusn (#25559)
EthanGuo-coder, wesleysimplicio, and zccyman are already in the map.