Commit Graph

2545 Commits

Author SHA1 Message Date
kshitijk4poor 70370e089c fix(skills): skill_view directory file_path + skill_manage categorized name resolution
Two agent-facing errors that recur constantly in optimization audit
logs (thousands of occurrences over five months):

1. skill_view(name, file_path='references') returned a raw
   '[Errno 21] Is a directory' OS error. The local-skill branch gated
   on target_file.exists(); a directory passes exists(), fell through
   to read_text(), and raised. The plugin-skill sibling branch already
   gated on is_file() — this aligns the local branch so a directory
   request gets the same helpful not-found payload with
   available_files listing instead of an OS error.

2. skill_manage rejected categorized names ('category/skill-name')
   with 'not found in active profile'. _find_skill matched only the
   bare directory name, while skill_view's own ambiguity hint tells
   the caller to use exactly the categorized form — every call that
   followed the hint failed. _find_skill now also matches the full
   relative path of the skill dir, giving skill_manage resolution
   parity with skill_view across edit/patch/delete/write_file/
   remove_file.

Both fixes are covered by regression tests that fail on main.
2026-08-30 20:04:40 +05:30
Teknium dce2ecb8a9 test: cron ContextVar-masking test now uses a chat platform
test_explicit_blank_masks_leaked_cron_env_for_gateway_classification
used platform=api_server as an arbitrary gateway platform; api_server
is now intentionally excluded from gateway approval contexts
(unattended class). Switch to telegram — the test's subject is the
blank-cron-ContextVar masking, not platform policy.
2026-08-30 07:04:16 -07:00
Teknium ef71f2cad8 fix(approval): widen webhook exclusion to all unattended platforms, deny by default
Builds on liuhao1024's webhook exclusion (#37317): instead of falling
through to auto-approve, unattended programmatic platforms (webhook,
msgraph_webhook, api_server) now resolve approval decisions instantly
via approvals.unattended_mode (default deny), mirroring cron_mode.

