Commit Graph

461 Commits

Author SHA1 Message Date
joaomarcos 9cf2046081 fix(photon): unify sidecar-deps check, harden log unlink and truncation
Addresses teknium1 review on #50983:

- cli.py's `hermes photon status` and adapter.py's _start_sidecar() still
  used the old node_modules/-existence check while check_requirements()
  had moved to a spectrum-ts content check. Extracted the check into a
  shared sidecar_deps_installed(), used by all three, so an
  empty/partial node_modules/ (aborted npm install) is rejected
  consistently instead of only in check_requirements().
- _install_sidecar()'s success-path _NPM_ERROR_LOG.unlink() only caught
  FileNotFoundError, so a PermissionError/OSError on a locked file would
  propagate. Broadened to OSError.
- npm stderr was truncated only when read back in check_requirements();
  an unbounded stderr was written to disk on every failed install. Now
  truncated to _NPM_ERROR_LOG_MAX_CHARS before write_text().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 18:20:06 -07:00
joaomarcos de5c39c033 fix(photon): surface npm install failures in check_requirements() diagnostic chain
Problema
--------
Quando o npm install do sidecar Photon falhava, o Hermes descartava toda
evidencia e continuava normalmente - deixando o adapter de iMessage
silenciosamente ausente, sem nenhuma mensagem de erro acionavel.

Tres falhas independentes formavam o caminho de falha silenciosa:

1. check_requirements() sem logging
   Cada branch de return False retornava sem emitir nenhum log. O core em
   platform_registry.py so consome o bool de check_fn() e loga uma mensagem
   generica com o install_hint - sem acesso ao motivo real da falha.

     if not HTTPX_AVAILABLE:     return False  # sem log
     if not shutil.which(node):  return False  # sem log
     if not node_modules.exists: return False  # sem log

2. node_modules/ parcialmente criado passava o guard (Risk 2)
   npm cria node_modules/ antes de abortar em ENOSPC, timeout de rede ou
   EACCES. O diretorio existia, check_requirements() retornava True (falso
   positivo), o adapter era registrado, e o crash acontecia em runtime com
   um erro de modulo ausente aparentemente nao relacionado ao setup.

3. stderr do npm descartado (Risk 3)
   subprocess.run sem stderr=PIPE. O output de erro aparecia no terminal
   durante o setup e sumia depois - diagnostico impossivel em CI/CD, Docker,
   VPS headless, e qualquer reinstalacao posterior.

Correcoes
---------
adapter.py - check_requirements() agora loga por branch:
  - httpx ausente       -> logger.warning com nome do pacote
  - node nao no PATH    -> logger.warning com nome do binario e env var
  - spectrum-ts ausente -> logger.debug com path do sidecar + ultimo erro npm
    (DEBUG nao WARNING: estado normal pre-setup; check_fn() e chamado de
    5 hot paths do core incluindo polling do /api/status)

adapter.py - content check em vez de existence check (Risk 2):
  antes:  if not (_SIDECAR_DIR / node_modules).exists()
  depois: if not (_SIDECAR_DIR / node_modules / spectrum-ts).exists()
  spectrum-ts e a unica dependencia do package.json. Checar sua presenca
  garante que instalacao parcial/abortada e detectada no boot do gateway,
  nao na primeira mensagem recebida via gRPC.

cli.py - stderr capturado e persistido (Risk 3):
  subprocess.run passa agora stderr=subprocess.PIPE, text=True em ambas as
  chamadas (npm ci e npm install fallback). O stderr capturado e:
    - impresso em sys.stderr imediatamente (output visivel no terminal)
    - persistido em _NPM_ERROR_LOG = sidecar/.photon-npm-error.log se
      returncode != 0, limitado a 300 chars
    - apagado de _NPM_ERROR_LOG se returncode == 0 (evita erro stale)
  check_requirements() le _NPM_ERROR_LOG quando spectrum-ts esta ausente e
  inclui o conteudo no DEBUG log - o erro do npm sobrevive ao terminal, ao
  restart do gateway e a reinicializacao da maquina.

sidecar/.gitignore - adicionado node_modules/ e .photon-npm-error.log.

Isolamento - sem impacto no core:
  - check_fn() continua retornando apenas bool; core nao e modificado
  - Logging usa namespace plugins.platforms.photon.adapter, isolado de
    gateway.* e hermes_cli.*
  - Cada plugin tem seu proprio check_requirements() independente
  - OSError no write/read de _NPM_ERROR_LOG e silenciado - nunca propaga

Testes - 24/24 passando:
  test_check_requirements_risks.py (7 testes):
    WARNING emitido quando httpx ausente
    WARNING emitido quando node nao no PATH
    DEBUG emitido (nao WARNING) quando spectrum-ts ausente, com path
    node_modules/ vazio agora retorna False (Risk 2 resolvido)
    _NPM_ERROR_LOG escrito no stderr do npm em falha
    _NPM_ERROR_LOG apagado apos npm bem-sucedido
    erro npm aparece no DEBUG log quando node_modules ausente

  test_npm_error_log_regression.py (9 testes - vetores de falha da solucao):
    return code contrato intacto (0 em sucesso, nao-zero em falha)
    OSError no write do log silenciado, exit code ainda propagado
    OSError no read do log silenciado, check_requirements() retorna False
    stderr vazio nao cria arquivo de log
    proc.stderr=None nao lanca AttributeError
    log stale apagado apos reinstall bem-sucedido
    DEBUG emitido mesmo sem log de erro (setup pela primeira vez)

Closes #50981

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 18:20:06 -07:00
liuhao1024 703de9bacb fix(photon): skip secret regeneration when existing credentials are valid
hermes photon setup unconditionally called regenerate_project_secret()
on every re-run, invalidating the credential held by a running sidecar
and causing all outbound sends to fail with AuthenticationError.

Now validates existing credentials via a lightweight list_users call
before deciding to regenerate. Only rotates when no credentials exist
or the existing ones are invalid, and warns the user to restart the
gateway when rotation occurs.

Fixes #50755
2026-07-28 18:20:06 -07:00
pierrenode b378bc6fab fix(photon): serialize auth.json writes with the shared cross-process lock
store_photon_token/store_project_credentials/store_user_numbers read-modify-
wrote auth.json without hermes_cli/auth.py's _auth_store_lock(), the
cross-process flock every other writer of that file (credential_pool
refresh, model_switch, fallback_cmd, nous_portal adapter, etc.) already
holds. A concurrent write from either side during the unlocked
load-mutate-save window silently drops the other side's update.
2026-07-28 18:20:06 -07:00
solyanviktor-star c3531cfac2 fix(photon): close the raw fd when os.fdopen fails in _save_auth
Review follow-up: if os.fdopen() raised before taking ownership of the
descriptor returned by os.open(), the cleanup handler unlinked the temp
file but leaked the fd. Close it explicitly on that path, mirroring the
credential-writer cleanup from #62837.

Strengthen the tests so the old writer could not pass them: an os.open
spy asserts O_CREAT | O_EXCL and an explicit 0o600 mode (the final-mode
check alone was also satisfied by the post-write chmod), and a forced
fdopen-failure test asserts the raw fd is closed and no temp file is
left behind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 18:20:06 -07:00
solyanviktor-star 2b30744c34 security(photon): create auth.json temp file with 0o600 atomically
_save_auth() wrote the bearer token with tmp.open('w') — created at
process umask (typically 0o644) — and only chmod'ed to 0o600 after the
write, leaving a window where the token sat world-readable. The temp
name was also fixed and predictable (auth.json.tmp), so it could be
pre-planted (symlink attack).

