Commit Graph

21 Commits

Author SHA1 Message Date
teknium1
dad0057271 fix(plugin-guard): v8 — four intake false-positive classes step down where inert
Real catalog pins from the 2026-09-20 intake batch scored on text that cannot run on
the installing host:

1. `.github/workflows/*.yml` — a CI step's own `os.environ['RUNNER_TEMP']` read scored
   `python_os_environ/high` and made a clean plugin `caution` (remarkable). A workflow
   runs on the forge's runner; it now takes the README prose cap (one step down,
   agent-facing shapes like `curl | sh` keep full severity).
2. "pip install" inside a user-facing message literal (`"... no pip install is needed"`,
   image-utils) scored `unpinned_pip_install/medium`; mid-literal, non-command position,
   no exec verb on the line → low. `"pip install x"`, `python -m pip install`, `uv pip`,
   `subprocess.run("pip install …")` keep medium.
3. `desktop_surface_findings()` was being run by batch tooling over every `*.js`/`*.mjs`
   in a repo and flagged a Node sidecar's lazy `import('jszip')` (remarkable). The
   product check was already scoped to `desktop/`; expose that scope as
   `is_desktop_surface()` / `desktop_surface_hits()` so tooling shares it.
4. `127.0.0.1:<port>` (README, .mcp.json, client defaults) scored `hardcoded_ip_port` as
   network egress; a line whose every IP:port is loopback → low. A routable address on
   the line keeps medium.

Every finding stays in the report. PLUGIN_SCANNER_VERSION → plugin-guard-v8 so cached
verdicts on quarantined pins are re-evaluated.
2026-09-20 11:49:00 -07:00
teknium1
3d14b20de9 chore(guard): bump scanner versions after the hardcoded_secret carve-out
Cached verdicts are keyed on the scanner version; without the bump a plugin or
skill already scanned "dangerous" for an env-var NAME constant would keep its
cached verdict and stay blocked.
2026-09-20 11:30:03 -07:00
teknium1
ec015c906c fix(plugin-guard): plural test-file names and delegation prose are not attack shapes
Two install-scan false positives from catalog intake, each red on the real pinned tree:

* memory-review (apoapostolov/hermes-agent-awesome-plugins@b270520, plugins/memory-review):
  tests_state.py:86 holds '/etc/passwd' in a quoted traversal-probe list and scored
  system_passwd_access:critical -> dangerous (unoverridable). The test-tree name rule only knew
  test_*.py / *_test.py; a single-module plugin without a tests/ dir names its file tests_*.py.
  Accept the plural prefix/suffix so the quoted fixture is a note, exactly as tests/test_x.py is.
