Commit Graph

48185 Commits

Author SHA1 Message Date
teknium1
7e19f4138b fix(telemetry): record auxiliary model calls with their real outcome
Every failed auxiliary model_route row read error_class=unknown because the
aux logical scope only carried outcome/model/provider and the projection
hard-coded unknown for any non-success. Every exception escaping call_llm,
including Hermes' own aborts (Ctrl+C / /stop mid-compression, interrupted
aux streams, task cancellation), was recorded as failed.

- classify the error that ended an aux call (and the last one a successful
  call recovered from) with agent.error_classifier; HTTP-200 bodies that
  carry a provider error object are classified from that object
- Hermes aborts (non-Exception BaseExceptions, InterruptedError) close the
  call as cancelled/none, matching the contract
- MoA aggregator streams report the same way
- aux calls that finish under the turn's own live scopes (title generation
  runs beside the turn) were never counted: the scope could not be popped
  and the turn-close drain closed it as an orphan without a route. The
  drain now closes it with the stored real result.
2026-10-03 13:38:29 -07:00
teknium1
bf73ef9363 chore(plugin-catalog): delist herdr-auto-reconcile and localsend, whose repos no longer exist
Both upstream repositories return 404 (chris-yyau/hermes-herdr-auto-reconcile,
tylerbrevard/hermes-localsend; the owner accounts still exist), so the
pinned SHAs cannot be fetched and an install from the catalog fails.
2026-10-03 13:34:38 -07:00
teknium1
3421f29d9b fix(plugins): no core override check catches patch helpers that are methods
The parameter-patcher pass recorded a method's rebinding parameter at its
index counting self, so a call site like self._patches.bind(module, name, fn)
was checked at args[1] (the attribute name) instead of args[0] (the module).
A plugin that routed every core rebind through a class helper passed the
gate with zero findings.
2026-10-03 13:34:08 -07:00
teknium1
4b2c2ae999 fix(telemetry): one-shot CLI runs report entrypoint one_shot, not interactive
Fleet telemetry labelled every `hermes -z` / `chat -q` / `-Q` run as
entrypoint=interactive because the cli surface mapped straight to
interactive and no caller declared anything else, so 95% of CLI sessions
read as one-turn humans and scripts could not be told apart.

- New closed entrypoint `one_shot` (contract + v3 schema enum). The task
  scope start declares it for cli-surface runs carrying the
  HERMES_SINGLE_QUERY_SESSION marker the one-shot paths already set (the
  signal session source and cache_ttl: auto read). Children Hermes
  dispatchers spawn (kanban worker source=kanban, A2A forward source=a2a)
  read the existing `background`. Delegated children, other surfaces and
  the REPL are unchanged.
- Engagement keeps treating one_shot like interactive (its prior label).
- `hermes -z` exits via os._exit, skipping the atexit hook that closes the
  metrics session: its pre-exit cleanup now runs the runtime shutdown, so
  task_run.finished and session.count are actually written (before, only
  task_run.started survived and the store kept a hot journal).
- Smoke expectations move to one_shot (it drives `chat --query --quiet`).
- relay-shared-metrics.md documents the entrypoint vocabulary.
2026-10-03 13:25:25 -07:00
kshitijk4poor
8840751b8d fix(cron): reuse the gateway's hook registration helper inside the secret scope
The external worker now calls GatewayStartupMixin._register_config_hooks (the
helper gateway startup and each multiplexed profile already use) instead of a
copied load_config/register_from_config sequence, and calls it after
set_secret_scope rather than before. Registered outside the scope, a
hooks.outbound target with secret_env hits get_secret with no scope while
multiplexing, which raises UnscopedSecretError; the never-raise guard swallowed
it, so that profile's outbound webhooks were silently dropped again.

