Commit Graph

22311 Commits

Author SHA1 Message Date
ethernet 1fdc4cee95 chore(npmrc): drop the age-gate excludes that have outlived their reason
Each of these carried its own removal instruction ("remove when > 2wks
old"), and every locked version is now older than min-release-age, so
resolution succeeds without them. Produced by the lint's own fixer
rather than by hand.

website/.npmrc is swept for the first time here; two of its entries
(undici, @nous-research/image-size) matched no locked package at all,
which no clock expires.

`npm ci` in both projects returns exit 0 with a byte-identical
lockfile. That covers the vite / rolldown / @oxc-project/types group,
whose comment warned that dropping it too early fails the whole install
with ETARGET.
2026-08-14 09:05:00 -04:00
ethernet 3f867b2ca9 feat(lints): require an age-gated .npmrc beside every package-lock.json
npm reads the project's own .npmrc, $HOME, and the global config. It
does not walk up parent directories (npm/npm#11437 is still open), so
the root .npmrc gates the root install and nothing else. Two nested
projects were resolving with no supply-chain quarantine at all:
scripts/whatsapp-bridge and plugins/platforms/photon/sidecar. Both now
carry their own gate, and `npm ci` in each returns its lockfile
byte-identical.

A project is a directory holding both package-lock.json and
package.json -- the pair npm resolves against. A lockfile with no
manifest beside it is a vendored artifact; nix/ keeps one for a
hash-pinned fetch that never consults npm config.

The required floor is read from the root .npmrc instead of hardcoded,
so the standard is raised in one place. A nested project may be
stricter, never weaker.
2026-08-14 09:04:06 -04:00
ethernet e5bd77a71c chore(ci): align the autofix patch artifact on upload-artifact v7
The autofix workflow was the only caller left on the v4 SHA; the other
20 upload sites across .github/workflows use v7. The paired download
stays on v4, matching docker.yml and osv-scanner.yml, which already run
that combination.
2026-08-14 09:00:23 -04:00
ethernet 05650c9944 feat(lints): hold package.json overrides to the exact-version rule
An override forces a version on every transitive consumer, so a ranged
override re-floats packages the manifest never names. That is a wider
hole than a ranged direct dependency, and the lint did not look at it.

Overrides nest, so the walk is recursive. `npm:<pkg>@<version>` aliases
are now recognised too, and held to the same rule on their version
tail; before this they were rejected outright even when exact.

Three ranges are pinned to close the lint on the live tree. yauzl and
the bridge's protobufjs take their locked versions; the root protobufjs
override is dormant (no protobufjs resolves anywhere in the root tree)
and takes its own declared floor. Re-resolving both lockfiles with
`npm install --package-lock-only` returns them byte-identical.
2026-08-14 08:59:56 -04:00
ethernet be3fa8b6a5 fix(lints): anchor uv exclude-newer-package key edits to whole keys
An unanchored key regex matched inside a longer key with the same
suffix: removing `h2` from `{ python-h2 = false, h2 = "..." }`
produced `{ python- }`. The structural verifier caught the unparseable
result and refused the write, so no file was corrupted -- but the fixer
raised, and `run.py --fix` is deliberately not continue-on-error, so
the autofix job would go red on main and stay red.

A word boundary is insufficient: \b does not fire between `-` and `h`.
Use a lookbehind that rejects word characters, dots, and hyphens.

Both edit paths and both TOML forms are covered; reverting the anchor
fails the new tests.
2026-08-14 08:57:23 -04:00
ethernet 5b15b192d6 lints: adopt three AGENTS.md policies as enforced checks
- no-hardcoded-hermes-home: the Known Pitfalls rule (source of 5 bugs
  in PR #3575). flags Path.home()/'.hermes' + expanduser variants in
  production code; a get_hermes_home()/HERMES_HOME mention within 8
  lines counts as a guard hint (fallbacks and deliberate HOME anchors
  like the desktop-ssh token root pass untouched), '# hermes-home: ok'
  is the explicit override. found and fixed one real bug: the telegram
  adapter's gmail-triage script path ignored the active profile.
- no-ansi-erase-eol: \033[K leaks as literal ?[K under prompt_toolkit
  patch_stdout; comments documenting the pitfall are exempt.
- skill-frontmatter-standards: the HARDLINE authoring rules that were
  review-only — description <=60 chars/ends with period/no marketing
  words, name present, platforms: required when skill scripts import
  POSIX-only primitives. live tree already clean on all three.
2026-08-14 08:33:56 -04:00
ethernet e63dbadcae autofix: reconcile patch guard with dep-version-gate excludes
the rebase onto main merged 12299ca54 (keep review-gated files out of
the autofix patch) textually but stitched the produce-patch step badly:
one stale js-fix.patch filename and the old grep guard in place of the
derived one. re-merged properly — the EXCLUDES pathspec strips
review-gated files (package manifests, eslint configs, .github/) from
the patch entirely so the bot PR never gates itself, then the derived
guard (js/ts/json + .py + lint fix_touches globs, lockfiles carved out)
validates what remains.
2026-08-14 08:33:55 -04:00
ethernet 6514dea0a3 lints: migrate tests + infographic workflow, add dep-specifier lints
five new registered lints, all blocking, none autofixable:

- engines-satisfiable: from tests/test_engines_satisfiable.py, which
  lived in the python pytest lane — a package.json-only PR never ran
  it, the exact classifier gap that let the npm-12 floor outage ship.
- no-shadowed-test-definitions: from the pytest guard; same AST scan,
  per-file annotations instead of one aggregated assert.
- no-committed-infographics: from infographic-check.yml; deletes the
  workflow and its ci.yml wiring (one fewer runner per PR).
- workflow-sha-pins: the AGENTS.md action-pinning policy (commit SHA,
  never tag/branch), previously enforced only by review. repo is
  already clean — every uses: ref is pinned.
- pyproject-dep-bounds: the AGENTS.md upper-bound policy (>=floor,<
  ceiling; git deps SHA-pinned) plus the pin-consistency invariant
  pulled out of tests/test_packaging_metadata.py (the lazy_deps-coupled
  tests stay put). self-referential hermes-agent[extra] includes exempt.
- packagejson-exact-versions: every tracked package.json dependency
  must be an exact version or file:/link:/workspace: ref — ranges float
  onto minutes-old releases via unreviewable lockfile churn. fixed the
  one violator (whatsapp-bridge: express/pino/qrcode-terminal caret
  ranges pinned to their locked versions, lockfile mirror regenerated).

autofix bot also gains ruff check --fix (continue-on-error like npm
run fix; ruff-blocking stays the PR gate), with .py added to the
patch-guard allowance.
2026-08-14 08:33:55 -04:00
ethernet 4874e15dc6 lints: support uv's exclude-newer-package section form too
reading was always plain tomllib — the inline-only restriction was a
write-side artifact. the [tool.uv.exclude-newer-package] section form
is the same one-key-per-line shape the fixer already edits in uv.lock,
so both files now share one section editor; plan() reads entries via
tomllib regardless of form. the loud 'unsupported form' failure is
gone — both TOML shapes are managed.
2026-08-14 08:33:55 -04:00
ethernet 9377af7c33 lints: add uv-stale-exclude (autofix, pyproject + uv.lock)
the python twin of npmrc-stale-exclude, for [tool.uv]
exclude-newer-package. two findings, both autofixable:

- 'pkg = false' disables the age gate for that package forever — an
  unbounded hole. replaced with the uv-documented explicit timestamp:
  the locked version's newest upload-time (from uv.lock) + 1 day, so
  the exception admits exactly the release it was added for.
- entries whose locked versions have all outgrown the exclude-newer
  span (+1d grace) are removed; so are entries matching no locked
  package.

no network: publish dates come from uv.lock's upload-time fields;
versions without one fail open as young. names are PEP 503-normalized
to bridge pyproject's huggingface_hub and the lock's huggingface-hub.

the fixer edits BOTH files, mirroring the change into uv.lock's
[options.exclude-newer-package] so uv lock --check stays green
without re-resolution (admitted locked versions are unchanged by
construction; verified against the real lock). before each write a
tomllib-based verifier strips the exclude-newer-package data from old
and new and refuses unless everything else is structurally identical
— a lock diff that touches a dependency url/hash aborts the fix.

live run on the repo: vercel + huggingface_hub entries removed (both
locked >14d), nemo-relay + h2 converted from false to timestamps.
2026-08-14 08:33:55 -04:00
ethernet a06b587af5 autofix: carve lockfiles out of the .json allowance; verify npmrc writes
guard: package-lock.json slipped through the .json extension allowance
(a hole inherited from the old js-autofix guard). rather than a global
deny list, the allowance itself now excludes lockfile basenames — they
fall through to the default rejection like any other unallowed path. a
bot-authored, auto-merged lockfile rewrite is exactly the shape of a
supply-chain attack.

fixer: before writing, verify the new .npmrc is the original minus
deletions of exclude lines, comments, and blanks only — every surviving
line must appear in the original in order, and every dropped line must
be an exclude entry, comment, or blank. any other delta (touched
directives, edits, insertions) is a fixer bug and aborts the write.
2026-08-14 08:33:55 -04:00
ethernet b7511316a4 lints: add npmrc-stale-exclude (autofix, network)
a min-release-age-exclude[] exists to let resolution pick a release
younger than min-release-age. once every locked version of the
package is itself older than the gate (+1d boundary grace), the
exclude is dead weight and a standing hole in the age gate — this
lint automates the 'remove when > 2wks old' instruction those
entries' comments already carry.

matches exclude patterns (exact, @scope/*) against the sibling
lockfile's locked versions and checks registry publish dates. fails
open per pattern on any fetch error or unknown date — never removes
an exclude on partial data. network lint, so advisory on PRs by
policy; enforcement is the autofix pass on main. the fixer drops a
group's comment block only when no exclude in the group survives.

'# lint: keep' marks intentional standing excludes; applied to
@assistant-ui/* and the ink blocks in .npmrc. also removes the five
already-stale entries the first live run found (react-router,
eslint, @eslint/*, tar, concurrently — each locked >14d).

discovery fix: register lint modules in sys.modules before exec so
@dataclass can resolve annotations.
2026-08-14 08:33:55 -04:00
ethernet cd9e0e4355 ci: generalize js-autofix into the project autofix bot
the on-merge fixer now runs npm run fix plus the registered lint
fixers (scripts/lints/run.py --fix). two-job security split unchanged:
unprivileged patch generation, privileged patch apply, bot branch
renamed bot/js-autofix -> bot/autofix.

the patch guard's allow-list is now derived — JS/TS/JSON sources for
npm run fix plus each lint's declared fix_touches globs via
--print-fix-globs — instead of a hardcoded extension regex. per-lint
attribution (a fixer writing a file only another lint's globs allow)
is enforced inside run.py --fix, which fails the job before a patch
is produced; the workflow guard is the coarse second layer.

paths filter dropped: lint inputs span the whole tree and the run
exits with an empty patch when there's nothing to fix. still no
schedule trigger — merges to main are the clock.
2026-08-14 08:33:55 -04:00
ethernet 179194a2b2 lints: bound each fixer by its own globs, not the union
run_fixes now snapshots the working tree around every fixer and
validates each change against that lint's OWN fix_touches — a fixer
that writes a file only another lint's globs allow is an error, and
under-reported paths are attributed to the fixer that made them.
snapshot uses git status --porcelain so created files count too.
2026-08-14 08:33:54 -04:00
ethernet 3a7ff7c00d ci: run project lints as one always-on job
replaces the windows-footguns job in lint.yml — that checker now runs
through the lint registry along with subprocess-stdin (previously
wired to no workflow at all).

standalone workflow, not a job in lint.yml: ci.yml only calls lint.yml
when the classifier says python changed, and lint inputs span the
whole tree (.npmrc, package.json, workflows, ...) — a npmrc-only PR
would have skipped the lints entirely. always-on is the point: it
closes the classifier-gap class where a lint's inputs don't match the
lane that runs it. cheap because the runner and all lints are
stdlib-only (no uv/deps setup).

findings land in the PR review comment via review-status-project-lints;
blocking findings gate merge via all-checks-pass.
2026-08-14 08:33:54 -04:00
ethernet c864fc1a0f add project-wide lint registry + runner
lints declare severity (blocking/advisory) and fixability (autofix)
explicitly. runner gates PRs only on blocking unfixable findings;
fixable ones are advisory because the autofix bot resolves them on
merge. first two registered lints wrap the existing checkers:
windows-footguns (was its own workflow job) and subprocess-stdin
(was wired to no workflow at all).
2026-08-14 08:33:54 -04:00
kshitij 1b1975781f test: use tmp_path fixture for import test HERMES_HOME
The subprocess import test was creating .tmp-hermes-exec-ask-import/
in the repo root without cleanup. Switch to pytest's tmp_path fixture
so the temp directory is auto-cleaned and never appears as untracked.
2026-08-14 17:48:28 +05:30
xxxigm d6b4083f41 test(approval): cover CLI EXEC_ASK leak and fix slash-worker Path mock
Regression tests for silent pending_approval when ask-mode leaks into
interactive CLI, plus a Path-typed hermes_constants mock so the
slash-worker profile_home test survives per-file isolation.
2026-08-14 17:48:28 +05:30
xxxigm e37a0321eb fix(approval): show CLI Dangerous Command prompt when ask-mode has no notifier
HERMES_EXEC_ASK (and gateway platform markers without a notify callback)
were short-circuiting interactive CLI into silent pending_approval, so
the Approve/Deny panel never appeared. Prefer the registered CLI callback
when present, and set HERMES_EXEC_ASK only in start_gateway so importing
gateway.run from CLI tools cannot poison the process.
2026-08-14 17:48:28 +05:30
kshitij 1f8fdc7bd8 chore: map contributor email for @infocentr 2026-08-14 16:53:40 +05:30
Laura f84f1dd132 fix(tests): return Path from get_hermes_home mock in slash_worker profile test
test_slash_worker_accepts_profile_home mocks hermes_constants with
get_hermes_home=MagicMock(return_value="/tmp/hermes_test"), a str. In
production get_hermes_home() returns a Path, and hermes_state.py's
module-level DEFAULT_DB_PATH = get_hermes_home() / "state.db" does path
division. Under the str mock that becomes str / str, so importing
tui_gateway.server inside the patch raises TypeError and the test fails on
every main run (slice 4). Wrap the mock return in Path(...) so it matches the
real return type. Test-only; no production code change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 16:53:40 +05:30
webtecnica 56a41715dc fix: persist provider on model switch and add billing_provider fallback
Salvage of #79604 (webtecnica) + #85721 (pierrenode), combined and
rebased onto current main with simplify-code findings folded in.

#79604: update_session_model() wrote the model name to sessions.model
but never persisted the provider into model_config. On resume, the
runtime recombined the persisted model with the config.yaml primary
provider (which may not serve that model), producing auth errors.
Fix: add optional provider parameter to update_session_model, merged
into model_config via the shared _merge_model_config_json helper (not
hand-rolled SQL). Wire both gateway /model call sites to pass
result.target_provider.

#85721: session_gateway_runtime() had no billing_provider fallback.
A CLI session that never ran /model has no gateway_runtime or
top-level provider in model_config — billing_provider (written on
every session's first accounted API call) is the only durable record.
Fix: add billing_provider as the last-resort fallback in
session_gateway_runtime(), filtering bare billing buckets (auto/custom)
that are not routable identities.

Simplify-code findings addressed:
- Use _merge_model_config_json instead of 40 lines of branched SQL
- Share _BARE_BILLING_PROVIDERS from hermes_state.py (was duplicated
  as a set in tui_gateway/server.py)
- Merge None-filtering from #85920 with the billing_provider fallback
  into one coherent return path

Co-authored-by: pierrenode <298902573+pierrenode@users.noreply.github.com>
2026-08-14 14:39:08 +05:30
Teknium 4bbc6f258a
feat(gateway): share_auth on profiles.create + MCP servers in describe/configure (#85963)
Three widenings for capabilities UIs (Bot Mode's bot builder):

1. profiles.create share_auth (default false): skip the auth.json
   COPY so the new profile reads OAuth/token state through the
   existing global-root fallback and refreshes write through to it.
   A copy forks token state — the first refresh on either side
   invalidates the other for single-use refresh tokens; sharing keeps
   ONE live token pool for the main profile and every bot. Static
   .env keys still copy (no refresh semantics). Receipt:
   mirrored.auth = 'shared'.

2. profiles.describe reports mcp_servers
   [{name, enabled, transport}] from the profile's config.

3. profiles.configure accepts enabled_mcp_servers (replace
   semantics): toggles via the standard disabled flag; enabling a
   server the profile lacks copies its definition from the launch
   profile's catalog (names never invented). Launch catalog read
   BEFORE the home override flips config resolution.

E2E: describe keys include mcp_servers; create with share_auth ->
mirrored.auth='shared' + no auth.json in the profile dir; configure
applied.mcp_servers=true.
2026-08-14 02:08:16 -07:00
Teknium 16b54e2a0f
fix(tests): resolve cost-guard fixture collision between pricing distrust and gpt-5.5-pro confusion nudge (#85970)
54cc39aa15 (distrust foreign pricing for custom providers) tested with
openai/gpt-5.5-pro fixtures; 83d373aae6 (salvaged #70324) made that exact
id warn unconditionally as a known-confusion model. Each was green alone;
together the distrust tests fail on every main run (slice 6).

Use a neutral fixture id for the distrust tests and add a regression test
pinning the composed behavior: the id-keyed nudge survives custom-provider
pricing distrust.
2026-08-14 02:03:38 -07:00
Teknium 3c5fd918e3
feat(sdk): warmProfile — background socket pre-dial for roster UIs (#85954)
Clicking an agent in a multi-profile roster pays the entire backend
spawn + WebSocket dial cost on first open — several seconds of
'loading' (Bot Mode report). Expose the existing pool-only primitive
(openGatewayForProfile: opens/pools the socket WITHOUT activating it,
already no-ops for the primary and shared-remote routes) as
host.warmProfile(name) so rosters can pre-dial after mount and the
first click lands on a live socket. Fire-and-forget by design;
failures stay silent — the real open path re-runs its own ensure.
2026-08-14 01:33:29 -07:00
Teknium 6f53373eb2 fix(cli): route startup guard through the selection-guard registry; cover the light oneshot fast-path
Follow-ups on top of the salvaged #70324:
- _confirm_startup_expensive_model_override evaluates the unified
  registry (combined_selection_warning) so id-keyed guards like the
  data-training-tier warning fire at startup too, not just the cost guard.
- The Termux-adjacent light oneshot fast-path (added after the PR
  branched) ran _run_and_exit_oneshot without the guard — same bug
  class, third sibling site now covered.
2026-08-14 01:31:31 -07:00
lkz-de 83d373aae6 fix(cli): guard expensive startup model overrides
Run the expensive-model warning for explicit startup `-m` / `--provider`
overrides before the chat loop starts, and fail closed for non-interactive
invocations that select an expensive or known-confusing model.

Also classify Nous paid-model 404s that say credits are required as billing
exhaustion so they fail fast with billing guidance.

Tested:
- scripts/run_tests.sh tests/hermes_cli/test_cli_startup_model_cost_guard.py tests/hermes_cli/test_model_cost_guard.py tests/agent/test_error_classifier.py -- --tb=short -q
2026-08-14 01:31:31 -07:00
Teknium 1e18f68148 chore: map contributor email for @dpersek 2026-08-14 01:31:26 -07:00
Teknium f2678b8706 test: registry cost-guard passthrough uses a models.dev-trusted provider
The custom-provider pricing-trust fix makes provider="test" (not a
models.dev provider) correctly silent — use anthropic in the fixture.
2026-08-14 01:31:26 -07:00
Dustin Persek 54cc39aa15 fix(models): don't trust foreign catalog pricing for custom/unknown providers
Custom providers (custom:xxx) serve their own pricing; models.dev stores
OpenRouter prices for the same model ids. The cost guard fired on that
foreign pricing and blocked composer/CLI model switches on custom
providers with a wildly wrong warning (#54348).

expensive_model_warning now only trusts model_info/models.dev pricing
when the provider maps to a models.dev provider and the info's
provider_id matches, and only consults the pricing-entry lookup when
the billing route is known. Salvaged from #54422; the PR's desktop-hook
half predates the use-model-controls rewrite and is superseded by the
hook's existing rollback handling.
2026-08-14 01:31:26 -07:00
Turgut Kural 9a8af40192 fix(cli): run /model expensive-confirm off the main thread (#79401)
The command path (/model <name> --provider <p>) called
_confirm_expensive_model_switch() inline. That modal blocks its calling
thread on a response queue (see _prompt_text_input_modal); on the
prompt_toolkit main thread the TUI event loop freezes, the modal never
renders, and the switch silently cancels after the 120s timeout — the
user sees a frozen terminal and 'Model switch cancelled.' without ever
seeing the warning. The picker path already dispatched confirm+apply on
a worker thread; the command path now mirrors that contract.

Extract the inline confirm+apply block into
_confirm_and_apply_cli_model_switch() (preserving --once restore and
persist semantics) and dispatch it on a daemon thread when a TUI app is
present, keeping the synchronous path for non-interactive/test use.

Tests: new test_model_switch_confirm_thread.py pins (a) confirm runs off
the main thread when _app is present, and (b) the no-app path stays
synchronous. Existing _StubCLI helpers forward to the extracted method.
2026-08-14 01:31:21 -07:00
dsad 858a6008be fix(gateway): keep process notification routing off-loop 2026-08-14 01:21:52 -07:00
Teknium 7619564fbd fix(gateway): gate background-process completions on spawning-session boundary
Plain type=completion events built in _run_process_watcher carried only
session_key (chat/thread routing) with no spawning-session stamp, so after
/new (or a session switch) a completion notification from the OLD session
was injected into the chat's NEW session. Main already solved this exact
class for async delegations via the _classify_completion_target pre-flight
(_USER_BOUNDARY_END_REASONS drop on user-closed sessions, deliver on
idle-ends, follow the compression-tip chain), but the gate only ran for
type=async_delegation events.

Kernel salvage of #16455:

- Stamp the spawning conversation's session-db id (HERMES_SESSION_ID via
  session-scoped env) on the ProcessSession and the pending_watchers entry
  at spawn time in tools/terminal_tool.py; persist it through the process
  registry checkpoint/restore so recovered watchers keep the stamp.
- Thread the stamp into the completion_evt built by _run_process_watcher
  (watcher entry first, ProcessSession fallback for recovered watchers).
- In _deliver_completion_notification, run the SAME pre-flight classifier
  for stamped type=completion events: terminal -> drop with a log (output
  stays available via process(action='log')), retry -> False so the
  watcher re-polls, deliver -> proceed. The policy has exactly one owner
  (_classify_completion_target); nothing is forked. Unstamped legacy
  events keep today's deliver-always behavior, and the async-delegation
  path is untouched.

Based on the session-boundary approach from #16455 by @Tosko4 (original PR
was over-scoped across adapters/slash-commands/cron; this lands the kernel
only).

Tests: completion from a /new-closed session is dropped; completion after
an idle-end still delivers; unstamped legacy event delivers; retry verdict
returns retryable False without adapter injection; async_delegation gate
unchanged; stamp survives checkpoint recovery.
2026-08-14 01:09:42 -07:00
Brooklyn Nicholson e10e344204 fix(desktop): make the HUD exit button findable, and only when wanted
The bare dimmed glyph had nothing to read against. Over a light document in
a light theme it is a pale mark on white, and every rest opacity tried
(0.35, then 0.45, then 0.75 behind a text halo) came back reported as the
button being gone.

Give the control its own substrate, which is what every shipped overlay
does: Apple's HIG puts controls on a material rather than directly on
content, Firefox picture-in-picture draws close/unpip as opaque chips, and
Discord's overlay adds a contrast layer over the game. Deriving contrast
from the backdrop is not available to us either way -- mix-blend-difference
composites against the page, and behind a transparent Electron window that
is nothing.

The chip now wears the composer bar's own tokens (fill, hairline, radius,
bottom shadow), so it inverts with the theme and with the OS appearance
under mode 'system'. It rests hidden and fades in while the bar, the band,
or the chip itself is hovered, with a hold on the way out so it survives the
reach across the gap -- reaching for the HUD is the motion that means "I
want the app". Hover rather than focus: the caret gate behind #81893 broke
the escape hatch exactly when it was needed.
2026-08-14 03:09:21 -05:00
Teknium b9e7bead13 fix(gateway): coalesce same-tick async-delegation completions into one turn (#70300)
The async-delegation watcher drained the completion queue as a batch but
then delivered each event as its own synthetic turn, flooding the session
when a fan-out of background subagents finished together. Builds on the
per-process completion batching salvaged from PR #71898 (thanks
@yuzilongleif-collab) which coalesces concurrent _run_process_watcher
completions behind a short per-route fan-in window.

This commit adds the async-delegation half: group the drained batch by
full routing key (session_key + parent_session_id + platform/chat/thread/
user) and inject ONE consolidated turn per group. Durable-ack handling
stays honest: sibling rows are claimed up front via claim_event_delivery;
rows another consumer owns are excluded from the consolidated text (no
double-delivery); sibling claims are acknowledged only after adapter
acceptance and released (still pending) on failure. Events for different
sessions never coalesce, and a single-event group rides the existing
per-event path unchanged (latency and text identical).

Tests: 3 same-tick events -> exactly one adapter.handle_message carrying
all 3 results with all 3 durable rows delivered; 2 sessions -> 2 turns;
single-event path unchanged; failed batch releases claims and retries;
foreign-claimed sibling excluded and left pending.
2026-08-14 01:08:25 -07:00
yuzilongleif-collab a96cd10349 fix(gateway): force-redact coalesced completion output 2026-08-14 01:08:25 -07:00
yuzilongleif-collab 7536655b8f fix(gateway): own completion batch task lifecycle 2026-08-14 01:08:25 -07:00
yuzilongleif-collab cf09a30a9a fix(gateway): coalesce concurrent process completions 2026-08-14 01:08:25 -07:00
Teknium 8dc9401d7e fix(gateway): persist internal synthetic turns typed as internal_notification (#82888)
Async-delegation batch completions and background watch notifications
re-enter the gateway as synthetic MessageEvent(internal=True) turns via
_inject_watch_notification, but were persisted as bare role='user' rows —
indistinguishable from real user input in transcripts and the desktop UI.

Thread the event's internal flag through to persistence: when
event.internal is set, the turn's persisted user row is stamped
display_kind='internal_notification' (the existing DB-only presentation
sidecar used by auto_continue / model_switch rows). Wired through
_run_agent → _run_agent_inner → TurnContext → run_conversation's
persist_user_display_kind, and onto the three gateway-side fallback user
rows (transient failure, no-new-messages, pre-run crash), whose
append_to_transcript writer now forwards display_kind/display_metadata
to SessionDB.append_message.

Invariants preserved: role stays 'user' (alternation untouched), no new
injections, no past-context mutation, and display_kind is already popped
from every provider-bound copy in conversation_loop, so replayed sessions
never leak the marker to the API.

Regression tests: internal turn marked, real user turn unmarked, fallback
rows marked/unmarked per event, and a DB round-trip proving replay keeps
role/content intact while the provider copy drops the marker.
2026-08-14 01:08:18 -07:00
Teknium 1292a31851 chore: map salvage contributor emails 2026-08-14 01:08:13 -07:00
handnewb 84b4fb9eca fix(gateway): force user-facing redaction on the direct completion and running-output sends
The terminal redactor is called without force=True on all three
user-facing sends in _run_process_watcher, so process output reaches the
platform raw when security.redact_secrets is disabled. The agent-notify
path was already covered; this covers the two direct adapter.send()
paths the sweeper identified.

Review: teknium1 (#73547)
2026-08-14 01:08:13 -07:00
handnewb c0d204810d fix(gateway): redact secrets in background process completion output
_output passes through redact_terminal_output (force=False) but
not _redact_gateway_user_facing_secrets (force=True). When
security.redact_secrets is disabled or the command is not an
env-dump, the output can leak credentials (Authorization: Bearer,
PGPASSWORD=, etc.) into the session transcript and chat platforms.

Add the same redaction gate applied to _command so both paths
are protected by force=True + _GATEWAY_SECRET_PATTERNS.
2026-08-14 01:08:13 -07:00
dsad 668396c2e1 test(security): add tests for notification-path redaction
Add TestNotificationRedaction class with two tests:

1. test_completion_notification_redacts_secret — verifies _move_to_finished
   redacts API keys in completion notifications before enqueueing
2. test_watch_match_notification_redacts_secret — verifies _check_watch_patterns
   redacts secrets in watch_match notifications before enqueueing

These tests cover the gap identified in #43025 where the explicit process
tool path (poll/log/wait) was redacted but the automatic notification
delivery path was not.
2026-08-14 01:08:13 -07:00
dsad 6c7cfd6621 fix(security): redact secrets in background process notifications
Apply _redact_process_result() to completion and watch_match
notifications before enqueuing them in the completion_queue.

Previously, the explicit process tool path (poll/log/wait) applied
redact_terminal_output() via _redact_process_result(), but the
automatic notification delivery path (notify_on_complete, watch_patterns)
only applied strip_ansi(). This meant API keys, tokens, and other
secrets from background process output were injected into the LLM
conversation unmasked.

The fix ensures both code paths apply the same redaction, matching
the foreground terminal tool behavior.
2026-08-14 01:08:13 -07:00
spfcraze d2d7766750 fix(tools,gateway): format watch_overflow events instead of dropping them
format_process_notification had no case for watch_overflow_tripped /
watch_overflow_released, so a watch-pattern notification flood surfaced
as '[IMPORTANT: Background process  exited (exit code ?)]' — a phantom
exit notification for a process that never existed — while the actual
'watch flood, N notifications suppressed' summary in the event's
message field was silently dropped. The gateway delivery path was
worse: _drain_gateway_watch_events retained only watch_match and
watch_disabled, discarding overflow events entirely before formatting.
Route both event types through the message field in the shared
formatter and the gateway formatter, and retain them in the gateway
drain.
2026-08-14 01:08:13 -07:00
Lidang-Jiang 9b554758bd fix(gateway): honor notification-off watch reinjection
Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
2026-08-14 01:08:13 -07:00
fangliquanflq 8cf9e8a61b fix(compression): ignore background process notifications 2026-08-14 01:08:13 -07:00
Teknium 9166530942 feat(models): unify selection-time guards into one registry across all surfaces
Adds hermes_cli/model_selection_guards.py: a single evaluation point that
runs every selection guard (cost + the new data-policy guard) and returns
the warnings that fired. All seven model-selection surfaces (CLI picker,
cli.py TUI modal, gateway typed /model, dashboard web_server, TUI gateway,
Telegram and Discord pickers) now call the registry instead of importing
model_cost_guard directly — so the data-training-tier warning from
PR #81416 fires everywhere at once, and future guards need zero surface
wiring.

Guard modules keep their public APIs; existing mock patch points
(hermes_cli.model_cost_guard.expensive_model_warning) remain valid.
2026-08-14 01:06:13 -07:00
Beto de Paola a06f1d7617 feat(models): warn on data-training tiers at model selection
muse-spark-1.2-contributor is heavily discounted BECAUSE Meta uses your
prompts and completions to train future models. Selecting it for the price
without realising the data trade-off is a footgun.

Add hermes_cli/model_data_policy_guard.py (mirrors model_cost_guard):
data_training_warning(model_id, provider, base_url) -> DataTrainingWarning|None,
driven by a vendor-agnostic rule table. The status is not machine-readable on
/v1/models or models.dev, so the v1 rule keys on the documented '-contributor'
model id (fires regardless of provider, so it also covers custom/gateway
routes). Message mirrors Meta's pricing-doc language and figures
(https://dev.meta.ai/docs/pricing-rate-limits/).

Wire it into the CLI model picker's confirm flow (auth.py) as a [y/N]
disclosure, chained after the expensive-model cost guard. Fires only on the
contributor tier; silent on muse-spark-1.1/1.2 and all other models.
2026-08-14 01:06:13 -07:00
kshitij 9cb456a9b9 fix: unify route dict or-None discipline in /model persist
The route dict in _persist_model_switch_to_session used  filtering
(omits falsy values) while the top-level keys used  (writes
explicit None to trigger deletion in _merge_model_config_json). This
asymmetry meant stale keys from a previous /model switch survived in
the nested gateway_runtime dict even after the fix in #85261 that
properly deleted them from the top-level keys.

Fix: build the route dict with  and derive the top-level keys
from **route so both shapes always use identical deletion semantics.
Also filter None values in session_gateway_runtime's reader since
gateway_runtime is replaced as a whole dict (not deep-merged), so None
values written by the persist path survive in the nested dict.

Found by /simplify-code 3-reviewer review on #85261 (all 3 reviewers
converged on the route dict asymmetry as the verdict-relevant finding).
2026-08-14 13:18:53 +05:30