Commit Graph

22221 Commits

Author SHA1 Message Date
teknium1 78bc9acdf1 chore(skills/document-to-action-items): promote to bundled tier
Fleet audit showed these task skills are commonly needed across users;
shipping bundled per Teknium's direction. Docs and tests follow the
bundled paths.
2026-08-07 10:35:42 -07:00
teknium1 7b8d0d800c chore(skills/document-to-action-items): tighten to hardline standards, move to optional
- description 214 -> 59 chars
- author credits Ben Barclay (benbarclay) first
- moved skills/productivity -> optional-skills/productivity (not a daily driver)
- dropped dangling 'linear' related_skills entry; prose points at approved destinations
- framed steps through Hermes tools (read_file, web_extract, xlsx, notion)
- trimmed template safety/verification boilerplate to doc-specific rules
- modern section order (When to Use / Procedure / Pitfalls / Verification)
- tests at tests/skills/test_document_to_action_items_skill.py (8 passing)
- docs regen scoped: per-skill page + one catalog row + one sidebar line
2026-08-07 10:35:42 -07:00
Ben Barclay ff2fa40b13 feat(skills): add document-to-action-items 2026-08-07 10:35:42 -07:00
kshitij c015663b21 fix(models): corrupt-at cache rows degrade to live fetch in cached_provider_model_ids
Surfaced during the post-merge review pass on our own #81113 follow-up:
cached_fetch_api_models gained _cache_entry_valid (numeric-'at'
validation) but its sibling cached_provider_model_ids still did
float(entry.get('at', 0)), which raises ValueError/TypeError on a
hand-edited or corrupted provider_models_cache.json row and propagates
uncaught into the /model picker call sites. Same fix, same helper:
corrupt rows are now a cache miss (live fetch), never an exception.
Both wrappers now share the identical validity predicate, closing the
divergence the 'mirrors' docstring promised away.

Also two test nits from the same review: unused OrderedDict import
dropped and the drain-order assertion strengthened to pin LRU-first
FIFO order in tests/gateway/test_agent_cache_pressure.py.

