Skip to content

feat(tools): compose deferred tools in execute_code - #82243

Draft
DomGrieco wants to merge 16 commits into
NousResearch:mainfrom
DomGrieco:feat/execute-code-deferred-tools
Draft

feat(tools): compose deferred tools in execute_code#82243
DomGrieco wants to merge 16 commits into
NousResearch:mainfrom
DomGrieco:feat/execute-code-deferred-tools

Conversation

@DomGrieco

@DomGrieco DomGrieco commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • let local execute_code programs search, inspect, and invoke deferred MCP and plugin tools through Hermes' existing Tool Search bridge
  • build the execute_code schema from the final per-session tool surface while preserving its established direct terminal, file, and web helper umbrella
  • preserve session identity and toolset scope through nested dispatch
  • keep deferred bridges local-only and unavailable to remote file RPC
  • JSON-frame every RPC result, including plain and multiline strings
  • keep terminal schemas, agents, environments, cleanup, probes, locks, aliases, overrides, and working directories scoped to the active profile and terminal configuration

Closes #82287.

Problem

Hermes already had two complementary paths:

  1. Tool Search keeps deferred MCP and plugin schemas out of the model-facing tool list.
  2. execute_code lets one Python program compose several Hermes tool calls.

Those paths did not compose. Once an MCP or plugin tool was deferred, execute_code could not search for it, inspect its schema, or invoke it.

Changes

  • generate deferred bridge stubs only from the final explicit per-session tool list
  • allow one local sandbox program to call tool_search, tool_describe, and tool_call
  • keep execute_code's existing direct helper umbrella at runtime and document it in the dynamic schema
  • rebuild that schema after Tool Search assembly
  • carry session_id, enabled_toolsets, and disabled_toolsets through nested dispatch
  • keep deferred bridges off remote backends until those transports have a session-private capability channel
  • JSON-encode all RPC result shapes before newline framing, including plain text, multiline text, JSON-looking strings, structured values, multimodal results, and errors
  • resolve terminal settings from the active profile snapshot and fingerprint the full effective backend configuration
  • namespace cached environments, creation locks, task overrides, working-directory records, and subagent aliases by routed profile and terminal configuration
  • track every environment's owning profile, session, backend fingerprint, and lifetime so expiry and teardown use the creating profile's policy
  • evict dead remote environments, creation locks, and file-operation caches through one structured degraded-mode path, including connection failures from the file-RPC poller
  • resolve the active profile backend before consulting the process-wide Python toolchain probe cache, skip local probes for remote profiles, and propagate profile context into the probe worker
  • revalidate backend fingerprints after creation-lock waits and publish file-operation wrappers inside the same lock boundary
  • apply the shared task CWD policy to lazy sandbox creation, fail closed while an RPC poller remains live, and tear down ephemeral prompt-probe environments
  • keep internal multiplex namespace keys distinct from user-controlled task IDs and preserve task-owned workspace mounts for every raw ID shape
  • apply administrator-managed terminal policy before profile snapshots, fingerprints, schemas, cache identity, and execution
  • retain one stable creation lock per task key while retiring stale environments
  • evict dead terminal, file, and vision environments after established connections fail
  • stop remote execute_code promptly when its RPC transport fails while the generated client is waiting
  • close delegated children before parent sandbox teardown and clean every terminal task identity used by an agent
  • serialize stale retirement, backend cleanup, replacement creation, and file-wrapper publication under stable per-task locks
  • propagate remote RPC cancellation through standard and managed execution backends
  • use the shared container and SSH policy builders for terminal, file, and code-execution environment creation
  • clear task overrides, session working directories, and aliases during canonical teardown even when no environment remains cached
  • tag shared file wrappers with their raw session owner so stale-wrapper recovery cannot overwrite another session's workspace
  • preserve current main's execute_code argument recovery while forwarding session, toolset, and deferred-bridge scope through the validated registry handler
  • preserve adjacent tolerant-client and multi-frame transport work owned by fix(execute_code): tolerate tool payloads with a trailing hint #74100 and fix(execute_code): split TCP RPC buffer into lines so concatenated responses parse (#81610) #81622