Create the temp file with os.open(O_WRONLY|O_CREAT|O_EXCL, 0o600) and a
per-process random suffix, fsync before the atomic replace, and clean
the temp file up on failure. Mirrors hermes_cli/auth.py:_save_auth_store
(#19673, #21148), which hardened the same pattern in the core writer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 18:20:06 -07:00
MattMaximo 482d1ab0f6 fix(photon): unwrap dashboard project responses 2026-07-28 18:17:52 -07:00
liuhao1024 21264c4343 fix(photon): mark adapter as not supporting message editing to suppress streaming cursor
Photon (iMessage) has no real edit API for already-sent messages. When
streaming completes, the gateway attempts to edit the message to remove
the streaming cursor (▉). Without edit support, this cursor gets stuck
in the final message, corrupting Unicode characters.

This change sets SUPPORTS_MESSAGE_EDITING=False on PhotonAdapter, which
causes the gateway to suppress the streaming cursor entirely for this
platform (via _effective_cursor in gateway/run.py). This prevents the
stale tofu square (▉) from appearing in streamed iMessage responses.

Fixes #49253
2026-07-28 18:17:52 -07:00
kelsia14 fd4f756492 fix(photon): ignore iMessage media placeholders
Cherry-picked from PR #54514; dropped frozen AUTHOR_MAP hunk in scripts/release.py, contributor mapping added instead.
2026-07-28 18:17:52 -07:00
Florian Valade afab7ed46e fix(photon): address review — U+FFFC before _record_last_inbound, MIME-based CAF promotion, add tests
- Move U+FFFC placeholder detection before _record_last_inbound() so the
  placeholder message id is not recorded as the reaction target
- Cancel pending U+FFFC tasks in disconnect() to prevent task leaks
- Check mimeType audio/x-caf in addition to filename for CAF→VOICE promotion,
  fixing unnamed attachments that default to '(unnamed)'
- Add 7 Photon adapter tests: CAF named/unnamed promotion, U+FFFC no-dispatch,
  U+FFFC not recorded as last inbound, U+FFFC+attachment cancel, timeout fire,
  disconnect cleanup
- Add 6 transcription tests: ffmpeg conversion, afconvert fallback, all-fail,
  CAF→WAV before Groq, conversion failure error, local provider skip
2026-07-28 14:06:56 -07:00
kosta e25d516c8c fix(gemini): bump native provider aux default to gemini-3.6-flash
The native Gemini provider profile's default_aux_model and the curated
model picker catalog were still pinned to gemini-3.5-flash, a stale
generation now superseded by gemini-3.6-flash (documented GA). Bump
both so the auxiliary-task default and the picker stay in sync with
the current model.

Contract test asserts the durable lockstep invariant only
(default_aux_model is a member of _PROVIDER_MODELS["gemini"]) rather
than pinning either side to a frozen model-name string, so it doesn't
need updating on the next model-generation bump.
2026-07-28 11:54:46 -07:00
Alex Fournier 14bed44c8c Reapply "feat(observability): integrate NeMo Relay runtime and shared metrics"
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 21:10:51 -07:00
Jeffrey Quesnelle 841a5a744a
Revert "feat(observability): integrate NeMo Relay runtime and shared metrics" 2026-07-27 22:28:08 -04:00
Jeffrey Quesnelle 9216198601
Merge branch 'main' into feat/hermes-relay-shared-metrics 2026-07-26 23:14:26 -04:00
HexLab98 aff48958d3 fix(deepseek): drop retired models from picker and provider defaults
Stop offering deepseek-chat/reasoner in the static catalog and point
fallback/aux defaults at the permanent v4 IDs. Keep retired aliases in
a detection-only map so /model deepseek-chat still resolves to deepseek.
2026-07-26 16:28:41 -07:00
Alex Fournier 45580cc93a Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-26 09:21:19 -07:00
teknium1 6179da5496 fix(dashboard): one gateway liveness ladder for status + channels
The sidebar strip and the Channels page could contradict each other on
the same page load — "Gateway running" next to "The gateway is not
running." /api/status and /api/messaging/platforms each open-coded their
own liveness ladder: status probed GATEWAY_HEALTH_URL and scoped its
PID/state reads to the requested profile, messaging did neither and used
the uncached raw PID probe.

Three deployments hit the split: a cross-container gateway (no local PID,
only the health probe can see it), a profile-scoped dashboard (messaging
borrowed a DIFFERENT profile's runtime state, reporting a false
"connected" that hides a real outage — #71211), and a launch-service
managed gateway with no PID file.

Adds resolve_gateway_liveness() in gateway/status.py as the single ladder
(cached PID -> HTTP health probe -> runtime-status PID with
expected_home) and routes both endpoints, /api/messaging/platforms/{id}/test,
and the kanban dispatcher-presence probe through it. Probe callables are
injectable so the existing monkeypatch seams keep working, and
GatewayLiveness.probe_error distinguishes "down" from "couldn't tell" so
the kanban warning keeps failing OPEN instead of crying wolf.

Closes #71211.
2026-07-26 08:08:38 -07:00
teknium1 064b6e40c5 fix(image-gen): stop reporting the Codex tool_choice 400 as an account limit
The Codex image backend rejected our own request shape for every account, and
we then translated that rejection into "Image generation is not enabled for the
current Codex account. Switch the image provider to OpenAI API key, FAL, or
xAI." — telling every affected user to abandon a provider that had never
actually been tried. That message is why this reads as a setup failure rather
than a bug: the wire error was replaced with a confident, wrong diagnosis.

Removes the classifier and its exception, so any HTTP failure surfaces
verbatim. The paired request-shape fix (previous commit) is what makes the
400 stop happening; this commit makes the next one diagnosable.

Also fixes error-body truncation: bodies were head-truncated at 500 chars, and
Codex error payloads can carry hundreds of bytes of leading metadata, so the
user got a wall of padding and no message. _summarize_error_body() prefers the
parsed error.message and falls back to a truncated raw body.

Docs: drop the unqualified image-to-image claim for the Codex backend and note
that the hosted tool call cannot be forced, so it is best-effort.

Verified E2E against a local fake Codex backend: success path writes a real PNG
with no tool_choice on the wire; the 400 path now returns api_error carrying
"Tool choice 'image_generation' not found in 'tools' parameter" (148 chars)
instead of the entitlement message. Sabotage run confirms all 4 regression
tests fail when the old behavior is restored.

Refs #19505, #49008, #31335.
2026-07-25 16:41:57 -07:00
Tranquil-Flow 2bfe9fabbc fix(image_gen/codex): remove unsupported tool_choice from request payload (#19505)
The chatgpt.com/backend-api/codex backend 400s on every tool_choice
shape for the hosted image_generation tool — it looks up tool_choice as
a function name and never recognizes hosted-tool entries. Removing the
field from _build_responses_payload() lets the host model decide; the
instructions field nudges it toward the tool.

Salvaged from PR #19979 (originally targeted the old
client.responses.stream call, which no longer exists on upstream/main;
the live request now flows through _build_responses_payload + httpx in
_collect_image_b64).
2026-07-25 16:41:57 -07:00
solyanviktor-star f1ea4a56c2 fix(memory): cover the remaining setup-time .env reads with utf-8-sig
Follow-up to review feedback:

- mem0 _prompt_api_key read .env with the locale default, so a Notepad
  BOM hid the first key from the masked current-value lookup; read it
  with utf-8-sig + errors=replace like the canonical readers in
  hermes_cli/config.py.
- hindsight _load_simple_env used plain utf-8; it also parses the Hermes
  .env during post_setup, where a BOM stuck to the first key. Switch to
  utf-8-sig + errors=replace.
- Add hindsight regressions: BOM key matching in _load_simple_env and in
  the cloud post_setup writer, plus non-ASCII round-trip preservation,
  and a mem0 regression for the BOM'd masked-key lookup. The BOM tests
  fail without the fix on any platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:10:39 -07:00
solyanviktor-star 75afc47baa fix(memory): read/write .env as UTF-8 in mem0 and hindsight setup
The mem0 and hindsight memory-provider setup routines round-trip the
user's ~/.hermes/.env: they read existing lines, update the keys they
manage, and rewrite the whole file preserving every other line verbatim.
Both used env_path.read_text() / write_text() with no encoding.

read_text()/write_text() with no encoding fall back to the system locale
(cp1252/GBK on Windows), so on a non-UTF-8 host the preserved lines get
mangled or the call crashes on any non-ASCII value, and — because the
reader never strips a BOM — a Notepad-edited .env makes the first key
fail the in-place match and get duplicated instead of updated.

Match the canonical .env readers in hermes_cli/config.py: read with
encoding='utf-8-sig' (BOM-tolerant) and write with encoding='utf-8'.
mem0/_setup.py already pins utf-8 for mem0.json, so this just aligns the
.env path in the same file. Fixes both memory plugins in one class fix.

Adds regression tests: a BOM'd .env updates the first key in place
(locale-independent, fails without the fix) and non-ASCII existing lines
survive the round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:10:39 -07:00
Hao Zhe 1cfe23c6e4 fix(openviking): serialize client refresh state 2026-07-24 13:00:53 +05:30
Hao Zhe cf0bd5dd4c fix(openviking): stop pending runtime start on shutdown 2026-07-24 13:00:53 +05:30
Hao Zhe c4d0f1c1d6 fix(openviking): serialize local runtime recovery starts
Avoid spawning multiple local OpenViking server processes while a runtime autostart waiter is already active. Remote endpoints still retry on later accesses because they do not install a local waiter.
2026-07-24 13:00:53 +05:30
Hao Zhe 8fdc9c58e0 fix(openviking): use readonly config loader 2026-07-24 13:00:53 +05:30
爪爪 5ea3abc3b3 fix(openviking): match tenant-header errors structurally instead of hard-coding strings
The _needs_trusted_identity_retry method was hard-coding specific
server-side error strings to detect when a request failed due to
missing X-OpenViking-Account / X-OpenViking-User headers.  Each new
server-side error variant required another string added to the client.

Replace the string enumeration with a structural match: the error
message mentions one of the tenant headers AND the HTTP status is 400.
This covers all current error variants:

  - "Trusted mode requests must include X-OpenViking-Account and User"
  - "ROOT requests to tenant-scoped APIs must include X-OpenViking-Account"
  - "Trusted mode requests must include X-OpenViking-Account."
  - "Trusted mode requests must include X-OpenViking-User."

The 400 status guard avoids false-positives on 403 errors such as
"USER API keys cannot override X-OpenViking-User", which must not
trigger a retry.

All 176 existing tests pass.

(cherry picked from commit 5a24d6766c)
2026-07-24 13:00:53 +05:30
Hao Zhe 21a634b98a test(openviking): keep root tenant errors out of trusted retry 2026-07-24 13:00:53 +05:30
Hao Zhe 36f01d2e54 fix(openviking): sanitize splitline env separators 2026-07-24 13:00:53 +05:30
koshaji d3520944c7 fix(openviking): join runtime-autostart thread on shutdown (SIGABRT-at-exit)
`OpenVikingMemoryProvider.shutdown()` joins in-flight writers, deferred-commit
threads, and prefetch threads, but not `_runtime_start_thread` — the tracked
`daemon=True` waiter that runs `_finish_runtime_openviking_start`, which blocks
on network health probes (`_wait_for_openviking_health` polling + a
`_VikingClient.health()` request).

If the local OpenViking runtime is slow or unreachable, that waiter can still
be blocked in network I/O at interpreter exit. CPython then forcibly kills it
during `Py_FinalizeEx` (`PyThread_exit_thread` -> `__pthread_unwind` ->
`abort()`), producing SIGABRT (exit 134) with no traceback — the same daemon-
thread-at-exit failure class fixed for the Honcho provider.

Fix:
- `shutdown()` now joins `_runtime_start_thread` (timeout-bounded) alongside the
  other tracked threads.
- `_wait_for_openviking_health()` gains a `should_stop` callback; the waiter
  passes `lambda: self._shutting_down` so the poll loop bails out promptly once
  `shutdown()` flips the flag, instead of lingering up to the 60s autostart
  timeout and timing out the join (which would leave the thread alive).
- Add tests/plugins/memory/test_openviking_shutdown.py covering the short-circuit
  and the shutdown-joins-runtime-thread behaviour.

(cherry picked from commit 5471ec7021)
2026-07-24 13:00:53 +05:30
pprism13 9291b786b4 fix(openviking): sanitize embedded newlines when writing .env secrets
`_write_env_vars` in the OpenViking memory provider interpolates each
secret straight into a `KEY=VALUE` line, but the values only ever pass
through `_clean_config_value`, whose `value.strip()` trims surrounding
whitespace and leaves internal CR/LF intact. Because the file is strictly
line-oriented and is re-read via `read_text().splitlines()`, a value that
carries an embedded newline spills onto a second physical line, and the
tail is re-parsed as an independent `KEY=VALUE` entry on the next round
trip. A secret pasted with a trailing record (e.g. an `OPENVIKING_API_KEY`
copied with an extra line) therefore injects an arbitrary additional
variable into the persisted credentials file and silently corrupts it.

The fix neutralizes the line terminators at the single chokepoint where
values reach the file. A small `_env_line_safe` helper strips `\r`, `\n`,
and the NUL byte from each value, and both write sites in `_write_env_vars`
(the existing-key update branch and the appended-key branch) route through
it, so a value can only ever occupy the single line it is written on.

## What does this PR do?

Hardens the OpenViking memory provider's `.env` writer so a malformed or
pasted secret value can no longer break out of its `KEY=VALUE` line and
inject a rogue variable into the profile-scoped credentials file.

## Related Issue

N/A

## Type of Change

- [x] 🐛 Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `plugins/memory/openviking/__init__.py`: add `_env_line_safe()` which
  removes `\r`, `\n`, and `\x00` from a value, and apply it to both the
  updated-key and appended-key write branches in `_write_env_vars()`.
- `tests/plugins/memory/test_openviking_provider.py`: add two regression
  tests covering a fresh write and an in-place key update with embedded
  CR/LF, asserting no injected line survives the read-back.

## How to Test

1. Run the targeted tests:
   `pytest tests/plugins/memory/test_openviking_provider.py -k env_writer -q`
2. Reverting the `_env_line_safe` sanitization makes
   `test_openviking_env_writer_strips_embedded_newlines_in_values` and
   `test_openviking_env_writer_strips_newlines_when_updating_existing_key`
   fail with a rogue `INJECTED_KEY=`/`ROGUE=1` line appearing in the file,
   confirming the tests pin the bug.
3. `ruff check plugins/memory/openviking/__init__.py` and
   `python scripts/check-windows-footguns.py plugins/memory/openviking/__init__.py`
   both pass.

## Checklist

### Code

- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains only changes related to this fix
- [x] I've run the relevant tests and they pass
- [x] I've added tests for my changes (required for bug fixes)
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)

### Documentation & Housekeeping

- [x] I've updated relevant documentation (docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — N/A
- [x] I've considered cross-platform impact (strips CR as well as LF) — done
- [x] I've updated tool descriptions/schemas if I changed tool behavior — N/A

(cherry picked from commit f29dd2df84)
2026-07-24 13:00:53 +05:30
Alex Fournier 5e1b68ce16 test(telemetry): mock profile consent source
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:06:33 -07:00
Alex Fournier b4e105031a Merge upstream main into feat/hermes-relay-shared-metrics
# Conflicts:
#	MANIFEST.in
#	pyproject.toml
#	tests/test_project_metadata.py
2026-07-23 07:22:03 -07:00
Teknium c1b0f6f3c1
feat(kanban): per-task model dropdown — set/override worker model+provider from the board (#69876)
Adds the missing write path for the per-task model_override column (which
was previously only settable via manual SQL) and pairs it with a
provider_override so cross-provider switches resolve correctly:

- kanban_db: provider_override column (+migration), set_model_override()
  with model_override_set event, create_task(model_override=,
  provider_override=), dispatcher spawns worker with -m <model>
  [--provider <name>]
- dashboard: Model row in the task drawer — dropdown fed by a new
  /model-options endpoint (build_models_payload substrate, provider-grouped,
  free-text fallback), PATCH + bulk model override support
- CLI: kanban create --model/--provider, new kanban set-model subcommand,
  show prints the provider
- agent tools: kanban_create accepts model/provider; show/list expose
  provider_override

Rate-limit recovery flow: override is settable on running tasks and takes
effect on the next dispatch, without touching the worker profile's config.
2026-07-22 22:23:24 -07:00
Alex Fournier 761fce7f79 Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-22 08:55:25 -07:00
Alex Fournier 70caf30203 fix(observability): coordinate Relay plugin lifecycle
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-22 07:22:21 -07:00
wernerhp cc84af9fad fix(memory): preserve genuine pre-delimiter content in merged compaction rows
teknium1 review on #57690: harvesting logic was skipping the ENTIRE merged
row when a compaction summary was appended to the tail message, discarding
real prior user content that context_compressor retains before the
_MERGED_SUMMARY_DELIMITER. Extract and harvest that pre-delimiter segment
instead of dropping it wholesale.

Revert-to-fail: reverting plugins/memory/holographic/__init__.py alone
drops test_merged_into_tail_preserves_genuine_pre_delimiter_preference
(19 passed, 1 failed); restoring the fix returns 20/20 passed.
2026-07-22 06:59:14 -07:00
wernerhp 004de13f14 fix(memory/holographic): don't harvest compaction summaries; honor auto_extract=false string
Two compounding defects in the holographic memory provider (#57682):

1. The on_session_end gate used plain truthiness on auto_extract, but the
   plugin's own config schema declares it as a string enum with default
   "false" — and not "false" is False, so extraction ran for users who
   had it configured off. Coerce with the shared utils.is_truthy_value
   (same fix class as the merged byterover no-op fix).

2. _auto_extract_facts scanned every role=user message. Context-compaction
   handoff summaries can be inserted as role=user messages and their prose
   reliably matches the decision patterns (we decided/agreed, the project
   uses), so the compactor's own output was persisted as a durable project
   fact on every rollover following a compaction — recreated even after
   manual deletion.

Adds agent.context_compressor.is_compaction_summary_message(), a public
helper that prefers the in-process COMPRESSED_SUMMARY_METADATA_KEY marker
and falls back to _is_context_summary_content() (covers merged-into-tail
and historical prefixes), since the metadata key is stripped by wire
sanitizers and doesn't survive all session-store round-trips. The plugin
skips summary messages before pattern matching.

Fixes #57682
2026-07-22 06:59:14 -07:00
Hao Zhe 8af2133009 fix(openviking): align session context with shared profile contract 2026-07-22 14:12:50 +05:30
Flownium 11c1ca01c5 fix(openviking): inject session-start memory context
(cherry picked from commit 18b474d0bd)
2026-07-22 14:12:50 +05:30
kshitij c3c80e1796 refactor: cleanup follow-up for salvaged PR #58871
- Remove dead current_sid parameter from _recover_pending_sessions
- Remove dead cleanup parameter from _release_owner_run_claim (always True)
- Set _run_lock_path after flock succeeds, not before
- Collapse redundant BlockingIOError branch (covered by OSError+errno check)
- Track _pending_marked_sids to skip re-writing marker file on every sync_turn
2026-07-22 14:05:28 +05:30
Chris Korhonen 81fc424592 fix(openviking): chunk structured session sync
Preserve ordered structured turns across OpenViking's 100-message batch limit and resume retries from the first unconfirmed message.

Based on the OpenViking batching work from commit 1a567f7067 in #58981.
2026-07-22 14:05:28 +05:30
Hao Zhe 3fd96583f4 fix(openviking): serialize orphan session recovery 2026-07-22 14:05:28 +05:30
Hao Zhe 323e9baf5d fix(openviking): recover pending session commits 2026-07-22 14:05:28 +05:30
Alex Fournier 67863777ca Merge remote-tracking branch 'origin/main' into tmp/hermes-relay-pr-merge-20260717
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-20 11:07:39 -07:00
RenoMG 95c616be20 fix(supermemory): complete self-hosted endpoint routing 2026-07-20 00:40:40 -07:00
Dhravya Shah 24ac26a3da feat(supermemory): support custom base URL for self-hosted servers
The supermemory SDK already honors SUPERMEMORY_BASE_URL, but the raw
urllib call used for session-end conversation ingest hardcoded
https://api.supermemory.ai/v4/conversations, so ingest always hit the
cloud even when pointing at a self-hosted server (e.g.
http://localhost:6767).

Resolve the base URL as config (supermemory.json base_url) >
SUPERMEMORY_BASE_URL env var > https://api.supermemory.ai, strip any
trailing slash, and use it for both the SDK client and the
/v4/conversations ingest endpoint.
2026-07-20 00:40:40 -07:00
Alex Fournier 4dedaa4237 refactor(runtime): consolidate Relay lifecycle ownership
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:59:06 -04:00
Alex Fournier 9bc521b138 refactor(runtime): manage Relay LLM tool and subagent execution
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:57:38 -04:00
Alex Fournier 056e7df0e0 fix(observability): harden Relay metrics isolation and aggregation
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:55:07 -04:00
Alex Fournier 64faff6768 refactor(observability): move Relay runtime ownership into core
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:54:49 -04:00
Alex Fournier 3bd338d2a9 feat(observability): add Relay shared metrics pipeline
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:54:27 -04:00
teknium1 c84c0c5277
Merge branch 'pr-51020' into lane/c3-memory-panel 2026-07-18 15:13:04 -07:00
Teknium 07f07c7b51 fix(mem0): migrate legacy OSS base URL aliases
Normalize stale api_base keys to each mem0 provider's accepted URL field before Memory.from_config, without mutating the saved config.
2026-07-17 13:49:29 -07:00
Teknium e4f87557b9
feat(kanban): modal create-task dialog, editable board project directory, comment workflow hint (#66333)
Community feedback (@LSanapalli on X): the inline task-creation form is
cramped inside a ~280px column with no way to resize; board-level
workspace defaults can't be changed after board creation; and users
believe they must block a task, comment, then unblock just to talk to
a worker.

- Create-task dialog: replace the inline column form with a centered
  modal (reuses hermes-kanban-dialog chrome, 36rem wide) with labeled
  fields for title, assignee, priority, skills, workspace kind/path,
  goal mode, and parent task. Same request shape; Enter/Escape behavior
  preserved; submit disabled until a title is present.
- Board settings dialog: new Settings button in the board switcher opens
  a modal to edit display name, description, and the board-level default
  project directory (default_workdir). PATCH /boards/:slug now accepts
  default_workdir (validated absolute existing dir; empty string clears;
  omitted leaves unchanged) and returns the recomputed
  default_workspace_kind so task-creation defaults follow immediately.
- Comment workflow hint: the task drawer's comment box now explains that
  comments land on the thread immediately and reach the worker on its
  next run/kanban_show() — no block/unblock dance needed — with a fuller
  tooltip for when blocking IS the right tool.
- i18n: new keys optional in the kanban namespace with English fallbacks
  in the bundle (established pattern; avoids churning 17 locale files).
- Docs: dashboard section updated for the dialog + Settings button.
2026-07-17 07:23:54 -07:00
amanning3390 311a5b0a55 feat(kimi): discover K3 on coding endpoint 2026-07-16 13:33:02 -07:00
arminanton cf73b3d411 fix(copilot): clamp reasoning effort to the nearest supported level, not xhigh->high
The Copilot provider profile unconditionally mapped ``xhigh`` to ``high`` before
checking the model's catalog, so models that DO support ``xhigh`` (e.g. the
gpt-5.x family per the live /models catalog) were silently capped one level
down.

Honor the requested effort when the catalog lists it as supported, and only
downgrade when it does not, choosing the nearest weaker supported level
(xhigh->high, minimal->low, else medium, else the first supported level). This
matches the nearest-down clamp behavior used elsewhere for the ``max`` effort.

Adds tests/plugins/model_providers/test_copilot_profile.py covering forward,
downgrade, and fallback paths (catalog lookup stubbed).
2026-07-16 08:47:10 -07:00
kshitijk4poor 5d9a72b7c2 fix(ollama-cloud): capability-gate reasoning_effort + correct disable semantics
Three follow-up fixes to the salvaged reasoning_effort support, all verified
live against ollama.com /v1/chat/completions + /api/show on deepseek-v4-pro,
gemma3, and qwen3-coder:

1. Capability-gate on /api/show 'thinking'. The original ignored the
   supports_reasoning flag and emitted reasoning_effort for every model. Now
   gated: only models whose native /api/show capabilities list contains
   'thinking' (deepseek-v4 yes; gemma3 / qwen3-coder no) get reasoning_effort.
   Mirrors the LM Studio pattern — capability resolved once per (model,
   base_url) in run_agent._supports_reasoning_extra_body via a cached probe
   (hermes_cli.models.ollama_model_supports_thinking), threaded into the
   profile hook as supports_reasoning. No live HTTP in the per-request path.

2. Disable actually disables. Ollama Cloud defaults to thinking ON and IGNORES
   the extra_body.thinking:{type:disabled} shape (verified: still returned
   reasoning). The only working off switch is top-level reasoning_effort:'none'.
   The salvaged code returned ({}, {}) for enabled:false / effort:none, leaving
   thinking ON. Now emits {'reasoning_effort': 'none'}.

3. Omit unrecognized effort. The original forwarded any unknown string verbatim
   including 'minimal' (a real Hermes effort level). Ollama Cloud rejects
   unrecognized values with a hard HTTP 400 (accepted set: low/medium/high/
   max/none), so forwarding 'minimal' would break the request. Now omitted.

Core touches (run_agent.py, hermes_cli/models.py) add the capability probe;
the plugin profile only consumes the resolved flag. 24/24 profile tests green;
194 provider/transport tests unaffected.
2026-07-16 07:58:04 -07:00
otsune 3fccd698fd feat(kanban): attachment toolset + CLI to match the dashboard surface
The kanban board has had full attachment storage and a dashboard HTTP
API (upload/list/download/delete) since #35338, but there was no agent
toolset tool and no `hermes kanban` CLI verb for attachments. Agents and
scripts that don't go through the dashboard server (or can't touch the DB
directly) had no way to create or read real attachments — only links in
comments.

Close that gap by mirroring the existing comment surface:

- `kanban_db.store_attachment_bytes()` — one shared write path (validate
  name, enforce the 25 MB cap, write the blob under the per-task dir with
  collision-free naming, insert the metadata row, clean up an orphan blob
  if the insert fails). `_MAX_ATTACHMENT_BYTES`, `_safe_attachment_name`,
  and a new `_collision_free_path` move here so the dashboard, the tool,
  and the CLI all share one implementation and can't drift.
- Tools (`tools/kanban_tools.py`): `kanban_attach` (inline base64),
  `kanban_attach_url` (server-side http/https fetch with the same cap),
  `kanban_attachments` (list). Write tools respect worker task-ownership;
  list is read-only. Registered in the `kanban` toolset.
- CLI (`hermes_cli/kanban.py`): `attach <id> <path>`, `attachments <id>`,
  `attach-rm <attachment_id>`.
- Dashboard `upload_task_attachment` now imports the shared helpers and
  uses `_collision_free_path` — behavior identical (still streams to disk
  with the cap, still 413 on overflow).
- Docs (AGENTS.md, kanban-worker skill) and toolset membership updated.

Tests: tool round-trip + oversize + bad base64 + ownership; attach_url
against a local HTTP fixture incl. oversize-mid-stream and non-http
scheme rejection; CLI attach/attachments/attach-rm; shared-helper unit
tests; dashboard parity preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 07:33:14 -07:00
AhmetArif0 5a39f0501c fix(video-gen): omit duration for range-based FAL families when unspecified
_clamp_duration returned durations[0] for all families when duration=None,
causing pixverse-v6, seedance-2.0, and kling-v3-4k to always send their
minimum value (1s, 4s, 3s respectively) instead of omitting the field and
letting the FAL endpoint apply its own default.

Range families are now detected via the existing _is_duration_range
heuristic and return None (field omitted) when no duration is requested.
Enum families like veo3.1 keep sending their first entry as the default.
2026-07-16 04:24:10 -07:00
Jeff Watts d68ac9092a test(photon): cover hidden Windows sidecar spawns 2026-07-16 01:03:43 -07:00
Erosika 155a792013 Merge origin/main: keep unified declared-schema config surface
Conflict resolutions:
- web_server.py: keep the branch's unified provider-config handlers
  (declared schema served by default, profile-scoped) over main's
  surface=declared query param. Main's declared-surface helpers and the
  hermes_cli.memory_providers import are dropped — the branch deleted
  that module when provider schemas moved into their plugins, so main's
  code path could no longer import.
- hermes.ts: keep profileScoped() calls without ?surface=declared to
  match the unified backend.
- constants.ts: take main's new reasoning effort values (max, ultra),
  keep the branch's memory provider ordering.
- config-settings.tsx: take main's FallbackModelsField import, drop the
  now-unused SECTIONS import (branch replaced it with
  sectionFieldEntries).
- provider-config-panel.test.tsx: file moved to settings/memory/ on the
  branch; main's act() fixes targeted the old tests, and the rewritten
  suite produces no act() warnings, so the old path stays deleted.
- test_web_server.py: keep both sides' new tests (main's MoA endpoint
  tests plus the branch's Honcho provider tests).
2026-07-15 17:50:31 -04:00
Teknium 7c954969b7
fix(auxiliary): route direct-create aux callers through call_llm (#65029)
* fix(auxiliary): route direct-create aux callers through call_llm (#35566)

Five callers (kanban_decompose, kanban_specify, profile_describer, and
goals.py's judge + draft-contract) built raw clients via
get_text_auxiliary_client() and passed extra_body=get_auxiliary_extra_body()
— which only returns Nous portal tags and ignores
auxiliary.<task>.extra_body from config.yaml entirely. That was the
remaining half of #35566 after the call_llm path was fixed.

Routing them through call_llm(task=...) gives each caller the full
auxiliary contract for free: task extra_body, the reasoning_effort
shorthand, transient retries, provider-profile projection, and fallback
chains. goal_judge gains a DEFAULT_CONFIG block (it had none — its
provider/model overrides silently didn't exist as documented keys).

get_auxiliary_extra_body() now has zero non-test callers; kept for
plugin back-compat.

Fixes #35566.

* test: migrate kanban dashboard + CLI specify mocks to call_llm

Two more consumers of specify_task mocked the old
get_text_auxiliary_client symbol (missed in the first sibling sweep —
they live outside tests/hermes_cli's kanban files): the dashboard
plugin's /specify endpoint tests and the /kanban slash-command E2E.
Same migration as the rest: mock call_llm at the source, no-provider
now surfaces via the LLM-error branch.
2026-07-15 07:39:17 -07:00
Epoxidex 8662254ab2 fix(ollama): emit top-level reasoning_effort=none on /v1/chat/completions (#25758)
Ollama's /v1/chat/completions silently ignores extra_body.think (it only
honours it on /api/chat — ollama/ollama#14820), so agent.reasoning_effort:
none never actually disabled thinking on OpenAI-compatible Ollama routes.
Emit the top-level reasoning_effort='none' field (which Ollama respects)
alongside think=False (kept for proxies and the native /api/chat path).

The PR's second half (propagating reasoning_config to the background-review
fork) already landed on main via agent/background_review.py, so only the
provider-profile change is salvaged here, resolved onto the current
GLM/effort-aware profile.

Salvaged from PR #29820 by @Epoxidex.
2026-07-15 06:38:28 -07:00
Ben Barclay 9884b4faad
fix(cron/chronos): cache PyJWKClient across fires to stop JWKS fetch storm (#64641)
The inbound cron-fire verifier constructed a fresh PyJWKClient on every
fire, discarding the client's key cache and forcing a synchronous JWKS
HTTP GET to the portal on each fire. Under a burst of concurrent fires
(a hosted instance with several cron jobs firing in the same window) this
fanned out into N simultaneous JWKS fetches that the portal rate-limited
(HTTP 403 -> verification fails -> agent 401), or that blocked the event
loop long enough that the fire webhook could not return its 202 before
the relay's 30s timeout (observed in prod as relay 504s concentrated on
high-job-count instances).

Cache one PyJWKClient per JWKS URL at module scope (double-checked lock)
so the signing keys are reused across fires; NAS keys rotate rarely, so
the steady state is zero JWKS fetches per fire.

Regression test proves 5 fires -> 1 client construction (was 5).
2026-07-15 09:27:35 +10:00
kshitijk4poor 1f41bdbecd fix(upstage): collapse unknown future efforts to high; behavior-contract tests
Review findings from the 4-angle pass:
- Unknown-but-enabled effort levels now collapse to Solar's strongest
  (high) instead of silently downgrading to the medium default — guards
  against the next #62650-style vocabulary addition. Explicit-empty
  effort keeps the medium default.
- fallback_models test now asserts the behavior contract (non-empty, no
  denied families) instead of freezing the exact model tuple
  (change-detector, AGENTS.md reject reason).
- Drop unused pytest import in test_upstage_provider.py.
2026-07-15 00:09:24 +05:30
kshitijk4poor f88cac71bc fix(upstage): map 'ultra' reasoning effort to Solar's high
Main added max/ultra effort levels (#62650) after this PR branched;
without the mapping 'ultra' silently fell through to the medium default.
Matches the xhigh/max collapse-to-strongest convention used by other
profiles.
2026-07-15 00:09:24 +05:30
Changhyun Min 35d3fc3b09 refactor(agent): drop the solar-pro rolling alias, default to solar-pro3
Pin the Upstage default to the concrete solar-pro3 instead of the
solar-pro rolling alias:
- plugin fallback_models is now ("solar-pro3",); entry [0] is the setup default
- drop the "solar-pro" context-window fallback entry (solar-pro3 covers it)
- update the reasoning default-on docstring and profile tests accordingly

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:09:24 +05:30
Changhyun Min 0031c5c371 refactor(agent): treat unknown Solar models as reasoning-capable
Invert the reasoning-support check from an allow-list (solar-pro,
solar-open) to a deny-list of the known non-reasoning families
(solar-mini, syn-pro). Newly released Solar models now get
reasoning_effort by default instead of having it silently dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:09:24 +05:30
Changhyun Min 20502b407c feat(agent): add Upstage Solar as a model provider
Adds Upstage Solar as a bundled model-provider plugin. Solar exposes an
OpenAI-compatible chat-completions endpoint at https://api.upstage.ai/v1, so
the generic chat_completions transport handles request/response/streaming/tool
calls — the profile is the core integration.

Provider registration (Upstage isn't in models.dev, so each registry that does
not auto-wire from the plugin layer needs an explicit entry — same pattern as
nvidia/gmi):
- plugins/model-providers/upstage/: UpstageProfile + plugin.yaml. Picker default
  and offline catalog list only the agentic Solar Pro models, led by `solar-pro`
  (rolling alias for the latest Pro). default_aux_model empty so aux tasks use
  the main model. `solar` alias. UPSTAGE_BASE_URL overrides the host.
- hermes_cli/providers.py: HERMES_OVERLAYS + label + `solar` alias, so
  resolve_provider_full('upstage') resolves (without this, an explicit
  `provider: upstage` in config was dropped and fell through to auto-detect).
- hermes_cli/auth.py: PROVIDER_REGISTRY entry + `solar` alias, so `hermes
  doctor` / resolve_provider recognise upstage (the static-registry path the
  lazy profile-extension doesn't reliably cover at validation time).
- hermes_cli/models.py: CANONICAL_PROVIDERS entry places Upstage Solar in the
  curated picker order (above the auto-appended `custom`).
- agent/model_metadata.py: context-window fallbacks (/v1/models omits
  context_length); `solar-pro` carries the 128K Pro context as the catch-all.

Reasoning: UpstageProfile.build_api_kwargs_extras wires Solar's top-level
`reasoning_effort` (low|medium|high; xhigh/max→high). Reasoning-capable families
are solar-pro* and solar-open*; solar-mini/syn-pro never receive it. Defaults ON
at medium when unset (matches the /reasoning "medium (default)" label);
`/reasoning none` disables; explicit/saved settings are honored. No
reasoning_content echo handling needed (unlike DeepSeek/Kimi).

Web dashboard:
- web/src/pages/EnvPage.tsx: add an "Upstage Solar" provider group so
  UPSTAGE_API_KEY / UPSTAGE_BASE_URL appear under LLM Providers (not "Other").

Docs/tests:
- .env.example: documents UPSTAGE_API_KEY / UPSTAGE_BASE_URL.
- tests: profile wiring, reasoning_effort mapping (pro/open/mini, efforts,
  disabled, default-on), provider-resolver regression (resolve_provider_full /
  get_provider / solar alias / overlay), `solar-pro` default.

Testing: pytest tests/providers tests/plugins/model_providers
tests/hermes_cli/test_upstage_provider.py tests/run_agent/test_provider_parity.py
tests/hermes_cli/test_api_key_providers.py; ruff clean. Verified end-to-end:
`hermes doctor` shows "Upstage Solar", and live chat works via both
`--provider upstage` and `--provider solar`. Reasoning wire format per
https://console.upstage.ai/api/docs/for-agents/raw. Platforms tested: macOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:09:24 +05:30
Jeffrey Quesnelle 77d5b2d573
Merge pull request #59846 from bbednarski9/bbednarski/nemo-relay-upgrade
feat(nemo-relay): nemo-relay observability version upgrade to support dynamic plugin activation
2026-07-14 13:15:49 -04:00
Bryan Bednarski 7e201fa1b6
fix(nemo-relay): align dynamic plugin configuration
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-07-13 17:01:08 -06:00
kshitijk4poor 2fc3f9c1ff fix(deepinfra): harden multimodal provider routing
Prevent credential forwarding across catalog redirects, retain explicit opt-in semantics for paid media backends, fail closed on invalid provider configuration, avoid mixed-catalog and output-limit assumptions, and reserve native STT provider names.
2026-07-14 02:59:39 +05:30
Georgi Atsev fe002eb124 feat(providers): Support DeepInfra as an LLM provider 2026-07-14 02:59:39 +05:30
Bryan Bednarski 2d2bed5891
Merge origin/main into bbednarski/nemo-relay-upgrade
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-07-13 14:50:20 -06:00
Teknium 2bd721cebc test(kanban): remove duplicate final-results footer 2026-07-13 01:49:35 -07:00
Teknium 98b4562947 fix(kanban): make Done-card results actionable 2026-07-13 01:49:35 -07:00
iborazzi deae8e3b4d feat(kanban): surface final_result for Done cards; show run summary when task.result is empty 2026-07-13 01:49:35 -07:00
Teknium a10081f83b test(image-gen): cover Codex capability HTTP boundary 2026-07-12 23:43:49 -07:00
Teknium 402969670d fix(image-gen): classify unsupported Codex image accounts 2026-07-12 23:43:49 -07:00
Teknium aaf5691261
feat(kanban): collect project directory when creating boards (#63249) 2026-07-12 17:51:30 -07:00
Teknium f67aae3230
fix(kanban): make scratch cleanup explicit in dashboard (#63123) 2026-07-12 04:15:59 -07:00
Teknium 7550c594ce
feat(reasoning): add max and ultra effort levels (#62650) 2026-07-12 00:26:49 -07:00
teknium1 31152ae108 fix(providers): align Fireworks integration with project policy 2026-07-11 05:43:35 -07:00
Alex Jestin Taylor c97d9a4c07 feat(providers): add Fireworks AI as preferred provider
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.

- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
  so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
  models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
  override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
  valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
  to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
  profile fallback_models, so the default is an agentic chat model instead of
  whatever the live catalog returns first (Fireworks listed an image model,
  flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
  Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
  i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
  doctor incl. the slash-form regression, aux headers, credentials), discovery
  spot-check, and a live smoke test driven through the Hermes runtime.

Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
2026-07-11 05:43:35 -07:00
Erosika f000fbe5c5 revert(memory): drop the provider actions extension point
No bundled provider declares actions, and the motivating request
(openviking, #56309) needs dynamic select options rather than actions.
Removing the unconsumed POST dispatch surface keeps this PR focused on
the config panel; the extension point can return as its own PR with its
first real consumer.
2026-07-10 15:41:52 -04:00
teknium1 a801046669 fix(memory): resolve() the shared-connection registry key; symlink test
Follow-ups for salvaged PR #43819: the registry key was
str(Path(db_path).expanduser()) — a symlinked or relative path to the
same DB file got its own connection, silently reintroducing the exact
multi-writer contention the registry prevents. Key on Path.resolve()
(OSError-tolerant fallback). Adds a symlink regression test and the
AUTHOR_MAP entry for adambiggs.
2026-07-09 18:17:40 -07:00
Adam Biggs b5226caff8 fix(memory): share one SQLite connection per holographic store database
Every MemoryStore instance opened its own SQLite connection guarded by
its own RLock. Several providers coexist in one process (the main agent
plus every delegate_task subagent), so instances pointing at the same
memory_store.db raced as independent WAL writers. Combined with writes
that were not rolled back on error, one connection could leave an open
write transaction that pinned the write lock and made every other
connection's writes fail with "database is locked" for the full busy
timeout.

Instances for the same database now share ONE process-wide connection
and ONE re-entrant lock, so access is fully serialized and
cross-connection contention is impossible. The shared connection is
refcounted: closing one instance never tears it out from under a live
sibling, and the last close releases it. The connection runs in
autocommit (isolation_level=None) so a write that raises mid-method can
never leave a dangling transaction holding the write lock; the existing
explicit commit() calls become harmless no-ops.

The provider's shutdown() now calls the refcount-guarded close() instead
of just dropping the reference: leaving finalization to GC kept the
connection (and its write lock) alive indefinitely on long-running
gateways, prolonging the exact contention this fix removes. The last
provider now releases the connection deterministically while siblings
stay live; regression tests fail without the wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:17:40 -07:00
Erosika 1508ece2d9 Merge origin/main: unify declared-schema and instance-schema provider config
Main's #60569 (dashboard memory provider switching) rewrote the same
GET/PUT /api/memory/providers/{name}/config routes this PR owns — the
dashboard and desktop share one backend. Resolve by dispatching: providers
that declare a config_schema.py get the declared path (host-block storage,
locked honcho writes, profile scoping, actions); everything else keeps
main's instance get_config_schema()/save_config path unchanged, so the
dashboard's PluginsPage behavior is preserved for instance providers.
Setup manifests, the /setup endpoint, provider switching, and name
validation from #60569 apply to both paths. Instance payloads gain the
docs_url/actions keys the desktop panel expects; declared payloads gain
the setup block the dashboard expects. Main's hindsight/honcho instance-
schema tests are updated for the dispatch, and its status test gets the
HOME pin it was missing (it read the developer's real ~/.honcho).
2026-07-08 15:03:24 -04:00
teknium1 5e51b123f3 feat(mem0): add self-hosted mode to the setup wizard
The salvaged SelfHostedBackend made self-hosted servers reachable via
mem0.json / MEM0_HOST, but the setup wizard still offered only Platform
and OSS — exactly the gap users hit (Discord report: 'At memory setup
there's only 2 options'). Adds a third wizard mode:

- interactive picker: Platform / Self-hosted server / Open Source
- non-interactive: hermes memory setup mem0 --mode selfhosted
  --host http://... [--api-key ...] [--dry-run]
- host -> mem0.json (behavioral), API key -> .env as MEM0_API_KEY
  (secret), optional key for AUTH_DISABLED servers
- best-effort reachability check against the server, non-fatal
- README + memory-providers docs updated with the wizard path
2026-07-07 14:07:16 -07:00
kshitijk4poor 9a322726ae fix(mem0): prune dead get_all, wire rerank config default, warn on MEM0_HOST env override
Review follow-ups on the salvage:

- get_all() pruned from the ABC and all three backends: mem0_list (its
  only caller) was removed by the recall-tuning commit, leaving new,
  tested, unreachable code — including SelfHostedBackend's _MAX_TOP_K
  over-fetch workaround. Tests for it dropped; fake-class stubs remain
  harmlessly. (The #52921 true-total fix lives on in the PR history if
  a lister ever returns.)
- The persisted rerank config key was write-only (setup prompted for it,
  nothing read it). initialize() now parses it into _rerank_default and
  mem0_search uses it when the model doesn't pass rerank explicitly;
  per-call args still win. Guard test added.
- Platform-mode setup now warns when MEM0_HOST is set in the environment:
  the json host-clear can't help there (_load_config seeds host from the
  env var, docs tell users to put it in .env) — the user would silently
  keep routing to the self-hosted server.
- SelfHostedBackend: connect-level retries (httpx.HTTPTransport(retries=2))
  so a single transient blip doesn't count toward the provider breaker;
  transport now injectable and the test helper uses the real __init__
  instead of mirroring it via __new__.
- plugin.yaml description no longer leads with reranking (off by default,
  platform-only); docs em-dash typo fixed.
2026-07-08 01:57:11 +05:30
kshitijk4poor 53edf6f983 fix(mem0): make prompt label + platform setup honor host routing precedence
Follow-up on the salvaged #55614. The PR added host-based routing to
_create_backend (precedence: oss > host > platform) but two sibling surfaces
didn't mirror it:

- system_prompt_block() checked host before oss, so an oss+host config ran
  OSS but told the model it was self-hosted HTTP. Reordered to match routing.
- Platform setup (hermes memory setup mem0 --mode platform) left a stale host
  in mem0.json; since host beats platform, the user kept routing to the
  self-hosted server. save_config merges (no delete), so clear host to ""
  rather than pop() so the merge actually overwrites it.

Adds regression tests for both (mutation-checked).
2026-07-08 01:57:11 +05:30
kartik-mem0 2a14205ff4 feat(mem0): self-hosted dashboard backend + recall tuning (salvage #55614)
Salvage of #55614 by @kartik-mem0 (mem0 maintainer). Adds a SelfHostedBackend
that talks to a self-hosted Mem0 Docker server over httpx (X-API-Key auth,
/search + /memories routes), gated behind `host`. Also folds in the mem0
research-team recall tuning that rides with it: rerank defaults to false across
all modes, the mem0_list tool is removed (5->4 tools), search guidance is
de-shouted, and self-hosted get_all reports the true stored total (#52921).

Supersedes the self-hosted portion of #52487 (@liuhao1024, first-submitted).

Closes #52478
Fixes #52921
2026-07-08 01:57:11 +05:30
Erosika 35099685be feat(memory): provider actions extension point
Providers with behavior beyond fields (validate a server, start a local
instance, link a CLI profile) previously had no mount besides forking the
panel. Let the schema declare actions and one generic endpoint run them:
ProviderAction on ProviderConfigSchema, POST
/api/memory/providers/{name}/actions/{action} dispatching to
ACTION_HANDLERS in the plugin's config_actions.py (path-loaded and
import-light, like the schema), profile-scoped and off the event loop.
Handlers get the submitted values dict, return a JSON-able result, and
raise ValueError for user-facing 400s. The panel renders declared actions
as generic buttons beside Save. No bundled provider declares actions yet.
2026-07-07 11:00:25 -04:00
Erosika 76c063b3d9 fix(memory): profile-scope the provider config endpoints
The settings page follows the desktop's active-profile switcher, but the
provider config calls didn't: no profileScoped() on the client and no
profile param on the backend, so a multi-profile desktop edited the serving
process's config while every surrounding card showed the selected profile's.

Accept ?profile= on both endpoints and resolve inside _profile_scope (the
skills/toolsets contract), spread profileScoped() into the two client calls,
and key the schema cache on the resolved config_schema.py path instead of
the provider name — user-installed plugins are per-profile, so one profile's
lookup must never answer for another's.
2026-07-07 10:53:43 -04:00
teknium1 9420f1acb6 test(google_meet): assert ladder-based dependency install instead of bespoke pip argv 2026-07-07 04:09:35 -07:00
Bryan Bednarski f846a350c7
fix(nemo-relay): harden dynamic plugin lifecycle
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-07-06 18:04:06 -06:00
Bryan Bednarski 2f74e29e3b
fix(nemo-relay): preserve managed interceptor outcomes
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-07-06 17:06:04 -06:00
Bryan Bednarski 0c8cf21882
feat(nemo-relay): activate dynamic plugins
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-07-06 14:59:36 -06:00
Jash Lee 3cd93f6aa8 fix(photon): auto-reinstall stale sidecar deps before start
A `hermes update` that bumps the spectrum-ts pin rewrites the Photon
sidecar's package-lock.json but never reinstalls node_modules. The sidecar
then spawns against the old install and the v8 postinstall patch throws
"@spectrum-ts/imessage dist not found", so the gateway retries the photon
platform every 300s forever without ever repairing the deps. Observed in
the wild: a June pin bump to spectrum-ts 8.0.0 left node_modules at 3.1.0,
and inbound/outbound iMessage stayed dead for days with the reconnect loop
faithfully restarting into the identical broken state.

_start_sidecar only checked that node_modules exists, not that it matches
the lockfile, so restart never became repair. Detect the skew with the same
signal npm ci uses: the top-level package-lock.json being newer than npm's
node_modules/.package-lock.json install marker. When stale, reinstall
(npm ci, falling back to npm install) before spawning. The reinstall runs
via asyncio.to_thread so a cold install can't block the event loop and stall
every other platform's traffic; worst case it heals on the next reconnect
tick instead. First-run "deps not installed" behavior is unchanged, and a
missing/unreadable marker fails safe to "not stale" so start is never
blocked.

Reuses the existing npm ci -> npm install fallback from
`hermes photon install-sidecar`. Adds unit tests for the staleness signal
(stale / fresh / missing-marker).
2026-07-05 17:38:32 -07:00
Teknium a6079dd350
feat(providers): GLM-5.2 native reasoning_effort controls (#58884)
Port from Kilo-Org/kilocode#11555: GLM-5.2 exposes a native
reasoning_effort knob with two enabled levels (high / max) on its
OpenAI-compatible endpoints. Previously the zai profile (direct Z.AI
/api/paas/v4) used the base ProviderProfile and emitted nothing, and the
OpenCode Go profile only handled Kimi K2 / DeepSeek — so a user's effort
preference for GLM-5.2 was silently dropped on both routes.

- zai: ZaiProfile maps effort onto high/max (xhigh/max -> max, lower -> high)
- opencode-go: same mapping for GLM-5.2, alongside existing Kimi/DeepSeek
- alias spellings recognized (glm-5.2 / glm-5-2 / glm-5p2, vendor-prefixed)
- disabled / no effort leaves the server default untouched
2026-07-05 13:48:01 -07:00
dsad e02cef0d0d fix(memory): guard local uploads against credential reads 2026-07-05 03:14:49 -07:00
kshitijk4poor 67df958dbe fix(custom-provider): emit reasoning_effort at the live profile path
PR #57601's original branch added a top-level reasoning_effort emit to the
LEGACY build_kwargs path (agent/transports/chat_completions.py), but
provider=custom resolves to CustomProfile (plugins/model-providers/custom/),
so chat_completion_helpers takes the profile path and returns early — the
added branch was unreachable dead code for every custom endpoint.

Move the fix to its real site, CustomProfile.build_api_kwargs_extras(), and
follow the DeepSeek/Zai profile precedent:
  - disabled            -> extra_body.think = False (unchanged)
  - enabled + effort    -> TOP-LEVEL reasoning_effort (the OpenAI-compatible
                           format GLM-5.2/ARK expect), passed through verbatim
                           incl. max/xhigh
  - enabled + no effort -> omit, so the endpoint's server default applies
                           (avoids silently forcing 'medium' as the original
                           branch did)

Deliberately does NOT force think=True on enable — that flag is Ollama-only
and risks a 400 on GLM/vLLM endpoints that don't recognize it; thinking is
already server-default-on for these backends.

Verified end-to-end through the real profile dispatch (temp HERMES_HOME):
custom+high -> reasoning_effort=high; custom+max -> reasoning_effort=max;
custom+none -> think=False; custom+unset -> nothing; num_ctx composes.

Adds tests/plugins/model_providers/test_custom_profile.py (13 cases).
Addresses the custom-provider half of #55276.

Co-authored-by: huanshan5195 <huanshan5195@users.noreply.github.com>
2026-07-04 14:19:44 +05:30
kshitijk4poor 104232979d fix(xai): route video-gen local inputs through the shared read guard
Fold the xAI video credential-read guard into the same shared
agent.file_safety.raise_if_read_blocked chokepoint this PR introduces for
the image providers, so the whole image+video bug class is covered by one
enforced boundary. Consolidates the parallel salvage of #57695 (xAI
image+video) into this PR; #57727 is now redundant and will be closed.

- video_gen/xai: guard _image_ref_to_xai_url and _video_ref_to_xai_url
  (the video image + video byte-read chokepoints) via the shared helper.
- Regression tests: symlinked auth.json with .png/.mp4 names are blocked
  across both video read paths (mutation-checked).
2026-07-03 18:47:53 +05:30
kshitijk4poor c1826e2690 fix(image-gen): route local-input credential guard through one shared chokepoint + cover xai (#57698)
Follow-up to the per-provider guards. Three improvements from review:

1. Extract agent.file_safety.raise_if_read_blocked() as a single shared
   chokepoint and route the OpenAI, OpenRouter, and (newly) xAI image
   providers through it, replacing the 3x-duplicated inline try/except.
   Fixes the whole bug class: xai/_xai_image_field read a model-supplied
   local path via open() with no guard — the same vulnerability the PR
   fixed for OpenAI/OpenRouter, in a sibling provider it missed.
2. Strengthen the regression tests from pass-on-any-ValueError to true
   security invariants: spy open()/read_bytes() and assert the blocked
   credential is NEVER read; add negative controls (legit local image
   still loads; remote/data: URIs pass through unguarded) so a
   block-everything regression can't pass.
3. Guard is best-effort by design (defense-in-depth, not a security
   boundary) — documented on the shared helper.

- agent/file_safety.py: raise_if_read_blocked()
- plugins/image_gen/{openai,openrouter,xai}: route through helper
- tests: no-read spies + negative controls across all three providers
2026-07-03 18:47:53 +05:30
dsad 587be5b5b4 fix(image-gen): guard local provider inputs against credential reads 2026-07-03 18:47:53 +05:30
Erosika 41e59f6126 fix(memory): retry failed config schema loads instead of caching None
A syntax error in a provider's config_schema.py was cached as 'no schema'
until process restart, rendering a silent empty panel even after the file
was fixed. Cache only successful loads and genuine file-absence.
2026-07-02 18:42:31 -04:00
Erosika 2a632807e0 refactor(memory): move provider config schemas into their plugins
Each provider now declares its config surface in config_schema.py inside
its own plugin dir (plugins/memory/<name>/), loaded by file path like the
plugins themselves so plugin __init__ imports never reach the web server.
hermes_cli/memory_providers.py is gone; the shared field primitives and
loader live in plugins/memory/config_schema.py, and the schema tests move
to tests/plugins/memory/ alongside the other per-plugin suites.
2026-07-02 16:55:39 -04:00
Brooklyn Nicholson 5a6720b884 fix(desktop,tui-gateway,zai): stop thinking-off from reverting to medium
A Z.ai desktop user reported thinking reverting to medium after one turn,
burning ~200% of a week's credits in 4 days despite reasoning_effort: false
in config.yaml. Four compounding bugs:

- _session_info reported reasoning_effort "" for disabled reasoning,
  indistinguishable from unset — the desktop adopted it after the first
  turn, wiping its sticky "thinking off" pick so every later chat
  reverted to the default effort.
- config.set key=reasoning always wrote agent.reasoning_effort to global
  config.yaml, so every desktop model-menu selection (preset.effort ??
  'medium') clobbered the user's configured value. Now session-scoped
  like the messaging gateway's /reasoning, landing on
  create_reasoning_override so lazily-built sessions keep it too.
- YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced
  to "" by every loader's `str(x or "")`, silently re-enabling thinking.
  parse_reasoning_effort now treats False/"false"/"disabled" as
  {"enabled": False}; loaders (tui gateway, gateway, cli, cron,
  delegate) pass the raw value through. The desktop config reader also
  crashed on the boolean (false.trim()), aborting voice/STT settings.
- The zai provider profile never sent thinking on the wire, and GLM-4.5+
  defaults to thinking ON server-side — so disabling reasoning was a
  silent no-op on direct Z.ai, the actual token burner. The profile now
  emits extra_body.thinking {"type": "enabled"|"disabled"} for
  thinking-capable GLM models, mirroring the DeepSeek profile.

Also: /new (session reset) now carries reasoning_config across the
rebuild like model_override; config.get reasoning prefers the session's
live value and maps a config False to "none"; Settings shows "Off"
instead of a blank select for hand-written false.
2026-07-02 15:23:47 -05:00
kshitijk4poor 019950560d refactor(image-gen): reuse shared image sniffer + raster allowlist in codex backend
Replace the plugin-local _IMAGE_MAGIC_MIME table + _sniff_image_mime
body with a delegation to agent.image_routing._sniff_mime_from_bytes,
the canonical magic-byte sniffer already used across the codebase, then
gate its result to the raster formats gpt-image-2's Responses
input_image actually accepts (png/jpeg/gif/webp).

The shared sniffer also recognizes SVG/TIFF/ICO; without the allowlist
those would pass local validation and be rejected server-side with an
opaque HTTP 400. Gating locally fails them cleanly as invalid_image_input.
Adds a regression test for SVG rejection.

Follow-up on top of @CrazyBoyM's #55828.
2026-07-02 17:12:24 +05:30
CrazyBoyM 460235d584 test(image-gen): cap Codex reference inputs 2026-07-02 17:12:24 +05:30
CrazyBoyM ecffd290a3 feat(image-gen): support Codex image inputs 2026-07-02 17:12:24 +05:30
martinramos002-bot d173e8c3a7 fix: protect cron output root from cleanup
Only classify files below cron/output/ as disposable cron output.
The cron/output directory itself is a durable container for retained
job history and should not be tracked or deleted wholesale.

Add regression coverage for both category detection and cleanup of a
stale tracked entry pointing at the output root.
2026-07-01 15:42:04 +05:30
Teknium 259e6b87a7 fix(teams-pipeline): reject dot-only recording display_name
Path(raw).name reduces '..'/'.'/'' to themselves, so basename
extraction alone still let a Graph-provided display_name of '..' or
'../' escape the temp recording directory (tmp_dir / '..' resolves to
the parent). Reject the dot-only basenames explicitly and fall back to
the artifact id. Extends @outsourc-e's regression coverage with the
dot-only cases.
2026-07-01 02:03:48 -07:00
Eric ac18a8658b test(teams-pipeline): cover path traversal sanitization 2026-07-01 02:03:48 -07:00
cyb3rwr3n cb6d6d46ab fix(memory/holographic): sanitize FTS5 queries for natural-language recall
The FactRetriever's _fts_candidates passed the raw query string directly
to FTS5's MATCH operator. FTS5 defaults to AND-between-tokens, which
means any multi-word prose query like 'what happened with the deployment
rollback' required every single token to co-occur in a fact — dropping
recall to zero on the kind of queries agents actually issue via prefetch().

Fix: add _sanitize_fts_query() that:
- tokenizes the query and drops English stopwords
- strips FTS5 operator characters per token
- OR-joins the remaining content tokens as phrase literals

For pathological inputs (all stopwords, empty), falls back to the raw
query so the caller sees zero results instead of a SQL error.

This is a pure-retrieval-quality fix — the HRR + Jaccard reranking
stages still keep precision high. Ships with 10 tests covering the
sanitizer and retrieval integration.
2026-06-30 15:55:11 -07:00
xxxigm 63731fe856 test(image-gen): cover Nous/OpenRouter top-level model resolution
Assert image_gen.model, explicit model kwargs, and Nous provider wiring
so the config path mismatch cannot regress.
2026-06-30 19:11:49 +05:30
Kartik c6eb7f9e72
fix(memory/mem0): recall on the current question + stronger search guidance (#55535) 2026-06-30 15:51:08 +05:30
Jaaneek 9ce79cd642 feat(xai): Imagine public-URL storage, chaining & video edit/extend
Add durable public-URL output and URL-based chaining to xAI Grok Imagine:

- Store generated media on files-cdn with permanent public HTTPS URLs
  (public_url: true, no expiry by default).
- Chain by URL: generate -> edit -> extend each take a prior result's
  public HTTPS URL (or a data URI / local file for inputs).
- Add provider-specific xai_video_edit and xai_video_extend tools.
- Image generation: public-URL/storage output, multi-reference edits,
  and ~/ local-path support for image edits.

Credentials use xAI Grok device-code OAuth (separate PR).
2026-06-29 21:11:58 -07:00
Ben Barclay 53a75f147f
feat(dashboard_auth): support confidential clients (client_secret) in self-hosted OIDC (#55344)
The self-hosted OIDC dashboard provider was public-client + PKCE only, with
two `# TODO(confidential-client)` seams. Authentik and Keycloak commonly
default a new OIDC client to *confidential*, whose token endpoint rejects an
unauthenticated exchange (`invalid_client`) — so a self-hoster who accepts
their IDP's default could not complete dashboard login without manually
flipping the client to public.

Add optional confidential-client support:

- New optional `client_secret` (env `HERMES_DASHBOARD_OIDC_CLIENT_SECRET`,
  or `dashboard.oauth.self_hosted.client_secret`; env-wins-config, empty
  treated as unset). It is a credential, so docs steer operators to the
  `.env` file; config.yaml is supported only for precedence symmetry.
- `_token_endpoint_auth()` selects `client_secret_basic` (HTTP Basic header)
  vs `client_secret_post` (form body) from the IDP's advertised
  `token_endpoint_auth_methods_supported`, defaulting to basic (the OIDC
  default) when absent. Applied to complete_login, refresh_session, and
  revoke_session (RFC 7009 §2.1).
- PKCE is sent in BOTH modes — the secret is client authentication layered
  on top, never a replacement (OAuth 2.1 / RFC 9700 keep PKCE mandatory).
- Basic header url-encodes client_id/secret before base64 per RFC 6749
  §2.3.1, so reserved chars (`:`, `@`, space) round-trip correctly.

Non-breaking: with no secret configured the provider is a pure public PKCE
client, byte-identical to prior behaviour (no Authorization header, no
client_secret in the body). The secret is never logged — register() reports
only a `confidential=<bool>` flag.

Tests: 16 new cases covering basic/post selection, default-when-absent,
public-unchanged contract, PKCE-preserved, reserved-char url-encoding,
blank-secret-is-public, refresh + revoke auth, no-secret-in-logs, and
env/config register wiring. Full dashboard-auth suite (nous provider,
middleware, gate, cookies, WS, 401-reauth, status endpoint) — 396 tests —
green, proving no existing auth path regressed.
2026-06-30 13:32:51 +10:00
Joey Kerper f3d2dfbec6
fix(dashboard_auth): allow any http:// host in self-hosted OIDC redirect_uri (#55099)
The self-hosted OIDC dashboard login rejected any http:// redirect_uri
whose host was not localhost/127.0.0.1, surfacing "redirect_uri may only use http:// for localhost/127.0.0.1" before reaching the IDP. This broke self-hosted dashboards reached over plain HTTP (including LAN IPs, internal hostnames, and reverse proxies that terminate TLS upstream).

#38827 already dropped this check from the nous provider, but the generic self-hosted provider  copied the old localhost-only
branch and reintroduced the bug for HERMES_DASHBOARD_OIDC_ISSUER setups.

The IDP's own allowlist is authoritative on which redirect_uris are
permitted; this client-side _validate_redirect_uri is only a fast-fail for
obvious operator error and should not second-guess valid http:// deployments.

Fix: drop the localhost-only branch on the http scheme. Validation now enforces only that the scheme is http(s) and the path ends with
/auth/callback. Updated the docstring to explain the relaxed contract,
and added test_allows_http_with_arbitrary_host covering an internal
hostname and a LAN IP alongside the existing localhost case.
2026-06-30 09:45:11 +10:00
Ben Barclay 1289f12812 fix(memory): lazy-install supermemory + mem0 SDKs like honcho/hindsight
The supermemory and mem0 memory providers shipped third-party SDKs
(supermemory / mem0ai) that are not core dependencies, but — unlike the
honcho and hindsight providers — they imported those SDKs directly with
no tools.lazy_deps.ensure() preflight and had no LAZY_DEPS allowlist
entry. On the published Docker image the agent venv is sealed
(HERMES_DISABLE_LAZY_INSTALLS=1) and lazy installs are redirected to a
writable durable target (HERMES_LAZY_INSTALL_TARGET). honcho/hindsight
route through ensure() and install fine there; supermemory/mem0 never
called it, so their SDK was never installed on a hosted instance and the
provider silently reported itself unavailable even with the API key set.

Fixes:
- Add memory.supermemory + memory.mem0 to the LAZY_DEPS allowlist
  (tools/lazy_deps.py), pinned to current PyPI releases.
- Call ensure('memory.<x>', prompt=False) at each SDK-import chokepoint
  (_SupermemoryClient.__init__; Mem0MemoryProvider._create_backend),
  mirroring honcho's wrapped try/except shape.
- Drop the SDK-import gate from supermemory's is_available() — it was a
  chicken-and-egg trap (provider never loaded on a sealed venv, so
  ensure() never ran). Now key-presence only, like honcho/mem0.
- Add matching pyproject extras [supermemory]/[mem0]; update the
  lazy-covered-extras contract test (excluded from [all] by policy).

Tests prove each path fails without the fix and the real sealed-venv
durable-target gate accepts both features.
2026-06-29 00:25:36 -07:00
Teknium 7c0a5def58
fix(memory/holographic): close DB connection on shutdown instead of leaking to GC (#54133)
HolographicMemoryProvider.shutdown() dropped its MemoryStore reference
without calling the existing MemoryStore.close(). Since the connection is
opened check_same_thread=False (one per session), its fd was released by
refcount/GC at a non-deterministic time on a non-deterministic thread,
churning a DB fd through the kernel free pool on every session teardown.
Call close() so the fd is released deterministically.

Reported by @alfranli123 (#44037), who pinpointed the exact code location.
Note: the report's TLS-fd-recycle corruption attribution could not be
reproduced from the code — dropping a sqlite connection flushes valid
SQLite pages via the VFS, never TLS framing, and the provider is at most a
releaser of DB fds, not a TLS-flushing socket owner. This change is correct
resource hygiene that removes per-session fd churn regardless.
2026-06-28 02:41:52 -07:00
LeonSGP43 52a09d8faf fix(byterover): honor auto extract config 2026-06-27 04:04:15 -07:00
Mahesh Sanikommu 1b75b3fd90 feat(memory): add Supermemory setup connection summary
Add post_setup() and get_status_config() to the Supermemory memory
provider so `hermes memory setup` and `hermes memory status` print a
one-line connection summary (container, profile fact count,
auto_recall/auto_capture). Point API-key onboarding at the Hermes
connect URL (app.supermemory.ai/integrations?connect=hermes).

Salvage of #52988. Two fixes folded in:

- Test isolation: the new probe/status tests mocked _SupermemoryClient
  but not the __import__("supermemory") guard inside
  _probe_supermemory_connection, so they passed only where the optional
  supermemory package was installed and failed on a clean checkout / CI
  (the PR shipped with red CI). Added _stub_supermemory_importable()
  mirroring the existing test_is_available_false_when_import_missing
  pattern; the suite now passes with supermemory absent.

- post_setup: `if api_key and api_key not in os.environ` checked whether
  the key's *value* named an env var (always false in practice). Fixed to
  compare the value: `os.environ.get("SUPERMEMORY_API_KEY") != api_key`.

Verified: 38/38 in test_supermemory_provider.py and the full
tests/plugins/memory/ suite green with supermemory not installed.

Closes #52988
2026-06-27 15:07:34 +05:30
underthestars-zhy 8827300267 fix(photon): correlate tapbacks to bot message context
Populate `reply_to_message_id`, `reply_to_text`, and
`reply_to_is_own_message` on reaction events so the gateway injects
`[Replying to your previous message: "..."]` when the agent receives
a tapback.

The sidecar now extracts a capped text preview from the hydrated
reaction target (plain text and mixed group messages; null for
attachment/voice-only targets), emitting it as `targetText` in the
NDJSON reaction payload. The Python adapter reads this field and sets
the reply correlation fields on the `MessageEvent`.
2026-06-27 00:51:34 -07:00
underthestars-zhy 4345b3e767 fix(photon): upgrade spectrum-ts sidecar to v8.0.0
v8 made `richlink` outbound-only; inbound rich links now arrive as
plain `text`. Remove the `getBalloonBundleId`/`toRichlinkMessage`
branches from the iMessage mapper patch and update the fixture,
lockfile, and README accordingly.
2026-06-27 00:51:34 -07:00
underthestars-zhy 5636c22828 feat(photon): upgrade spectrum-ts sidecar to v7.0.0
Update the Photon platform plugin's Node.js sidecar from spectrum-ts
3.1.0 to 7.0.0, which splits the SDK into scoped `@spectrum-ts/*`
packages with `spectrum-ts` as the umbrella re-export.

- Bump exact pin in package.json/package-lock.json to 7.0.0
- Update mixed-attachments patch script to target the new
  `@spectrum-ts/imessage/dist/index.js` path and tab-indented output
- Rewrite test fixture to match v7.x mapper shape (tab-indented,
  `const ... = async` declarations, single-line builder calls) and
  point at `@spectrum-ts/imessage/dist/index.js`
- Update README upgrade guide to document the v5 package split and
  the postinstall patch validation step
- Update comments in cli.py and index.mjs to reference v5/v7 changes
2026-06-27 00:51:34 -07:00
Ben Barclay fbf748b282
fix(dashboard-auth): follow redirects on self-hosted OIDC discovery (#53399)
The self-hosted OIDC provider fetched the discovery document with a bare
httpx.get(). httpx defaults to follow_redirects=False (unlike curl -L or
the requests library), so when an IDP answers GET
/.well-known/openid-configuration with a 3xx — Authentik canonicalises the
.well-known path, and any IDP behind a reverse proxy doing an http→https
upgrade redirects too — the bare redirect (empty body) tripped the
status != 200 guard and raised 'OIDC discovery returned 302', which
routes.py maps to the provider_unreachable audit event and a 503. The
browser surfaced 'Auth provider self-hosted unreachable'.

The user's smoking gun (curl -o writing zero bytes from inside the
container) is exactly a redirect with no body — the same wall the code hit.

Add follow_redirects=True to the discovery GET only. It's safe: the
issuer-pin check and _require_https_or_loopback still validate the resolved
document and every endpoint, so a redirect can't smuggle in a bad issuer or
a cleartext endpoint. The token/revocation POSTs deliberately keep the
no-follow default (they carry an auth code / refresh token and the endpoint
is already the canonical absolute URL).

Existing discovery tests mocked httpx.get with a canned 200 and never
exercised a real 3xx. Add a regression test that runs a real loopback
server returning a 302 on the .well-known path — fails without the fix
(ProviderError: discovery returned 302), passes with it.
2026-06-27 14:14:51 +10:00
Nacho Avecilla dbe734beff
fix(dashboard-auth): exclude non-interactive providers from interactive login surfaces (#53239)
* Return None instead of erroring on drain login failure

* Fix login on drain

* Remove login for drained endpoints flow and clean the code

* chore: drop unrelated credits changes from this PR

* Remove extra comments that were not really necessary
2026-06-27 10:08:13 +10:00
Ben 2e322466b1 feat(dashboard-auth): drain shared-bearer-secret provider plugin
Task 2.0b: the concrete shared-bearer-secret auth provider, the FIRST consumer
of the generic token-auth capability (Task 2.0a). Implements decisions.md Q-A.

plugins/dashboard_auth/drain/ (bundled, discovered like dashboard_auth/basic):
- DrainSecretProvider: non-interactive provider, supports_token=True. Verifies
  an inbound Authorization bearer token against a per-agent shared secret with
  hmac.compare_digest (constant-time, no timing oracle) and, on a match,
  vouches for the caller as the "drain-control" principal scoped to "drain".
  The five interactive ABC methods raise NotImplementedError; verify_session
  returns None (stacks harmlessly in the cookie-verify loop).
- assess_secret_strength(): fail-closed entropy gate. Rejects secrets shorter
  than 43 url-safe-b64 chars (~256 bits), with < 16 distinct characters, or
  below 128 bits Shannon entropy — so a weak/structured/repeated secret can
  never be silently accepted. Enforced both at register() (friendly skip
  reason) and in __init__ (raises — defence in depth).
- register(ctx): no-op + skip reason when HERMES_DASHBOARD_DRAIN_SECRET is
  unset; rejects a weak secret fail-closed (drain endpoint stays gated). On a
  strong secret, registers the provider AND opts /api/gateway/drain into the
  generic token-auth seam via register_token_route().

Config: the secret is a CREDENTIAL → carried via HERMES_DASHBOARD_DRAIN_SECRET
(per-agent, provisioned by NAS at deploy). Behavioural knobs only
(dashboard.drain_auth.{scope,min_secret_chars}) live in config.yaml — added to
DEFAULT_CONFIG with the .env-is-for-secrets rationale documented inline.

Tests: tests/plugins/dashboard_auth/test_drain_provider.py — entropy gate
(strong pass; empty/short/repeated/few-distinct/custom-min reject), verify_token
(match → scoped principal, wrong/empty → None, custom scope), protocol
compliance, interactive-methods-raise, and register() (skip-no-secret,
fail-closed-weak-secret, strong-env-secret registers + route opt-in, config
scope + min_secret_chars). 21 new tests; drain + token-auth suites 44 passed.
Verified the plugin is discovered as dashboard_auth/drain alongside basic/nous.

Intentionally deferred:
- The begin/cancel-drain endpoint handler itself — Task 2.1.
- The dashboard→gateway control channel — Task 2.2.

Build status: dashboard-auth + drain-plugin suites green.
2026-06-26 00:47:19 -07:00
rob-maron 2c02583c2b fix shape 2026-06-25 12:38:33 -07:00
rob-maron 525ee58b43 krea 2026-06-25 12:38:33 -07:00
Brooklyn Nicholson e92b5c6af8 feat(pets): quality-first OpenRouter model chain + stronger atlas gates + global pet-gen notifications
OpenRouter/Nous image gen now runs a quality-first model chain by default:
attempt the highest-fidelity OpenAI image model first, then fall back to
Gemini 3 Pro Image when it's access-gated/unavailable/times out. An explicit
OPENROUTER_IMAGE_MODEL / config model override pins one model with no fallback.

Atlas validation rejects malformed model output instead of shipping it: adds a
per-state collapse guard (a single sliver/fragment row no longer passes because
other rows are healthy), on top of the existing postage-stamp + multi-pose
checks.

Desktop: pet-gen native notifications are now "global" (not tied to a chat
session), so a background generation started from the command center fires an
OS notification when the user is away even with no active session. Adds a
neutral "This can take up to 5 minutes." banner on step 1, and lets the
provider picker auto-size.

Tests updated/added for the OpenRouter fallback chain, the collapse guard, and
the global notification path.
2026-06-24 23:11:21 -05:00
Brooklyn Nicholson 3faf768cde feat(pets): OpenRouter + Nous Portal image backend
Reference-grounded image provider over the OpenRouter-compatible
chat-completions image protocol (Gemini Flash Image et al.). Nous Portal
proxies OpenRouter, so one provider serves both — giving pet generation a
reference-capable backend beyond OpenAI gpt-image.
2026-06-24 13:48:38 -05:00
kshitijk4poor ab9134bf16 feat(openviking): add full recall prefetch policy
Salvage of PR #48927 by @ehz0ah, which consolidates OpenViking recall
work from #41706 (@huangxun375-stack), #33260, #49975, and #32444.

Replaces stale background post-turn prefetch warming with synchronous
current-query recall. The old queue_prefetch warmed the PREVIOUS user
message while turn-start recall consumed the CURRENT one, so injected
context was always about the wrong topic.

Changes:
- prefetch() now does session-aware /api/v1/search/search with the
  current query, falls back to /api/v1/search/find on failure
- Contract-safe payloads: limit, score_threshold, context_type,
  session_id — no top_k, no search-body mode, no target_uri
- L2 content reads for items with level=2 or empty abstracts, capped
  at full_read_limit (default 2)
- Local ranking (score + query-token overlap + leaf boost), dedup,
  score threshold, and injected-char budget
- queue_prefetch() is now a no-op (background warming removed)
- Additive batched viking_read: uris param accepts up to 3 URIs
- Per-request timeout support on _VikingClient.get/post/delete
- Removes stale _prefetch_result/_prefetch_thread/_prefetch_generation
  state and _invalidate_prefetch_state()
- Strengthened system_prompt_block guidance

Salvage follow-up fixes:
- Expose all 8 recall config knobs in get_config_schema() (PR #48927
  had removed them; #41706 correctly exposed them). Env vars remain
  as internal mechanism but are now visible in setup wizard.
- Lower default timeout 8s→4s, request_timeout 6s→3s, full_read_limit
  3→2 to reduce per-turn blocking latency.

Co-authored-by: Hao Zhe <haozhe4547@gmail.com>
Co-authored-by: Eurekaxun <eurekaxun@163.com>
2026-06-24 18:53:49 +05:30
uperLu 0d4cecb352 fix(cron): avoid provider package shadowing core cron 2026-06-23 23:39:22 -07:00
teknium1 7f1c278db8 fix(photon): intercept console.log so 'stream interrupted' bursts escalate
spectrum-ts routes stream telemetry through @photon-ai/otel's createLogger,
which sends severity>=ERROR to console.error and WARN/INFO to console.log.
The two lines the health monitor keys off land on different channels:
log.error("stream persistently failing") -> console.error (caught), but
log.warn("stream interrupted; reconnecting") -> console.log (was missed).

The original interception patched console.error only, so the recovering->
degraded escalation counter never saw the interrupt bursts that are the
primary silent-inbound symptom. Verified live against spectrum-ts 3.1.0 +
@photon-ai/otel: 3 real log.warn('stream interrupted') calls now escalate
to degraded -> process.exit(75) -> adapter reconnect.

Adds a shared classifyStreamLog() fed by both console.error and console.log,
plus a regression test asserting both channels are intercepted.
2026-06-23 21:33:10 -07:00
XU SUN 0952acbf4d fix(photon): label upstream CatchUpEvents failures 2026-06-23 21:33:10 -07:00
helix4u 06cbc3bae9 fix(photon): recover degraded upstream stream 2026-06-23 21:33:10 -07:00
s010mn 221cd60242 feat: add reasoning_effort support to ollama-cloud provider
Map Hermes xhigh→max to unlock DeepSeek V4's 'Max thinking' tier
through Ollama Cloud's OpenAI-compatible /v1/chat/completions endpoint.
low/medium/high pass through unchanged; disabled/none suppress
reasoning entirely.

Empirically confirmed: reasoning_effort:max produces ~2.5× more
thinking tokens than high on deepseek-v4-pro:cloud (1576 vs 642).
2026-06-23 11:51:43 -07:00
Hao Zhe 027cb649ef fix(memory): fail closed on unclear write results 2026-06-22 07:00:42 -07:00
Hao Zhe c7e0501e9b fix(openviking): drain memory mirror workers on shutdown 2026-06-22 07:00:42 -07:00
Hao Zhe 70e7132e2f fix(openviking): gate memory writes and add viking_forget
Mirror built-in memory writes to external providers only after the native memory tool succeeds and is not staged for approval. Keep OpenViking's built-in memory mirroring add-only, since Hermes native memory entries do not yet have stable OpenViking file URIs for replace/remove.

Add a narrow viking_forget tool for exact user memory file deletion and document the current OpenViking write/delete behavior.
2026-06-22 07:00:42 -07:00
Kartik 2e779d11a0
feat(mem0): v3 API, OSS mode, update/delete tools, telemetry & review fixes (#15624)
* fix: update to version 3 endpoints and adding update and delete tool

* chore: removing the test md file

* fix: prevent circuit breaker on client errors in Mem0 provider

* chore: add telemetry for platform version

* feat: add OSS mode support to Mem0 memory provider

* chore: bump mem0ai dependency to >=2.0.1 in memory plugin

* refactor: enhance dependency checks and embedder config in mem0 backend

* refactor: adjust fact storage message for OSS mode

* refactor: expand user paths, add collection recreation on dimension change for Qdrant

* fix(mem0): make MEM0_USER_ID override gateway-native ids and tag writes with channel

When MEM0_USER_ID was configured (env or mem0.json), the gateway-native id
from kwargs (Telegram numeric id, Discord snowflake, ...) still won, so the
same human ended up under different user_ids per channel and memories never
merged across CLI / Telegram / Slack / Discord. Mirrors openclaw's cfg.userId
pattern: configured override wins, gateway-native id is the fallback.

The legacy "hermes-user" placeholder default written by the setup wizard is
treated as unset to avoid silently bucketing every gateway user together.

Also tag every write with metadata.channel (cli/telegram/discord/...) so the
dashboard can offer per-channel filtered views without coupling identity to
the channel; document the read/write filter asymmetry as intentional
(reads scope to user_id only for cross-agent recall).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: improve Mem0 memory provider backend, pagination, config, and error handling

* refactor: update mem0 telemetry code, docs, and bump version

* fix(mem0): make get_config_schema() return unified schema with mode-aware required flag

Schema always includes api_key field so picker shows "API key / local" for
both modes. In OSS mode api_key.required=False so status won't mislead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: improve mem0 telemetry, add env var key and OSS mode detection

* chore: bump mem0ai lower bound to 2.0.4 (latest SDK release)

* refactor: set telemetry sample rate to 1.0 and update docs for opt‑out

* fix(mem0): resolve 15 correctness, thread-safety, and resource bugs

Thread safety:
- Protect circuit breaker counters with _breaker_lock (race between
  prefetch/sync daemon threads and main thread)
- Wrap sync_turn thread creation in _sync_lock; skip if previous sync
  is still alive after 5 s join to prevent duplicate memory ingestion
- Guard _schedule_flush timer creation under _queue_lock (TOCTOU race)
- Capture local `backend` reference in prefetch/sync closures so
  shutdown() nulling self._backend cannot crash in-flight threads

Correctness:
- Fix bool("false")==True for rerank param; parse string values explicitly
- Guard page/top_k with max(1,...) and move int() inside try blocks
- Fix fact_count=0 always in OSS mode (Memory.add returns list, not dict)
- Fix prefetch() not clearing result when thread still alive after timeout
- Fix atexit.register accumulating on repeated initialize() calls

Backend / setup:
- Handle Qdrant named-vector collections in _recreate_collection_if_dims_changed
  (vectors is a dict; .size access raised AttributeError, swallowed silently)
- Wrap QdrantClient and psycopg2 conn/cursor in try/finally to prevent leaks
- Resolve ollama_bin at top of _ensure_ollama; use it for ollama pull
- Fix embedder key lookup when LLM provider has no env_var (e.g. ollama)

Also: remove _telemetry_enabled cache (env var check is cheap), bump
required mem0ai to >=2.0.7, minor README wording fix.

* fix(mem0): fix brittle qdrant path test + add telemetry sample-rate docs

- Replace generator-throw lambda with a proper def in
  test_qdrant_path_not_writable; use tmp_path instead of a hardcoded
  /nonexistent path so the test is root-safe
- Add MEM0_TELEMETRY_SAMPLE_RATE to memory-providers.md (was only
  in the plugin README, not the user-guide docs)

* revert: remove MEM0_TELEMETRY_SAMPLE_RATE from user-guide docs

* refactor: remove telemetry from mem0 plugin and update documentation

* fix(mem0): set stdin=DEVNULL on setup subprocess calls

The TUI stdin guard (scripts/check_subprocess_stdin.py) requires every
subprocess call in plugin code to set stdin= so it can't inherit the
gateway's JSON-RPC stdin fd. Muzzle the docker/ollama calls in the OSS
setup wizard with stdin=subprocess.DEVNULL (none need interactive input).
Also covers the docker-inspect call the linter's regex misses.

---------

Co-authored-by: chaithanyak42 <chaithanya.kumar42a@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-22 12:30:47 +00:00
峯岸 亮 5b45fb269a fix(security): sanitize kanban markdown html 2026-06-21 13:10:17 -07:00
Teknium 5e3e89cc05
feat(hindsight): configurable embedded daemon health grace timeout (#50341)
On resource-contended hosts the embedded Hindsight daemon can exceed a
single 2s /health check; upstream then waits a grace window before
treating it as stale and killing+restarting it (hindsight-embed reads
HINDSIGHT_EMBED_PORT_HEALTH_GRACE_TIMEOUT, default 30s, into a
module-level constant at import time). Users on busy boxes had no
Hermes-side way to raise it short of hand-setting an env var.

Add a 'port_health_grace_timeout' config.json option to the Hindsight
plugin. When set, initialize() exports it to the process env BEFORE
daemon_embed_manager is imported (the import-time read is the contract).
setdefault() so an explicit operator env override always wins. Exposed
in 'hermes memory setup' for local_embedded mode.

Follow-up to #50308 / issue #13125 comment thread.
2026-06-21 12:20:53 -07:00
teknium1 e267237671 test(photon): cover overflow retry, typing cooldown, sidecar-crash detection
Follow-up for salvaged PR #50256. Unit tests for the three behaviors:
retryable classification of Envoy/sidecar overflow strings, per-chat typing
cooldown with stop_typing reset, and the _supervise_sidecar crash-detection
path that raises a retryable fatal (and the clean-shutdown no-op).
2026-06-21 12:15:44 -07:00
LehaoLin 7bc6f18062 fix(hindsight): skip local_embedded daemon when running as root
PostgreSQL's initdb refuses to run as root, so the embedded Hindsight
daemon could never initialize its data directory under root. The
daemon-start thread would fail, retry, and loop forever — each cycle
reloading embedding models (~958MB RAM, ~33% CPU) with no user-visible
error, leaving Hermes sluggish on a common VPS/cloud root setup.

initialize() now detects root (os.geteuid() == 0) before spawning the
daemon thread, disables local_embedded mode, and surfaces a clear
warning to both the log and the terminal so the user knows to run as a
non-root user or switch to cloud / local_external mode.

Closes #13125.

Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
2026-06-21 11:47:02 -07:00
ruangraung 8cf7df867e fix(plugins): silence raft check_fn log spam for users without raft CLI
The raft platform plugin's check_raft_requirements() logged a WARNING every
time it returned False. Since check_fn is called on every load_gateway_config()
(~every 10s during normal gateway operation), users who don't have the raft
CLI installed get their logs flooded with no way to suppress it — hermes plugins
disable doesn't work for bundled platform plugins, and platforms.raft.enabled:
false doesn't gate the check_fn call.

Fix: make check_raft_requirements() a silent predicate (return True/False
only, no logging), matching the convention documented and used by other
platform adapters (e.g. teams/adapter.py). The caller in
gateway/platform_registry.py create_adapter() already emits its own warning
when requirements aren't met and an adapter is actually requested — that's the
correct place for a user-facing warning (fires once per connect attempt, not
once per config load).

Fixes #49234
2026-06-19 17:12:58 -07:00
teknium1 a58287afcb
Merge remote-tracking branch 'origin/main' into pr48275-rebase
# Conflicts:
#	cron/scheduler.py
2026-06-19 07:40:29 -07:00