Commit Graph

51368 Commits

Author SHA1 Message Date
Teknium
cdce56faf2 plugin-catalog CI: enforce rule 17 — scan pinned entries for unscannable payloads
The admission gate ran the install scanner (dangerous fails) but nothing refused
a compiled binary, committed bytecode, or an archive hiding either — the shapes
the scanner cannot read. This adds that enforcement to pinned-source-validate.

- scripts/scan_plugin_payloads.py (stdlib-only, so the no-install validator and
  third-party CI can run it): flags compiled binaries by magic header (ELF,
  Mach-O, PE/MZ, WebAssembly — not by extension, which a rename defeats),
  .pyc/.pyo bytecode (incl. sourceless/hash-forged caches that run instead of
  the .py beside them), and archives whose members are not reviewable source.
  Plain-text source bundles and inert assets (fonts, images) pass.
- plugin-catalog-ci.yml: after `hermes plugins validate`, each changed entry is
  scanned; a finding fails it. An entry may list `attested_binaries:` paths,
  each verified with `gh attestation verify --repo <repo> --source-digest <sha>`
  against the pinned commit; verified paths are dropped before the scan, any
  other binary still fails. Repo-controlled filenames are newline-stripped in
  the log so none can forge a workflow command.
- validate_plugin_catalog.py: `attested_binaries` is a known key, shape-checked
  as a list of relative paths.
- Tests: unit tests for the detector and two new workflow-driven bypass cases
  (bundled .exe, committed .pyc) in the confinement harness, which runs the real
  `run:` block.