Safety

  • process-global tool names cannot authorize deferred bridges
  • deferred bridges require final-session authorization and a local backend
  • tools outside the parent session's toolset scope remain unavailable
  • deferred bridge tools cannot recursively invoke themselves
  • tool_call still routes the underlying tool through normal schema validation, middleware, hooks, approvals, and dispatch
  • remote shared-file RPC exposes only the established direct helper surface
  • direct helper schema and runtime surfaces match on cache misses and hits
  • terminal configuration changes cannot reuse stale environments or local-only schemas
  • delegated children inherit parent sandbox overrides and do not destroy the shared parent environment when the child closes

Review fixes

The follow-up commits address the reported integration and lifecycle cases:

  1. The dynamic schema documents the established direct helper umbrella, while runtime stub generation keeps those helpers alongside any authorized deferred bridges.
  2. The server JSON-frames every supported result, so plain and multiline plugin output round-trips through generated UDS and file clients.
  3. Gateway agents and model tool definitions rebuild when the active profile's terminal configuration changes.
  4. Child-first sandbox creation resolves the parent's image and working-directory overrides before environment creation.
  5. Remote container execution rejects host-only working directories and uses the shared terminal working-directory policy.
  6. Remote creation, execution, and file-RPC polling return structured degraded results and evict all dead cached state on connection loss.
  7. Session teardown retires every runtime namespace owned by that profile and session. Secondary-profile agents restore their own profile scope before cleanup.
  8. The cleanup worker applies each environment's stored lifetime instead of reading one process-global profile policy.
  9. Remote profiles cannot consume or warm the host's cached Python toolchain probe, and local probe workers inherit the active profile context.
  10. Lazy image-triggered sandbox creation uses the same task override, session CWD, and container remapping policy as terminal, file, and code execution.
  11. Terminal, file, and code-execution waiters revalidate an environment's fingerprint after acquiring its creation lock without removing the held lock from the registry.
  12. Remote execution cannot return success while its file-RPC poller remains live after the bounded join.
  13. Prompt backend probes use nonpersistent environments and clean them in a finally block.
  14. User-controlled task IDs that begin with mpx: cannot bypass the active profile and backend namespace.
  15. Per-session Docker workspace selection uses the raw task identity, including IDs whose final segment is default.
  16. Administrator-managed terminal values win consistently in profile snapshots, backend fingerprints, schema/cache identity, and execution.
  17. Stale retirement retains the original creation lock, so a concurrent creator cannot publish a file wrapper for a retired environment.
  18. Established terminal, file, and vision connection failures evict cached state, and remote RPC loss aborts a blocked model-facing call promptly.
  19. Agent teardown closes active children first and cleans every terminal task identity recorded across its turns.
  20. Stale detection, slow backend cleanup, replacement creation, and file-wrapper publication share one stable per-task lock, including idle expiry, degraded eviction, and explicit teardown.
  21. Standard and managed remote execution observe RPC cancellation, terminate the active command, and stop the script worker before returning a degraded result.
  22. File and code-execution creation use the same complete container and SSH policy builders as terminal creation, including managed Modal selection, mounts, forwarding, extra arguments, and persistence policy.
  23. Canonical teardown clears task overrides, session working-directory state, and aliases in a finally path even after the active environment has already been evicted.
  24. Stale shared file wrappers restore working-directory state only to the raw session that created them; a later session reusing the collapsed environment key keeps its own workspace override.
  25. The rebased registry handler keeps current main's malformed-argument guidance and forwards session_id, toolset scope, and deferred-bridge authorization to execution.

