Brings the X Chat adapter to parity with mature gateway platforms:
- Encrypted media, both directions. Inbound attachments are downloaded
(GET /2/chat/media/{conv}/{hash}), decrypted with the conversation key
for the EVENT's key version (post-rotation media stays readable),
size-capped, cached locally, and surfaced on MessageEvent
(media_urls/media_types + correct MessageType) so vision/file tools
see them. Outbound send_image/send_image_file/send_voice/send_video/
send_document encrypt with the latest conversation key
(encrypt_stream), upload via the 3-step chat-media flow
(initialize/append/finalize, base64 JSON segments), and attach by
media_hash_key. Standalone sends (cron/send_message_tool) carry
media_files the same way.
- Native threaded replies. A bounded per-conversation cache of decrypted
events lets send(reply_to=...) use encrypt_reply against the real
target event; unknown targets fall back to a plain send. Inbound
reply context (reply_to_message_id/text/author,
reply_to_is_own_message) now propagates on MessageEvent.
- New-conversation initiation. Standalone send to a bare numeric user id
performs the conversation-key handshake: fetch both parties' public
keys, verify each identity↔signing binding (verify_key_binding — a
substituted key must never receive the conversation key), wrap a
fresh key per participant (prepare_conversation_key_change), POST to
add-conversation-keys, then encrypt under the returned raw key.
- meta.conversation_key_events. The events endpoint returns KeyChange
events SEPARATELY in meta — previously they were never decrypted, so
conversations whose key changes fell outside the data array could
never seed a key. Both the poll loop and the standalone sender now
feed them through the batch decrypt path (after signing-key
registration) before processing messages.
- Read receipts (opt-in, XCHAT_SEND_READ_RECEIPTS, default off) via
POST /2/chat/conversations/{id}/read.
- Latest-key-version tracking per conversation for media encrypt and
correct key selection after rotations.
api.py: media_upload (chunked 3-step), media_download, mark_read.
crypto.py: encrypt_reply, encrypt_media/decrypt_media, verify_key_binding,
prepare_conversation_key_change (SDK->API body mapping incl.
action_signatures), attachments/explicit-key support on encrypt_text,
latest_key_version surfaced from decrypt_events, message_attachments,
detect_mime_type/detect_image_dimensions helpers.
Docs: media/replies/handshake/read-receipts documented; stale "text
only" / "reply flows only" limitations removed; capability row added to
the messaging comparison table; media.write scope noted in setup + docs.
Tests: 51 total — inbound attachment decrypt-and-cache, outbound
encrypt-upload-attach (+ no-key failure), threaded-reply cache hit and
fallback, meta key-event absorption order, read-receipt opt-in/default,
reply-context propagation, full handshake happy path (bindings verified,
key change POSTed, explicit key used), chunked upload reassembly,
media download, mark-read body.
Addresses santiagomed's review on #68930:
- api.py: drop created_at_msec from chat_message_event.fields — the
endpoint 400s on unknown fields, so every poll failed. Use the
documented created_at instead, and document the valid field set.
- adapter.py: replace the in-memory backlog/dedup scheme with a
persisted per-conversation cursor (~/.hermes/xchat/cursors.json).
Fixes three defects at once: re-replies to old messages after
dedup prune eviction, dropped bursts >50 events between polls
(the poll now pages back to the cursor), and messages received
while the gateway was down being swallowed as backlog on restart.
- adapter.py/crypto.py: dispatch MessageEdit events — the feed can
return an edited message only as the edit event, which previously
made it invisible to the bot forever.
- adapter.py: _standalone_send now fetches participants' public keys
and calls set_signing_keys BEFORE decrypt_batch, so KeyChange
verification can pass and the conversation key actually seeds
against the real SDK.
- cli.py: refuse to regenerate over an existing private-key blob when
the registration marker is missing/corrupt (no forward secrecy —
overwriting permanently kills every conversation); --force now backs
the old blob up first, and the register subcommand accepts --force too.
- cli.py: write the key blob via os.open(..., 0o600) — no world-
readable window between write_text and chmod.
- docs: XCHAT_PRIVATE_KEYS_B64 compromise warning (messaging page +
env-var reference); inbound section updated for cursors/edits.
- tests: API-layer coverage (401 refresh + rotation persistence,
single-retry guard, proactive expiry refresh, 429 reset, documented
event fields, path hyphenation), a StrictCrypto fake that refuses
keys until set_signing_keys is called (would have caught the
standalone-send bug), cursor restart/burst/edit tests, and CLI
blob-guard/0600 tests.
Connects the Hermes gateway to X's end-to-end encrypted direct messages
via the official X Chat API. All plaintext stays local: inbound
encoded_event blobs are decrypted with the Chat XDK (chatxdk) and
outbound replies are encrypted + signed before they reach X.
- plugins/platforms/xchat/: adapter (polling inbound, encrypted send,
typing, group mention gating, allowlist/pairing, cron standalone
sender), async httpx API client with OAuth2 refresh-token rotation,
Chat XDK crypto wrapper, and a resume-safe 'hermes xchat setup'
CLI (token -> user id -> keygen -> rate-limit-aware key registration)
- tools/lazy_deps.py + pyproject.toml: chatxdk lazy-install entry
(platform.xchat) + xchat extra for packagers
- hermes_cli/main.py: resolve a deferred bundled platform's CLI
subcommand when invoked as 'hermes <platform>' — also fixes
'hermes photon' being unreachable since the lazy-load perf change
(#54448)
- docs: messaging guide, env-var reference, sidebar, platform tables
- tests: 24 offline unit tests (dispatch, dedup, backlog seeding,
KeyChange handling, mention gating, registry parity, crypto wrapper)
The distributions guide framed export/import as local backup only, so the
new slash commands read as a competing path instead of the lightweight
half of one story. Give profile-distributions.md a comparison table up
front (git repo vs single file: updates, versioning, setup cost, what
each carries), rewrite the Not-a-fit bullets that mislabeled export, and
add a full Export/import section covering the CLI, TUI, and desktop
entry points, the desktop.json overlay, and what an archive actually
contains — including that it can carry memories and sessions, which a
distribution never does.
Also register /export and /import in the slash-command reference (they
shipped undocumented), point the profile-command entries at their chat
and desktop doors, and cover the desktop Export/Import UI on the desktop
page.
The write_file / patch file tools hard-denied ~/.ssh/config as a
"protected system/credential file", while the terminal tool only
*asked* for approval on ~/.ssh writes. That inconsistency meant a write
to ~/.ssh/config was refused via write_file but succeeded via terminal
after an approval prompt -- the same operation flip-flopping between
denied and OK depending on which tool ran it.
The SSH client config carries no private-key material, and editing it
(host aliases, ProxyJump, VS Code Remote-SSH targets) is a routine,
user-initiated task. It CAN carry ProxyCommand / Match exec directives
that run commands, so a free write is still inappropriate -- approval,
not a flat refusal, is the right policy, matching what the terminal tool
already does.
Changes:
- agent/file_safety.py: remove ~/.ssh/config from the flat credential
deny; add build_write_approval_paths() + is_write_approval_required(),
and short-circuit it out of the ~/.ssh/ prefix deny so the file is
allowed at the classifier layer. Private keys, authorized_keys, and
everything else under ~/.ssh/ stay hard-denied.
- tools/file_tools.py: _check_approval_required_write() routes ssh config
writes through the shared _run_approval_gate (once/session/always,
honors --yolo, fail-closed with no human), wired into write_file_tool
and patch_tool right after the protected-instruction gate.
- Non-interactive consumers fail closed: the ACP file bridge
(copilot_acp_client) rejects approval-required paths outright, and the
TTS output-path picker refuses them as before.
- Docs + tests updated (security.md exception note;
TestSshConfigApprovalGate covers config approval-gated, keys still
hard-denied).
The distributions guide framed export/import as local backup only, so the
new slash commands read as a competing path instead of the lightweight
half of one story. Give profile-distributions.md a comparison table up
front (git repo vs single file: updates, versioning, setup cost, what
each carries), rewrite the Not-a-fit bullets that mislabeled export, and
add a full Export/import section covering the CLI, TUI, and desktop
entry points, the desktop.json overlay, and what an archive actually
contains — including that it can carry memories and sessions, which a
distribution never does.
Also register /export and /import in the slash-command reference (they
shipped undocumented), point the profile-command entries at their chat
and desktop doors, and cover the desktop Export/Import UI on the desktop
page.
Hermes has no browser PDF, file upload, or clipboard tools. The fallback
mechanism only covers commands in _FALLBACK_ELIGIBLE (open, snapshot,
screenshot, eval, click, fill, scroll, back, press, console, errors).
The original docs described Lightpanda's general limitations, not
Hermes's actual behavior.
* fix(windows): SSH ControlMaster gating + stop hijacking the user's python
Two Windows environment-integrity fixes:
1. tools/environments/ssh.py (#73927): Windows OpenSSH has no
Unix-domain-socket ControlMaster support, so unconditionally passing
ControlPath/ControlMaster/ControlPersist failed EVERY tool call on a
Windows-hosted ssh terminal backend with 'getsockname failed: Not a
socket'. Gate the three multiplexing options behind a module-level
_SSH_MULTIPLEX = (os.name != 'nt'); the scp upload path is gated the
same way. On Windows the backend now works without connection pooling
(each command a fresh connection); POSIX behavior is unchanged. The
teardown 'ssh -O exit' is naturally inert because the socket never
exists on Windows.
2. scripts/install.ps1 (#83797): the installer put the whole
venv\Scripts directory on the user PATH, which contains python.exe /
pythonw.exe / pip.exe and so silently hijacked the 'python' command in
every terminal on the machine — unrelated projects started resolving
python to Hermes' runtime interpreter. Now copy only the launchers
(hermes.exe, hermes-acp.exe) into a dedicated $InstallDir\bin and put
THAT on PATH. Existing installs are migrated: the legacy venv\Scripts
entry is stripped from the user PATH on the next install/update. The
new bin dir is under $InstallDir (…\hermes-agent), which the uninstall
PATH sweep already matches via its \hermes-agent marker.
Updated the stale hermes_cli/update_cmd.py docstring that described the
old venv\Scripts-on-PATH layout.
Tests: SSH ControlMaster gating pinned both directions (multiplex on →
flags present; off → absent but BatchMode/StrictHostKeyChecking retained).
install.ps1 parses clean via the PowerShell AST parser.
* docs: update windows-native install docs for the bin\ launcher layout
CI (test_windows_native_docs) pins the docs and installer to the same
PATH layout. The #83797 fix moved the PATH entry from venv\Scripts to a
dedicated $InstallDir\bin holding only the hermes launchers, so update
the Windows-native guide to match: PATH-after-install section, the
install-steps list, the directory-layout table, the Get-Command
verification line, and the 'command not found' pitfall. Test now asserts
the bin\ layout and guards against a regression back to venv\Scripts on
PATH.
* fix: keep install.ps1 pure ASCII (PowerShell 5.1 codepage safety)
The two comments I added in the #83797 PATH-hijack fix used em-dashes,
tripping tests/test_install_ps1_ascii_only.py — Windows PowerShell 5.1
reads a BOM-less .ps1 in the system ANSI codepage (not UTF-8), so a
non-ASCII byte can misdecode into a stray quote and desync the parser
(issues #66994/#67000). Replace the em-dashes with ASCII '--'.
* fix: make verify_on_stop opt-in everywhere (default False, not auto)
The verify-on-stop nudge was already judged more noise than signal: the
v31 migration flips existing installs off, the v32 migration catches the
baked-in literal-true population, and the docs tell users to 'treat off
as the effective default and opt in explicitly'. But DEFAULT_CONFIG still
shipped the "auto" sentinel, so exactly one population kept getting the
nudges: fresh installs (and any config missing the key), where "auto"
resolves ON for CLI/TUI/desktop surfaces. Live symptom: repeated
'[System: You edited code ... run verification]' interruptions the user
never asked for and had to hunt down in source to disable.
- DEFAULT_CONFIG: agent.verify_on_stop "auto" -> False (opt-in).
- verify_on_stop_enabled(): missing/unrecognized value now falls back
OFF instead of surface-aware; explicit "auto" still selects the
legacy surface-aware behavior, explicit bools unchanged, and the
HERMES_VERIFY_ON_STOP env override is untouched.
- No migration needed: v31/v32 already normalized existing installs,
and this only changes the merged default for configs without the key.
- Docs updated; default-path E2E test now asserts OFF, plus a new
missing-value regression test. Also added the standard win32 skip
marker to the symlink-based temp-dir test (pre-existing Windows
failure, same class as tests/cron/test_cron_script.py).
* test: update config goldens — verify_on_stop=False is now stripped as default
With the DEFAULT_CONFIG flip to False, the migration-write invariant
(_persist_migration / save_config strip_defaults) no longer materialises
verify_on_stop: false to disk unless the user explicitly set the key:
- V20 floor fixture (agent: {} on disk): v31's write is stripped —
agent stays {} and load_config() supplies False at read time.
- V12 floor fixture (explicit verify_on_stop: true on disk): the key is
a user-set path, so the v32 flip stays materialised as false.
- Partial-write and _persist_migration regressions now assert the key is
absent from disk and (for the merge case) that the merged view still
resolves False.
Behavior verified with a one-shot migrate_config run against both
fixture shapes.
* feat(relay): ambient token endpoint mode for gateway.idp.token_url
When gateway.idp.token_url is configured WITHOUT client_id/client_secret,
treat the URL as a metadata-server-style ambient credential endpoint:
plain GET, response body is the token (raw JWT or {"access_token": ...}
JSON envelope). Covers workload-identity proxies such as Domino's
$DOMINO_API_PROXY/access-token, which mint short-lived user-scoped OIDC
tokens with no client registration.
Previously this configuration was a hard error (client_id/client_secret
missing), so no working deployment changes behaviour: creds present keeps
the OAuth2 client_credentials POST, no token_url keeps Nous Portal. The
misconfig error now self-diagnoses (names the ambient fallback and how to
select the client_credentials grant instead).
* fix(relay): reject short plain-text bodies in ambient token shape gate
Review finding: the shape gate accepted any base64url-alphabet word, so an
IdP answering the ambient GET with a terse error body ('unauthorized',
'error', 'null') had that word returned as a bearer token instead of the
fail-closed misconfiguration error. Tighten the gate to JWT-like dotted
tokens (3+ segments) or long opaque tokens (>= 32 chars); short bare words
now raise the self-diagnosing ambient error.
* fix(relay): partial IdP client credentials keep the loud error, never select ambient GET
The ambient-endpoint dispatch used 'not client_id or not client_secret',
so configuring exactly one credential (a mistyped client_credentials
setup) silently issued a GET at the IdP token endpoint and then raised
'no client_id/client_secret configured' — factually wrong for that
operator, and a stray request the old hard error never made.
Ambient mode now requires NEITHER credential; a partial pair raises
immediately, names the missing key, and issues no HTTP request (tests
assert urlopen is never called). Docstring and relay.md now say
'neither' instead of 'without'.
* fix(relay): ambient JSON envelope requires a string access_token, no coercion
Review finding (P2): the JSON-envelope branch accepted any truthy
access_token via str() coercion — a number became '12345…', a boolean
became 'True', an object became its Python repr — bypassing the fail-
closed contract and deferring the failure to the connector, where it
hides the real endpoint problem.
The envelope value must now be a non-empty string, the same contract the
client_credentials path enforces on its token response. Deliberately NO
shape gate on envelope values: an envelope is an intentional token
response (mode-1 symmetry), and opaque tokens may use the standard-base64
alphabet the raw-body gate rejects. Mutation check: reverting the branch
to str() coercion sends the 3 coercion tests red (3 failed, 15 passed).
---------
Co-authored-by: Ben Barclay <ben@nousresearch.com>
Adds the comment-based hotspot convention (no new primitives) across three
guidance surfaces:
- KANBAN_GUIDANCE worker lifecycle: new step 7 — when a file keeps colliding
with siblings or appears in other cards' recent comments, leave a
'hotspot: <path> — <reason>' kanban_comment and repeat it in completion
metadata so the orchestrator can decompose the file first.
- kanban.md (en + zh-Hans): 'Collision hotspots in parallel campaigns'
subsection — the convention, the orchestrator response (2+ flags on one
path => dedicated decomposition card before queuing more work touching
it), and the cross-link to merge-reconciler for conflicts that already
happened.
- merge-reconciler SKILL.md Pitfalls: repeated conflicts on the same file
across rounds are a hotspot signal — flag for decomposition rather than
serially reconciling.
Live-verified: guidance renders once via real import (6152 chars); hotspot
comment round-trips through add_comment -> list_comments -> worker context
on an isolated HERMES_KANBAN_DB; kanban tools, review-surfaces, and
merge-reconciler skill tests green (45 passed).
Design decisions belong to the orchestrator: decide naming schemes,
schemas, file formats, and API shapes before fanning out; never let two
subtree cards decide the same question; stamp every decision into each
dependent card body since workers cannot see sibling context. Mirrored
in the kanban docs (en + zh-Hans) with an exporter/importer worked
example, and bounded KANBAN_GUIDANCE size with an invariant test.
Add a non-terminal "review" status so a worker that finished implementation
can hand off for human review without abusing kanban_block. The old
kanban_block(reason="review-required: ...") convention routed the handoff
through the unblock-loop breaker, so a normal review -> changes -> review
cycle was falsely escalated to triage.
- kanban_db: request_review (running/ready -> review, non-block, emits
review_requested), reopen_review_task (review -> ready/todo, review_reopened),
complete_task accepts review -> done, and a review_dispatch gate (default off,
shared by the dispatcher loop and the gateway health probe).
- kanban_request_review worker tool + `request-review` / `reopen-review` CLI
verbs; tool wired through toolsets, EXPOSED_TOOLS, _POLISHED_TOOLS.
- Gateway notifier wakes the origin subscriber on review_requested and
block_loop_detected; the subscription survives until done/archived, so every
review cycle re-notifies.
- Dashboard PATCH + bulk route the review transitions (request_review /
reopen_review_task) and render the review column.
- goals.py goal-loop and KANBAN_GUIDANCE recognize review as a terminator.
- Docs (reference tables, user guide, AGENTS.md, zh-Hans mirrors) + tests.
needs_input / failed are unchanged: they still route through kanban_block,
still count toward block_recurrences, and still escalate to triage.
An unset browser.backend ("") now resolves to Browser Use mode whenever
the browser-use CLI is runnable (installed binary or uvx); otherwise the
built-in browser tools are kept so browsing never silently breaks.
Camofox setups always keep the built-in tools (no CDP surface), and
backend: off (including YAML 1.1 bare off -> False) forces the built-in
stack. hermes tools row highlighting follows the same effective-mode
resolution, and tests/tools/ pins CLI discovery off so host uvx installs
can't flip built-in-browser tests.
The upstream ahujasid/blender-mcp and ahujasid/ableton-mcp GitHub repos
were hijacked on 2026-08-08: the maintainer (@sidahuj) publicly reported
his account was compromised and ownership stripped, and both repos now
redirect to an attacker-controlled org (MCPBlender, created the same
day, pushing new commits since).
Although our catalog pinned blender-mcp==1.6.4 from PyPI (pre-compromise,
sha256 verified unchanged), the server is only half the bridge: the
manifest's post-install instructions and the optional skill directed
users to download addon.py — arbitrary Python executed inside Blender —
from the now-compromised GitHub repo (the raw URL currently 404s, and
the addon ships in no PyPI artifact). There is no trustworthy source
for the addon half, so the entry cannot be installed safely end-to-end.
Removing the catalog entry and skill entirely until the maintainer
confirms account recovery; re-adding is a follow-up PR once upstream
is verified clean.
- optional-mcps/blender/: removed
- optional-skills/creative/blender-mcp/: removed
- docs: catalog rows, sidebar entry, skill pages (en + zh-Hans) removed
- cross-references in unreal-mcp and kanban-video-orchestrator cleaned
The Hugging Face agent-harness registry matches standard-var values
EXACTLY against the harness id. Our registry id is 'hermes-agent'
(huggingface.js agent-harnesses.ts), so AI_AGENT=hermes was counted as
'unknown' — fixed at both entry points.
Remote terminal backends (Docker/SSH/Modal/Daytona/Singularity/Vercel)
never inherit the Hermes process env, and the cross-session leak guard
deliberately strips HERMES_SESSION_* from subprocess envs in engaged
multi-session hosts — so hf/huggingface_hub traffic from those shells was
unattributable. _wrap_command now exports AI_AGENT/HERMES_AGENT inside
every wrapped command with ${VAR:-default} semantics (outer harness is
never clobbered), and the snapshot dump excludes both names so a baked
value can never shadow a later outer harness.
E2E: verified against real huggingface_hub 1.27.0 detect_agent() with a
cached registry — 'hermes-agent' detected via AI_AGENT and via
HERMES_SESSION_ID; old 'hermes' value reproduced the 'unknown' bug.
CLI and gateway entry points now set AI_AGENT=hermes (the emerging
cross-agent standard read by e.g. huggingface_hub agent detection) and
HERMES_AGENT=true, via setdefault so an outer harness is never
clobbered.
Adds skills/autonomous-ai-agents/merge-reconciler — a bundled skill teaching
a neutral third-party agent to resolve git merge conflicts between two
agents' branches: gather both diffs + intents, classify each hunk
(disjoint-intent / same-question-different-answer / superseded), resolve
under an impartiality contract, verify, and hand back a per-hunk summary.
Procedure was live-tested end-to-end against a real conflict fixture.
Includes contract tests (tests/skills/test_merge_reconciler_skill.py) and a
kanban docs cross-reference (en + zh-Hans): assign a third neutral profile a
reconciliation card with both conflicted cards as parents.
Surface the existing planner/worker cost-split capability as an explicit
strategy in the docs:
- delegation.md: new 'Cost strategy: frontier planner, inexpensive workers'
subsection under Model Override, with a config.yaml snippet using the
verified delegation.model / delegation.provider keys, the resolution order
(base_url > provider > inherit parent; model applies in all cases, empty =
inherit), and a note that delegate_task has no per-task model parameter —
quality-sensitive tasks should use kanban's per-task override instead.
- kanban.md: matching 'Cost strategy: frontier orchestrator, inexpensive
workers' subsection using the verified per-profile config mechanism
(dispatcher injects profile-scoped HERMES_HOME at worker spawn) and the
existing per-task model_override (--model/--provider, set-model, dashboard).
- zh-Hans mirrors for both pages.
- cli-config.yaml.example: cost tip comment under the delegation section.
Config resolution was live-verified against tools/delegate_tool.py
(_load_config + _resolve_delegation_credentials) with a temp HERMES_HOME:
delegation.model pins children to the sentinel model; with no delegation
keys, children inherit the parent model and credentials.
Adds 'Handing context to follow-up cards (the parent link)' to the kanban
feature page and a CI-remediation worked example to the tutorial, with
zh-Hans mirrors. Claims live-verified against kanban_db on an isolated
board: create_task creates children of done parents directly in ready,
recompute_ready leaves children of open parents in todo, and
build_worker_context surfaces the parent's completion summary and
metadata under '## Parent task results'.
Reframe (per review): browser.backend: browser-use is now a DRIVER over
whatever browser source is configured, not a competing backend choice.
- browser_exec resolves its CDP endpoint through the same chain the
built-in tools use: BU_* env override > BROWSER_CDP_URL/browser.cdp_url
(/browser connect) > the configured cloud provider via browser_tool's
_get_session_info() — sharing the per-task session cache, expiry
replacement, inactivity reaper, and atexit cleanup instead of
duplicating them. Live-validated against Browserbase (session created,
driven, reaped) and gateway-provisioned Browser Use cloud browsers.
- Direct-API Browser Use configs skip provider resolution (the CLI talks
to their cloud natively via BU_AUTOSPAWN); the Nous-gateway variant
resolves through the provider, so subscribers get CLI mode without a
raw BROWSER_USE_API_KEY.
- Camofox: only true fallback — Firefox-based, custom HTTP API, no CDP
surface (its own health probes fail on CDP-schema calls). Active
Camofox setups keep the built-in browser tools even with
backend: browser-use set.
- hermes tools picker: provider rows and the Browser Use row are no
longer mutually exclusive; selecting a provider keeps the driver
choice, and both rows highlight when composed.
- Docs updated for driver-over-source semantics.
Follow-ups on the salvaged Browser Use CLI integration (PR #66476):
- browser_exec runs model-written Python on the host. Strip it at
tool-definition time for sessions whose resolved toolsets exclude
'terminal' so terminal-less surfaces (locked-down messaging configs)
don't silently regain host code execution through the browser toolset.
Session-level gate in model_tools, not a check_fn (check_fn results are
TTL-cached process-wide across sessions).
- Replace the live 'browser-use skill' schema fetch with a pinned helpers
digest: no third-party version-drifting text in the prompt, byte-stable
schema across machines. A/B benchmarked (108 runs, opus-4.8 + kimi-k3,
6 multi-step web tasks x 3 arms x 3 reps): pinned digest matches the
full skill dump 36/36 vs 36/36 at ~equal tokens; both cut total task
tokens ~60% vs the legacy browser_* toolset.
- Docs note for the terminal gate; contributor mapping for salvage.
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).
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
Two bugs reported on the docker terminal backend (desktop app, sandboxed
profiles with container_persistent: false):
1. A NEW chat's container inherited the PREVIOUS session's workspace,
bind-mounted rw at /workspace, because the mount source was the
process-global TERMINAL_CWD env var (written by the workspace picker,
outliving its session) and all sessions shared one 'default' container.
2. Every command failed with exit 126 because the desktop gateway recorded
the HOST launch directory as the session cwd, and each command was
prefixed with 'cd /Users/<user>/...' inside the container.
Fixes (class-wide, single owners):
- container_persistent: false + docker now keys containers PER SESSION:
fresh container per chat, removed at session close/idle. delegate_task
children share the parent's container via an explicit alias registry.
container_persistent: true keeps the documented ONE-long-lived-container
contract unchanged.
- _resolve_task_host_cwd() is the single owner of the cwd->/workspace mount
policy across all four env-creation sites; under isolation it refuses
process-global cwd sources and mounts only the session's own attached
workspace (tui_gateway now tags overrides with cwd_source).
- _resolve_command_cwd() gains the same host-path guard the env-creation
sites already had (#50636/#54447 sibling site): a recorded host cwd is
discarded on container backends instead of cd-ing every command into a
nonexistent path.
E2E-tested against real Docker: distinct containers per session, no stale
mount in a fresh session, no exit 126 from host cwd records, containers
removed at session teardown.
Personality persistence used to be split per surface: the TUI/desktop wrote
the NAME to display.personality while the CLI/gateway wrote rendered TEXT
into agent.system_prompt (and their /personality none only blanked the
text, leaving the name behind). When #81946 made display.personality
authoritative everywhere, stale names written long ago resurrected
personalities users had turned off - kawaii defaulting on after updating.
- hermes_cli/personality.py: new single owner of personality state.
Built-in personality definitions, neutral-name normalization, rendering,
availability (built-ins overlaid by agent.personalities), overlay
resolution, and the ONLY sanctioned persistence path
(persist_personality -> display.personality; never agent.system_prompt).
- v34 config migration: one-time reset of display.personality to none
(announcing which personality was cleared and how to re-enable), plus a
scrub of agent.system_prompt when it verbatim-equals a known personality
render (machine-written by the old CLI/gateway). Hand-written manual
prompts are never touched.
- All surfaces rewired through the module: CLI /personality (incl. active
marker in the list), gateway /personality, TUI config.set + slash path
(which previously applied without persisting), TUI config.get (reports
the EFFECTIVE personality), completer, hermes config display, and the
tui_gateway health probe.
- cli.py/config duplicates removed: built-ins now defined once; the
desktop mirrors them from one lib module (src/lib/personalities.ts).
- Docs updated: selection lives in display.personality, built-ins always
available, one-time reset note.
Explain that schema.description is model-facing while register_tool(description=...) only populates ToolEntry metadata, and remove the duplicated hello-world description in English and zh-Hans docs.
Refs #60735
Co-authored-by: Shiki <132348332+songshikang0111@users.noreply.github.com>
Agent Plugins v1 packages with 'streamable-http' mcp.json entries now load
through Hermes' existing URL-based MCP client instead of being reported and
skipped. The stdio-only limitation was the agreed follow-up slice from
PR #81196.
Boundary rules from the v1 spec (§7.2.1) are enforced:
- URL must be absolute http(s), no user information, no fragment; plain
HTTP only for localhost/loopback hosts.
- Configured package headers are never forwarded across a cross-origin
redirect: translation marks entries strict_redirect_headers, and the
redirect hook in the native runtime strips those headers (plus
Authorization) whenever a redirect leaves the original origin. On mcp <
1.24.0, where the client cannot hook redirects, such servers fail closed
with an actionable upgrade message.
- Legacy 'sse' entries remain reported and skipped.
The redirect hook is extracted into a testable module-level factory
(_make_redirect_header_stripper); default behavior for native config
servers is unchanged (Authorization-only stripping).
Document the actual transport split: legacy editable drafts by default, optional rich drafts, persistent rich final sends, and in-place rich final edits for edit-based streams.
New tests/skills/test_authoring_standards.py parametrizes every bundled +
optional SKILL.md (1148 checks) against the mechanically-verifiable subset
of the hardline standards:
- required frontmatter fields (name/description/version/author/license/
platforms) + tags
- frontmatter name == directory name
- description <= 60 chars, ends with period, no marketing words
- related_skills resolve in-repo
- no machine-local paths
- <= 100k chars
Grandfather dict for legacy debt ships EMPTY — all pre-existing violations
fixed in this PR:
- 13 frontmatter names canonicalized to their directory names (the install
identifier); all related_skills references updated (comfyui -> stable-
diffusion). Fixes the class behind PR #42788's report; also fixes
here.now's invalid dot-name.
- optional-skills/devops/cli -> inference-sh-cli (dir was the generic
'cli'; fm name was right) incl. docs pages (en + zh-Hans), catalog row,
sidebar entry.
- pytorch-fsdp: 157k generated 'Quick Reference' dump moved to
references/common-patterns.md; SKILL.md 159k -> 2.5k with a pointer.
- research-paper-writing: 31.7k Phase 5 drafting section moved to
references/phase5-paper-drafting.md; SKILL.md 103k -> 71k.
Docs regenerated with scope discipline.