- _UNATTENDED_APPROVAL_PLATFORMS set + _is_unattended_platform_approval_context()
- approvals.unattended_mode config key (deny | approve), default deny
- Deny branches in _run_approval_gate, check_all_command_guards (with
  tirith parity), and check_execute_code_guard (#87509 sibling site)
- Docs: security.md; config_defaults.py comment + default

Fixes #37284. Also fixes the api_server half of #87509.
2026-08-30 07:04:16 -07:00
liuhao1024 73f8fb74e0 fix(approval): exclude webhook sessions from gateway approval context
Webhook sessions trigger the gateway approval branch because
HERMES_SESSION_PLATFORM is set, but the webhook adapter has no
send_exec_approval and no way to receive /approve replies.  This
blocks the session for the full approval timeout (60-300 s) with
no human who can resolve it.

Fix: _is_gateway_approval_context() now returns False when the
session platform is 'webhook', falling through to the non-interactive
path (auto-approve with warning, or deny if cron).

Regression tests added for webhook, non-webhook gateway, cron, and
no-platform scenarios.
2026-08-30 07:04:16 -07:00
Teknium 8a8aa850f1 fix(delegate): inherit endpoint-scoped capability map only on the parent's exact route
Same-class follow-up to #94036/#97292: a subagent spawned on the parent's
exact provider+base_url inherits the trusted-proxy capability map
(openai_native_compaction), so it keeps native compaction instead of
silently falling back to local summarization. Any provider- or
endpoint-changing delegation override stays DEFAULT-DENY, matching the
/model switch posture.
2026-08-30 05:16:10 -07:00
Teknium d5fd2e9366 fix(browser): real-profile browsing runs headless — no focus-stealing window
Real-profile browsing (browser.use_real_profile) is meant to drive a COPY of
the user's profile headlessly in the background so they can keep working while
the agent acts on their behalf. Instead, on any host with a display it launched
the user's real browser binary HEADED, popping a window that grabbed focus on
every turn.

Root cause: the native launch (which bypasses agent-browser's own launcher to
avoid --use-mock-keychain dropping keychain-encrypted cookies) only added
--headless=new when Linux had no DISPLAY/WAYLAND_DISPLAY. On a normal desktop
the guard was false, so Chrome opened a visible window.

Fix: launch headless by default on every platform. Chrome's NEW headless mode
shares the profile's normal cookie store (unlike legacy --headless), and the
cookie drop we guard against comes from --use-mock-keychain, not from headless
— so real-profile auth still loads. Users who want to watch can opt in via the
existing browser.headed / AGENT_BROWSER_HEADED toggle (honored for real-profile
now, same as the rest of the browser stack); display-less hosts stay headless
regardless so the launch doesn't die at startup.

Live A/B on a real X seat: old argv mapped a Chrome window (focus steal),
--headless=new mapped zero windows while still exposing a working CDP port.
Updated the stale test that pinned 'never passes --headless' (a legacy-headless
premise) to positively assert the chrome launch is --headless=new by default.
2026-08-29 20:40:04 -07:00
liuhao1024 6b290b81d5 fix(tools): reduce false positives in exfil_curl/exfil_wget patterns
Anchor env var name matches with \b to avoid matching legitimate
env vars that contain KEY/TOKEN/API as substrings (e.g.,
$TRILLIUM_ETAPI_URL). The patterns now require KEY/TOKEN/SECRET/PASSWORD
to appear at the END of the env var name, reducing false positives on
common API-usage documentation in SOUL.md while still catching actual
exfiltration attempts.

Fixes #63977
2026-08-29 20:39:31 -07:00
Teknium d041ed7ab2 fix(skills-hub): reconcile salvaged install fixes with full-directory fetch
Follow-up reconciling the four cherry-picked contributor fixes with the
full-dir GitHubSource.fetch() that landed in #98246:

- Missing SKILL.md-linked support paths now warn and install without the
  file at all three sources (GitHub full-dir, GitHub fallback, UrlSource) —
  dangling links are prose over-matches or repo-only dev tools, not install
  blockers (#66760/#90081). A referenced path present in the tree as a
  SYMLINK stays a hard rejection.
- Extension requirement dropped from the glob/placeholder filter:
  references/LICENSE is a legitimate support file (82236's tests pin this).
  Truncated prose placeholders (references/type-<name>.md -> 'type-') are
  still rejected via the trailing-separator shape.
- percent-quoted Contents-API path (82236) merged with revision pinning
  (96336) in _fetch_file_bytes.
- Fixture typo fix: four cherry-picked test strings used '\---' where
  '\n---' was meant (DeprecationWarning + frontmatter never parsed).

Validation: 167/167 across tests/tools/{skills_hub,skills_guard,
skill_bundle_provenance} + tests/hermes_cli/test_skills_hub.py; live GitHub
fetches (impeccable 163 files rev-pinned; anthropics frontend-design).
2026-08-29 20:39:09 -07:00
liuhao1024 7e95b67ad8 fix(skills): review follow-up — revision pinning, canonicalization, case-fold guards
Addresses the review on #96336:

- Every GitHub byte fetch in an install (SKILL.md included) now carries
  the resolved tree's SHA as ?ref=, closing the pre-existing TOCTOU
  where /contents floated to the default-branch HEAD and bytes could
  come from a newer revision than the tree the paths were validated
  against. The tree is resolved first (idempotent + cached) so the pin
  covers the whole install.
- Same-dir link targets are canonicalized before validation: query/
  fragment stripped via urlsplit, percent-decoded, leading ./ removed —
  the same normalization the support-dir branch applies.
- A case-variant link to skill.md never ships as a bundle entry, and a
  case-folded collision among accepted siblings (A.md + a.md) drops the
  pair — both would overwrite/collide on case-insensitive filesystems.
2026-08-29 20:39:09 -07:00
liuhao1024 86dda3cd36 fix(skills): fetch explicitly linked same-directory siblings on install (#96310)
_referenced_support_paths only kept links whose first path segment was
one of the five support directories (references/templates/scripts/
assets/examples), so a SKILL.md linking same-directory siblings —
mattpocock/skills' domain-modeling links ./CONTEXT-FORMAT.md and
ADR-FORMAT.md — installed 'successfully' with those files silently
omitted: the bundle came out semantically incomplete with unresolved
links.

A second pass now collects same-directory markdown-link targets
(](./FILE.ext) or ](FILE.ext)) that name an extension-bearing file,
carry no internal slash, and are not SKILL.md itself; a leading '..'
is rejected fail-closed exactly like the support-dir traversal branch,
external URLs/anchors/mailto/site-absolute targets are left to their own
resolution, and every accepted name still runs the bundle path
validator. Unlinked siblings remain excluded — the fetch-minimization
contract is unchanged; only files the document explicitly links ship.
2026-08-29 20:39:09 -07:00
JUNZE f449374f0c skills hub: don't abort installs on missing referenced support files
A SKILL.md that mentions a support-path token which doesn't exist in the
repo (a prose glob like `references/type-*.md`, a placeholder truncated
by the regex, or a repo-only dev script) previously made GitHubSource
and UrlSource fetch fail with 'Could not fetch ... from any source',
even though every real file was present.

- _referenced_support_paths: skip glob/placeholder tokens and bare
  prefix matches that can't name an actual file.
- GitHubSource/UrlSource fetch: warn and continue when a referenced
  support file is missing or unfetchable, bundling what exists, instead
  of aborting the whole install. Non-regular tree entries (e.g. git
  symlinks) are still rejected.

Regression tests for both syntax filtering and the fetch loops.
2026-08-29 20:39:09 -07:00
Fidias Feliciano 972d812429 [verified] fix: ignore glob-shaped skill support paths 2026-08-29 20:39:09 -07:00
PRATHAMESH75 9d813c7352 test(cli): add install-flow E2E for skipped unreachable URL support file (#66760)
Covers the reviewer-requested end-to-end path: a referenced support file
that 404s is skipped, and the URL skill still installs through quarantine,
scan, install, and lock provenance with only the reachable files on disk
and in the lock file.
2026-08-29 20:39:09 -07:00
PRATHAMESH75 7d87bab502 fix(cli): skip unreachable support files instead of aborting URL skill install
UrlSource.fetch() aborted the whole SKILL.md URL install if any single
referenced support file (references/templates/scripts/assets) 404'd or
was otherwise unreachable, even when the SKILL.md itself and most
companion files were fine. Skip the missing file with a warning and
keep the ones that were fetched successfully.

Fixes #66760
2026-08-29 20:39:09 -07:00
Teknium 0f7981b8a1 fix(browser): real-profile snapshot auth files are owner-only (#96729)
The snapshot dirs were 0700 but every file inside landed umask-wide:
shutil.copy2 preserves Chrome's own 0644 profile-file modes and
sqlite3.connect creates the online-backup destinations as plain umask
files — so the copied Cookies / Login Data / Web Data (the user's live
session credentials) sat 0644. The 0700 parents contain it by default,
but the documented HERMES_HOME_MODE traversal hatch makes group/world-
readable children a real exposure.

snapshot_real_profile now reconciles every file (0600) and nested dir
(0700) inside the snapshot through the house helpers (_secure_file /
_secure_dir — managed-mode and container carve-outs included) at the
end of every pass, so snapshots written by older builds heal on their
next launch. Best-effort, never blocks a launch.

Tests: owner-only walk under umask 022 (fails on the pre-fix code —
sabotage-verified) + heal-on-refresh for a pre-existing 0644 Cookies.

The issue's other two findings are already fixed on main: mock-keychain
flags eliminated by the direct native-binary launch (#98249, salvage of
#96763); the 'Device not configured' TTY failure is superseded by the
same launch-path rework.
2026-08-29 20:07:53 -07:00
Teknium 45d9c33d85 feat(skills-hub): impeccable joins the optional-skills catalog, content pulled live from upstream
hermes skills install impeccable (and the docs-page install button) now
installs the impeccable frontend-design skill as an official optional-skills
entry. The local optional-skills/creative/impeccable/ dir is a catalog STUB:
its frontmatter declares metadata.hermes.upstream (repo + path), and
OptionalSkillSource.fetch() pulls the real 163-file bundle live from
pbakaus/impeccable:.hermes/skills/impeccable — the Hermes-native bundle
upstream maintains and verifies. Nothing vendored, never stale.

New mechanism (generic, not impeccable-specific):
- OptionalSkillSource._upstream_pointer(): parses/validates the upstream
  pointer (owner/name repo, clean relative path, traversal rejected).
- _fetch_from_upstream(): delegates to GitHubSource.fetch(), relabels the
  bundle official/<rel> at trust 'trusted' (curated endorsement, but
  third-party content — dangerous scan verdicts still block).
- The live-repo fallback path redirects stubs the same way, so stale local
  checkouts behave identically.

Three real gaps this surfaced, all fixed:
- GitHubSource.fetch() only downloaded SKILL.md plus paths linked from a
  canonical support dir (references/, scripts/, ...). Impeccable keeps its
  playbooks under reference/ (singular) and links scripts only from
  reference files, so fetch shipped 1 of 163 files. fetch() now downloads
  the full skill directory via the git tree (same approach as the
  optional-skills live fetch), still rejecting symlinks/hidden/unsafe paths
  and still failing on a missing SKILL.md-linked references/ path.
- The five env_exfil_* scanner patterns flagged loopback requests as
  critical exfiltration: impeccable's live mode polls
  http://localhost:PORT/status?token=TOKEN and scored two CRITICALs.
  Scheme-anchored loopback exemption added; evil.com/?u=localhost decoys
  still fire (10-case regex matrix in tests).
- unified_search() truncated to limit before ranking, so official catalog
  entries got crowded out by skills.sh mirrors and bare-name installs
  stalled on an ambiguity table. Results now stable-sort by trust rank
  before the cut, and _resolve_short_name prefers a sole official exact
  match over community mirrors.

Also fixes pre-existing test pollution: TestInstallPathSafety's fixture
monkeypatched the PEP 562 dynamic SKILLS_DIR, permanently shadowing dynamic
resolution and breaking the served_repo E2E tests in any combined run
(reproducible on main).

Validation: live E2E do_install("impeccable") against real GitHub —
resolves to official/creative/impeccable, verdict SAFE, 163 files on disk,
skill loads, /impeccable slash command registers. 128/128 targeted tests;
full-dir fetch test sabotage-verified. Docs: optional-skills catalog row,
generated skill page, sidebar.
2026-08-29 19:15:32 -07:00
Drexuxux ab9d85287d fix(cron): accept documented "every <weekday> <time>" schedules
parse_schedule's "every " branch passed everything after the prefix
straight to parse_duration(), so documented natural-language schedules
like "every monday 9am" and "every day at 9am" (AGENTS.md, SKILL.md,
cron docs) were rejected with "Invalid duration". Convert weekday and
daily/weekday/weekend phrases to cron expressions before the duration
fallback; "every 30m"/"every 2h" interval parsing is unchanged.
2026-08-29 19:14:51 -07:00
Teknium 0f3fcacd3f feat: /plan graduates from bundled skill to built-in command on every surface
The bundled plan skill's auto-generated slash command fell off the capped
Telegram/Discord command menus for most installs (skills are the only tier
trimmed at the platform caps, alphabetically — 'plan' sat past the cutoff at
index 57 of 82 bundled skills). Converting it to a first-class CommandDef
gives it a guaranteed core-tier menu slot on every platform.

- agent/plan_prompt.py: build_plan_prompt() — plan-mode rules + authoring
  craft distilled from the retired skill; prompt-injection pattern like
  /learn and /init (no engine, no model-tool footprint, cache-safe).
- CLI: _handle_plan_command mixin handler (pending-input injection).
- Gateway: /plan branch rewrites event.text and falls through (role
  alternation preserved).
- TUI: command.dispatch branch ('plan' was already in
  _PENDING_INPUT_COMMANDS).
- Removed skills/software-development/plan/ + docs pages (EN + zh-Hans),
  catalog rows, sidebar entry, related_skills references.
- PROTECTED_BUILTIN_SKILLS is now empty (mechanism kept); dependent
  curator/usage tests moved to monkeypatched sentinels.

Salvages #67292 by @webtecnica (credit: first /plan command submission,
issue #67264); reworked from inline planning prompt to the prompt-injection
pattern with workspace-saved plans. Closes #67264, closes #36821 (empty
/plan infers task from conversation context).
2026-08-29 19:14:15 -07:00
Teknium bacb90fe20 feat(delegation): honor delegation.request_overrides on all three resolution branches with explicit-over-runtime merge precedence
Completes the #90953 salvage on post-#98237 main:

- New _merge_request_overrides helper defines the precedence contract:
  explicit delegation.request_overrides merges OVER runtime/parent-derived
  overrides — explicit top-level keys win; extra_body is deep-merged one
  level so runtime extra_body keys survive unless redefined. Inputs are
  copy.deepcopy'd so transport-side mutation can't leak into config or the
  provider runtime cache.
- Direct base_url branch: explicit key now merges over the #98237
  provider-alongside-base_url runtime overrides instead of being a separate
  return shape; max_output_tokens preserved.
- Named-provider branch and parent-inherit branch now honor the key too, so
  delegation.request_overrides never silently no-ops.
- _build_child_agent honors override_request_overrides whenever set
  (previously only when override_provider was set), enabling the inherit
  branch's merged value to reach the child.
- DEFAULT_CONFIG: delegation.request_overrides entry with comment.
- Tests: expanded tests/tools/test_delegate_request_overrides.py — deep-copy
  proofs, explicit-over-runtime precedence on the provider-alongside-base_url
  path, named-provider branch, inherit branch, and merge-helper unit tests.
- Docs: configuration.md delegation section + features/delegation.md document
  the key, precedence, and example YAML (OpenRouter extra_body.provider.sort).
2026-08-29 19:13:23 -07:00
fabiantax d3bfd2e9b1 feat(delegation): forward delegation.request_overrides on direct-endpoint branch
The direct base_url branch of _resolve_delegation_credentials returned no
request_overrides key, so a direct OpenRouter delegation (provider=custom,
base_url=openrouter.ai/api/v1) could not pass routing hints to its children.
The named-provider branch already forwards runtime request_overrides; this
gives the direct branch the same contract, honouring delegation.request_overrides
from config (dict → forwarded, anything else → None).

Primary use: extra_body.provider = {"sort": "throughput"} so delegation
children route to the fastest OpenRouter provider for their model, per the
fab-swarm throughput work (#901).
2026-08-29 19:13:23 -07:00
itsflownium 393af4a310 fix(todo): live task state via revisioned snapshots and a dedicated todo.updated event
Salvaged from PR #97815 by @itsflownium, slimmed to the schema-free core:
- TodoStore gains a monotonic in-memory revision; the todo tool result
  returns it so clients can reject stale updates
- tui_gateway emits a dedicated todo.updated full-snapshot event that
  bypasses optional tool-progress display settings
- session resume/activate responses attach the authoritative todo
  snapshot; renderer restores it with revision arbitration
- desktop store tracks per-session revisions and rejects regressions

The session_todo_state DB table from the original PR is intentionally
dropped: canonical todo tool results already persist in conversation
history, so resume paths derive the snapshot from the stored transcript
instead of a parallel store.
2026-08-29 18:40:51 -07:00
Zane Chee b2e24b986f fix(computer-use): stop launching retired browser-grant runtimes 2026-08-29 18:35:17 -07:00
Jason Pollak 8e746668ba fix(browser): real-profile browsing on macOS - launch real binary, kill sqlite hang, normalize profile copy
Four fixes for real-profile browsing (browser.use_real_profile), found and
verified end-to-end on macOS with a live Chrome:

1. _copy_auth_file: sqlite3.connect('file:...?mode=ro') on a live Chrome
   auth DB can block indefinitely inside lock negotiation - the busy
   timeout never fires, so the 'fail fast' path hangs the launch forever.
   Try immutable=1 first (reads instantly, correct for a committed
   snapshot of a file another process owns); mode=ro stays as fallback.

2. Launch shape: agent-browser's own launch injects --use-mock-keychain /
   --password-store=basic / --headless=new. On macOS the mock keychain
   makes Chrome treat every keychain-encrypted cookie as undecryptable
   and drop it - the copied profile launches signed out (~3 anonymous
   cookies instead of the full jar). Launch the user's real browser
   binary directly on the copy (no mock-keychain switches), wait for
   DevToolsActivePort, then attach agent-browser via --cdp.

3. Snapshot copy: Local State was copied verbatim, still naming the
   SOURCE profile (last_used='Profile 2', info_cache listing several)
   while the copy only contains Default. Chrome opens the missing profile
   dir and starts signed out. Normalize the copy's Local State to
   Default-only.

4. CDP resolution: the agent-browser daemon may report the endpoint of a
   browser IT spawned (throwaway temp profile) instead of the real
   browser we launched on the copy. Trust the port our browser wrote to
   DevToolsActivePort.

Also adds browser.real_profile_pin (optional): pin which source Chromium
profile dir is snapshotted instead of following profile.last_used - on a
machine with a work profile and a personal one, last-used roulette can
silently give the agent the wrong identity. A pin naming a missing dir
fails closed (signed out) rather than falling back to last_used.

Tests: 4 new pin tests + 3 launch tests reshaped to the direct-launch
contract (Popen the real binary, agent-browser attaches). 77 passing.
2026-08-29 18:35:12 -07:00
Teknium b095c3d958 fix(tests): carry hermetic guards across the anthropic adapter module split
The adapter godfile split moved credential resolution into
agent/anthropic_credentials.py, which silently disarmed two repository
guards still pointed at the old seam:

- tests/conftest.py::_neutralize_macos_keychain_creds patched only the
  adapter re-export, so the default suite lost its protection against
  reading the operator's real macOS Keychain. Patch the implementation
  owner AND the adapter alias.
- test_oauth_setup_token_keeps_inherited_stdin read only the old source
  file; it now scans both seams and fails loudly if the call moves again.
- test_hermetic_side_effect_guards isolates the owner module directly.
2026-08-29 18:34:35 -07:00
Ayush Nangia 5cace31708 fix(delegation): carry provider request_overrides through the base_url path (#65035) 2026-08-29 18:16:04 -07:00
Teknium b6d535dd88 feat(browser): Brave Origin works for real-profile browsing and default-browser detection
Extends the real-profile machinery (PR #95620) to Brave Origin — Brave's
standalone paid build with a fully separate install identity:

- new canonical key 'brave-origin' in _CHROMIUM_BROWSERS
- Windows: BraveOHTML ProgId -> brave-origin; channel ProgIds BraveOBHTML/
  BraveODHTML/BraveOSHTM fail closed (identifiers from brave-core
  install_static)
- macOS: com.brave.Browser.origin bundle id (exact match); .beta/.dev/
  .nightly channel bundles fail closed; /Applications/Brave Origin.app
- Linux: brave-origin.desktop matched BEFORE the bare 'brave' fragment
  (substring scan would otherwise resolve an Origin default to stable
  Brave and drive the wrong profile — #95549 wrong-principal invariant);
  brave-origin-{beta,nightly,dev} fail closed
- profile dirs: BraveSoftware/Brave-Origin on all three OSes (per
  brave-core kProductPathName + Homebrew cask zap paths)
- /browser connect launch tables: Brave Origin split into its OWN group
  so a 'brave' executable lookup can never resolve to the Origin binary
- user-facing strings/docs/desktop tooltip updated

Tests: progid/bundle/desktop map params + data-dir resolution for all
three OSes; 125 passed in the three browser test files.
2026-08-29 18:13:33 -07:00
nftpoetrist d6a6d87c4a fix(tools): restore setup_mcp's never-hand-edit instruction
9d9f44d638 removed the desktop platform hint's "never hand-edit
mcp_servers config for them" sentence, reasoning it was a "word-for-word
duplicate of the setup_mcp tool schema... taught on every call." The
schema has never contained that instruction — only "never re-ask after
a decline." setup_mcp is desktop_ui-toolset-only and no runtime guard
in agent/file_safety.py covers mcp_servers config, so removing the only
place teaching this left a real gap: a model asked to add/configure an
MCP server could just write_file into mcp_servers config directly,
bypassing the consent-card/OAuth flow the tool exists to enforce.

Restored the instruction directly in SETUP_MCP_SCHEMA's description —
completing the original commit's stated intent (move it to the schema)
rather than reverting to the platform hint, since the schema reaches
every setup_mcp call regardless of platform hint wording changes.

Added a regression test asserting the schema description forbids
hand-editing mcp_servers config, so a future prompt-diet pass can't
silently drop it again without a test failing.
2026-08-29 17:58:51 -07:00
Teknium b6bd681e89 feat(todo): nested subtasks via optional parent field
The todo tool now supports hierarchical task lists: an item's optional
'parent' field points at another item's id, making it a subtask.

- tools/todo_tool.py: parent validated (self-ref dropped), dangling refs
  and cycles sanitized; merge mode can set/clear parent; post-compression
  injection renders the tree indented and keeps a finished parent visible
  while any descendant is still active; the in-progress reorder pass is
  skipped for nested lists (a flat move would tear subtasks from parents).
- Schema cost: ~45 tokens added to the cached tool schema (one string
  property + one behavior sentence).
- acp_adapter/tools.py: todo result markdown indents by parent depth.
- Desktop: TodoItem carries parent; todoTree() DFS helper; composer
  status stack renders subtask rows indented (depth-capped), stabilizer
  compares depth.
- Docs: tools-reference todo entry mentions nesting.

Hydration/replay paths (gateway fresh-agent, API-server history) work
unchanged: parent rides inside the same todos array.
2026-08-29 07:25:12 -07:00
Adolanium 1315e65a52 fix(skills): a failed rollback restore keeps the skill and the snapshots
Rollback removed the live skill directory before restoring its
snapshot. When copytree then failed (disk full, locked file, path too
long on Windows) the except only added a note, and the finally deleted
the snapshot directory too, so nothing survived: the skill was gone
with a success-shaped error payload.

The broken state is now renamed aside first and deleted only after the
snapshot is restored. If the restore still fails, the broken state is
renamed back, so the worst outcome is the half applied batch instead of
no skill at all. When rollback reports any failure the snapshots are
kept on disk and their location is logged, instead of being deleted by
the finally.

Follow-up to #97692, same batch executor.
2026-08-29 19:38:31 +05:30
Clifford Garwood 1ff8ec9a49 test(skills): cover the patch recovery loop end to end
Asserting an error string contains "read" proves the wording, not that a
model obeying it reaches a working patch. TestPatchRecoveryLoop walks the
loop through the public tool — half-formed patch, read the error, follow it,
patch succeeds — and pins the invariants that centralizing validation could
have broken: ordering ahead of the skill lookup, new_string='' still
deleting, missing new_string still rejected, and a rejected patch leaving
the file byte-identical.

The two recovery tests fail against the pre-fix dispatcher; the rest pass
either way as regression guards.
2026-08-28 23:17:35 -07:00
Clifford Garwood 4f4e778db8 fix(skills): make skill_manage patch failures recoverable instead of a dead end
`skill_manage(action='patch')` rejected a missing `old_string` with:

    old_string is required for 'patch'. Provide the text to find.

That is a dead end. The model cannot tell whether it omitted the argument or
supplied text that did not match, so it retries blindly — and then escapes to
the neighbour that always works: `action='write_file'`, which rewrites the
entire skill file and destroys unrelated content. `skill_manage`'s own action
enum puts that destructive path one token away from the failing one.

The error now names the recovery route: `old_string` must be the EXACT text
currently in the file, read the target first (the skill's SKILL.md, or the
file named by `file_path`), copy the snippet verbatim, and do not fall back
to `action='write_file'`.

Validation lives in `_patch_skill` rather than the dispatcher. `skill_manage()`
previously returned its own bare missing-argument error before ever calling
the helper, which would leave the new guidance unreachable through the public
tool. Removing that duplicate makes the helper the single source of truth; its
{"success": False, "error": ...} flows through the same json.dumps path, so
the serialized shape is unchanged, and validation still precedes the skill
lookup — a missing old_string on an unknown skill still reports the argument
error rather than "skill not found".

Fixes #33064
2026-08-28 23:17:35 -07:00
Teknium 217ab2f8df refactor(desktop-tools): consolidate preview + project, diet the desktop_ui suite (3,861 → 2,293 tok/call, −41%) (#97659)
* refactor(desktop-tools): consolidate preview(open/close/read) + project(create/switch/list), diet the desktop_ui suite — 3,861 -> 2,293 tok/call on desktop sessions (-41%)

* rename: preview -> desktop_preview, project -> desktop_project — namespace desktop-app tools against MCP/plugin name collisions

* test: sync remaining old-name pins — per-file registration import, GUI_TOOLS set, post-hook case read_preview -> desktop_preview action=read
2026-08-28 23:10:01 -07:00
Gille 9a1eef7a29 fix(tools): narrow MCP OAuth lock scope 2026-08-28 20:16:26 -07:00
Teknium dadbfd8990 refactor(patch): V4A mode gated to OpenAI-family mains — base schema is replace-only with real required (365 -> 195 for everyone else, -149; handler accepts both shapes from any model) (#97403) 2026-08-28 12:59:41 -07:00
Teknium 72874b0675 feat(skill_manage): operations[] is the call — each op names its skill; atomic with cross-skill rollback (#97295)
* feat(skill_manage): operations[] batch — several ops on one skill, atomic with rollback (memory-tool pattern); staged as ONE pending write under the approval gate

* refactor(skill_manage): operations[] IS the interface — single op = list of one (maintainer-directed); flat fields unadvertised handler compat; delete = sole-op routing

* guard(skill_manage): reject intra-batch same-file clobbers — double write/remove per path, full rewrite after an earlier SKILL.md edit; patch chains stay legal

* refactor(skill_manage): name-per-op — the call IS the operations array; cross-skill batches with all-touched-skills rollback

* guard(skill_manage): unify the intra-batch conflict guard — any destructive op on an already-touched file is rejected, with path normalization

Aggressive live testing found three holes in the two-part guard:
patch-then-write and patch-then-remove on the same supporting file
silently discarded the patch, and './references/x.md' //-style path
spellings slipped past the duplicate-write check. One rule now covers
the class: a destructive op (write_file/remove_file/full rewrite) on a
(skill, normalized-path) any earlier op touched is rejected pre-effect;
additive patches stay legal, so patch chains and write-then-patch still
work. Tests cover all three holes plus the pre-effect assertion.
2026-08-28 12:15:18 -07:00
Teknium baa344dee7 refactor(process): schema diet — enum names the verbs, description keeps only non-obvious semantics; write-vs-submit trap teaching emphasized (306 -> 228 tok/call, -25%) (#97279) 2026-08-28 09:23:00 -07:00
Teknium 7b5e1911f8 refactor(todo): schema diet — item shape and merge semantics taught once, by the param schema (323 -> 232 tok/call, -28%) (#97257) 2026-08-28 08:58:12 -07:00
Teknium a9e72f1b58 refactor(read_file): schema diet + bundle anydoc 0.2.4 + typed NeedsOcrError/hosted-OCR wiring (#97195)
* refactor(read_file): capability-gate the anydoc format list; PDF coverage teaching lives in the response-time warning (426 -> 244/291 tok/call)

* feat(read_file): bundle firecrawl-anydoc 0.2.4 in core, typed NeedsOcrError handling, config-gated hosted OCR with local-OCR-first guidance

* refine(read_file): NEEDS-OCR warning hints at checking for an OCR skill without naming one; hosted_ocr knob unadvertised (maintainer-directed)

* simplify(read_file): drop the anydoc schema gate — bundled core dep makes absence a broken install, not a variant; formats stated unconditionally (263 tok/call)

* feat(read_file): PDF wording upgrades to 'scanned or text' when a trusted hosted-OCR route exists (direct key or explicit config; nous gateway excluded until Parse proxy works)

* simplify(read_file): FIRECRAWL_API_KEY is the ONLY hosted-OCR gate — nous gateway route removed (Parse proxy broken), config true no longer unlocks; false still disables
2026-08-28 08:46:11 -07:00
Teknium 95cf7dc9e8 feat: session temp root moves off tmpfs /tmp to ~/.hermes/cache/terminal by default; auto-pruned after 72h
Follow-up on top of @rahlquist's terminal.temp_dir knob (#97182): the
default itself now avoids RAM-backed tmpfs. Resolution order on the
local backend: terminal.temp_dir > TMPDIR/TMP/TEMP > HERMES_HOME/cache/
terminal (managed, pruned) > /tmp fallback. Pruning: hourly via gateway
housekeeping + once-per-process best-effort sweep; hermes_bg_* triplets
are aged as a group so a live server's fresh .log protects its .pid.
2026-08-28 07:50:33 -07:00
rahlquist d7be3f649d feat: expose terminal.temp_dir to redirect session temp root off tmpfs
Some Linux distros (notably RAM-based tmpfs /tmp on several Arch-based
setups) cap the temp directory at a small size, so Hermes runs out of
space for session temp files (background logs/pid/exit files, code-
execution sandboxes). Add a terminal.temp_dir config key that points
these at real storage.

- Add terminal.temp_dir default (empty) in config_defaults.py
- Bridge it to TERMINAL_TEMP_DIR via TERMINAL_CONFIG_ENV_MAP
- Honor TERMINAL_TEMP_DIR first in LocalEnvironment.get_temp_dir(),
  falling through to TMPDIR//tmp//gettempdir when unset/invalid
- Add tests covering override, process-env, missing-dir, and empty
2026-08-28 07:50:33 -07:00
Teknium eff97a8a05 refactor(profiles): retire the cross-profile write guard — profiles are not isolated (maintainer decision); mirror lost-write guards (#32049) survive; patch/write_file schemas drop cross_profile (-83 tok/call) (#97165) 2026-08-28 06:36:22 -07:00
Teknium 9978706e93 refactor(skill_manage): schema diet — patch args defer to the patch tool's semantics, file_path states its skill-dir-relative shape, authoring curriculum compressed (517 -> 427, -17%) (#97152) 2026-08-28 05:48:15 -07:00
Teknium 536adb35c4 refactor(video_generate): capability-gated dynamic schema (~814 → 458/377 tok/call) (#97095)
* refactor(video_generate): capability-gated dynamic schema — 6 optional args render only when the active provider/model honors them; fleet capability declarations + declaration<->implementation contract tests

* fix(video_gen): H3/Grok/Happy-Horse/Gemini audio is ALWAYS-ON native, not absent — new audio_native family key + audio_always_on capability surfaces as description line (maintainer catch)

* test(video_gen): duration-span test pins the active-model contract — resolved family's real window, short families not inflated, union fallback still spans 30s
2026-08-28 05:10:33 -07:00
Teknium d48adfa789 test(vision): fixtures use a structurally valid PNG now that decode gates reject magic-bytes-only fakes
The salvaged proactive validation (#53307/#76896) rejects the
b'\x89PNG...' + zero-padding fixtures test_vision_tools.py used.
Replace all 11 sites with a real 1x1 PNG (+ ignored trailing padding
for size-sensitive tests), and pin _resize_image_for_vision passthrough
in the size-limit test so it exercises rejection, not resize recovery.
2026-08-28 04:58:21 -07:00
Teknium c0882093b8 test(vision): truncated-image test accepts rejection at either stacked gate
With both salvaged validators in place, the resolver-boundary verify()
(#53307) catches a truncated PNG before the full-decode gate (#76896)
runs. Either error message means the bytes stayed out of history.
2026-08-28 04:58:21 -07:00
CannibalKush 765142d94f fix: validate PNGs at shared image resolver 2026-08-28 04:58:21 -07:00
HaiyiMei ad0e83062f fix(vision): reject truncated images before embedding 2026-08-28 04:58:21 -07:00
liuhao1024 628a414d29 fix(browser): bind CDP binary exemptions to exact method result paths
Second review round: honoring base64Encoded recursively let any nested
dict spoof {"base64Encoded": true, "data": "<secret>"} past the redactor
(Runtime.evaluate returns arbitrary by-value JSON), and two carriers were
missed entirely — Network.streamResourceContent returns unflagged binary
bufferedData, and Network.getRequestPostData's postData was not covered.

Replace the ambient field sets with per-method exact result-path specs:
_CDP_ALWAYS_BINARY_PATHS for declared-binary paths (screenshots, PDFs,
streamResourceContent, beginFrame screenshotData, the nested
CacheStorage.requestCachedResponse.response.body) and
_CDP_FLAGGED_BINARY_PATHS for paths whose carrier object's base64Encoded
sibling gates the exemption (Network/Fetch.getResponseBody body, IO.read
data, getRequestPostData postData). Path suffixes propagate only into the
matching subtree, so base64Encoded is type information solely on trusted
carrier objects — never ambient trust in nested JSON.
2026-08-28 04:57:52 -07:00
liuhao1024 b2a17bfe82 fix(browser): scope the CDP binary-payload exemption to typed fields (#94138)
Architecture-review follow-up: the method-scoped binary_payload flag skipped
redaction for every string anywhere in the result of the two listed methods,
and the same corruption stayed reachable through Network.getResponseBody /
Fetch.getResponseBody / IO.read / Network.streamResourceContent. Make the
exemption field-scoped instead: an explicit schema exempts exactly
Page.captureScreenshot.result.data and Page.printToPDF.result.data (carriers
with no flag of their own), and any dict whose base64Encoded sibling is
exactly True exempts its body/data/bufferedData string (the protocol's own
discriminator — text bodies with base64Encoded: false stay redacted). Every
other string in every result keeps full secret redaction.
2026-08-28 04:57:52 -07:00
liuhao1024 a56885495e fix(browser): keep CDP binary payloads byte-identical through redaction (#94138)
_redact_cdp_output applied redact_sensitive_text(force=True) to every string
in CDP results, including the base64 screenshot/PDF payload of
Page.captureScreenshot and Page.printToPDF. The Fernet pattern (gAAAA + base64
alphabet) matches arbitrary spans inside such payloads wherever gAAAA follows a +
or /, collapsing them to first6...last4: decoded PNGs came out corrupt (valid header,
CRC failures mid-IDAT, no IEND), the persisted full copies in tool_result_storage
were redacted too, and vision_analyze then embedded corrupt images that the provider
rejected with 400 invalid_image - killing resume sessions with a misleading provider
error. Skip redaction for the two binary-payload methods: the payload is binary,
not free text, so there is no secret to protect there. Every other method keeps
full redaction.
2026-08-28 04:57:52 -07:00