Behavioral tests cover code-execution-only sessions, final Tool Search surfaces, remote withholding, generated-client round trips, cache hits, terminal configuration transitions, concurrent profiles, same-task environment isolation, all-namespace teardown, profile-scoped cleanup, per-environment expiry, task overrides, working directories, child aliases, child cleanup, multimodal results, errors, and concurrent calls.

Local verification

Verified against base and merge-base 45af7a71fcd420b4422d2c074b1ce58b9ce0d048 at exact head d711266f3e4118a5c214e5d7069b121e10242ab7.

The changed-surface matrix completed with:

343 passed, 1 skipped, 0 failed

It covers the changed agent, gateway, tool-definition, code-execution, terminal, file, managed-backend, environment-probe, image-source, approval-routing, and profile-home paths. It includes the stale cleanup and held-lock interleavings, complete managed container policy, local and managed cancellation, blocked remote RPC cancellation, canonical teardown cleanup, raw-session file-wrapper ownership, and current-main approval-routing regressions.

Additional exact-head checks passed:

python -m ruff check <changed Python files>
python -m py_compile <changed Python files>
python scripts/audit_pr_attribution.py
git diff --check

A full local scripts/run_tests.sh run was also attempted. The current macOS development environment lacks several optional packages and external binaries, so that broad run had unrelated collection and runtime failures. The two tests/tools/test_file_tools.py failures are the known macOS /tmp versus /private/tmp path mismatch. The scoped suite above is clean, and GitHub CI is authoritative for the full Linux matrix.

GitHub CI at this exact head passed all required checks, including all 12 Python test slices, E2E tests, macOS and Windows tests, Ruff and type checks, contributor attribution, supply chain scans, and both Docker builds. A fresh independent exact-head blocking review returned PASS with no findings and ended READY FOR MAINTAINER REVIEW: yes.

Related work

This PR includes the functional session-ID forwarding commit from #52000 with Christopher Schulze's original authorship preserved. That change also fixes #51931.

@DomGrieco
DomGrieco marked this pull request as draft August 9, 2026 04:12
@DomGrieco
DomGrieco force-pushed the feat/execute-code-deferred-tools branch from 9c786fd to 42d5ccb Compare August 9, 2026 04:30
@DomGrieco
DomGrieco marked this pull request as ready for review August 9, 2026 05:35
@DomGrieco

DomGrieco commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

@teknium1 I think this would be a useful addition to Hermes.

Copy link
Copy Markdown
Contributor

Interlock — execute_code capability scope, RPC framing, and backend-specific schema

Reviewed at 5e8095383310fa7c57a4256b431ae3260e35e759.

Findings to resolve

  1. High, policy-dependent: an explicit empty/disjoint session scope still fails open to every direct helper. _resolve_sandbox_tools(["execute_code"], allow_deferred_bridges=True) returns all seven of patch/read_file/search_files/terminal/web_extract/web_search/write_file. code_execution is a normal one-tool surface, and the per-session schema advertises no helpers, but both local and remote runtime paths generate/authorize all seven. This is the exact class owned by fix(tools): close execute_code sandbox allow-list fail-open #41297; fix(security): sandbox tool isolation bypass — distinguish None from explicit empty/non-overlapping enabled_tools #47494 and fix(security): enforce execute_code sandbox tool isolation #6614 contain the earlier implementation and the maintainer objection that execute_code was intended as an umbrella capability. Before merge, please resolve that contract explicitly: either fold fix(tools): close execute_code sandbox allow-list fail-open #41297's None-vs-explicit semantics into this helper, or make the seven implicit grants part of the toolset/schema/policy so restricted callers know that granting execute_code grants them too. The current intersection docs and tests imply fail-closed while tests/tools/test_code_execution.py::test_nonoverlapping_tools_fallback enshrines fail-open.

  2. P2, introduced on the new deferred path: plain and multiline plugin results are not valid RPC frames. _serialize_rpc_result() returns strings verbatim, while both generated clients immediately call json.loads(raw); multiline text also breaks newline-delimited framing. This deterministically breaks the in-tree deferrable A2A tools, whose registered handlers intentionally return raw/multiline text. JSON-encode every supported result (including strings); the clients already perform the compatibility unwrap. fix(execute_code): tolerate tool payloads with a trailing hint #74100 owns the broader generated-result parser and fix(execute_code): split TCP RPC buffer into lines so concatenated responses parse (#81610) #81622 owns concatenated TCP/NDJSON responses, so preserve both fixes while reconciling the shared template.

  3. P2, introduced by backend-dependent schema generation: the tool-definition and gateway-agent caches omit the effective terminal backend. A focused local→SSH probe produced local=true, ssh_cached=true, ssh_after_clear=false for whether the cached execute_code description advertises tool_search. When ~/.hermes/.env changes TERMINAL_ENV without a terminal section, the gateway reloads it per turn but the quiet definitions cache keys only config.yaml/profile inputs, and the agent cache signature also omits the effective backend. Key both caches on the resolved local-vs-remote capability and add a behavioral local→remote cache regression. feat(tools): add named and dynamic execution targets #80275 is the architectural follow-on: named local and remote targets will require this capability to become target-aware rather than process-global.