Both registries are idempotent per (home, event, ...) so a worker running
several payloads registers each hook once. The kept test gains a secret_env
target on a multiplexed payload to pin the ordering; the broken-config test is
dropped (register_from_config never raises on a malformed block, and the
helper's guard is already covered by the gateway path).

Refs #131764
2026-10-04 01:53:17 +05:30
liuhao1024
946eafdf2f fix(cron): register the owning profile's config hooks in the external worker
The restart-safe external worker (python -m cron.scheduler
--external-worker-file) called discover_plugins() under the payload's
home override but never agent.shell_hooks.register_from_config() nor
agent.outbound_webhooks.register_from_config(), so hooks: blocks in the
owning profile's config.yaml silently stopped firing for cron sessions
once gateway fires were handed to the worker instead of running
in-process. Plugin hooks kept working, which made the gap invisible.

Register both right after plugin discovery + secret hydration, under the
same home override, with the gateway's never-raise shape (#102504):
consent resolves non-interactively from the owning profile's allowlist /
HERMES_ACCEPT_HOOKS / hooks_auto_accept, and a broken hooks block logs a
warning instead of taking the cron execution down.

Fixes #131764

(cherry picked from commit d1971c698a4429868e9d43d058032930ec62d7b9)
2026-10-04 01:53:17 +05:30
kshitijk4poor
d6997d3ff2 refactor(cron): import Future at module level in cronjob_tools
concurrent.futures is stdlib with no cycle or startup-cost reason to defer it;
the module already imports its other stdlib deps at the top.
2026-10-04 01:52:54 +05:30
cdb-review
677e454c3d fix(cron): preserve direct-run liveness and registration ownership
Register direct workers with a live future and fence dispatcher release and future attachment with per-registration identity tokens. Preserve profile-scoped release keys.

Port the in-memory fix from 251b99332550be4dc5a7ba265109e367842e9081, without timestamp-based durable-state guards.

Fixes #132223. Refs #88688.

(cherry picked from commit dd834c874fef04a7d56658e0d05baa3bee5decba)
2026-10-04 01:52:54 +05:30
kshitijk4poor
5ab4659fbf chore: map cdb-lumen for salvage of #132236
Commit email chernodub98@gmail.com belongs to GitHub user cdb-lumen.
2026-10-04 01:52:54 +05:30
kshitijk4poor
343500b354 refactor(web): drop the gate's leftovers and keep the tier rules tested
Review and simplify pass on the guest fast-search restore.

- tools/tool_backend_helpers.py: _account_info was split out so two gates
  could share one account read. Only managed_nous_tools_enabled is left, so
  fold it back into its pre-salvage shape. Keep the branched force_fresh
  call, because tests stub the reader with zero-arg lambdas.
- plugins/web/perplexity/provider.py: the managed= parameter on
  _managed_gateway only fed the removed refusal helper. search() now calls
  resolve_free_search_gateway() directly once it has the route answer. The
  module docstring now names guests.
- hermes_cli/nous_account.py: the _error_info docstring described the
  removed gate. The tier carried on a failed lookup still picks the "needs
  a Nous account" copy over billing or re-login guidance.
- Tests: the case-insensitive tier match and the tier on error snapshots
  lost their only coverage when the gate tests went, so cover them at
  their consumer, format_nous_portal_entitlement_message. The fast-search
  premise now asserts that the guest case really reads as the anonymous
  tier. Remove the dead exp_offset parameter.
- Docs: the Perplexity section said there is no anonymous tier, which is
  true for direct keys only. It now names the managed guest route.
2026-10-04 00:10:54 +05:30
kshitijk4poor
96ac3d349f fix(web): restore free fast search for anonymous guest users
The free managed Perplexity fast-search route exists for anonymous guest
Portal users. The salvage of #130121 (cee3000c24, via #130660) added a
registered-account gate, fast_search_entitled(), that excluded the
anonymous tier. That defeated the original change: guests fell back to
the keyless ring and never sent a gateway request.

This removes the gate, so resolve_free_search_gateway() decides on
identity alone again, as rewbs wrote it. The guest-disabled, expiry
refresh and dead-guest rules still apply through the token reader. Every
other vendor, and the billed managed-Firecrawl fallback, keep the paid
entitlement gate.

Kept from the salvage: the case-insensitive is_anonymous_tier, the
account tier carried on error snapshots, and the single route walk per
search dispatch. Tests: the anonymous identity is back in the autodetect
and rescue cases. The tests that pinned the guest exclusion are removed.
2026-10-04 00:10:54 +05:30
kshitijk4poor
5d3c05977b test(tui_gateway): pin session.save to the RPC pool
Gate r1 (2ab, optional): nothing failed if session.save left
_LONG_HANDLERS and went back to running its full stored-session read
+ JSON render inline on the RPC reader. Extend the existing pool
assertion to session.save; dropping the entry turns it red.
2026-10-03 23:28:02 +05:30
kshitijk4poor
d0b786f354 fix(tui_gateway): forward a compute-host control.error code only when set
Gate r1 (quality, Low): every control.error from a classified route,
including the compress and codeless failures, carried `"code": null`
on the wire once session.save started forwarding its 4131 cap code.
Send the key only when the handler's error has a code. The compress
caller still maps every failure to its own 4009, so it is unchanged.
2026-10-03 23:28:02 +05:30
kshitijk4poor
399eac705f fix(tui): write the /save json in-memory fallback in the importable shape
Gate r1 (2c + quality, Low): with no stored row, TUI/Desktop /save json
wrote the legacy {model, session_id, session_start, system_prompt,
messages} shape. It has no `id`, so import_sessions rejected it
("session id is required") while every other /save json file imports.
Use the CLI fallback's canonical keys (id = the live row key,
started_at as an epoch or None); keep system_prompt as an extra key.
No reader of the old keys exists in apps/, ui-tui/ or web/.
2026-10-03 23:28:02 +05:30
kshitijk4poor
a9a5c70ee9 test(tui): pin TUI /save json to the session profile's export cap
Gate r1 (2c + 2ab, Low): the cap check monkeypatched
resolved_max_export_messages, so dropping the
_session_profile_runtime_scope around the stored-session read in
session.save stayed green. The cap now comes from a real config.yaml,
and a tui-profile row launches the process from an uncapped
HERMES_HOME: only the session profile's config refuses with 4131.
Removing the scope line turns that row red.
2026-10-03 23:28:02 +05:30
kshitijk4poor
355ab82811 perf(tui_gateway): run session.save on the RPC pool
session.save now reads the whole stored session and renders it (up to sessions.max_export_messages rows,
~0.8s for a 20k-row session on a laptop); inline on the reader thread that stalls every other RPC.
2026-10-03 23:28:02 +05:30
kshitijk4poor
b564815995 fix(tui_gateway): TUI/Desktop /save json exports the stored session and obeys the export cap
session.save wrote the in-memory history, so turns in-place compaction archived were missing from the
snapshot and sessions.max_export_messages never applied, unlike CLI and messaging /save json since the
#130339 fix. The two existing call sites' cap check + export + undone-row filter move into
hermes_cli.session_export.load_save_snapshot; the CLI, the messaging gateway (one off-loop hop) and the
tui_gateway handler all call it. The TUI handler reads the session's own profile db under that profile's
config, refuses an oversized session with rpc error 4131 (the same message as the CLI), and keeps the
in-memory export as the fallback when no stored row exists. Turn-isolated sessions run the same handler on
the compute host; the host now forwards the handler's error code so the refusal arrives as 4131, not 5011.