* pstack (Cloeille/pstack@ac5e5ab, #116381 pin): skills/poteto-mode/SKILL.md:128
  'Send subagents the minimum context they need' hit context_exfil:high. Handing context to the
  agent's own subagent is an in-process handoff; the bare-'context' branch now skips a
  subagent/worker/delegate recipient named right after the verb. External destinations, bare
  'your context', and 'send agents your context' still match.

Two tightenings guard the widened test-file surface (both pre-existing gaps, now closed):
  - _EXEC_ON_LINE gains open(: open('/etc/passwd') in a test steps down once (high, confirmable)
    instead of reading as quoted data (medium, note).
  - the JS regex-literal lexer accepts only real flags [dgimsuvy]; with [a-z]* an unquoted Unix
    path lexed as /etc/ + flags 'passwd', so 'cat /etc/passwd | curl ...' in a test script was inert.

PLUGIN_SCANNER_VERSION plugin-guard-v6 -> v7 so cached verdicts re-scan.
2026-09-19 19:41:42 -07:00
teknium1
50e9cd7bd5 fix(plugin-guard): two intake false positives — regex literal <script, allowlist "printenv"
Desktop lint: /<script[\s\S]*?<\/script>/gi in a feed sanitiser scored as
'script injection' and failed pinned-source-validate for rss-reader. Mask
JS regex literals for the markup-shaped rule only; <script in a string
literal (an innerHTML payload) and createElement('script') still fail.

Install scanner: "printenv" as a whole-string entry of a read-only
allowlist (frozenset({..., "printenv"})) fired dump_all_env high →
caution on hermes-jev. Extend the literal-token demotion: a token that is
the ENTIRE quoted literal on a line that executes nothing steps down like
an alternation member; "sudo" inside subprocess.run([...]) and
os.system("printenv") keep high.

A/B vs origin/main: attack probes identical (23 rows), in-tree sweep 319
entries 0 worse/0 changed; both new tests red on base. Bumps
PLUGIN_SCANNER_VERSION to v6 so cached caution verdicts refresh.

Signed-off-by: teknium1 <teknium1@users.noreply.github.com>
2026-09-19 14:30:36 -07:00
teknium1
5a0c2fb89e fix(plugins): install scanner scores inert context as context, not as plugin behaviour
A plugin repository is a codebase, and the threat regexes were written for a SKILL.md
the agent executes verbatim. The same text in a README uninstall step, a refusal list,
a test fixture, a JSON scenery data URI, a redaction regex or a `gh api | base64 -d |
grep` dev script was scored as the plugin's own runtime behaviour: crypto-prices
(#115353) was hard-blocked by `rm -rf "$HOME/.hermes/plugins/crypto-prices"` in its
README, and a dozen catalog pins sat at `caution` on fixtures and prose alone.

`tools/plugin_guard_context.py` recognises each class of inert context and only ever
lowers a finding; nothing is deleted and every finding stays in the report:

- documentation prose (`.md/.txt/.rst/.html`, not `SKILL.md`, `after-install.md` or a
  bundled `skills/` tree): command/path shapes step down once, so a doc line can never
  be `dangerous`; the plugin's own-install-dir `rm` is a note. Injection, Markdown
  exfil, agent-config edits, `curl | sh`, `authorized_keys` and leaked keys keep full
  severity.
- test trees / fixtures (root test dirs, `__tests__`/`__fixtures__` at any depth,
  `*.test.*`, `test_*.py`): quoted-only hostile strings and key-shaped corpora are
  notes; test code that executes on import steps down once (caution).
- whole-line comments and CHANGELOG.md score as prose.
- `encoded_exfil` on base64 whose decoded head is a media magic (PNG/JPEG/WOFF/PDF...)
  is informational.
- `sudo` / `env|` as an alternation member inside a regex or string literal is a note;
  the same word in a command string is not.
- `base64 -d` piped into a text filter is a note; into a shell/interpreter it is not.

Bumps PLUGIN_SCANNER_VERSION to v5 so cached verdicts re-evaluate.

Closes-blocker-for: #115353
2026-09-19 03:20:05 -07:00
teknium1
dd2141b928 fix: cap only generic sample tokens, one step, last; bump plugin-guard to v4
Follow-up to the cherry-picked #112146 (@KoNit-K):

- Move the __main__ demotion to the end of _filter_findings behind a
  severity == "critical" guard and a MAIN_GUARD_DEMOTIONS table (the
  DOC_PROSE_DEMOTIONS idiom), so it is a one-step cap that can never
  re-raise a finding an earlier remap already lowered.
- Treat ValueError from ast.parse (NUL bytes, undecodable text) like a
  SyntaxError: no cap, the file is runtime code (fail closed).
- Bump PLUGIN_SCANNER_VERSION to plugin-guard-v4: the verdict semantics
  for a plugin shape changed, and the version is what install output
  and recorded provenance show.
- Trim to two invariant tests: the reporter's repro shape (token inside
  the guard -> caution + confirmable/--force, same token above the guard
  -> dangerous, --force refused) and narrowness (destructive payload and
  a provider-shaped sk- key inside the guard, plus an unparseable file,
  all stay critical -> dangerous).
- Docs: one paragraph on the __main__ cap next to the test-tree cap.

Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
2026-09-16 17:01:30 -07:00
KoNit-K
4037b009b6 fix(plugins): demote main-guard sample tokens 2026-09-16 17:01:30 -07:00
teknium1
bb1d255a77 chore(scanners): bump skills-guard to v4 and plugin-guard to v3
Five scanner rule changes land together (prose/comment demotion, own-denylist
demotion, shell_rc/sudo token fixes, Markdown link masking, ssh write-verb
gate). Cached verdicts keyed on the old versions would keep previously
blocked skills and plugins blocked; one bump re-scans them.
2026-09-14 16:12:07 -07:00
teknium1
05e74268ea test: trim prose/comment guard coverage to invariants; drop the unused CSS comment prefix
Fold the three PRs' overlapping tests into two invariants per behaviour:
- comment/changelog prose demoted to caution and confirmable; trailing comments,
  runtime code and agent-facing docs still dangerous (#111193)
- Markdown plan/design prose demoted to caution; the same content in runtime
  code still dangerous (#103364)
- skills_guard: context_exfil needs a transfer directive; rm -rf under temp
  roots is not destructive_root_rm

The #111199 regression test is kept (behaviour is the same); its mechanism
(re-read the file per finding, cap every .md) was not carried since the
match-based cap already covers it without touching agent-facing docs.
'//' is not a CSS comment marker, so .css is dropped from the prefix table.
2026-09-14 16:12:07 -07:00
webtecnica
fd0de74bd9 fix(plugins): cut plugin-guard false positives on prose and agent-config-file refs (#103364)
(cherry picked from commit 4ca17d1de68c25e56fabd0a72dbca6c90b877bba)
2026-09-14 16:12:07 -07:00
Konstantin Khlopkov
a8b7af8586 fix(plugins): stop the install scanner from scoring hardening comments and changelogs as un-overridable criticals
A whole-line code comment or a changelog entry describing the threat a defense
rejects ("# a symlink could point at /etc/passwd, so ...") scored full severity,
driving the verdict to dangerous and making security-hardened community plugins
un-installable: --force does not override dangerous, so the only way through was
deleting the documentation of what was defended against. Whole-line comments and
CHANGELOG entries now cap one severity step lower (critical->high), keeping the
finding visible and the verdict at caution: blocked by default, --force
overridable. Trailing comments (executable code on the line), runtime code and
agent-facing docs keep full severity.

Fixes #111193

(cherry picked from commit 2a79f0a67e70eddcd387e759b1570d4feaee1e97)
2026-09-14 16:12:07 -07:00
Adolanium
30d78cd85e fix(plugin-guard): reconcile current scanner rules and test install confirmation 2026-09-14 23:42:17 +03:00
teknium1
1e76efbe28 fix: scan plugin test trees again, cap their criticals at caution
Skipping `tests/`, `spec/`, ... in EXCLUDED_DIRS made those trees
invisible to the guard, but `plugins_loader._load_directory_module`
sets `submodule_search_locations=[plugin_dir]`, so a plugin
`__init__.py` doing `from .tests import evil` imports and runs whatever
lives there: a `tests/evil.py` with a destructive root remove scanned
`dangerous` on main and `safe` on this branch. `_walk` also matched the
names at any depth, so `src/spec/handler.py` — plain runtime code — went
unscanned.

Keep scanning everything; instead cap a critical finding located under a
ROOT-level test dir at `high`, so the verdict is `caution` (confirmation
required, `--force` overridable) rather than the un-overridable
`dangerous`. Fixture strings still cannot brick an install, which was
the reported problem, while a critical in any runtime file (`setup.sh`,
`src/spec/...`) still yields `dangerous`. Trade-off stated in the PR
body: hostile code deliberately placed under `tests/` is now
force-installable rather than blocked outright.

Docs no longer claim test code never runs.
2026-09-13 14:43:04 -07:00
joaomarcos
fe97c84c73 fix(plugin): identify critical findings in install blocks 2026-09-13 14:43:04 -07:00
liuhao1024
07b60880cf fix(plugins): stop the security scanner from reading test trees
plugin_guard walks the whole plugin clone, and EXCLUDED_DIRS skipped
caches and vendored dirs but not tests/. A security-conscious plugin's
test suite SHOULD contain adversarial fixtures — a test asserting the
trust boundary holds round-trips the injection string verbatim — and
any single critical finding makes the verdict dangerous, which --force
explicitly cannot override. Scanning tests therefore made exactly the
plugins that test their security unconditionally uninstallable, and the
only workaround was obfuscating the payload strings, weakening the
tests and inverting the incentive. Fixtures are never loaded into an
agent's context at runtime the way README/plugin.yaml are.

Add the conventional test/spec/fixture directory names to
EXCLUDED_DIRS, alongside the existing cache/vendored skips.
2026-09-13 14:43:04 -07:00
Adolanium
db2b5266c6 fix(plugin-guard): require confirmation for ambiguous JS capability references 2026-09-12 07:30:41 +03:00
Teknium
1545afb892 refactor(tools): simplify registry, schema_sanitizer, self_repo_guard, plugin_guard, project_tools, path_security (-25% LOC)
Zero behavior change; tool schemas byte-identical; golden corpus (376 guard
commands, 93 heredoc forms, sanitizer/unrename cases) identical old vs new.

registry.py (1263 -> 924): _memo_check per-pass check_fn memo (2 sites),
_grouped/_toolset_entries toolset grouping (6 sites), _unique_env replaces
_extend_unique, _attr accessor for get_schema/get_toolset_for_tool/get_emoji/
get_max_result_size, fold cache-prune loops, flatten _callable_module walk,
merge guard try-blocks, docstring/comment compaction (all WHY kept).
schema_sanitizer.py (511 -> 392): _rewrite bottom-up tree map shared by
_strip_ref_siblings/strip_nullable_unions/collapse_const_unions, _dict_nodes
generator replaces nested _walk closure, collapsed _const_branch_type guards.
self_repo_guard.py (678 -> 517): _scope_keys state machine as one if/elif
ladder, heredoc opener parsed by one regex (_HEREDOC_OPENER_RE), _masked_line
inlined, _operator_before via rstrip, folded tail expressions.
plugin_guard.py (235 -> 163): positional Finding ctor, packed tables, docs.
project_tools.py (181 -> 156): _activated shared by create/switch, _ACTIONS
dict dispatch replaces the if-chain in _handle_project.
path_security.py (24 -> 18): unused logger/logging import dropped.
2026-09-02 23:56:07 -07:00
Teknium
5c919161d0 refactor(tools/approval): split approval.py into smart/human-wait/gateway-wait modules; dedupe guards 2026-09-02 14:44:14 -07:00
Teknium
d4cec15b47 refactor(tools): first-wave simplification of tools/ (file ops split, lazy_deps, code_exec, approval, browser, delegate, mcp, skills, terminal, voice, media)
Behavior-neutral structural pass over tools/*: god-file extractions into
sibling modules (file_operations_common/lint/search, file_tools_paths/
read_tracking/write, code_execution_env/rpc, tool_search_catalog/names/
validation, tts_command_provider, ...), duplicate helper unification,
if/elif -> dispatch tables, dead-code removal, docstring compaction.
Tool schemas (get_tool_definitions) verified byte-identical to base.
2026-09-02 14:43:45 -07:00
kshitijk4poor
8c098e9e81 fix(skills): catch sed flag variants; exempt content-contract prose in plugin code
Review-fold from the 3-angle simplify pass:

- sed -Ei / -iE / --in-place now match the shell-critical tier (the
  bare '\s-i\b' token missed combined short flags and the GNU long
  form); read-only sed stays unflagged. Regression tests added.
- agent_config_contract joins plugin_guard's CODE_EXEMPT_PATTERN_IDS:
  content-contract prose in plugin code files (docstrings/comments)
  is the same false-positive class the existing agent_config_mod
  exemption suppresses. Doc/config files keep the full pattern set.

Efficiency reviewer: 1.24x full-scan cost (+3.4ms/file, install-time
only), worst-case adversarial line 55us — no ReDoS exposure.
2026-08-28 03:24:43 -07:00
Teknium
d44a295492 Inspired by Claude Cowork: security scanning for plugin install/update
Claude Cowork (Aug 6, 2026) added skill & plugin security scanning:
third-party skills and plugins are automatically checked for malicious
content on upload/edit, returning pass/warn/fail. Hermes already scans
hub-installed skills (tools/skills_guard.py), but `hermes plugins
install` cloned and activated arbitrary Git repos completely unscanned —
and plugins run Python in-process, making them the more dangerous
surface.

- tools/plugin_guard.py: plugin-adapted scanner reusing the skills_guard
  pattern engine. Exempts the documented provider-plugin patterns (own
  requires_env API-key reads, HTTP calls with keys) on code files while
  keeping true threat signals (foreign credential-store access, reverse
  shells, destructive/persistence/obfuscation patterns, prompt injection
  in docs). Plugin-sized structural limits; VCS/venv dirs excluded.
- hermes_cli/plugins_cmd.py: scan the temp clone before it is moved into
  ~/.hermes/plugins/. safe=install, caution=confirm (interactive prompt
  or --force), dangerous=blocked (--force does NOT override). Re-scan on
  `hermes plugins update`; a dangerous updated tree is deactivated until
  the user reviews the findings. Dashboard install path returns
  structured scan_blocked/scan_findings.
- Config gate: plugins.scan_on_install (default true) in config.yaml.
- Validated against all 60 bundled plugins: 57 safe, 3 caution (real
  sudo / curl|sh content in their docs), 0 false-positive blocks.
- 15 new tests incl. E2E through _install_plugin_core with real git
  clones.
2026-08-16 22:08:37 -07:00