feat: add pre_tool_call rewrite support for plugin tool arg transformation#1
Closed
elasticdotventures wants to merge 3 commits into
Closed
feat: add pre_tool_call rewrite support for plugin tool arg transformation#1elasticdotventures wants to merge 3 commits into
elasticdotventures wants to merge 3 commits into
Conversation
…ation Adds get_pre_tool_call_directives() that fires pre_tool_call hook ONCE and returns both block_message and rewritten_args. Existing get_pre_tool_call_block_message() kept as backward-compat alias. New get_pre_tool_call_rewrite() alias added. Updates all 3 call sites (_invoke_tool, concurrent loop, sequential loop) and handle_function_call in model_tools.py to use the combined function.
There was a problem hiding this comment.
Pull request overview
Adds a new plugin helper to fire the pre_tool_call hook once and return both blocking and argument-rewrite directives, then wires tool execution paths to use it (needed for b00t integration until upstream merges).
Changes:
- Introduces
get_pre_tool_call_directives()returning(block_message, rewritten_args)and keeps backward-compat wrappers. - Updates tool invocation paths (
run_agent.py,model_tools.py) to use the new helper and apply rewritten tool args. - In the concurrent execution path, re-checks guardrails when args are rewritten.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| run_agent.py | Uses get_pre_tool_call_directives() across invocation paths; applies rewritten args and handles plugin blocks. |
| model_tools.py | Uses get_pre_tool_call_directives() in the dispatcher to support block + rewrite without double-firing hooks. |
| hermes_cli/plugins.py | Adds the new directives helper and backward-compatible wrappers for block-only / rewrite-only access. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
9452
to
9456
| except Exception: | ||
| block_message = None | ||
| pass | ||
|
|
||
| if block_message is not None: | ||
| block_result = json.dumps({"error": block_message}, ensure_ascii=False) | ||
| else: | ||
| if block_result is None and _block_msg is None: | ||
| guardrail_decision = self._tool_guardrails.before_call(function_name, function_args) |
Comment on lines
+9439
to
+9447
| from hermes_cli.plugins import get_pre_tool_call_directives | ||
| _block_msg, _rewritten = get_pre_tool_call_directives( | ||
| function_name, function_args, task_id=effective_task_id or "", | ||
| ) | ||
| if _block_msg is not None: | ||
| block_result = json.dumps({"error": _block_msg}, ensure_ascii=False) | ||
| elif _rewritten is not None: | ||
| function_args = _rewritten | ||
| # Re-check guardrails with rewritten args |
Comment on lines
+9277
to
+9284
| from hermes_cli.plugins import get_pre_tool_call_directives | ||
| _block_msg, _rewritten = get_pre_tool_call_directives( | ||
| function_name, function_args, task_id=effective_task_id or "", | ||
| ) | ||
| if _block_msg is not None: | ||
| return json.dumps({"error": _block_msg}, ensure_ascii=False) | ||
| if _rewritten is not None: | ||
| function_args = _rewritten |
Comment on lines
687
to
+690
| if not skip_pre_tool_call_hook: | ||
| block_message: Optional[str] = None | ||
| try: | ||
| from hermes_cli.plugins import get_pre_tool_call_block_message | ||
| block_message = get_pre_tool_call_block_message( | ||
| from hermes_cli.plugins import get_pre_tool_call_directives | ||
| _block_msg, _rewritten = get_pre_tool_call_directives( |
| tool_call_id: str = "", | ||
| ) -> Optional[str]: | ||
| """Check ``pre_tool_call`` hooks for a blocking directive. | ||
| ) -> tuple: |
Comment on lines
1186
to
+1216
| @@ -1196,16 +1195,56 @@ def get_pre_tool_call_block_message( | |||
| tool_call_id=tool_call_id, | |||
| ) | |||
|
|
|||
| block_message: Optional[str] = None | |||
| rewritten_args: Optional[Dict[str, Any]] = None | |||
|
|
|||
| for result in hook_results: | |||
| if not isinstance(result, dict): | |||
| continue | |||
| if result.get("action") != "block": | |||
| continue | |||
| message = result.get("message") | |||
| if isinstance(message, str) and message: | |||
| return message | |||
| action = result.get("action") | |||
|
|
|||
| if action == "block" and block_message is None: | |||
| message = result.get("message") | |||
| if isinstance(message, str) and message: | |||
| block_message = message | |||
|
|
|||
| elif action == "rewrite" and rewritten_args is None: | |||
| new_args = result.get("args") | |||
| if isinstance(new_args, dict): | |||
| rewritten_args = new_args | |||
|
|
|||
| return block_message, rewritten_args | |||
Routes terminal commands through b00t hive run --dry-run guards. Intercepts pre_tool_call hook to block, warn, or rewrite commands. Handles pip->uv, docker->podman, main-branch protection, and more.
elasticdotventures
added a commit
to elasticdotventures/_b00t_
that referenced
this pull request
May 4, 2026
…ite patch Vendor submodule pointing to PromptExecution/hermes-agent-b00t on feat/pre-tool-rewrite-hook branch. Contains the get_pre_tool_call_directives() patch required for b00t guard interposition via Hermes plugin hooks. Upstream PR: NousResearch/hermes-agent#19305 Internal PR: PromptExecution/hermes-agent-b00t#1
elasticdotventures
added a commit
to elasticdotventures/_b00t_
that referenced
this pull request
May 4, 2026
…ite patch Vendor submodule pointing to PromptExecution/hermes-agent-b00t on feat/pre-tool-rewrite-hook branch. Contains the get_pre_tool_call_directives() patch required for b00t guard interposition via Hermes plugin hooks. Upstream PR: NousResearch/hermes-agent#19305 Internal PR: PromptExecution/hermes-agent-b00t#1
elasticdotventures
added a commit
to elasticdotventures/_b00t_
that referenced
this pull request
May 4, 2026
…tocol (#369) * chore: remove plantuml-server embedded repo from git index * feat: guard escalation, parser stages, b00t-ast CLI, b00t-py bindings, violation persistence - Guard violation counter with JSONL persistence (~/.b00t/guard-violations.jsonl) - 🦨→💩 escalation: Warn→Block when violation_count >= repeat_threshold - check_guards() auto-persists violations on every match - K0mmand3rStage guards: pattern = { stage = "pre_parse" } in hive-guards.hive.toml - parser_stages wired into KmdLine::parse() at 7 phases - b00t-ast CLI binary: b00t-ast dir <path> [--format json|mcp|counts] - b00t-py: guard_check, emoji_lookup, register_stage_guard bindings - KmdLine fields made pub for serde serialization - Schema datums moved to _b00t_/schema/ (uppercase convention) - k0mmand3r crate edition 2024, clean lints - Rust 2024: #![allow]→removed, set_var unsafe wrappers - b00t_env_backend.py promoted from DESIGN to working Python backend - Hermes backend symlinked: just hermes-backend-enable * chore: add hermes-agent-b00t vendor submodule with pre_tool_call rewrite patch Vendor submodule pointing to PromptExecution/hermes-agent-b00t on feat/pre-tool-rewrite-hook branch. Contains the get_pre_tool_call_directives() patch required for b00t guard interposition via Hermes plugin hooks. Upstream PR: NousResearch/hermes-agent#19305 Internal PR: PromptExecution/hermes-agent-b00t#1 * feat: add SCM convention guards — branch naming, main protection, conventional commits New hive guards block or warn before git commands reach the shell: - BLOCK: git checkout main/master — use feature branches - BLOCK: git push origin main — use PRs instead - BLOCK: git merge main — use gh pr merge - WARN: git checkout -b without type/ — use feat/fix/chore/ prefix - WARN: git commit -m without colon — use Conventional Commits format * feat: add regex_match() to Rhai engine + SCM convention guards - Registered regex_match(cmd, pattern) on Rhai engine in hive.rs for future guard pattern matching - Added 5 SCM guards to hive-guards.hive.toml: BLOCK: git checkout main/master, git push origin main, git merge main WARN: branch without type/ prefix, commit without conventional format - All guards use simple cmd.contains() — readable, no escaping hell * feat: add b00t guard interposition Hermes plugin Bumps vendor/hermes-agent-b00t to include the new plugins/b00t/ directory with pre_tool_call hook that routes terminal commands through b00t hive run --dry-run guard evaluation. * crypto-sign: ed25519 signing for peer_facts in IrontologyPeerStore * hive-peers: gossip, mDNS discover, list --health, peer GC * docs: initial review plan Agent-Logs-Url: https://github.com/elasticdotventures/_b00t_/sessions/b13588c4-07c5-4b06-8575-5be55c579fb1 Co-authored-by: elasticdotventures <35611074+elasticdotventures@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
elasticdotventures
added a commit
to elasticdotventures/_b00t_
that referenced
this pull request
May 4, 2026
#373) * feat: guard escalation, parser stages, b00t-ast CLI, b00t-py bindings, violation persistence - Guard violation counter with JSONL persistence (~/.b00t/guard-violations.jsonl) - 🦨→💩 escalation: Warn→Block when violation_count >= repeat_threshold - check_guards() auto-persists violations on every match - K0mmand3rStage guards: pattern = { stage = "pre_parse" } in hive-guards.hive.toml - parser_stages wired into KmdLine::parse() at 7 phases - b00t-ast CLI binary: b00t-ast dir <path> [--format json|mcp|counts] - b00t-py: guard_check, emoji_lookup, register_stage_guard bindings - KmdLine fields made pub for serde serialization - Schema datums moved to _b00t_/schema/ (uppercase convention) - k0mmand3r crate edition 2024, clean lints - Rust 2024: #![allow]→removed, set_var unsafe wrappers - b00t_env_backend.py promoted from DESIGN to working Python backend - Hermes backend symlinked: just hermes-backend-enable * chore: add hermes-agent-b00t vendor submodule with pre_tool_call rewrite patch Vendor submodule pointing to PromptExecution/hermes-agent-b00t on feat/pre-tool-rewrite-hook branch. Contains the get_pre_tool_call_directives() patch required for b00t guard interposition via Hermes plugin hooks. Upstream PR: NousResearch/hermes-agent#19305 Internal PR: PromptExecution/hermes-agent-b00t#1 * feat: add SCM convention guards — branch naming, main protection, conventional commits New hive guards block or warn before git commands reach the shell: - BLOCK: git checkout main/master — use feature branches - BLOCK: git push origin main — use PRs instead - BLOCK: git merge main — use gh pr merge - WARN: git checkout -b without type/ — use feat/fix/chore/ prefix - WARN: git commit -m without colon — use Conventional Commits format * feat: add regex_match() to Rhai engine + SCM convention guards - Registered regex_match(cmd, pattern) on Rhai engine in hive.rs for future guard pattern matching - Added 5 SCM guards to hive-guards.hive.toml: BLOCK: git checkout main/master, git push origin main, git merge main WARN: branch without type/ prefix, commit without conventional format - All guards use simple cmd.contains() — readable, no escaping hell * crypto-sign: ed25519 signing for peer_facts in IrontologyPeerStore * chore: update vendor/l3dg3rr submodule to ledgrrr (rebranded upstream) - Submodule URL: https://github.com/PromptExecution/l3dg3rr → git@github.com:PromptExecution/ledgrrr - Submodule pointer: 1ed3b3d → 2168595 (includes PR #80, dashboard-generated-panels-51-rebased) - Remote changed from HTTPS to SSH for consistent auth
elasticdotventures
pushed a commit
that referenced
this pull request
May 30, 2026
Four findings from Copilot's review on PR NousResearch#22891, all in the AX elements-array cap added by 22fa1ed: 1. The truncation note ("response truncated to N of M elements") was appended unconditionally — including in the som/vision multimodal path, whose response carries a screenshot rather than an `elements` array. The note described a payload field that wasn't present. Moved the note into the AX-text branch where the array actually appears. 2. `_format_elements(cap.elements)` ran on the full untrimmed list with its own `max_lines=40` cap, so a caller passing `max_elements=10` would see summary lines referencing `NousResearch#11..NousResearch#40` even though the JSON `elements` array only held #1..NousResearch#10. Format on `visible_elements` instead so the summary indices always exist in the response. 3. `_coerce_max_elements` enforced a lower bound but no upper bound, so `max_elements=10_000_000` silently disabled the safeguard and reintroduced the original context-blow-up. Added a hard cap (`_MAX_ALLOWED_MAX_ELEMENTS = 1000`) that clamps oversized values. 4. The schema string said "Default 100" but the property carried no `default` field, and claimed `max_elements` had no effect on som/ vision while the image-missing fallback path can still return an elements array. Added `"default": 100`, `"maximum": 1000`, and clarified the fallback-path wording. Each finding gets a regression test: - test_capture_ax_clamps_oversized_max_elements_to_hard_cap - test_capture_ax_summary_indices_match_returned_elements - test_capture_multimodal_summary_omits_truncation_note - test_schema_max_elements_documents_default_and_upper_bound Verified with `pytest tests/tools/test_computer_use.py` (53 passed, including the 5 new cases). Confirmed each new test fails on the pre-fix code path before applying the production change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
elasticdotventures
pushed a commit
that referenced
this pull request
May 30, 2026
…NousResearch#31416) PR NousResearch#31416 (avoid persisting borrowed credential secrets) added sanitize_borrowed_credential_payload, which strips access_token from any auth.json pool entry whose (provider, source) isn't in the _PERSISTABLE_PROVIDER_SOURCES allowlist. (copilot, gh_cli) is borrowed (not in the allowlist), so the test fixture's pre-seeded access_token now gets stripped at load_pool() time, leaving the pool empty. resolve_target('1') then fails with 'No credential #1. Provider: copilot.' Fix: align the test with the new contract. At runtime, copilot tokens are hydrated by resolve_copilot_token() — mock that path so the pool gets an entry the test can remove. The behavior under test (suppression of gh_cli + env variants on remove) is unchanged. CI repro on origin/main HEAD; reproduced locally with stock checkout.
elasticdotventures
pushed a commit
that referenced
this pull request
May 30, 2026
…s reached After key #1 is marked exhausted the retry still called the API with key #1 due to env-var bias in _get_cached_client / resolve_api_key_provider_credentials. Fix: peek the pool and pass the active entry's key as explicit_api_key. Secondary: api_key_hint in mark_exhausted_and_rotate pins the correct entry under concurrent CLI+gateway calls; _is_payment_error matches GoUsageLimitError; extract_api_error_context parses "Resets in Xhr Ymin".
elasticdotventures
pushed a commit
that referenced
this pull request
May 30, 2026
…ookies
Mission-control style deploys reverse-proxy the dashboard at a path
prefix (e.g. mission-control.tilos.com/hermes/* -> :9119) and inject
X-Forwarded-Prefix: /hermes on every request. The SPA mount already
honoured this for asset URLs and the bootstrap __HERMES_BASE_PATH__,
but the OAuth gate didn't:
1. The gate's Location: header to /login and the 401 envelope's
login_url were built bare ("/login?next=..."). Under a /hermes
prefix the browser follows that to mission-control.tilos.com/login
which the proxy doesn't route to the dashboard.
2. _redirect_uri (the OAuth callback URL handed to the IDP) used
request.url_for() which doesn't honour X-Forwarded-Prefix
(Starlette/uvicorn only proxy_headers Host + Proto + For). The
IDP redirects back to /auth/callback instead of /hermes/auth/
callback → 404 in the user's browser.
3. Cookies were set with Path=/ which leaks them to other apps on
the same origin and won't be sent back on requests under the
prefix in the first place.
Fix threads the normalised prefix through every boundary:
* New hermes_cli/dashboard_auth/prefix.py — single source of truth
for X-Forwarded-Prefix parsing. web_server._normalise_prefix
becomes a re-export so the SPA mount, the gate, and the cookies
helper all agree.
* middleware._unauth_response builds login_url = f"{prefix}/login".
* routes._redirect_uri splices the prefix into the path component
of the IDP-bound URL (with full validation of the header).
* cookies.{set,clear}_{session,pkce}_cookie now take prefix="".
Path attribute switches to /hermes when set; cookie name switches
name variant (see below). Every caller passes the request's
normalised prefix.
Cookie hardening (Teknium's lesser-note #1 in the PR review): adopt
the __Host- / __Secure- cookie name prefixes per draft-west-cookie-
prefixes. The variant is selected from (use_https, prefix):
* Loopback HTTP → bare "hermes_session_at" (both prefixes require
Secure, incompatible with HTTP).
* HTTPS, direct deploy (Path=/) → "__Host-hermes_session_at".
Strongest spec: bound to exact origin, no Domain attribute, Secure
required.
* HTTPS, behind a proxy prefix (Path=/hermes) →
"__Secure-hermes_session_at". __Host- forbids Path != "/"; the
explicit Path=/hermes covers same-origin app isolation.
Setter and reader BOTH consult the prefix because the cookie *name*
changes — a reader that looked up the bare name when the setter wrote
__Secure- would never find the value. The reader falls back across
all three variants so a request whose shape changed mid-session (e.g.
post-deploy from no-prefix to /hermes) still picks up the existing
cookie until it expires.
Test coverage:
- tests/hermes_cli/test_dashboard_auth_prefix.py — new file. 11 tests
pinning:
• Location: /hermes/login on the gate's HTML redirect
• 401 envelope login_url carries the prefix
• Malformed X-Forwarded-Prefix is ignored (header-injection
defence; the script-tag value is normalised to empty string)
• _redirect_uri splices /hermes into the path (the property
that prevents the IDP-returns-to-404 failure)
• PKCE cookie uses Path=/hermes + __Secure- when proxied
• Session cookies use __Host- when direct, __Secure- when
proxied, bare on loopback HTTP
• End-to-end round trip with hand-managed PKCE cookie carriage
(TestClient can't simulate a Path=/hermes cookie automatically)
- tests/hermes_cli/test_dashboard_auth_cookies.py — rewritten to pin
each (use_https, prefix) shape produces its expected cookie name,
plus reader-side coverage that __Host- and __Secure- variants are
both recognised.
- Existing tests across middleware / 401-reauth / etc. updated to
match the new cookie names (substring contains instead of
startswith).
Mutation-tested: reverting _unauth_response to build the bare
"/login" URL trips exactly the two tests that pin the prefix
carriage, confirming the suite discriminates the regression.
elasticdotventures
pushed a commit
that referenced
this pull request
May 30, 2026
Two CI flakes surfaced on PR NousResearch#34572 (both in files this PR doesn't touch; pre-existing host-dependent flakes): 1. test_process_registry::TestPopenLeakOnSetupFailure — the failure-cleanup tests use a fake proc.pid (8888/9999) and assert proc.kill() runs. But spawn_local's primary cleanup is os.killpg(os.getpgid(pid), SIGKILL), falling back to proc.kill() only on ProcessLookupError/PermissionError/ OSError. When the fake PID happens to exist on a busy host, os.getpgid succeeds, os.killpg fires against an UNRELATED real process group, and proc.kill() is never reached -> flaky AssertionError (and a real risk of SIGKILLing an innocent process group from a unit test). Patch os.getpgid to raise ProcessLookupError so the fallback path runs deterministically and no real killpg is ever issued. 2. test_web_server::test_resize_escape_is_forwarded — the receive loop calls the blocking conn.receive_bytes() with no exception guard. Once the child prints its winsize and exits, the PTY closes; on a missed-marker run the next recv blocks until the 30s pytest-timeout instead of failing fast. Add a try/except break (matching the working sibling tests) and bump the child's pre-read sleep 0.15s -> 0.5s so the resize reliably lands first. Verified: 4/4 pass across 3 consecutive runs; root cause for #1 reproduced (os.getpgid(1) succeeds -> old code skips proc.kill).
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.
Internal PR for b00t integration. Upstream: NousResearch#19305
Required until upstream PR is merged. Adds get_pre_tool_call_directives() that fires pre_tool_call hook ONCE and returns both block_message and rewritten_args.