feat(browser): store full snapshots on truncation; make eval denylist opt-in - #65923
Merged
Conversation
Collaborator
tonydwb
reviewed
Jul 16, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment
Looks Good
- Browser hardening: store full snapshots on truncation, make eval denylist opt-in
- Clean additions to
browser_tool.py,config.py, and corresponding tests - No security concerns
Reviewed by Hermes Agent
… opt-in Two harness fixes motivated by BU_Bench results where fixed-verb + lossy observation cost Hermes heavily vs code-driven browser agents: 1. Snapshot truncation no longer loses content. When a snapshot exceeds the 8000-char threshold, the complete accessibility tree is saved to cache/web (same truncate-and-store pattern as web_extract) and the truncated view / LLM summary includes the file path plus a ready-made read_file call. Element refs beyond the cut are recoverable without re-snapshotting. Stored copies are force-redacted and capped at 2MB; content-hash filenames dedupe repeated snapshots of the same page. 2. The browser_console(expression=...) sensitive-primitive denylist is now opt-in via browser.restrict_evaluate (default false). The names-based denylist blocked legitimate DOM extraction — any selector or expression containing 'fetch', 'cookie', 'input', etc. — which crippled the agent's only programmatic page-inspection path. The SSRF/private-URL egress guards in _browser_eval are independent of this policy and remain always-on. browser.allow_unsafe_evaluate keeps its meaning (bypass the denylist) for configs that already set it.
…relevant_content test_normal_content_returned pinned the exact return value; the summary now carries a pointer to the stored full snapshot. Assert the summary passes through and the pointer is present instead.
…udget SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract truncate-and-store paths give the model the same per-page budget. _truncate_snapshot's default max_chars now follows the constant. Invariant test added; docs (en+zh) and CLI tip updated.
teknium1
force-pushed
the
hermes/hermes-8fcc18e8
branch
from
July 17, 2026 06:29
5ffe2d2 to
7f63265
Compare
rdnot
added a commit
to rdnot/hermes-agent-medical-research
that referenced
this pull request
Jul 19, 2026
Upstream commit 0f102fa (PR NousResearch#65923) added an invariant test requiring SNAPSHOT_SUMMARIZE_THRESHOLD (browser_tool.py) == DEFAULT_EXTRACT_CHAR_LIMIT (web_tools.py). The fork intentionally diverges these: - DEFAULT_EXTRACT_CHAR_LIMIT = 400_000 (fork #9, raised for medical article extraction — full PubMed/AHA article bodies returned whole) - SNAPSHOT_SUMMARIZE_THRESHOLD = 15_000 (unchanged from upstream — browser snapshots are interactive session context, not research documents) Mark the test as skipped with a documented fork-divergence reason rather than aligning the thresholds, per user decision. This was caught by upstream CI on merge NousResearch#56 (8952d7b) after local verification missed it (test file was not in the merge-changed filter set).
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
… opt-in (NousResearch#65923) * feat(browser): store full snapshots on truncation; make eval denylist opt-in Two harness fixes motivated by BU_Bench results where fixed-verb + lossy observation cost Hermes heavily vs code-driven browser agents: 1. Snapshot truncation no longer loses content. When a snapshot exceeds the 8000-char threshold, the complete accessibility tree is saved to cache/web (same truncate-and-store pattern as web_extract) and the truncated view / LLM summary includes the file path plus a ready-made read_file call. Element refs beyond the cut are recoverable without re-snapshotting. Stored copies are force-redacted and capped at 2MB; content-hash filenames dedupe repeated snapshots of the same page. 2. The browser_console(expression=...) sensitive-primitive denylist is now opt-in via browser.restrict_evaluate (default false). The names-based denylist blocked legitimate DOM extraction — any selector or expression containing 'fetch', 'cookie', 'input', etc. — which crippled the agent's only programmatic page-inspection path. The SSRF/private-URL egress guards in _browser_eval are independent of this policy and remain always-on. browser.allow_unsafe_evaluate keeps its meaning (bypass the denylist) for configs that already set it. * test: update None-guard test for stored-snapshot pointer in _extract_relevant_content test_normal_content_returned pinned the exact return value; the summary now carries a pointer to the stored full snapshot. Assert the summary passes through and the pointer is present instead. * feat(browser): align snapshot threshold with web_extract's 15k char budget SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract truncate-and-store paths give the model the same per-page budget. _truncate_snapshot's default max_chars now follows the constant. Invariant test added; docs (en+zh) and CLI tip updated.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
… opt-in (NousResearch#65923) * feat(browser): store full snapshots on truncation; make eval denylist opt-in Two harness fixes motivated by BU_Bench results where fixed-verb + lossy observation cost Hermes heavily vs code-driven browser agents: 1. Snapshot truncation no longer loses content. When a snapshot exceeds the 8000-char threshold, the complete accessibility tree is saved to cache/web (same truncate-and-store pattern as web_extract) and the truncated view / LLM summary includes the file path plus a ready-made read_file call. Element refs beyond the cut are recoverable without re-snapshotting. Stored copies are force-redacted and capped at 2MB; content-hash filenames dedupe repeated snapshots of the same page. 2. The browser_console(expression=...) sensitive-primitive denylist is now opt-in via browser.restrict_evaluate (default false). The names-based denylist blocked legitimate DOM extraction — any selector or expression containing 'fetch', 'cookie', 'input', etc. — which crippled the agent's only programmatic page-inspection path. The SSRF/private-URL egress guards in _browser_eval are independent of this policy and remain always-on. browser.allow_unsafe_evaluate keeps its meaning (bypass the denylist) for configs that already set it. * test: update None-guard test for stored-snapshot pointer in _extract_relevant_content test_normal_content_returned pinned the exact return value; the summary now carries a pointer to the stored full snapshot. Assert the summary passes through and the pointer is present instead. * feat(browser): align snapshot threshold with web_extract's 15k char budget SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract truncate-and-store paths give the model the same per-page budget. _truncate_snapshot's default max_chars now follows the constant. Invariant test added; docs (en+zh) and CLI tip updated.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Browser snapshots that exceed the truncation threshold (now 15,000 chars, aligned with web_extract's per-page budget) now save the complete accessibility tree to disk with a
read_filepointer in the output (nothing is lost to truncation anymore), and thebrowser_console(expression=...)sensitive-primitive denylist is now opt-in instead of on-by-default.Both changes address BU_Bench findings where lossy observation and a names-based eval denylist crippled the agent's browser performance: truncation could drop the exact element ref a task needed, and the denylist blocked legitimate DOM extraction like
document.querySelector('#fetch-results')because the expression contained the word "fetch".Changes
tools/browser_tool.py:_store_full_snapshot()— mirrorsweb_tools._store_full_text: writes the full snapshot tocache/web(mounted read-only into remote backends), force-redacts secrets before writing, caps stored copies at 2MB, dedupes by content hash._truncate_snapshot()— truncation note now includes the stored file path and a ready-to-useread_file path=... offset=... limit=200call; falls back to the old note if storage fails. Output stays withinmax_chars(note budget is reserved, clamped for tiny caps)._extract_relevant_content()— LLM summaries append a pointer to the stored full snapshot._enforce_browser_eval_policy()— the denylist now only applies whenbrowser.restrict_evaluate: trueis set.browser.allow_unsafe_evaluatekeeps its existing meaning (bypass the denylist) for configs that already set it. The SSRF/private-URL egress guards in_browser_evalare independent of this policy and remain always-on.browser_snapshotschema description mentions the saved-file pointer.hermes_cli/config.py: newbrowser.restrict_evaluate: falsedefault (no version bump — additive key).website/docs/user-guide/features/browser.md: both behaviors documented.SNAPSHOT_SUMMARIZE_THRESHOLDraised 8000 → 15000, equal toweb_tools.DEFAULT_EXTRACT_CHAR_LIMIT, with an invariant test pinning the alignment;_truncate_snapshotdefault follows the constant.All truncation/summarization call sites route through these two functions —
browser_navigateauto-snapshot,browser_snapshot, and both Camofox paths — so every consumer gets the stored-file pointer with no per-site changes.Validation
expression="document.cookie"(default config)expression=...w/restrict_evaluate: trueE2E-tested against a temp
HERMES_HOMEwith real file I/O and a realconfig.yaml: store/dedupe/redaction verified on disk, default-open + opt-in restrict + allow_unsafe override verified through the real config read path. Targeted suites:test_browser_hardening,test_browser_console,test_browser_secret_exfil,test_browser_eval_ssrf,test_browser_camofox,test_config— 256 tests, 0 failures.Infographic