Refs #130339
2026-10-03 23:28:02 +05:30
teknium1
e94fc4e26a fix(process): first heartbeat carries output produced before arming
The terminal tool arms a heartbeat only after its spawn bookkeeping, and a
fast-starting process has already written its first lines by then.
arm_heartbeat reset the output baseline to the current total, so those lines
were in no heartbeat at all: the agent never saw them until completion.

Baseline is the spawn (field default 0). Every produced line now lands in
exactly one heartbeat regardless of how long the spawn->arm gap is.

CI: tests/tools/test_process_heartbeat.py FLAKY on main (run 36033989953),
beats read ["", "", "second\n", ""]. Reproduced locally with a 0.3 s gap
between spawn_local and arm_heartbeat: base loses "first", fixed keeps it.
2026-10-03 10:52:57 -07:00
teknium1
620ceb86fc catalog(custom-dangerous-patterns): raise requires_hermes to >=0.21.4
0.5.0 gates block patterns through the pre_tool_call approve action
(0.18.1+) and imports tools.approval_detection (0.21.4+); on older
cores block patterns silently stop gating.
2026-10-03 10:36:37 -07:00
Stephen Cross
5100232b75 catalog: bump custom-dangerous-patterns to 0.5.0 (sha 788dc41) 2026-10-03 10:36:37 -07:00
teknium1
09b0666ad5 catalog(skill-retrieval): describe the llm_request middleware accurately and disclose the opt-in rerank
The description named a register_system_prompt_section call the plugin
never makes; the opt-in rerank sends data off-host, so it gets a
Disclosure line.
2026-10-03 10:35:56 -07:00
Meng Huey
9abd05934e chore(catalog): skill-retrieval 0.6.0 — real pin bump, environ-only key, outbound cap disclosure 2026-10-03 10:35:56 -07:00
kshitijk4poor
2eceb02755 test(state): name the title-yield test after the rule it pins
Gate r1 quality finding: test_empty_ghost_session_does_not_block_title
read as "ghosts never block", but three of its four rows assert the
opposite. Rename it to
test_title_conflict_yields_only_to_ended_empty_visible_holder, make the
holder_messages count a holder_has_message bool (only zero vs non-zero
matters), and state the rule in the docstring: a holder yields only when
ended, empty, visible and unarchived; every other holder conflicts.
2026-10-03 23:03:42 +05:30
kshitijk4poor
a557e6ede8 fix(api): let POST /api/sessions titles yield to ended empty ghosts
Gate r1 2c finding (same bug class as #81888): _handle_create_session ran
its own "SELECT id FROM sessions WHERE title = ?" and 400'd whenever any
row held the title, so creating a session named after an abandoned empty
chat still failed even though renaming to that name now succeeds.

Extract the conflict block of _set_session_title into
SessionTitlesMixin._resolve_title_conflict(conn, session_id, title) and
call it from both the rename path and the API create transaction, so the
uniqueness rule (compression ancestor transfer, retired Bot Chat, ended
empty visible ghost, otherwise ValueError) lives in one place. On
ValueError the create still deletes its just-inserted row and returns
400 invalid_title; the message is now the DB's "Title 'X' is already in
use by session Y". A brand-new session has no compression ancestors, so
that branch is a no-op on create. api_server.py net growth is 0.
2026-10-03 23:03:42 +05:30
kshitijk4poor
3c17b9fd66 fix(state): free a ghost's title at rename time instead of skipping it
Filtering the conflict check with ``message_count > 0`` let two sessions
hold one title in the DB: the full unique index then raised
IntegrityError, and under the PR's partial ``message_count > 0`` index the
ghost's first append_message would fail instead. Keep the full index and,
in the same CAS transaction (like the archived Bot Chat branch), retire
the title from a visible row matching _EMPTY_SESSION_WHERE: ended, no
messages, not archived -- the same definition delete_empty_sessions uses.
Live empty rows (a /title queued before the first turn), hidden rows (a
fresh canonical Bot Chat) and archived rows keep their names.
2026-10-03 23:03:42 +05:30
Abhijat Saxena
fb4aac296b fix(state): exclude empty ghost sessions from title uniqueness
Empty sessions (message_count=0) created by interrupted flows (e.g.,
desktop session creation that never received a message) would block
session rename attempts even though they were invisible in the UI.

Changes:
- Updated title uniqueness index to exclude sessions with 0 messages
- Modified _set_session_title conflict check to skip empty sessions
- Added migration to drop and rebuild the index with new filter
- Added regression test for ghost session rename blocking

Fixes #81888

(cherry picked from commit a28e084278694a3b7e5f0adbfae90b14ac575db2)
2026-10-03 23:03:42 +05:30
kshitijk4poor
091400cc76 chore: map AbhijatSaxena for salvage of #108677 2026-10-03 23:03:42 +05:30
teknium1
b412ee9206 fix(platform): an unreadable GPU classifies as unknown, so requires.gpu fails open as documented
When every DRM card's vendor file or every Windows display adapter's
MatchingDeviceId was unreadable, classify_gpu_vendors returned 'none' and
availability refused with unsupported_gpu, contradicting the documented
'unreadable GPU passes' rule. Windows now records an unreadable numbered
adapter as an empty id so the classifier sees it.

Also drops the type-narrowing assert in availability() and makes
_action's gpu_label required.
2026-10-03 10:26:12 -07:00
alt-glitch
f1c79255fe feat(plugins): requires.gpu gates a plugin server on the host's GPU vendor
A portable plugin can now declare `requires: {gpu: nvidia}` for an MCP
server. On a host without an NVIDIA GPU, Hermes then refuses the
install, hides the server's tools, and the catalog card says "needs an
NVIDIA GPU". The check is the existing
`hermes_platform.host.facts.gpu_class()` read.

The requirement is independent of `app:`. The RTX Remix Toolkit is the
first consumer: it is a package the user unpacks anywhere, so there is
no install location to check, but it needs an NVIDIA GPU.

An unreadable GPU (`unknown`) passes, so a failed registry or sysfs
read does not lock out a machine that has the GPU. Only `nvidia` is
accepted: gpu_class() reports the highest-priority vendor, which
answers the question exactly for NVIDIA and not for AMD or Intel.

A registered server on such a host reports the new `unsupported_gpu`
state, carried through the gateway contract and the Plugins tab, with
a sentence that names the GPU instead of "Install <app>".
2026-10-03 10:26:12 -07:00
Cody292
b3e4fb8aa8 chore(plugin-catalog): point hermes-zh pin at 9ee5c26 2026-10-03 10:21:57 -07:00
teknium1
b71ba34819 chore(plugin-catalog): delist 11 plugins that override Hermes core or Desktop at runtime
README rule 9 (#130795): listed plugins extend Hermes only through public
surfaces. These 11 entries rebind core functions/methods/module tables or
reach into the Desktop app's own markup, and fail the no core override /
app DOM reach admission checks at their pinned shas.

Plain delisting, not removed.yaml: installed copies keep working, and each
plugin is welcome back with a SHA bump once it moves to a public hook. Every
author has an issue on their repo explaining the finding and the public path.

Dropped from the original list because their compliant re-pins were
approved: clinepass (#130439), prompt-enhance (#130900),
custom-dangerous-patterns (#131920), hermes-zh (#131464) and
skill-retrieval (#131290).
2026-10-03 10:19:59 -07:00
teknium1
33eedf29c9 docs(plugins): pin plugins.isolation in the managed scope for shared deployments
plugins.isolation is profile config, so a profile (or its agent) can set it
back to in_process. The managed scope already beats profile config and makes
`hermes config set` refuse, and the isolation reader goes through that
overlay; document it as the operator lock and pin the contract with a test
(red when isolation_mode() reads the raw profile file).
2026-10-03 10:15:11 -07:00
teknium1
21564d22dd fix(plugins): model-provider plugins load under plugins.isolation: host on Windows
The credential-free profile extraction child got HOME but not USERPROFILE.
Windows resolves Path.home() from USERPROFILE only, so the child exited with
"Could not determine home directory" and every user model-provider plugin was
silently dropped under host isolation.

Reported-by: Enough1122
2026-10-03 10:15:11 -07:00
teknium1
55c06b2fd6 test(plugins): run the plugin host's cross-platform tests on the Windows and macOS lanes
Three host tests were unmarked, so the OS lanes never imported them and the
Windows-only model-provider extraction failure shipped green.
2026-10-03 10:15:11 -07:00
teknium1
ad5c8e336e fix: plugin host review findings (async ctx, caller session, restarts, per-profile dashboard)
Independent review of the plugin host found eight defects; each is fixed here and
the reproducible ones are covered by two new invariant tests (red on the previous
head, green now).

- Async plugin code: `await ctx.<facade>.<async method>` works (the facade probe
  reports coroutine methods and the host returns a coroutine function), and ctx
  calls made from coroutines keep the caller's session. The wire origin is now a
  ContextVar carried onto host event-loop tasks, and the host's fallback context
  starts empty instead of copying whichever caller created the host.
- Memory / context-engine / cron providers survive a host crash: proxies record
  the host generation, and a category proxy reloads its plugin into the new host
  on next use (bounded by the same restart budget). A proxy from an earlier host
  can no longer address a recycled object id.
- Hosted provider objects no longer leak: the host imports a category plugin's
  module once and drops objects whose Hermes proxies were collected.
- Every public attribute of a hosted object is read live (class-level defaults
  included), and attributes Hermes assigns reach the plugin's object unless they
  hold a live handle.
- A user memory provider's config_schema.py runs in the plugin host.
- Dashboard plugin APIs resolve the plugin in the requesting profile (its copy,
  its plugins.enabled, its host) and keep repeated headers such as Set-Cookie.
- platform_actions coroutines run on the gateway loop their adapters use.
- Each incoming request gets its own thread (no pool exhaustion under nested
  callbacks); a late close from a replaced host no longer triggers a reload; the
  model-provider extraction cache is keyed by full path.
2026-10-03 10:15:11 -07:00
teknium1
17a7a9cb6b docs(plugins): plugin host isolation mode and its invariants 2026-10-03 10:15:11 -07:00
teknium1
5fa55d1678 feat(plugins): run third-party plugins in a per-profile plugin host (plugins.isolation: host)
plugins.isolation: host imports every non-bundled plugin in a host process started
on demand per profile with only that profile's environment, and hands plugins a
RemotePluginContext; Hermes registers proxies for tools, hooks, commands, skills and
provider objects (subclasses of the ABC it checks). Re-entrant: handlers may call
ctx back into Hermes while Hermes waits on them. Every user-code import path routes
to the host (general, memory/context-engine/cron, model-provider profiles via a
cached credential-free extraction, dashboard APIs via an ASGI bridge) or refuses.
One table names what cannot cross (live gateway objects); the static audit shown
by `hermes plugins validate/show` reads it. A host that dies is restarted with its
plugins (bounded). Default stays in_process: nothing changes unless opted in.

Why: shared-pool deployments must never run one tenant's plugin Python in an
interpreter holding another tenant's data, without plugins becoming tenant-aware.
2026-10-03 10:15:11 -07:00
teknium1
3c9847f5e8 chore(contributors): map vadim@easeit.cz to grinco
#131846 landed grinco's vornik-companion catalog commit while
check-attribution was red on this unmapped email.
2026-10-03 10:01:00 -07:00
Teknium
141cc0dbc8 eval(core-tool-deferral): keep agents' crontab and HOME writes inside the run sandbox (#132314)
The worker isolated HERMES_HOME only. The longrange_backup_pipeline task
("schedule it nightly at 2am") let agents run the operator's real crontab
and write into the real ~/.hermes: a past battery left three nightly cron
jobs and two scripts behind on the host, pointing into deleted run dirs.

sandbox.isolate_host turns on the product's own TERMINAL_HOME_MODE=profile
contract, points the worker's HOME at the same profile home (in-process
~ expansion for write_file), puts a file-backed crontab shim first on PATH
and unsets the systemd user bus. README quickstart drops its /tmp paths.
2026-10-03 09:57:41 -07:00
teknium1
33234cd6be catalog(quota): disclose cursor, antigravity and Claude usage reads for v2.8.1 2026-10-03 09:56:09 -07:00
Ronald Reis
573b18bfba catalog(quota): re-pin to v2.8.1 2026-10-03 09:56:09 -07:00
kshitijk4poor
370b3a91c8 fix(tui_gateway): type cron last_delivery_unverified as a list of targets
The scheduler persists last_delivery_unverified as a list of unconfirmed
platform:chat_id[:thread_id] targets and cron.manage passes it through, but
CronJobRow typed it bool | None, so contract validation rejected any job
with an unconfirmed delivery. Regenerate the TS/OpenRPC contract.
2026-10-03 22:23:27 +05:30
teknium1
be3fd671d7 fix(desktop/bots): name a bot by what its own backend reports on every surface
Follow-up to #131853 and #131867 from an independent review, each finding
reproduced in native Electron against two real serve backends.

- Cold composer path: the cache reconciliation (mergeServerMeta) ran only in
  the Bots pane's publish effect, so a launch that never opened the pane kept
  a stale cached title ahead of the backend's own in @handles and group
  prompts ("You are @local-stale-name"). It now runs where every snapshot is
  produced (data.ts::fetchRosterSnapshot), with the same fetch-time write
  fence, so the pane query and primeRoster reconcile alike.
- Pane label vs @handle: displayName ranked display_name above the backend's
  Bot Mode title on a thin remote row (and a titled remote `default` with no
  display_name fell to the connection label), so the row read "Remote Human
  Name" while mentions and prompts said @remote-bot-title. It now takes the
  same backend-title rung botFriendlyNames uses.
- Relay trace: a connection removed and re-added under a new id was logged
  `MISROUTED (same install as <removed id>)`; the remembered owner is now
  pruned against the current connection set.

The roster-pane-lifecycle test probed "republished" via mergeServerMeta's call
count; reconciliation now happens per fetch by design, so that invariant is
asserted via trackInboundActivity alone.
2026-10-03 09:49:07 -07:00
teknium1
18fa384634 fix(plugin-guard): Google installed-app OAuth client secrets no longer hard-block a plugin install
A `GOCSPX-` literal is the client secret of a Google installed-app (desktop/CLI)
OAuth client. Google's OAuth docs state such secrets cannot be kept confidential:
gcloud ships its own in every copy. The generic
`hardcoded_secret` pattern scored it critical, so any plugin that talks to a
Google installed-app client (the catalog's quota plugin) scanned `dangerous` and
could not be installed even with --force. Cap that one shape at high: the
finding stays in the report and the install asks for review (caution); every
other secret-shaped literal keeps its critical severity.
2026-10-03 09:43:03 -07:00
Ken Thompson
4bb9e57bfd fix(mcp): SIGKILL a dead MCP server's descendants that ignored SIGTERM
Salvage of #131963 by @mekenthompson onto main after #131946.

The shutdown reaper's SIGKILL pass only checked whether the leader PID was
still alive. Once the leader had exited, a descendant that ignored SIGTERM
kept the process group alive and was never escalated. The pass now also
probes the group with killpg(pgid, 0) and sends SIGKILL through the same
start-time guard as the SIGTERM pass.

The per-spawn environment marker from #131963 is not carried over: it left
helpers launched with a scrubbed environment running on Linux and brought
back the dead-leader refusal on macOS when a start time was recorded.
2026-10-03 09:38:33 -07:00
kshitijk4poor
05d8d0a266 revert: stale worktree reaping revalidation (#132260)
Reverts the nine commits merged by #132260 (merge 05ab8766e7), from
31194ea00e "fix(cli): revalidate worktree cleanup gates before removal"
through 05ab8766e7, restoring hermes_cli/worktree_gc.py,
hermes_cli/worktree_ops.py and tests/security/test_gitspawn_config_injection.py
to their pre-merge state and removing
tests/hermes_cli/test_worktree_reclaim_revalidation.py.

Maintainer decision: back the behaviour change out (trees kept when their
state changed since the audit, plain `git worktree remove` under the tree's
git env, dirty submodule trees kept). No commit on main touched these files
after the merge, so this is the exact inverse of the merged diff.

The contributor mapping for @rudycelekli stays (attribution record only).
2026-10-03 22:03:44 +05:30
Ryan Hill
3251a180f0 fix(web): resolve Git Bash paths in Windows file APIs 2026-10-03 12:08:23 -04:00
kshitijk4poor
f5a9361dcc fix(debug): stop encoded URL param keys at encoded separators
Review finding (BR1 r3 efficiency, Low): the _ENCODED_URL_PARAM_RE key
group excluded only `%3D`, so a key could run across `%26`/`%3F`/`%23`/
`%3B`. Two consequences: a nested `?next=%2Fx%3Ffoo%26token%3D<secret>`
matched the key `foo%26token`, which is not a sensitive name, and the
secret leaked; and each separator restarted a match that scanned up to
160 chars, about 8x slower than base on 1 MiB of `%26a` (1.72s vs 0.21s;
0.36s now).

Exclude every encoded separator from the key so it ends at the nearest
one. The nested leak now redacts to `%3D%2A%2A%2A`, and the
`redirect=...%3Fapi_key%3D...` case is unchanged.
2026-10-03 21:15:17 +05:30
kshitijk4poor
eb4bba0b2d fix(debug): only treat a masked scheme as one before a token-shaped key
Review finding (BR1 r3 2c Medium / 2ab Low): the `***` arm added to
_BARE_HEADER_RE treated ANY same-line word after an already-masked
value as the secret, so benign prose was eaten and every re-scrub ate
one more word (`x-api-key: *** see docs for details` -> `... docs for
details` -> `... details`; same for `Authorization: *** then prose`,
`X-Api-Key: *** (set in env)`). The scrub was not a fixed point.

Require the follow-on value to look like a credential (>=8 token chars,
optional `=` padding, then a token boundary) before `***` counts as a
masked scheme. The floor is 8, not 16, so 15-char keys still redact.
The 336-case header sweep stays at 0 leaks / 0 unstable, the 18
`name: <scheme> <key>` cases are still redacted, and the benign lines
are stable over three passes.
2026-10-03 21:15:17 +05:30
kshitijk4poor
5381678d03 fix(debug): scrub the key after a masked scheme on API-key headers
agent.redact's _SECRET_HEADER_RE treats the first word after an API-key
header as the value. With `x-api-key: Basic <key>`, that word is the scheme,
so the scheme becomes `***` and the key is left in the text. The support scrub
then saw `***` as the value and stopped, so `x-api-key: *** <key>` reached the
debug/share upload with the raw key. This affects the x-api-key, api-key and
x-auth-token families with Bearer/Basic/Digest, in both bare and `-H '...'`
forms. Base and main leak the same input.

_BARE_HEADER_RE now also accepts `***` plus a same-line value as a scheme.
That value must not be another secret header, so `x-api-key: *** cookie=...`
is not consumed. The key is redacted, and the redacted output is a fixed
point. The fix stays in the support layer. Teaching agent.redact the scheme
does not help, because its YAML/assignment passes mask the scheme word first.

Gate finding: BR1 fold2 "still open", a bare-header scheme leak.
2026-10-03 21:15:17 +05:30