Mutation-checked: restoring the raising float() form makes the new
corrupt-at tests fail.
2026-08-07 23:00:28 +05:30
Jakub Wolniewicz 24b7ca7258 fix(desktop): preserve root recovery through StrictMode replay 2026-08-07 12:28:19 -05:00
Jakub Wolniewicz bb9434d307 fix(desktop): match current assistant-ui lookup errors 2026-08-07 12:27:34 -05:00
Jakub Wolniewicz 0405c26645 fix(desktop): recover root boundary from tapClientLookup races 2026-08-07 12:27:33 -05:00
Adolanium a62eaaf316 fix(desktop): self-retry transient boundary errors, reactive edit composer context
Two correctness holes left by the session-switch perf work (#72504 / #72524):

1. MessageRenderBoundary only cleared a swallowed transient useClientLookup
   error when the structural resetKey changed. Mid-turn, ids/roles/count are
   stable, so a lookup race during a stream left the boundary rendering null
   for the rest of the turn. The boundary now self-retries on a 0ms timer
   (rAF never fires in a parked renderer), bounded to 5 consecutive
   transient catches with the budget reset on recovery; the structural
   resetKey path is unchanged, and non-transient errors still re-throw.

2. cwd / gateway / sessionId were removed from the messageComponents memo
   deps and read through a render-time ref so session switches stop
   reminting the component types. But a mounted UserEditComposer only
   reads that ref when it renders, and a same-session change (cwd remap,
   gateway reconnect) leaves every ThreadMessageList prop referentially
   equal, so the memo'd list bails out and the open composer keeps stale
   values: @-completions, slash completions, and OS-drop uploads target
   the old cwd / gateway / session. Thread now provides the three values
   through a memoized ThreadEditContext; context propagates through the
   bail-out, the component type identity is untouched, and the transcript
   never remounts.
2026-08-07 12:27:23 -05:00
Teknium fa1a5c0485 Integrate verify subsystem with the existing verification stack
Rescope: hermes verify fills only the runtime-smoke gap and plugs into
the pieces Hermes already has instead of standing beside them.

- agent/verification_evidence.py: record_verify_run() — explicit ledger
  write for hermes verify results (shared _insert_evidence factored out
  of record_terminal_result). Passing runs mark the workspace passed
  like scripts/run_tests.sh; failures are recorded; --phase/--skip-start
  runs are recorded as targeted scope.
- hermes_cli/verify_cmd.py: record results into the ledger on completion
  (fail-silent, HERMES_SESSION_ID attribution); on the detect path merge
  detect_project_facts verify commands the recipe missed into the
  recipe's test list (never applied to a saved manifest).
- agent/verification_stop.py: recipe-aware nudge — when the workspace
  has a runnable recipe (start command or .hermes/environment.json),
  suggest hermes verify --json as the preferred full check; cheap,
  try/except-guarded detection that can never break the nudge path.
- agent/verify/recipes.py: document layer ownership (coding_context =
  cheap prompt facts; verify/recipes = deep runtime recipe).
- tests/verify/test_ledger_and_nudge_integration.py: 17 tests covering
  ledger pass/fail recording, the closed edit->nudge->verify->satisfied
  loop, recipe-aware nudge wording + fail-silence, and the facts merge.
2026-08-07 10:11:05 -07:00
Teknium cc1acfb229 fix: Windows-safe process-group teardown in verify runner (footgun CI) 2026-08-07 10:11:05 -07:00
Teknium 47a35d63c0 Port from superagent-ai/grok-cli: verify subsystem (run-recipe detection + environment manifest + hermes verify smoke runner)
Scoped port of grok-cli's verify subsystem:
- agent/verify/recipes.py: static run-recipe detection mirroring grok's
  detection order (Node frameworks w/ lockfile-based package-manager
  choice, Django/FastAPI/Flask/generic Python, Go, Rust, Maven/Gradle,
  Makefile targets, docker-compose)
- agent/verify/environment.py: versioned, user-editable manifest at
  <project>/.hermes/environment.json; tolerant loader; manifest wins
  over fresh detection
- agent/verify/runner.py: bootstrap -> build -> test -> background start
  -> HTTP readiness poll -> process-group teardown, structured result
- hermes verify CLI command (--detect-only, --save, --skip-start,
  --phase, --port, --json)

Sources:
https://github.com/superagent-ai/grok-cli/blob/main/src/verify/recipes.ts
https://github.com/superagent-ai/grok-cli/blob/main/src/verify/environment.ts
2026-08-07 10:11:05 -07:00
Jeffrey Quesnelle eaa53de4eb
Merge pull request #81203 from bbednarski9/chore/relay-0.7.1
fix(relay): upgrade to 0.7.1
2026-08-07 12:52:16 -04:00
GodsBoy 8cb066404e fix(plugins): address portable MCP review feedback 2026-08-07 09:44:21 -07:00
GodsBoy 6575fb0f80 fix(plugins): preserve opaque stdio commands 2026-08-07 09:44:21 -07:00
GodsBoy e288d93fc1 fix(review): harden portable plugin boundaries 2026-08-07 09:44:21 -07:00
GodsBoy ca78c6d7a6 feat(plugins): load portable agent components 2026-08-07 09:44:21 -07:00
GodsBoy c5117655b6 feat(plugins): validate portable agent packages 2026-08-07 09:44:21 -07:00
Bryan Bednarski 920eaf2fc4
chore(relay): require 0.7.1
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-07 10:31:49 -06:00
PRATHAMESH75 6e87d43a57 fix(tools): lazily bring up sandbox for vision_analyze reads
vision_analyze reads container-only images by exec-reading them inside the
sandbox, but unlike terminal_tool it never triggered environment creation. Under
a non-local backend (ssh, docker, ...), a session whose first action was
vision_analyze on a remote path failed with 'no active sandbox session' until an
unrelated terminal command happened to establish the connection.

Add terminal_tool.ensure_task_env(task_id), a public lazy get-or-create that
reuses the terminal tool's own creation machinery, and call it from
image_source._resolve_container_fallback before the in-sandbox read. Extract the
ssh/container config-dict builders so both paths derive settings identically
(no duplication). Best-effort and fail-closed: a failed bring-up leaves the
existing 'no active sandbox' error intact, never a host read.

Fixes #62825
2026-08-07 09:11:48 -07:00
Teknium bc80a0be5c test: stub EnvironmentConnectionError in environments.base module stub
The modal/browserbase test file replaces tools.environments.base with a
SimpleNamespace stub; terminal_tool now imports EnvironmentConnectionError
from that module, so the stub must provide it too.
2026-08-07 09:07:55 -07:00
Teknium 5c29566e8d feat(terminal): graceful degradation for remote backend connection failures
Connection-class infrastructure failures on remote terminal backends (SSH
host unreachable/timed out, Docker daemon down or missing, remote file
sync failing on a dead link) previously surfaced to the model as raised
RuntimeError tracebacks. The model got a stack blob with no guidance and
the failure was indistinguishable from a tool bug.

Now:

- New EnvironmentConnectionError(RuntimeError) in tools/environments/base.py
  carrying a reason + retry_hint. Subclassing RuntimeError keeps every
  existing catcher working.
- ssh.py classifies connect-refused, connect-timeout, scp, remote mkdir,
  bulk upload/download, and remote rm failures as connection errors.
- docker.py classifies all four _ensure_docker_available() failure paths
  (missing exe, non-executable exe, daemon timeout, `docker version`
  failure).
- terminal_tool catches EnvironmentConnectionError and returns a
  structured tool result the model can act on:
    {"status": "degraded", "reason": ..., "retry_hint": ..., "exit_code": -1}
  The failed backend is evicted from the environment cache so a later
  call retries from scratch — recovery is automatic once the backend is
  reachable again.
- Config gate terminal.degraded_mode: warn|fail (default warn) in
  config.yaml, bridged as TERMINAL_DEGRADED_MODE across all four bridge
  sites (cli.py env_mappings, gateway/run.py _terminal_env_map,
  TERMINAL_CONFIG_ENV_MAP, DEFAULT_CONFIG). "fail" preserves the
  historical error+traceback tool result.
- Command failures (nonzero exit, command-not-found) are NOT touched —
  only infrastructure failures classify as degraded.

Tests: tests/tools/test_terminal_degraded_mode.py (15 tests) covering
exception classification for ssh+docker, structured degraded results,
no-caching of degraded envs, recovery after the backend returns,
nonzero-exit results unaffected, fail-mode preservation, invalid-mode
fallback to warn, and the four-site config bridge invariant.

Inspired by: Claude Cowork degraded-backend behavior (idea-level,
docs-only evidence).
2026-08-07 09:07:55 -07:00
Teknium c228d1c559 fix(dashboard): fold one-field doctor category into general tab
doctor.live_probe_timeout is the only schema-surfaced doctor.* field;
merge it into the general tab per the no-orphan-category invariant.
2026-08-07 09:07:48 -07:00
Teknium 1006faa6f8 feat(doctor): add opt-in `hermes doctor --live` real-call backend probes
Adds a bounded, read-only health probe per CONFIGURED tool backend, run
only when the user explicitly passes `--live` (real network calls):

- Firecrawl: credit-usage metadata GET (auth check, no scrape spend)
- FAL: models metadata GET (never a generation)
- Browser: headless launch + about:blank + close (full cleanup)
- MCP: initialize + tools/list per configured server (reuses the
  `hermes mcp test` machinery in mcp_config._probe_single_server)
- TTS/STT: provider models/voices list GET (openai/groq/elevenlabs);
  local providers (edge/piper/faster-whisper/...) skipped

Invariants:
- Opt-in only: zero probes without --live (default False)
- Bounded: sequential, per-probe timeout (doctor.live_probe_timeout,
  default 10s, config.yaml knob)
- Never mutates state; unconfigured backends skip with a note
- Failure isolation: every probe wrapped in a catch-all; a probe crash
  can never break the doctor run; failures append to the issues summary

New: hermes_cli/doctor_live.py, tests/hermes_cli/test_doctor_live.py
(23 tests, probes mocked at the HTTP/client seam).
Wired: --live flag in subcommands/doctor.py; run_doctor calls
maybe_run_live_checks after all static checks.

Coordination: PR #70124 (--probe-routes) probes LLM routes; this flag
probes TOOL backends — different surface, no code-region collision
(the run_doctor hook here sits at the end-of-run summary, not the
API Connectivity section #70124 extends).

Inspired by: paradigmxyz/centaur tool-health-smoke (MIT/Apache-2.0);
sibling: #70124 (LLM route probes — different surface)
2026-08-07 09:07:48 -07:00
Teknium 0ebaa490b5 test: use valid 2-task batches in schema-rejection tests
The batch quality gate (#81141) now rejects 1-task batches before
schema coercion runs; exercise schema rejection with a valid batch.
2026-08-07 09:07:42 -07:00
Teknium d6ee58b583 feat(delegation): optional structured-output schema on delegate_task
Per-task `output_schema` (JSON Schema object) on task items plus the
top-level single-goal form — a one-time static addition to the tool
schema (never varies per call).

- Child side: the schema is appended to the child's context as an
  explicit OUTPUT CONTRACT block before spawn.
- Completion side: the parent validates the child's final answer with
  jsonschema; on failure it sends exactly ONE bounded retry turn
  carrying the validation errors verbatim (no schema re-paste).
- Result entries gain schema_valid (+ schema_retries, and schema_errors
  on final failure) ONLY when a schema was requested; schema-less calls
  keep a byte-identical result shape.
- Malformed schemas are rejected loudly at dispatch (coerce_output_schema
  meta-validates via jsonschema's validator_for/check_schema).
- New helpers in tools/delegation_output_schema.py: coerce, contract
  block, fence/prose-tolerant extraction+validation, retry message.

Pattern from: github/copilot-cli ctx.agent(prompt,{schema}) — PATTERN
ONLY, zero code/prompt text copied (proprietary); proven consumer:
delegate-task-output-patterns skill.

Tests: tests/tools/test_delegate_output_schema.py (24 tests — valid
first try, invalid->retry->valid, invalid twice -> schema_valid false +
errors surfaced, retry-exception degrade, no-schema legacy shape pin,
dispatch rejection, contract plumbing). Delegation suite: 221/221 green.
2026-08-07 09:07:42 -07:00
Teknium e166159f26 feat(vision): optional region zoom crop on vision_analyze
Add an optional `region: [x1, y1, x2, y2]` parameter to vision_analyze
(pixel coordinates in the ORIGINAL image space). The crop is applied
with Pillow BEFORE the downscale/embed-cap pipeline, so the cropped
region gets the full resolution budget — a zoom for reading small text
or UI details after a full shot.

- New `_crop_image_region` helper: clamps out-of-bounds coordinates to
  the image, rejects zero-area/inverted/malformed regions with an error
  naming the actual image dimensions so the model can retry sensibly.
- Wired into both the native fast path (`_vision_analyze_native`) and
  the legacy aux-LLM path (`vision_analyze_tool`).
- Schema gains one static optional param (byte-stable thereafter); the
  description documents the intended flow: full shot first, then zoom.
- No region supplied = behavior unchanged (regression-guarded).

Tests: tests/tools/test_vision_region.py (11 tests — crop applied,
clamping, zero-area rejection with dims, malformed input, pre-downscale
full-budget zoom, schema shape, handler pass-through, no-region
unchanged). Widened one narrow fake_native stub in test_vision_tools.py
to be kwargs-tolerant.

Ported from: QwenLM/qwen-code zoom-image.ts (Apache-2.0)
2026-08-07 08:58:49 -07:00
Teknium fe66596df3 feat(security): protected agent-instruction files always require write approval
write_file/patch targeting AGENTS.md, CLAUDE.md, SOUL.md, .cursorrules, or a
project-local .hermes config dir now ALWAYS prompt the human for approval —
even under --yolo/auto-approve — and fail closed when no human channel
exists. These files steer future agent behavior, so an injected write to
them is a prompt-injection persistence vector.

Design:
- New _check_protected_instruction_write() in tools/file_tools.py, a
  sibling of _check_sensitive_path that returns approval-required rather
  than a hard error. It realpaths before matching (symlink lesson from
  #41351), matches basenames case-insensitively in ANY directory, rejects
  './x/../AGENTS.md' traversal via normpath, and gates files whose
  immediate parent dir is `.hermes` (project-local config) while exempting
  the authoritative ~/.hermes home (governed by its own guards).
- Approval is ONE-OPERATION only: no session/permanent persistence, no
  yolo bypass — intentionally does not route through _run_approval_gate.
  Gateway sessions get the button round-trip with allow_permanent and
  allow_session both False; CLI uses the per-thread approval callback;
  no channel at all = BLOCKED (fail closed).
- Multi-file V4A patches: ONE protected file gates the ENTIRE patch (a
  single prompt lists all protected targets; deny applies nothing).
- Config: security.protected_instruction_files (default true) and
  security.protected_instruction_extra_patterns (fnmatch on basename).
  Config read failure keeps the gate ON.

Tests: 22 new cases in tests/tools/test_file_write_safety.py covering the
adversarial checklist — deny/approve/yolo-bypass attempt, symlink at a
protected target, case variants, relative traversal, arbitrary-directory
basenames, project-local .hermes, checkout-nested-under-~/.hermes
non-gating, patch replace + V4A multi-file atomicity, gateway round-trip,
fail-closed with no human, config off/extra patterns.

Ported from: RooCodeInc/Roo-Code RooProtectedController (Apache-2.0);
companion: #58631 (terminal vector), symlink lesson from #41351.
2026-08-07 08:58:38 -07:00
Teknium c8369e37f4 feat(mcp): trust-tier gating for write-capable MCP tools via readOnlyHint
Adds a per-server `trust: full|untrusted` config key
(mcp_servers.<name>.trust). On an untrusted server, every write-capable
tool call — any tool whose discovery-time annotations do not carry
readOnlyHint=True — routes through the existing approval surface
(tools.approval.request_elicitation_consent, same lazy-import +
surface-routing pattern the MCP elicitation handler uses) before the RPC
fires. Denied/cancelled/errored approvals fail closed: the RPC never
runs, including the lazy first-use server spawn.

Design points:
- Classification happens at CALL TIME from metadata captured at
  DISCOVERY (_record_tool_trust_metadata in _register_server_tools and
  the lazy cache-registration path). No toolset/schema mutation, so the
  toolset stays byte-stable and prompt caching is preserved.
- readOnlyHint is a server-supplied HINT: on an untrusted server a lying
  server can at most skip approval for tools it claims read-only — it
  can never widen access. Trust tiering itself is operator config.
- Missing/malformed annotations => write-capable (fail closed).
- Unrecognized trust values => untrusted (fail closed); missing key =>
  full (backward compatible, documented in mcp-config-reference).
- The schema cache now persists readOnlyHint so lazy-registered servers
  gate identically on next startup without spawning.

Tests: tests/tools/test_mcp_trust_gating.py (11 tests, TDD red->green):
approval invoked + accept proceeds, deny/cancel blocks RPC, readOnlyHint
=true skips gate, trusted/unconfigured servers skip gate, explicit
readOnlyHint=false gated, approval exception fails closed, trust
normalization, discovery-time capture (SDK objects and cached dicts).

Ported from: cloudflare-os classifyTool() (Apache-2.0), corroborated by
Claude Cowork (idea-level).
2026-08-07 08:58:32 -07:00
Teknium 37cc999926 feat(mcp): collapse const-only anyOf/oneOf unions to property enums
MCP servers generated from Rust/TypeScript union types commonly emit
closed value sets as const unions:

    {"anyOf": [{"const": "red"}, {"const": "green"}, {"const": "blue"}]}

Strict tool-calling backends reject or mishandle these; the equivalent
property-level enum form is universally supported. Add
collapse_const_unions() to tools/schema_sanitizer.py and wire it into
the _normalize_mcp_input_schema discovery pipeline after the nullable
strip.

Rules:
- Collapse only when EVERY non-null branch is a pure const of the same
  primitive type (bool never merges with integer).
- Mixed unions, non-uniform const types, and mismatched declared types
  pass through untouched.
- A single {"type": "null"} branch is tolerated: consts -> enum,
  null -> nullable: true hint (matches strip_nullable_unions, which
  leaves null+multi-const unions alone by its one-non-null-branch rule).
- Outer title/description/default/examples carried onto the replacement.
- Deterministic, branch-order-preserving, non-mutating — applied at
  discovery only, so schemas stay byte-stable per conversation.

Ported from: block/goose tool_schema_normalize.rs (Apache-2.0)
2026-08-07 08:58:25 -07:00
Teknium 9fad45fcda feat(kanban,mcp): orphaned-card reconciliation + per-server MCP identity header
Two small config-gated features:

1. Kanban orphaned-card reconciliation (kanban.reconcile_orphans, default
   true, config.yaml): a running card with broken claim bookkeeping
   (claim_lock or claim_expires NULL — crash mid-claim, manual SQL, DB
   restore) is invisible to all existing recovery paths
   (release_stale_claims requires claim_expires NOT NULL,
   detect_crashed_workers requires host-local lock + pid,
   detect_stale_running is config-disabled by default) and shows Running
   forever. New reconcile_orphaned_running() pass in kanban_db.py runs
   each dispatch_once tick: requeues orphans to ready with an explanatory
   comment, closes any leaked run, emits a 'reconciled' event, and defers
   when the recorded PID is still alive on this host (never requeue
   beside a live worker). Surfaced via DispatchResult.reconciled_orphans.

2. Per-server MCP identity header (mcp_servers.<name>.identity_header,
   config.yaml): optional {name, value_from: static|profile, value}
   mapping; the header is attached to that server's HTTP/SSE transport
   requests. 'static' sends the config value; 'profile' resolves the
   active Hermes profile name once at connect time (no per-call
   mutation). Explicit per-server headers of the same name (any casing)
   win. Invalid blocks warn-and-ignore; stdio servers warn-and-ignore.

Tests: tests/gateway/test_kanban_reconcile_orphans.py (9),
tests/tools/test_mcp_identity_header.py (13), all written first (RED)
then implemented (GREEN). No new HERMES_* env vars.

Inspired by: openai/symphony tracker reconciliation (Apache-2.0) +
Poke per-user MCP identity (idea-level).
2026-08-07 08:58:20 -07:00
Teknium 5db1b72b1f feat(cli): global emergency stop — `hermes pause` / `hermes resume`
Resumable ESTOP sentinel at $HERMES_HOME/ESTOP that halts NEW work only:

- agent/estop.py: sentinel engage/disengage/is_engaged (single stat, no
  caching), optional reason + timestamp stored as JSON, paused_reply()
  notice, check_paused() log-once-per-engagement helper. Corrupt/empty
  sentinel still pauses (fail safe); a `touch ~/.hermes/ESTOP` works.
- cron/scheduler.py: tick() skips dispatch while engaged (logged once per
  engagement, not per tick). Due jobs simply wait for the next tick after
  resume — in-flight runs are never touched.
- gateway/kanban_watchers.py: dispatcher skips auto-decompose and worker
  spawning while engaged; zombie reaping still runs and running workers
  finish naturally.
- gateway/run.py: new gateway turns (post-auth, non-internal) get a brief
  "Hermes is paused" reply instead of an agent run. Internal events
  (in-flight background completions) bypass the gate.
- hermes_cli/subcommands/pause.py: `hermes pause [--reason]` and
  `hermes resume`, wired into main() and _BUILTIN_SUBCOMMANDS.
- hermes_cli/status.py: `hermes status` shows a PAUSED banner (one stat).
- tests/test_estop.py: 20 tests — sentinel lifecycle, reason surfacing,
  log-once, cron skip + resume, kanban gate, gateway paused reply +
  internal bypass, CLI idempotence, builtin-set parity, status line.

Never kills in-flight work; resumable with no restart. Footprint ladder:
CLI command only, no new model tool, no new env vars.

Ported from: gastownhall/gastown estop.go (MIT); related prior art:
#26778 (/panic — kill/exit semantics, deliberately different: ours is a
resumable pause), #44617 (interrupt in-flight cron — out of scope here).
2026-08-07 08:58:14 -07:00
Teknium 5396da844a docs: DX sweep — 7 verified-absent documentation items
- developer-guide/codebase-ownership.md (new): subsystem -> source dirs ->
  docs entry point map; complements the narrow CODEOWNERS proposal in #23751
  (docs table only, no .github/CODEOWNERS).
- contributing.md: document the .agents/checks/*.md repo-local review
  checklist convention (idea from goose, Apache-2.0).
- integrations/index.md: "Quick connect links" table with prefilled
  create-your-app deep links (Telegram BotFather, Discord
  ?new_application=true, Slack ?new_app=1, LINE, Feishu). Poke-inspired.
- guides/agent-email-address.md (new): dedicated agent mailbox via the
  bundled himalaya skill — setup, cron polling pattern, prompt-injection
  safety notes. Poke-inspired.
- user-guide/features/browser.md: Chrome 136+ silently refuses
  --remote-debugging-port on the default user-data-dir; dedicated profile is
  now mandatory (diagnosis from oh-my-pi, MIT).
- developer-guide/adding-providers.md: "Tool-call wire format" section
  linking the OpenAI chat-completions reference as the canonical shape for
  convert_messages/convert_tools.
- user-guide/features/tools.md: shell-init pitfall — heavy/interactive rc
  files (nvm, TTY-expecting blocks) break non-interactive agent terminal
  calls; interactive-guard pattern documented (from cline, Apache-2.0).

Both new pages registered in sidebars.ts. Validated with npx docusaurus
build (en + zh-Hans green; zh-Hans relative-link warnings are the known
pre-existing untranslated-page noise).
2026-08-07 08:58:08 -07:00
Teknium d7635e43bb feat(delegation): surface per-delegation cost in the result entry
Each serialized result entry now carries cost_usd (rounded to 6 dp)
and cost_status (the child's session_cost_status — 'estimated',
'reported', 'included', or 'unknown') alongside tokens/api_calls/
duration, so the parent model can see what each delegation cost.

The internal _child_cost_usd field is still stripped before
serialization and the parent session cost rollup is untouched.
Tool schema is unchanged (byte-stable).

Inspired by: Perplexity Agent API result shape (idea-level)
2026-08-07 08:58:02 -07:00
Teknium 94bc3194b3 feat(delegation): validate batch task quality before spawning children
Reject malformed tasks=[...] batches before any child agent is spawned:

- exact-duplicate goals (case/whitespace-normalized), error names both
  task indices
- placeholder goals: bare 'TODO', bare 'task N', unexpanded <...> or
  {...} template markers, or goals shorter than 10 chars after strip
- 1-task batches, with an error pointing the model at the single
  `goal` form instead

All checks are batch-only — the single-goal form is exempt by design
(short goals like goal="test" are valid there). Error strings are
actionable: each tells the model exactly how to fix the call.

Tool schema is unchanged (byte-stable); validation is runtime-only in
the existing batch-validation region.

Existing tests using terse batch goals ("A"/"B"/"C") updated to
realistic distinct goals per the new contract.

Inspired by: MoonshotAI/kimi-code agent-swarm.md validation rules (MIT)
2026-08-07 08:57:57 -07:00
Teknium ed903f953e feat(cron): pre-dispatch configuration validation (blocked_config + alert-once)
Validate a job's configuration BEFORE any agent machinery is constructed:

- missing provider API key (AuthError from a read-only
  resolve_runtime_provider probe; skipped when a fallback_providers chain
  is configured, since auth-fallback may rescue the run)
- attached skill not ready (skill_view readiness_status=setup_needed —
  missing required env vars / commands / credential files)
- delivery platform unknown or unconnected (deliver=local/origin/all are
  never checked; gateway-config load failures fail open)

On a failing check run_job returns a [blocked_config]-marked error without
constructing AIAgent/MCP/etc, so a misconfigured job never burns an LLM
call. run_one_job records last_status='blocked_config' and delivers the
alert exactly ONCE across ticks (persisted preflight_alerted bit — the
alert-once shape from the #73506 dead-pin auto-pause); the next healthy
run clears the marker so a future break re-alerts. Every preflight check
fails open: only an affirmative misconfiguration verdict blocks.

Config: cron.preflight (default true); `cron.preflight: false` restores
the old fail-during-run behavior. Documented in the cron user guide and
config defaults.

mark_job_run gains an optional status= override (unblocked call shape
unchanged) and drops preflight_alerted on any successful run.

Tests: tests/cron/test_preflight_config.py (blocked_config + no agent +
single alert across two ticks, healthy job unaffected, recovery clears
dedup, fallback-chain rescue, opt-out restores old behavior, skill
readiness miss, unknown delivery platform, deliver=local never loads
gateway config). Full tests/cron/ + cronjob tool suite green (525 tests).

Ported from: paperclipai/paperclip execution-semantics §5 (MIT);
in-repo precedent: #27948, #73506
2026-08-07 08:57:53 -07:00
Teknium 04e8a661f2 feat(cron): per-job durable notepad — KV scratchpad surviving scheduled runs
- cron/notepad.py: SQLite-backed cron_notepad(job_id, key, value,
  updated_at) store in its own profile-local db (cron/notepad.db),
  following the executions.py connection/transaction pattern. APIs:
  set_note/get_note/delete_note/list_notes/clear_notepad +
  render_notepad_section. Documented size caps: 16KB per value,
  128-char keys, 64KB per job total; oversized writes raise ValueError.
- cron/scheduler.py: inject non-empty notepads into the job prompt at
  the context_from data-injection seam as a clearly-labeled
  "Job notepad (persistent across runs)" section that also documents
  the CLI write path. Empty notepad renders "" — byte-stable prompts
  for jobs that never use the feature.
- hermes_cli/cron.py + hermes_cli/subcommands/cron.py:
  `hermes cron notepad <job_id> [get|set|delete|list]` under the
  existing cron subcommand tree (no new top-level command, no new
  model tool — the agent writes via terminal + CLI).
- tests/cron/test_notepad.py: CRUD, durability, cap enforcement,
  prompt injection, byte-stable empty case, read-failure resilience,
  CLI handler + dispatch (TDD; watched fail first).

Inspired by: Amp (Sourcegraph) cron notepad (idea-level, proprietary —
zero code).
2026-08-07 08:57:48 -07:00
Teknium 6dff2109aa feat(cron): monitor-mode jobs — hash-suppressed change detection
Add monitor-mode cron jobs: a cheap monitor source (monitor_script or
monitor_url) runs on every tick BEFORE any agent machinery is built.
Its output is hashed as exact bytes and compared to the hash stored
from the last agent-triggering tick:

- unchanged  -> agent run suppressed entirely (no LLM, no delivery);
  the tick is recorded as a silent no_change run visible in the
  executions ledger doc
- changed    -> a MONITOR CHANGE DETECTED block (capped unified diff of
  previous vs current output + the new output) is injected into the
  prompt via the existing extra_prompt seam, then a normal agent run
- first run  -> always runs the agent with a baseline block
- source failure -> delivered as an ERROR alert, never treated as a
  change; the stored hash is untouched so recovery to prior output
  still suppresses

Implementation:
- cron/monitor.py (new): hash/diff/URL-fetch/state persistence.
  monitor_script reuses _run_job_script (same ~/.hermes/scripts/
  containment + interpreter rules); monitor_url is a bounded GET
  (30s, 256KB, http/https only). Output is exact bytes by design —
  scripts should emit stable output (documented).
- cron/jobs.py: additive job fields monitor_script / monitor_url /
  monitor_state {last_output_hash, last_changed_at}. JSON job records
  need no migration. create-time validation: sources are mutually
  exclusive and incompatible with no_agent=True.
- cron/scheduler.py: one tight monitor gate in run_job between the
  no_agent short-circuit and the LLM path (outside sibling-lane
  regions). State persists in jobs.json + a per-job snapshot file, so
  suppression survives scheduler restarts.
- tools/cronjob_tools.py: additive optional monitor_script/monitor_url
  params on the cronjob tool (create + update, empty string clears),
  path containment validated at the API boundary, surfaced in
  _format_job.
- hermes_cli: --monitor-script/--monitor-url on `hermes cron create`
  and `hermes cron edit`; `hermes cron list` shows the monitor source
  and last-changed time.

Tests (tests/cron/test_monitor_kind.py, TDD): unchanged suppresses,
changed injects diff, first run always runs, hash persists across
module reload (restart), script failure is error-not-change with hash
untouched, create/update validation, tool wiring + path-escape reject.

Inspired by: ChatGPT Work monitor tasks (idea-level, docs-only);
enabler: #80774
2026-08-07 08:57:44 -07:00
Teknium 563f0a6fde feat(cli): add `hermes approvals test` — dry-run approval verdict CLI
Answers "what would the approval system do with this command?" without
executing it, prompting anyone, or persisting anything. Composes the
REAL runtime evaluators from tools/approval.py in the same order as
check_all_command_guards: container-skip gate, hardline blocklist,
sudo-stdin guard, user approvals.deny rules, yolo/mode-off bypass,
permanent command_allowlist, dangerous-pattern detection. Because the
same functions run — including _command_detection_variants's
normalization/de-obfuscation path — an obfuscated command gets exactly
the verdict its plain form would get at runtime, and the output shows
the normalized-variant trace the detectors actually evaluated.

- hermes_cli/approvals_test.py: evaluate_command() + text/JSON output.
  Script-friendly exit codes: 0 allow, 1 usage, 2 ask-approval, 3 deny
  (hardline / sudo-stdin / user deny rule).
- hermes_cli/subcommands/approvals.py: `test` subparser with --env-type
  (default local), --json, and a REMAINDER command (dest command_words —
  NOT "command", which main.py's startup path reads as the top-level
  subcommand name).
- hermes_cli/approvals_suggest.py: dispatch `test` and mention it in the
  bare-`hermes approvals` usage text.
- tests/hermes_cli/test_approvals_test.py: verdict matrix (benign /
  hardline / dangerous / user-deny from config / container skip /
  mode=off vs hardline), obfuscated==plain verdict parity with
  normalized trace, spy proof that the real runtime detectors are the
  ones invoked, read-only invariants (nothing executed; prompt and
  persistence paths rigged to explode), JSON shape, dispatcher and
  parser wiring.

Read-only by construction: only detection/matching functions are
called; the approval gate, prompts, gateway notify, and allowlist
writers are never reached.

Inspired by: Amp `permissions test` (idea-level, proprietary — zero code)
2026-08-07 08:57:39 -07:00
kshitij 7cf71c32bb fix: follow-ups for salvaged PR #80740
- Give cached_fetch_api_models the same stale-while-revalidate tier as
  cached_provider_model_ids: TTL-expired entries within the 7d window are
  served instantly while a background refresh rewrites the cache —
  without this, every /model open an hour into the session re-blocked on
  the live probe (#72762's stall class, deferred).
- Generalize _spawn_swr_refresh(cache_key, refresh_fn) so non-slug
  custom:<base_url> keys reuse the same inflight-dedupe scaffolding;
  slug behavior unchanged (default refresh_fn preserved).
- Convert the missed sibling site: acp_adapter/server.py
  _named_custom_provider_catalogs() live-probed every custom_providers
  row's /v1/models per ACP catalog build.
- Extract _cache_entry_valid() (the fp/models predicate existed 4x) and
  validate 'at' is numeric so hand-edited/corrupt cache JSON degrades to
  a live fetch instead of raising through the picker's blanket except.
- Flatten the dead api_mode conditional (fetch_api_models declares
  api_mode=None; branch was behaviorally inert).
- Tests: 4 new guards (stale-serve, stale-window cutoff, generalized SWR
  write-through, corrupt-at degradation) — stale-serve and corrupt-at
  mutation-checked; 2 existing tests updated for the new behavior.
2026-08-07 21:02:40 +05:30
Prashant Jain fb435aae97 perf(model): disk-cache custom-provider /v1/models probes
Custom OpenAI-compatible endpoints (named custom_providers rows, bare
provider: custom, and per-endpoint-map entries) called fetch_api_models()
directly at three call sites in model_switch.py, with no disk cache — unlike
first-class providers, which go through cached_provider_model_ids(). Every
plain /model open live-probed the active custom endpoint's /v1/models,
regardless of how recently it had already been probed.

Adds cached_fetch_api_models() in hermes_cli/models.py: a TTL disk-cache
wrapper keyed on custom:<base_url> (custom endpoints have no
PROVIDER_REGISTRY slug to key on) and fingerprinted on api_key/api_mode/
headers, with the same stale-beats-nothing fallback policy as
cached_provider_model_ids(). Routes all three probe call sites through it.

Since prewarm_picker_cache_async() already calls list_authenticated_providers()
with probe_custom_providers defaulting True, this also fixes the endpoint
being warmed on boot (populating the disk cache) instead of that work being
discarded on every open — any custom endpoint (an LLM gateway, a
self-hosted vLLM/SGLang server, etc.), not just one specific provider.

Fixes #72762. Salvaged from #72810 per review feedback: extracts just the
verified custom-endpoint cache fix with real cache-contract test coverage
(hit/stale/rotation/refresh/fallback), leaving the credential-pool and
Copilot-token-exchange costs described in the issue for separate follow-up.
2026-08-07 21:02:40 +05:30
kshitij 83bad5cdda fix: follow-ups for salvaged PR #80795
- Drain the eviction plan (pop + del) before trim_memory: the batch
  thread previously held every evicted agent in its local list while
  gc.collect + malloc_trim ran, so the in-pass trim freed almost
  nothing, the next tick re-read a still-high RSS, and the valve
  over-evicted an extra batch of warm prompt caches per cycle.
- Clear _db_flush_scan_prefix in _release_evicted_agent_soft: it is a
  shallow copy of the flushed transcript (stamped on every successful
  flush) sharing every message dict — and pressure-evictable agents
  have flushed by definition, so it pinned the multi-MB content strings
  on exactly the agents the valve targets.
- Config-read failure now falls back to resolve_agent_cache_bounds({})
  instead of bare AgentCacheBounds(): the dataclass default disables
  the pressure pass, but an absent config section means 'auto' — a
  transient read failure must not permanently switch off the OOM valve.
- protect_recent: false (YAML bool; False == 0) keeps the default MRU
  protection instead of silently disabling it.
- 'No evictable session' warning now distinguishes sessions blocked on
  un-flushed persistence (e.g. session DB never initialized — NFS
  HERMES_HOME) from mid-turn agents, so operators can diagnose why the
  valve isn't shedding instead of being pointed at running turns.
- _cgroup_limit_bytes checks the process's own cgroup (via the existing
  gateway.cgroup_cleanup._own_cgroup_path) before the root files, so a
  systemd unit's MemoryHigh=/MemoryMax= is detected — the root
  memory.high/max read 'max' on those deployments.
- Tests: 5 new guards; drain-before-trim and scan-prefix-clear
  mutation-checked (revert each fix -> its guard fails).
2026-08-07 21:02:33 +05:30
HexLab98 2d0c2682c7 docs: document the gateway agent cache memory budget
Record why the cache needs a third bound and what the pressure pass will and
will not shed, so an operator tuning agent.agent_cache knows which knob to
reach for. Adds the config keys to the session-lifecycle appendix and a user
guide section covering the "auto" cgroup-derived budget.
2026-08-07 21:02:33 +05:30
HexLab98 6bbe55dd09 fix(gateway): bound the agent cache by memory, not just count and age
The per-session agent cache is capped at 128 entries with a 1h idle TTL, and
neither bound knows how many bytes it holds. Each cached agent pins
_session_messages -- the full transcript including tool output, tens of MB on
a session with 100+ tool calls -- so a gateway serving many chats keeps every
warm transcript resident: agents that took a turn inside the TTL are never
idle-swept, and the idle sweep additionally defers finalizable sessions until
they expire. RSS climbs until the cgroup throttles and SIGTERM can no longer
flush inside systemd's stop timeout.

Add the missing bound. Each session-expiry watcher tick compares the process's
anonymous RSS against a budget and, when over, sheds LRU agents through the
same soft-eviction path the cap enforcer uses, then runs malloc_trim so the
freed arenas actually return to the OS. Evicted sessions rebuild their
transcript from the persisted session on the next turn.

Three classes of session are never shed: agents mid-turn, the most recently
used ones, and any session whose transcript has not finished reaching disk
(_last_flushed_db_idx vs len(_session_messages) -- the same divergence the FTS
write-corruption guard reacts to when it preserves live history).

memory_high_mb defaults to "auto", deriving the budget from the cgroup limit
the gateway runs under, so a MemoryHigh/MemoryMax on the unit is respected
without a second number to keep in sync. The two existing bounds become
configurable alongside it under agent.agent_cache.

protect_recent is clamped to half the cache: a couple of sessions can exhaust
the budget on their own, and a fixed MRU guard would then protect everything
and leave the gateway climbing with nothing it would shed.

Fixes #80764
2026-08-07 21:02:33 +05:30
kshitij f15a38ee73
Merge pull request #81107 from kshitijk4poor/chore/author-map-prashantjain25
chore: add prashantjain25 to AUTHOR_MAP
2026-08-07 20:10:34 +05:30
kshitij 5ded99af45 chore: add prashantjain25 to AUTHOR_MAP
Needed before salvaging PR #80740 (contributor audit runs against main).
2026-08-07 20:06:53 +05:30
hermes-seaeye[bot] 48e2dcd7a0
fmt(js): `npm run fix` on merge (#81102)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-07 14:30:33 +00:00
webtecnica a4b235c4b2 fix(desktop): virtualize git file-tree to cap DOM nodes (#77257) 2026-08-07 19:51:06 +05:30
kshitij 4a3942d948 fix: show explicit member spend cap message instead of 'no credits'
When the Nous Portal returns paid_service_access.allowed=false with
reason=member_spend_cap_exceeded, Hermes was falling through to the
generic 'no active subscription or usable credits' message — even
though the user has ample purchased credits and the real blocker is
an org-level per-member spend cap.

This adds a dedicated branch that surfaces the actual cause: names the
spend cap, shows the cap/spend amounts, and tells the user to ask their
org admin to raise it. Also adds member_spend_cap_exceeded to the
billing error code set so the error classifier and auth error formatter
route it through the Nous entitlement message path.
2026-08-07 19:47:40 +05:30
kshitij 813793db23 test(tui_gateway): pin the raising-close swallow + no-retry contract
Review finding: the close block's comment promises a raising
session_db.close() is swallowed with the flag already cleared (no re-close
on a second agent.close()), but nothing pinned it. One test with a raising
_RecordingDB proves both halves.
2026-08-07 19:44:41 +05:30
Yishova be14a4bee3 tui_gateway: close dedicated profile SessionDB handles at teardown too
Follow-up to the review on the session.resume ownership fix. Closing the
pre-transfer early returns left two gaps, both real.

1. The transfer had no owner on the other side. Once ownership moved to the
   agent, teardown ran AIAgent.close() (via _teardown_session on session.close
   and the orphaned-session reaper), which called session_db.end_session() —
   that finalizes the session ROW, not the connection. A successfully resumed
   profile session kept its dedicated handle, its db/-wal/-shm fds and its
   background token-writer thread for the life of the gateway.

   AIAgent now carries an explicit _owns_session_db, defaulting False so the
   SHARED launch handle — which outlives every agent and backs every other live
   session — is still never closed there. Only the dedicated-open sites set it,
   at the point ownership actually changes hands.

2. session.resume was not the only profile-scoped open with no close on its
   failure paths. Covered here with the same flag, via a _transfer_db_to_agent
   helper that refuses the transfer unless the agent really holds that handle:

   - the deferred builder (_start_agent_build), including the session-reaped-
     mid-build case, where the built agent is discarded and never torn down, so
     transferring to it would leak exactly as before;
   - session.branch's branch_db;
   - the compute host's per-profile open;
   - AIAgent's own lazy open in _get_session_db_for_recall, which no other
     object ever references and so was unconditionally abandoned.

Where a handle has already reached a registered session, the drop is
unconditional and the transfer is best-effort on top: a refused transfer leaves
the old leak, which is survivable, whereas closing under a live session is the
permanent "Cannot operate on a closed database" break the original patch exists
to avoid.

Tests: tests/tui_gateway/test_session_db_ownership_teardown.py (new, 14).
11 of the 14 fail without this change; the 3 that pass are the "must NOT close"
guards, which hold in both directions by design.
2026-08-07 19:44:41 +05:30