Relationship graph

Suggested order: resolve the direct-helper authority contract, JSON-frame all result shapes while retaining #74100/#81622 behavior, key both schema caches on execution locality, then rebase the overlapping PRs onto the chosen implementation.

@alt-glitch alt-glitch added type/feature New feature or request comp/tools Tool registry, model_tools, toolsets tool/code-exec execute_code sandbox tool/mcp MCP client and OAuth P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation innovation Ambitious or experimental feature ideas labels Aug 11, 2026
@DomGrieco
DomGrieco marked this pull request as draft August 11, 2026 03:49
@DomGrieco
DomGrieco force-pushed the feat/execute-code-deferred-tools branch 6 times, most recently from a9b95c9 to af24e14 Compare August 12, 2026 00:25
@DomGrieco

DomGrieco commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed interlock. I addressed all three findings and kept the overlapping transport work separate.

  1. execute_code now states its established umbrella contract in the dynamic schema. Local runtime generation keeps the seven direct helpers available when execute_code is authorized. Deferred MCP and plugin bridges still require the final session surface and a local backend.
  2. Every RPC result is JSON encoded before newline framing. Plain text, multiline text, JSON-looking strings, structured values, multimodal results, and errors now round-trip through the generated UDS and file clients.
  3. Tool-definition and gateway-agent cache identity now includes the active profile's effective terminal configuration. Local to remote changes rebuild the schema and agent instead of retaining local-only deferred bridges.

The follow-up verification also exposed lifecycle cases around concurrent profiles and live terminal changes. The final head carries profile and backend identity through environment reuse, stable creation locks, file-operation caches, prompt probes, task overrides, working-directory records, aliases, expiry, and teardown. Stale detection, slow cleanup, replacement creation, and file-wrapper publication now share one lock boundary. Shared file wrappers retain their raw session owner, so stale recovery cannot overwrite another session's workspace. Remote RPC loss cancels the active standard or managed command and stops its script worker before the degraded result returns. Terminal, file, and code-execution creation also use the same complete container and SSH policy.

The PR body carries the current exact-head verification and regression summary so this reviewer disposition does not become a per-head status log.