Full-content re-sweep of all 543 pins: only tabro carries a native binary (it is
delisted in the companion PR). Three entries (crawl4ai, hermes-vision-inline,
mermail) commit __pycache__/*.pyc build litter and would fail rule 17 on their
next pin bump — pre-existing, not touched here.
2026-10-11 03:03:39 -07:00
Teknium
efd17a70b5 fix(release): staging checks each metadata row's commit and tag, not stamp-only fields
rc.10-v0.21.7's "Stage verified stable candidate artifacts" job failed with

  ValueError: Built package provenance does not match the release

assemble() ran stamp_matches() on each metadata row. stamp_matches() is the
check for an install-stamp.json and requires baseVersion, but a metadata row
is what record() writes (platform, arch, tag, commit, identity, version, ...)
and never carries baseVersion. So every bundled stable release failed here
once all packages were built. record() already verified each package's own
stamp (including baseVersion) when it wrote the row.

The row check is now what the row actually contains: commit and tag. The
staged-candidate fixture wrote a baseVersion into its rows, which is how the
mismatch stayed hidden; it now writes exactly the keys record() does. With
the corrected fixture, main's code fails 8 tests with the production error.
2026-10-11 02:56:13 -07:00
Teknium
d3bdf761b0 test(release): the cut-tunnel cell proves it reached the cut tunnel
Review on #136509 (Greptile): assert_nothing_changed accepts any non-zero
exit, so an unrelated early failure could pass. The cell now asserts the
proxy recorded the EOF on github.com and the output names the failed fetch.
2026-10-11 02:55:56 -07:00
Teknium
7831de8999 test(release): install and network E2E follow the new update-channel defaults
rc.10-v0.21.7 failed 13 release jobs because the release-only E2E still
assumed the old update behavior:

- Install/update E2E (Linux, macOS, Windows; hermes-update and desktop-app
  routes). A fresh install of the official repo now defaults to the stable
  channel. Installed at HEAD it is newer than v0.21.6 and correctly waits
  ("Already up to date"), so the HEAD -> NEXT legs never moved ("checkout
  landed on NEXT", "no successful update completion receipt"). The drivers
  now record `hermes update --set-channel main` before the user-state
  snapshot, the same way the pytest sandboxes already do (3a4b4b6f83).
  Releases older than --set-channel follow main already and are skipped by a
  help probe.
- Network E2E (test_offline_and_flaky, test_proxy_only_egress). They asserted
  that `main` reads and retries releases/channels/main.json through the
  proxy. #136429 removed that read on purpose (regional WAF 403s, #128295).
  Rewritten to the new contract: `main` reads no release record, survives a
  release-host outage, and a cut tunnel to github.com still fails fast and
  changes nothing.

The desktop unit test with the same assumption was already fixed on main
(0aa764fedd).
2026-10-11 02:55:56 -07:00
Teknium
decf192176 plugin-catalog: delist tabro; add rule 17 (no unscannable executable payloads)
tabro bundles a prebuilt Windows .exe (relay-native-host-0.4.2.exe) in its
plugin tree. The install scanner cannot read machine code, and the SHA-256 the
plugin checks it against is committed in the same commit, so it only proves the
file was not changed after that commit, not where it came from. A reviewer
cannot tell it matches the in-repo C++ source. That is a precedent worth
closing before someone ships a binary that is not benign.

- Delists tabro (deletes plugin-catalog/tabro.yaml). Existing installs keep
  working; not added to removed.yaml — there is no evidence of malice, only no
  way to verify the binary. An attested build (rule 17) is the way back.
- Adds admission rule 17: no compiled binaries, committed Python bytecode, or
  archives hiding either, detected by file header not extension. A native
  component is allowed only with a GitHub build-provenance attestation tying it
  to the plugin repo's own workflow at the pinned commit; a setup-time download
  is allowed only from the tool's official source, pinned by checksum and
  disclosed. Mirrored into the docs submission guide (the mirror test enforces
  identical text).

A full-content scan of all 543 pinned entries found tabro is the only one with
a native executable. The enforcing gate ships in a follow-up PR.
2026-10-11 02:53:45 -07:00
Teknium
c152ba5a82 docs(ci): prompt-surface changes need one core approval, not two
The prompt-surface-review ruleset was meant to require one other reviewer;
it is now set to one hermes-agent-core approval. CODEOWNERS, the CI comment,
the staleness-test message and the developer docs said two.
2026-10-11 02:26:46 -07:00
Teknium
ae3598c7cd feat(ci): reviewers see every change to the system prompt and tool schemas
A one-line code change can rewrite what every session sends to the model -- a
guidance constant moving tiers, a toolset gaining a tool, a dynamic rewriter
editing a description -- without the diff showing it. The rendered prompt
surface is now committed and reviewed like code.

- scripts/ci/prompt_surface.py renders the system prompt
  (build_system_prompt_parts) and the agent's tools[] through the production
  code for nine surfaces (cli x claude/gpt/gemini/hermes, cli in a code
  workspace, desktop, telegram, discord, cron) into
  tests/fixtures/prompt_surface/. A fresh home in a child process; clock,
  host OS, network, tool availability, env probe and machine paths pinned,
  so a laptop and CI produce the same bytes. Each tool schema is stored once,
  with a per-surface copy only where a surface serves a different schema.
- tests/ci/test_prompt_surface.py fails when the committed snapshot is stale,
  so a surface change must carry the regenerated snapshot in the same PR.
- prompt-surface-diff.yml (advisory, PR-only, stdlib-only, no PR code runs)
  posts per-surface token deltas, prompt hunks and per-tool
  description/param changes into the unified review comment.
- CODEOWNERS requests hermes-agent-core on the snapshot; a new
  prompt-surface-review ruleset requires two of their approvals.
- Docs: prompt-assembly.md (how it works), contributing.md (what to do when
  the test fails).
2026-10-11 01:21:18 -07:00
Teknium
0aa764fedd test(desktop): the main channel hand-off expects no channel-record request
4b351c751a made `main` resolve straight to the git branch without reading
releases/channels/main.json (a WAF 403 on that never-published record stopped
updates, #128295). The Desktop source hand-off test still served that record
and asserted apply() fetched it twice, so JS & TS checks went red on every
main push and PR since.

The fixture no longer serves main.json and the test asserts re-resolving main
makes zero requests -- a regression back to reading the record now 404s and
fails the same assertion.
2026-10-11 00:47:43 -07:00
Teknium
b30f95ac4a fix(release): discover orders attempts numerically so rc.10 is the version's record
Review on #136434 (@jquesnelle): discover() sorted records by version only,
so attempts inside a version kept git ls-remote's NAME order (rc.10 between
rc.1 and rc.2). reconcile indexes records by version with the last one
winning, so once a version reached rc.10 it handed rc.9 (burned, no release)
to publish_attempt, which refuses with "Final tag release database ID is
invalid". v0.21.7 is at rc.9, so its next cut is exactly rc.10.

- Sort by (version, attempt).
- The test fakes list refs sorted by name, as git does; insertion order hid this.
- rc.10 test (fails without the sort), and a same-commit retry test (rc.1
  abandoned, rc.2 re-cut at the same commit wins).
2026-10-10 23:58:25 -07:00
Teknium
3e1d9dc59a fix(release): an abandoned attempt is burned before its version's final tag is checked
Every Stable Release Publication run since v0.21.6 has crashed in
sequencer.discover():

  ValueError: v0.21.6 points at a different commit than rc.1-v0.21.6

v0.21.6 took four attempts: rc.1-3 were abandoned (each has its
abandoned-rc.<N> marker) and rc.4 won and became v0.21.6. discover() tested
"does the final tag exist" before "is this attempt abandoned", so for rc.1 it
compared v0.21.6's commit (rc.4's) with rc.1's and raised; the abandoned
branch was only reachable for versions that never got a final tag. Any
version whose winner was not rc.1 broke every later reconcile, including the
next release's publish.

The abandoned marker is now checked first: an abandoned attempt is burned
whatever its version became. A NON-abandoned attempt whose final tag points
elsewhere still raises.
2026-10-10 23:58:25 -07:00
Teknium
04eb00cef7 fix(browser): browser_exec uses the local fallback when the cloud browser provider fails
When the configured cloud browser provider fails to create a session (seen live:
Browser Use gateway HTTP 429 RATE_LIMIT_EXCEEDED), the session layer already falls
back to local Chromium and marks the session fallback_from_cloud, which keeps the
built-in browser_* tools working. browser_exec read only cdp_url from that session,
found none (local sessions launch lazily), and failed the whole call with "returned
no CDP endpoint, so Browser Use mode cannot drive it".

On a fallback session, resolve the packaged Chromium's CDP endpoint the same way the
no-provider path does, and log the provider failure once.
2026-10-10 23:51:59 -07:00
kshitijk4poor
0faf0938eb refactor(desktop): split the edit deep-cut phases out of runEdit; move the deep-cut tests into a sibling
The repo code-health ratchet blocked the push on two counts:
- runEdit had cyclomatic complexity 31 against a target of 20.
- index.test.tsx grew past its main-branch cap.

The pure steps move to rewind.ts:
- planEditAfterConfirm: plan, confirm, session-ownership check and
  re-aim.
- retryKeepsDeepConfirm: the stale-retry consent rule.

The hook gains retryStaleEdit, which holds the existing #82462 retry
unchanged apart from the consent rule, and rollBackEdit.
isCompressedAwayError becomes module-level. runEdit is now at CC 20.

The usePromptActions test harness moves to prompt-actions-harness.tsx.
The #133716 hook tests move to index.deep-cut.test.tsx, plus a new test
for a later user turn arriving during the dialog, which goes red with
the "moved" outcome removed. index.test.tsx drops below its main-branch
size. No behaviour change otherwise.
2026-10-11 12:14:05 +05:30
kshitijk4poor
10cf193745 feat(tui_gateway): ship the deep-cut gate warn-only for one release
Rollout decision for #133716: an older Desktop or a third-party client
that does not send confirm_deep_truncate would otherwise get 4033 on
every deep edit or restore as soon as only the backend upgrades. The
reporter's Windows Desktop plus WSL serve setup is exactly that pair.
This release logs the unconfirmed deep cut and still runs it. The
upgraded Desktop asks before a deep cut on its own.
DEEP_TRUNCATE_ENFORCED flips it to the 4033 refusal next release. The
refusal tests run with it on, a new test pins the warn-only behaviour,
and the integration doc and contract comment say so.
2026-10-11 12:14:05 +05:30
kshitijk4poor
183e955bd0 fix(desktop): an edit retry only reuses the deep confirm it covered; a session switch drops the edit
Sol r4 findings:
- P1: the 4018 stale-target edit retry carried the user's deep-cut
  confirm into a refreshed transcript that had gained later user turns,
  so the server gate was skipped for turns nobody confirmed. The retry
  now sends the confirm only when the refreshed transcript has the same
  number of later user turns. Otherwise it goes unconfirmed, and a
  server 4033 asks again.
- P2: the first deep-edit confirm did not re-check the session after
  the dialog. A switch left the busy flags on the new session while the
  edit went to the old one. The edit is now dropped when the session
  (or the tile's runtime) changed.
2026-10-11 12:14:05 +05:30
kshitijk4poor
579d3575c2 fix(desktop): keep the 4033 re-run flag off the public callbacks; an unseen later turn stops an edit
Delta review 2 Lows:
- The internal answeredFor parameter rode on the public onReload/onEdit
  callbacks, and assistant-ui calls onReload(parentId, config), so it
  received the config object on every reload. The 4033 re-run now goes
  through private runReload/runEdit, and the public callbacks take only
  their own arguments.
- A deep edit re-aimed after the confirm accepted a later user turn that
  landed during the dialog, which archived a turn the user never
  answered for. revalidateEditPlan now refuses when the count of later
  user turns changed. Regenerate already asked again in this case.
2026-10-11 12:14:05 +05:30
kshitijk4poor
4f5be98c29 fix(desktop): a 4033 confirm answered after a session switch is dropped
Sol r3: the re-run after an accepted 4033 read the active session
again. If the user had switched sessions while the dialog was open, a
parentId=null regenerate truncated and resubmitted the other session's
last turn without asking. The answer now belongs to the session that
was refused: the re-run happens only if the session (the tile's
runtime) is still the same. Applies to primary and tile, regenerate and
edit.
2026-10-11 12:14:05 +05:30
kshitijk4poor
d4a001341c fix: re-aim a deep edit confirmed mid-stream; count an unstamped carrier's own turn
Delta review findings:
- Medium: a confirmed deep edit during a live turn was silently
  dropped, because the stale check compared the transcript array by
  identity and streaming replaces it on every delta. The edit now
  re-plans against the current transcript (revalidateEditPlan). It
  proceeds when the edited turn keeps its address and the re-planned
  index is applied. If the turn moved, it stops with an "Edit failed"
  notice instead of returning silently. Applies to the primary chat
  and tiles.
- Low: a live carrier with no row id of its own never counted its own
  user turn, so a two-turn cut could pass without 4033. It now counts
  (fail closed).
2026-10-11 12:14:05 +05:30
kshitijk4poor
1b8a09f88d fix(desktop): a server 4033 re-runs the rewind from the top instead of retrying a stale plan
Sol r2 findings on bc1394c357:
- P1: the 4033 fallback dialog skipped the stale-transcript check.
  Accepting retried the captured plan even if the transcript changed
  while the dialog was open, and declining rolled back to the
  pre-submit snapshot over newer messages.
- P2: a slow answer let the optimistic busy arm expire, and the retry
  never re-armed it.

A refusal now rolls back at once (the refused submit wrote nothing).
If the user confirms, regenerate or edit re-runs from the top: the
session is re-read, the busy and stale guards apply again, and the
optimistic state is re-armed. The forced deep cut covers only the
transcript the user answered for, so a change during the dialog asks
again. submitWithDeepTruncateConfirm is removed.
2026-10-11 12:14:05 +05:30
kshitijk4poor
009fb2c98b fix(tui_gateway): count every durable user row a merged carrier holds in the deep-cut gate
Sol r1 finding: the depth gate counted live user turns, but repair
merges a user;user run into one carrier. Cutting at row 101 of a merged
101/102/103 carrier archived three user turns while the gate counted
one, so it skipped the confirm. The gate now counts a carrier's merged
run from the physical durable rows. Display markers it absorbed
(model-switch rows, #94486) are not user turns. Ids that cannot be
classified count as turns (fail closed). Rows of the run that survive a
durable-carrier cut are excluded. The resolver keeps main's return
shape.

The existing carrier test gave the absorbed row the same id as an
assistant row, which durable rows never share. It now uses a unique id.
2026-10-11 12:14:05 +05:30
kshitijk4poor
2afbaf10d1 refactor(desktop): one deep-cut confirm helper; drop a plan that went stale during the confirm
Lead fold from the independent review (efficiency Medium; reuse,
quality, thermo and 2c Lows):
- Stale plan: the confirm dialog is a wait as long as the user takes,
  so a turn that started or output that landed meanwhile made the plan
  and its rollback snapshot stale. Regenerate now returns without
  submitting when the session went busy or its transcript changed
  during the confirm. Edit, which interrupts a live turn on purpose,
  returns only when the transcript changed under a confirmed deep cut.
- One helper: planConfirmedReload and planConfirmedEdit share
  confirmIfDeepCut instead of two copies of the check, and the confirm
  thunk is built once per callback.
- Naming: deepReloadConfirmRequest is renamed deepCutConfirmRequest,
  since edit uses it too, and the 4033 code is a named constant.
- Tests: tile hook tests cover the stale-confirm drop and an
  unpredicted 4033 asking once and resubmitting confirmed.
2026-10-11 12:14:05 +05:30
kshitijk4poor
c72f4fa1dd fix(desktop): edit asks before a deep cut; a server 4033 asks once and retries
Review findings on #133748 (teknium1 Medium + Low, kvnloo concurring):
- Edit sent confirm_deep_truncate on every submit, so the server gate
  never applied to Desktop edits. planConfirmedEdit mirrors
  planConfirmedReload: an edit that would archive later user turns asks
  the same destructive confirm first. A tail edit or failed-turn edit
  asks nothing, and a decline sends nothing. The stale-target retry
  re-addresses the same turn and reuses the user's answer.
- The client and server count later user turns differently, so an
  under-asking client turned a deep cut into an opaque "Regenerate
  failed". submitWithDeepTruncateConfirm handles the server's 4033
  refusal by asking once and retrying confirmed. A declined confirm
  rolls back silently instead of showing an error toast.
Both apply to the primary chat and session tiles.
2026-10-11 12:14:05 +05:30
kshitijk4poor
45be34ba9c test(desktop): pin the explicit deep-truncate confirm on edit and tile restore retry
c1077745cc made an omitted confirmDeepTruncate mean not confirmed, so the explicit true at the edit and tile restore-retry submits is load-bearing; dropping either would turn an older-turn edit or restore into a silent 4033. Assert the flag on the existing edit and tile stale-target restore tests.
2026-10-11 12:14:05 +05:30
kshitijk4poor
b8eedfefa2 fix(tui-gateway): count live rows in the 4033 archived_messages on a carrier cut
Gate C18 r1 quality [Low, unverified -> reproduced]: on the
durable-carrier path `truncated` is the physical durable_prefix, so
`len(history) - len(truncated)` mixed live and physical rows. A probe
with target row 101 absorbed into a repaired live carrier reported
archived_messages=5 for a cut that drops all 6 live rows (the carrier
and everything after it). The refusal now counts from the live cut
index on that path; the plain path is unchanged.
2026-10-11 12:14:05 +05:30
kshitijk4poor
9c98cc19f6 refactor(tui-gateway): reuse the resolved user-turn count in the depth gate
Gate C18 r1 efficiency [Low]: the 4033 gate rescanned history with
_history_user_indices under history_lock although
_resolve_truncation_ordinal had just built the same list from the same
history. The resolver now returns the count on success (and always
returns durable_prefix, None when unused), so its one caller drops the
len() probing.
2026-10-11 12:14:05 +05:30
kshitijk4poor
eba445b402 refactor(desktop): make the deep-truncate confirm explicit at every rewind site
Gate C18 r1 quality [Low]: runRewindSubmit defaulted confirmDeepTruncate
to true while truncateSubmitParams defaulted it to false, so regenerate
had to write `?? false` and restore/edit opted into a destructive server
bypass by omitting a trailing argument. An omitted flag now means not
confirmed; restore/edit and their stale-target retries pass true
explicitly, and ReloadPlan.confirmDeepTruncate is a plain boolean
(planReload false, planConfirmedReload true after the confirm).

The parameter stays optional rather than required: it trails other
optional parameters, which TypeScript forbids for a required one.
2026-10-11 12:14:05 +05:30
kshitijk4poor
2c1f9e4b39 test(desktop): pin the deep-regenerate confirm wiring at both reload sites
Gate C18 r1 2ab [Medium, toothless]: reverting the planConfirmedReload
wiring in index.ts / session-tile-actions.ts left every test green. The
existing deep-regenerate tests now assert the confirm is asked once, a
decline sends no prompt.submit (main surface), and an accept sends
confirm_deep_truncate. Both go red when the wiring reverts to planReload.
2026-10-11 12:14:05 +05:30
kshitijk4poor
57f825763b fix(desktop): count failed later turns when deciding a deep regenerate
Gate C18 r1 2c [Medium]: planConfirmedReload used
visibleUserMessageIndices, which skips failed user turns, but the
gateway persists a failed turn's user row and counts it. Regenerating
before a failed later turn sent no confirm_deep_truncate and got an
unexplained 4033 (it worked before the gate). Any later visible user row
now asks for the confirm.
2026-10-11 12:14:05 +05:30
kshitijk4poor
df927be528 test(desktop): spell out the decline stub in the deep-reload test
Gate C18 r1 quality [Low]: `async () => (asked += 1) < 0` hid "count
and return false" in an always-false comparison.
2026-10-11 12:14:05 +05:30
kshitijk4poor
9e99692b28 refactor(tui-gateway): state the depth-gate WHY without incident narration
Gate C18 r1 quality [Low]: the comment above the 4033 gate retold the
incident; AGENTS.md keeps comments to the WHY.
2026-10-11 12:14:05 +05:30
kshitijk4poor
00e3276e92 test(tui-gateway): opt the real-sessiondb row-id rewind into the deep-cut confirm
Gate C18 r1 2ab [High]: the 4033 depth gate refuses this test's cut
(ordinal 1 of 3 user turns archives 2), which is an intended deep
rewind. The test now sends confirm_deep_truncate like Desktop restore
does. A full run of test_tui_gateway_server.py plus every tests/ file
using truncate_before_* found no other deep cut without the flag.
2026-10-11 12:14:05 +05:30
kshitijk4poor
06559414d3 test: move two existing tests into topical siblings to keep files under their size cap
Code-health ratchet: index.test.tsx (6584 > 6577) and test_tui_gateway_server.py
(23504 > 23503) may only shrink vs main, and the deep-truncate stack grew both.
Pure moves, bodies unchanged:
- the uploadComposerAttachment Windows-image staging test -> upload-composer-attachment.test.ts
- test_prompt_submit_empty_truncation_allowed_with_confirm -> test_tui_gateway_server_deep_truncate.py
  (the sibling now covers both history-cut opt-ins)
2026-10-11 12:14:05 +05:30
kshitijk4poor
0e630b920e fix(tui-gateway): refuse a prompt.submit cut that archives >1 user turn without confirm_deep_truncate
Every existing truncation guard validates that the anchor exists and matches.
In #133716 the anchor was valid but named the wrong row (a stale Desktop
binding), and the gateway archived 107 live messages (430 -> 323) with no
friction — nothing bounded how much history one submit may drop.

_truncate_history_for_submit now refuses (4033, data: archived_messages,
archived_user_turns) when the cut archives more than one user turn and the
submit lacks confirm_deep_truncate. One turn is the legit implicit-target
allowance: tail regenerate and edit-last-message drop exactly that turn.
History and DB stay untouched on refusal.

Desktop: restore and edit name their target explicitly, so runRewindSubmit
sends confirm_deep_truncate by default (via truncateSubmitParams); regenerate
sends it only after the Half-A confirm. The TUI sends no truncation params.
Contract field added to PromptSubmitParams and the generated TS/OpenRPC
regenerated with scripts/gen_gateway_contracts.py.

Trade-off: an older Desktop build against this backend gets 4033 on
restore/edit of an older turn until it updates.

Fixes #133716

Reported-by: @Enferlain
2026-10-11 12:14:05 +05:30
kshitijk4poor
cbb057c0b7 fix(desktop): confirm a regenerate that would archive later user turns
A regenerate's target comes from the bound message scope. In a long-lived
renderer that binding can go stale and name a turn hours back; planReload
trusted it, and the truncating prompt.submit silently archived every later
turn (430 -> 323, 107 live messages, in #133716).

planConfirmedReload (rewind.ts, shared by the primary chat and session tiles)
now routes a reload that would drop later user turns through the same
confirm restore-checkpoint uses (shared ConfirmDialog via store/confirm,
existing restore copy — no new strings). Declining submits nothing. The
normal tail regenerate has no later user turn and is unchanged.

Refs #133716

Reported-by: @Enferlain
2026-10-11 12:14:05 +05:30
Teknium
ce966fef71 chore: map shadus@gmail.com for attribution 2026-10-10 23:42:04 -07:00
Teknium
6c7c507060 test(browser): pin browser_exec's private-URL route to two invariants
Replace the salvaged 11-case class with two invariant tests, red on
origin/main and on the contributor's head:

- a call carrying a private URL (LAN, mixed public+private, a host
  rebinding private->public) runs on the `::local` sidecar under its own
  daemon, and IMDS / hybrid-off / public->private rebinding are refused;
  none of them reach the cloud provider;
- the sidecar daemon is separate from the cloud route's, and a URL-less
  follow-up call stays on the sidecar.
2026-10-10 23:42:04 -07:00
Teknium
7cbbb586c3 fix(browser): browser_exec's private-URL route reaches local Chromium on its own daemon
The salvaged route declined the provider in browser_use_cli, but the
managed-Chromium resolver still asked _get_session_info for the plain
cache key, and with a cloud provider configured that key builds a CLOUD
session — the private-URL call still created a Browser Use cloud browser.

- the sidecar call uses the `::local` cache key, so _get_session_info
  builds local Chromium (the same key browser_navigate's sidecar uses);
- it runs under its own harness daemon (BU_NAME `_local_<name>`): the
  daemon reads BU_CDP_* once at spawn, so reusing the cloud route's
  daemon would relay the private URL to the cloud browser;
- a follow-up call with no URL literal stays on the route the previous
  call took, like browser_navigate's _last_session_key.
2026-10-10 23:42:04 -07:00
shadus
5515e41725 fix: compute each URL's routing verdict once, not once per decision
Review caught that `_hybrid_routes_locally` resolves DNS (`_url_is_private` ->
`socket.getaddrinfo`) and is not memoised, so evaluating it separately for the
pre-flight and again for the backend choice lets a host whose answer changes
between the two lookups split them: the first lookup looks private, which
relaxes the private-address floor, and the second looks public, which clears
`force_local_sidecar` — so the call executes against the CLOUD provider with the
private URL in it. That is the leak this change exists to prevent.

`_code_url_verdict(code)` now makes ONE pass over the URL literals and returns
`_CodeUrlVerdict(blocked, local_sidecar)`; `browser_exec` consumes the returned
verdict instead of re-deriving it, so both decisions rest on the same captured
evidence. This is the discipline `browser_navigate` already applies — it derives
its safety relaxation from the session key (`_is_local_sidecar_key`) rather than
re-running the predicate. `_blocked_url_in_code` is gone rather than kept as a
wrapper: a caller that recomputed the route after it would reintroduce exactly
this bug.

Tests: `test_rebinding_host_cannot_split_verdict_from_route` stubs
`socket.getaddrinfo` to answer private then public, counts predicate evaluations
and asserts both that they are one per URL and that the call never reaches the
cloud provider — RED against the previous commit ("evaluated 2 times"), green
after. `test_public_then_private_answer_is_refused_not_leaked` pins the other
direction, where the safety check sees the private answer and refuses (the
over-block is the safe side). No live DNS in either. 10 existing cases moved to
the renamed helper. Full browser suite: 68 files, 830 passed, 0 failed.
2026-10-10 23:42:04 -07:00
shadus
7dc0df1be8 fix: browser_exec honours hybrid routing for private/LAN URLs
With a cloud browser provider configured (the picker's `nous`, or any
browserbase/browser-use key), `browser.auto_local_for_private_urls` is on by
default and `browser_navigate` routes private/loopback/LAN URLs to a LOCAL
Chromium sidecar so the cloud provider never sees them. `browser_exec`'s
pre-flight URL scan did not consult that decision, so *every* LAN URL was
refused with "Blocked: URL targets a private or internal address" — and in
Browser Use mode (`browser.backend` unset, the default) `browser_exec` is the
only browser tool, so the documented hybrid workflow was unreachable.

Relaxing the pre-flight alone would have been wrong: the exec call resolves ONE
CDP endpoint for the whole code blob (`_resolve_backend_cdp`), so an allowed
private URL would then have been executed against the configured cloud browser
— exactly the leak the guard exists to prevent. Verdict and route now move
together:

* Extract `_hybrid_routes_locally(url)` in browser_tool as the single source of
  truth for the predicate; `_navigation_session_key` reads it too, so the
  navigate and exec paths cannot drift apart.
* `evaluate_url_safety(url, *, auto_local=False)` forwards the relaxation to
  `_url_policy_error` (the cloud-metadata floor stays unconditional).
* `_blocked_url_in_code` passes `auto_local=_hybrid_routes_locally(url)`, the
  same verdict the navigation path reaches for itself.
* `browser_exec` computes `local_sidecar` from the URLs in the code and threads
  it through `_route_backend` → `_resolve_backend_cdp(force_local_sidecar=...)`,
  which declines the cloud provider so the local engine serves the call. One
  endpoint per call means a mixed call runs wholly local — the safe direction.

Scope is deliberately narrow: no cloud provider, a CDP override, or Camofox all
leave `_hybrid_routes_locally` False, so those paths are byte-for-byte unchanged.
The two operator overrides (BU_CDP_*/`browser.cdp_url`) still outrank it.