@DomGrieco
DomGrieco marked this pull request as ready for review August 12, 2026 01:19
@DomGrieco
DomGrieco marked this pull request as draft August 13, 2026 01:31
@DomGrieco
DomGrieco force-pushed the feat/execute-code-deferred-tools branch 3 times, most recently from f3d5573 to 19c5964 Compare August 13, 2026 01:48
DomGrieco and others added 15 commits August 19, 2026 09:06
_rpc_server_loop and _rpc_poll_loop dispatched nested tool calls (e.g.
read_file invoked by execute_code) via handle_function_call without
passing session_id. Plugin hooks on_pre_tool_call / on_post_tool_call
therefore saw an empty session_id and could not correlate nested calls
with the originating turn (NousResearch#51931).

The fix reads HERMES_SESSION_ID from the session context on the parent
thread (before spawning the RPC thread) and passes it explicitly through
to handle_function_call in both the local (UDS) and remote (file-based)
RPC paths.

Fixes NousResearch#51931

(cherry picked from commit 1749a2b3b65c70af9f24b9a3516de01e2ddcbda5)
JSON-encode every RPC result so plain and multiline plugin outputs remain valid frames across local and file transports.

Keep execute_code's established direct-helper umbrella visible in its dynamic schema while continuing to gate deferred Tool Search bridges on the final session surface. Rebuild cached gateway agents when the canonical or effective terminal backend changes so local-only bridge schemas cannot survive a switch to a remote backend.
Co-Authored-By: Dominic Griego <hello@mysticmages.xyz>
Co-Authored-By: Dominic Griego <hello@mysticmages.xyz>
Resolve all terminal settings from the active profile snapshot and namespace cached environments, locks, task overrides, working directories, and aliases by profile and effective backend configuration. This keeps schema selection and runtime dispatch aligned without changing single-profile behavior.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Keep the profile-scoped degraded-mode value when terminal configuration has loaded, and use a safe snapshot fallback when configuration resolution itself raises. This preserves structured redaction on connection failures.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Carry profile and terminal configuration identity through environment reuse, cleanup, prompt probes, and gateway agent caches. Preserve parent sandbox overrides for delegated children, sanitize remote working directories, and evict dead remote environments through structured degraded-mode handling.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Resolve the active profile backend before consulting the process cache, skip local toolchain probes for remote profiles, and propagate profile context into the probe worker.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Retry CI after the ripgrep installation step exited before tests began.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Revalidate cached environments after creation-lock waits, keep file wrappers inside the same publication boundary, apply task CWD policy to lazy creation, fail closed on a live remote RPC poller, and make prompt probes ephemeral.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Keep multiplex task keys internal, honor managed backend policy, retain stable creation locks, evict dead shared environments, abort failed remote RPC waits promptly, and clean every agent-owned terminal task identity.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Keep stale retirement, backend cleanup, replacement creation, and file wrapper publication under stable per-task locks. Propagate remote RPC cancellation through standard and managed execution backends, preserve complete container policy, and clear task-scoped state during teardown.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
Tag shared file wrappers with their raw session owner so stale wrapper recovery cannot overwrite another session's workspace. Replace source-shape network coverage with behavioral checks through the shared container policy.

Co-Authored-By: Dominic Grieco <hello@mysticmages.xyz>
@DomGrieco
DomGrieco force-pushed the feat/execute-code-deferred-tools branch from d711266 to 9f15ff4 Compare August 19, 2026 12:22
Ensure sandbox cleanup still runs when profile scope setup fails, and add the requested registry-level explicit-empty execute_code coverage.
@DomGrieco
DomGrieco force-pushed the feat/execute-code-deferred-tools branch from 9f15ff4 to a03e251 Compare August 19, 2026 12:30
@alt-glitch alt-glitch added comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery tool/browser Browser automation (CDP, Playwright) tool/file File tools (read, write, patch, search) tool/skills Skills system (list, view, manage) tool/terminal Terminal execution and process management tool/vision Vision analysis and image generation area/config Config system, migrations, profiles sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets innovation Ambitious or experimental feature ideas needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/browser Browser automation (CDP, Playwright) tool/code-exec execute_code sandbox tool/file File tools (read, write, patch, search) tool/mcp MCP client and OAuth tool/skills Skills system (list, view, manage) tool/terminal Terminal execution and process management tool/vision Vision analysis and image generation type/feature New feature or request

Projects

None yet

5 participants