Tests: 9 new cases in tests/tools/test_browser_use_cli.py covering the URL
verdict (LAN, loopback, public, IMDS still blocked, hybrid off, no provider) and
the endpoint the verdict depends on (LAN-only and mixed calls must never reach
the provider; public-only still must). Full browser suite: 663 passed.
2026-10-10 23:42:04 -07:00
Teknium
d4d2ab7b5d plugin-catalog: six official plugins install from hermes-official-plugins
hermes-memory-wiki, hermes-telegram-business, blender, snyk and touchdesigner
move to subdirectories of NousResearch/hermes-official-plugins (history kept)
and get catalog card images; accent-picker, the Desktop accent colour picker
that left the app bundle, joins the catalog from the same repo.

The re-pins carry no new tools, hooks or dependencies. telegram-business and
blender also pick up their unpinned main commits (an owner-check fail-open fix
with a regression test; a docs correction).
2026-10-10 22:01:13 -07:00
Teknium
5473cecb07 docs: Accent Picker links point at hermes-official-plugins (#136446)
The Desktop accent picker moved from NousResearch/hermes-desktop-accent-picker
into the official plugins monorepo and is now in the plugin catalog, so the
user guide says to install it from the catalog rather than via Install from Git.
2026-10-10 21:46:11 -07:00
teknium1
cca83848ec chore(memory): holographic leaves core for its community catalog plugin (re-land #136424)
Holographic has a standalone third-party maintainer (itpartypattaya) and a
reviewed catalog entry, so it leaves core now rather than on Oct 15. Restores
9e827685f2 exactly: reverts the revert (435b7bd3a7) and its health waivers
(b9ecc727e9). byterover and retaindb have no maintainer and stay bundled until
the announced date.
2026-10-10 21:18:25 -07:00
teknium1
b9ecc727e9 chore(health): waive pre-existing broad excepts in the restored holographic provider
The revert re-adds plugins/memory/holographic verbatim; the code-health ratchet
measures it against the post-removal tree, so its long-standing broad excepts
read as new. Comment-only waivers, no code change; the provider leaves core on
Oct 15.
2026-10-10 20:58:02 -07:00
teknium1
435b7bd3a7 Revert "chore(memory): holographic leaves core for its community catalog plugin"
This reverts commit 9e827685f2 (#136424). It landed before the announced
October 15, 2026 date for holographic leaving core; it re-lands on or after
that date. Restores plugins/memory/holographic and its tests/docs.
2026-10-10 20:58:02 -07:00
Teknium
4b351c751a fix(update): the main channel follows the git branch without reading an R2 record
Every `hermes update` on the main channel first fetched
hermes-assets.nousresearch.com/releases/channels/main.json. That record has
never been published; a 404 fell through to the git branch. Some regional
WAFs answer that same request 403 instead, which raised and stopped the
update outright (#128295; patched around in #128323 and #128305).

main IS the git branch, and the only thing its record could ever carry is a
retirement notice nobody has published. resolve_source_target now returns
the main branch target directly, the way stable reads GitHub releases
directly. Preview channels (e.g. pm-preview) keep their R2 records.
2026-10-10 20:53:13 -07:00
Teknium
87da183a8a refactor(evals): split reasoning-replay summarize into per-cell helpers
Keeps the matrix byte-identical while meeting the code-health CC/nesting
targets, and narrows the network catch to the errors urlopen can raise.
2026-10-10 20:49:30 -07:00
Teknium
7a70cbd0db fix(evals): reasoning-replay treats Anthropic refusals like xAI 403s
Anthropic answers a probe that asks about "earlier reasoning" with a 200
content_filter refusal and no usage block, which left cross-turn cells
without a baseline. Both refusal shapes now rerun the cell with neutral
wording, and opaque cells pick the newest turn-1 capture per position so a
partial rerun does not mix captures.
2026-10-10 20:49:30 -07:00
Teknium
2d1077b001 fix(evals): reasoning-replay harness measures opaque carriers reliably
Newer OpenAI/Gemini models emit no reasoning on a trivial turn 1, so step 2
had nothing to replay; turn 1 now poses a small arithmetic task and escalates
effort until a reasoning carrier and tool call come back. Opaque cells are
compared only within one turn-1 capture (run_id), xAI's safety-layer 403 on
"earlier reasoning" falls back to neutral probe wording per cell, xAI
cost_in_usd_ticks is read as billed cost, and keyless local servers cost $0.
2026-10-10 20:49:30 -07:00
Teknium
cb4342c238 feat(evals): reasoning-replay consumption harness
A 200 only proves a provider accepted a replayed reasoning field; the gateway or
chat template may still drop it. This stdlib-only harness measures whether each
carrier (reasoning_content / reasoning / reasoning_details / Responses reasoning
items / Gemini thought parts and signatures) reaches the model's prompt: the
prompt-token delta against a byte-identical baseline, plus canary recall, for
in-loop and cross-turn positions, and real turn-1 opaque carriers replayed
verbatim vs stripped. Portal calls are keyed by lane (gen- = OpenRouter) and
can be resampled until a direct-lane result exists. Results stay out of tree.
2026-10-10 20:49:30 -07:00
Teknium
42f210de11 fix: Alibaba's content guardrail ends the turn instead of a 4-minute retry loop
Qwen via OpenRouter / the Nous Portal relays Alibaba Model Studio's output
guardrail as an in-band 502 marked provider_unavailable ("Output data may
contain inappropriate content."), and its input guardrail as DashScope's 400
data_inspection_failed. Neither phrase was a content-policy pattern, so the
502 rode the server-error retry ladder and Hermes re-sent the same content
into the same refusal. Both now classify as content_policy_blocked.
2026-10-10 20:44:53 -07:00
Teknium
9e827685f2 chore(memory): holographic leaves core for its community catalog plugin
Holographic now ships from itpartypattaya/hermes-plugin-holographic through the
plugin catalog (plugin-catalog/holographic.yaml, v0.6.1 @ 46b6f908, a fork of
Nous's archived handoff copy). It keeps the provider name, memory_store.db and
plugins.hermes-memory-store, and memory_provider_migration already lists
holographic in _LEFT_CORE. Deleting the in-tree copy and its tests lets
`hermes update` and agent start install the catalog plugin for every home that
has memory.provider: holographic.

Host code that still releases memory-store handles on profile unserve/delete
resolves the store through import_provider_module, the same way profiles.py
already does, and no longer imports plugins.memory.holographic. Tests that used
holographic as "a bundled provider" now use byterover. The journal-mode
contract drops the holographic row because the plugin owns that store now.

People who installed the archived Nous repo by URL have no catalog record, so
`hermes plugins update` kept pulling the archive. read_catalog_sidecar now
adopts an install from that repo as the holographic catalog entry, and update
re-pins it to the maintained repo at the reviewed sha. That catalog re-pin
also needed the reviewed-pin scan trust that catalog installs already get. Without
it, update_plugin scanned the reviewed tree as an unreviewed community source
and blocked on caution findings, and that blocked every catalog re-pin of
holographic (the reviewed 46b6f908 tree has 14 caution findings).
2026-10-10 20:31:02 -07:00