Skip to content

fix(skills): translate skill_view skill_dir for sandbox backends#27514

Closed
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/skill-view-runtime-skill-dir-sandbox-27491
Closed

fix(skills): translate skill_view skill_dir for sandbox backends#27514
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/skill-view-runtime-skill-dir-sandbox-27491

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

Summary

When the terminal backend bind-mounts the skill tree into a sandbox (Docker, Singularity, Modal), skill_view's response and the ${HERMES_SKILL_DIR} template substitution return the gateway-side host path (e.g. /opt/data/skills/nextcloud). The agent then references that path inside its execute_code sandbox, which mounts the same skill at /root/.hermes/skills/nextcloud — so any bundled-script invocation fails. This PR adds a runtime_skill_dir field that translates the host path through the active mount mapping, and uses the translated path for ${HERMES_SKILL_DIR} substitution.

Fixes #27491.

The bug

tools/skills_tool.py::skill_view() returns:

result = {
    ...
    "skill_dir": str(skill_dir) if skill_dir else None,
    ...
}

where skill_dir is the gateway-side absolute path (the gateway runs in Docker with HERMES_HOME=/opt/data, so this is /opt/data/skills/nextcloud). The reporter (#27491) describes the symptom precisely:

The agent searches for the script in skill_dir which points to /opt/data/skills/nextcloud, which does not exist in the sandbox.

The same path is fed into agent.skill_preprocessing.preprocess_skill_content, which substitutes it for every ${HERMES_SKILL_DIR} token in the rendered SKILL.md body — so a skill that documents bash ${HERMES_SKILL_DIR}/scripts/run.sh renders the wrong path in the message the agent sees.

The skill tree is actually mounted at /root/.hermes/skills/<name> inside the Podman-in-Docker sandbox (the canonical container_base used by tools/credential_files.get_skills_directory_mount() and consumed by tools/environments/docker.py, tools/environments/singularity.py, tools/environments/modal.py).

The fix

Add a small helper _to_runtime_skill_dir(skill_dir, backend) in tools/skills_tool.py that:

  1. Returns skill_dir unchanged for the local backend.
  2. For docker / singularity / modal backends, walks the get_skills_directory_mount() results, finds the mount whose host_path contains skill_dir, and rewrites the prefix to that mount's container_path (/root/.hermes/skills/... or /root/.hermes/external_skills/<idx>/... for skills.external_dirs entries).
  3. Falls back to skill_dir when no mount matches — e.g. backends that use file_sync instead of bind mounts (ssh, daytona, vercel_sandbox), or when the skills mount registration hasn't run yet.

The skill_view result then exposes both fields:

"skill_dir": str(skill_dir) if skill_dir else None,                    # host path (unchanged)
"runtime_skill_dir": str(runtime_skill_dir) if runtime_skill_dir else None,  # sandbox path (or same for local)

…and the runtime path is the one passed into preprocess_skill_content, so ${HERMES_SKILL_DIR} substitution emits the path the agent actually needs at execute-code time.

skill_dir is intentionally left as the host path because agent.skill_commands._load_skill_payload reads it back as a Path to access the SKILL.md on the gateway-side filesystem (see PR #10587) — rewriting it to a sandbox path would break that caller.

Test plan

  • New regression tests in tests/tools/test_skills_tool.py::TestSkillView:
    • test_skill_view_returns_host_skill_dir_for_local_backendruntime_skill_dir == skill_dir == <host path> when TERMINAL_ENV=local.
    • test_skill_view_returns_sandbox_skill_dir_for_docker_backend — when TERMINAL_ENV=docker and get_skills_directory_mount() returns the canonical host_path → /root/.hermes/skills mapping, the response has skill_dir == <host path> and runtime_skill_dir == /root/.hermes/skills/nextcloud.
    • test_skill_view_template_substitution_uses_sandbox_path${HERMES_SKILL_DIR} inside SKILL.md substitutes the sandbox path under the same backend; the host path no longer appears in the rendered content.
    • test_skill_view_categorized_skill_keeps_subdirs_in_sandbox_path — categorized skill integration/nextcloud ends up at /root/.hermes/skills/integration/nextcloud (relative path preserved).
    • test_skill_view_runtime_path_falls_back_when_no_mount_matches — empty mount list ⇒ runtime_skill_dir == skill_dir; no confidently-wrong sandbox path is emitted.
  • Regression guard: against pre-fix code, the four sandbox tests fail because runtime_skill_dir is absent from the response and ${HERMES_SKILL_DIR} substitutes the host path. With the fix, all five pass.
  • Focused suite: uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/tools/test_skills_tool.py::TestSkillView -v — 19 passed.
  • Adjacent suites: tests/tools/test_skills_tool.py tests/agent/test_skill_commands.py tests/tools/test_credential_files.py — 160 passed.
  • ${HERMES_SKILL_DIR} substitution under the default local backend is unchanged (existing test_skill_view_applies_template_vars still passes — runtime_skill_dir == skill_dir so the rendered string is byte-identical to before).

Related

  • Fixes [Bug]: skill_dir returned from skill_view tool is incorrect when sandboxed #27491.
  • Builds on #10587 (fix: use absolute skill_dir for external skills) — that PR added the absolute-host skill_dir field; this one adds a sandbox-aware sibling for the agent's runtime view.
  • Mount mapping uses the same source of truth (tools/credential_files.get_skills_directory_mount) that tools/environments/docker.py already feeds to --volume flags — so the runtime path matches the actual bind mount.

Sibling code paths that may need the same fix: agent/skill_commands._build_skill_message also surfaces a [Skill directory: ...] line constructed from the host skill_dir, which has the same wrong-path problem when sandboxed. Intentionally left out of this PR's scope to keep the diff small — happy to widen to that callsite as well if preferred.

Copilot AI review requested due to automatic review settings May 17, 2026 17:20
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) backend/docker Docker container execution backend/singularity Singularity container execution backend/modal Modal.com cloud execution labels May 17, 2026
@briandevans

Copy link
Copy Markdown
Contributor Author

CI audit — all 21 test failures are pre-existing baselines on clean origin/main (f36c89c). Zero failures are in touched code (tools/skills_tool.py, tests/tools/test_skills_tool.py).

Reproduced the exact same 21 failures with identical error text on main's push CI run 25989069834:

Test Symptom Root cause on main
tests/acp/test_registry_manifest.py::test_agent_json_version_matches_pyproject assert '0.13.0' == '0.14.0' acp/agent.json lagging pyproject version bump
tests/acp/test_registry_manifest.py::test_agent_json_pins_uvx_package_to_pyproject_version 'hermes-agent[acp]==0.13.0' == '...==0.14.0' Same as above
tests/gateway/test_background_command.py::test_telegram_dm_topic_completion_preserves_reply_anchor_metadata Extra direct_messages_topic_id key Thread-metadata helper now emits direct_messages_topic_id alongside telegram_dm_topic_reply_fallback
tests/gateway/test_google_chat.py::test_standalone_send_refreshes_token_and_posts_message 'trust_env' unexpected kw Fake aiohttp stub in test fixture missing trust_env parameter
tests/gateway/test_google_chat.py::test_standalone_send_propagates_api_failure Same trust_env Same
tests/gateway/test_teams.py::test_standalone_send_acquires_token_and_posts_activity Same trust_env Same
tests/gateway/test_teams.py::test_standalone_send_propagates_token_failure Same trust_env Same
tests/gateway/test_telegram_thread_fallback.py::test_gateway_runner_busy_ack_replies_to_triggering_message_for_telegram_dm_topic Extra direct_messages_topic_id key Thread-metadata helper change
tests/gateway/test_voice_command.py::test_auto_voice_reply_uses_thread_metadata_helper Extra direct_messages_topic_id key Same
tests/hermes_cli/test_model_switch_custom_providers.py::test_list_groups_same_name_custom_providers_into_one_row Model-list ordering mismatch Test fixture's expected order doesn't match current catalog
tests/hermes_cli/test_startup_plugin_gating.py::test_builtin_set_covers_every_registered_subcommand _BUILTIN_SUBCOMMANDS is missing ['send'] New send subcommand not yet added to _BUILTIN_SUBCOMMANDS
tests/hermes_cli/test_tools_config.py::test_reconfigure_provider_runs_post_setup_for_env_var_providers[Browserbase-agent_browser] StopIteration Test iterator exhausted; fixture mismatch
tests/hermes_cli/test_tools_config.py::test_reconfigure_provider_runs_post_setup_for_env_var_providers[Browser Use-agent_browser] StopIteration Same
tests/hermes_cli/test_tools_config.py::test_reconfigure_provider_runs_post_setup_for_env_var_providers[Firecrawl-agent_browser] StopIteration Same
tests/hermes_cli/test_gateway_service.py::test_system_unit_uses_target_user_home_not_calling_user PermissionError: '/root/.hermes/node/bin' Test attempts to write outside tmpdir under CI's non-root user
tests/hermes_cli/test_gateway_service.py::test_system_unit_remaps_profile_to_target_user Same PermissionError Same
tests/run_agent/test_deepseek_reasoning_content_echo.py::test_non_kimi_provider _needs_kimi_tool_reasoning() == True for non-kimi Detection helper now over-matches
tests/run_agent/test_provider_parity.py::test_openrouter_always_wins 'google/gemini-2.5-flash' != 'google/gemini-3-flash-preview' Test fixture references retired model
tests/tools/test_transcription_dotenv_fallback.py::test_explicit_xai_sees_dotenv 'none' == 'xai' dotenv fallback provider gate regression
tests/tools/test_transcription_dotenv_fallback.py::test_xai_key_only_in_dotenv_before_fix assert False is True Same
tests/tools/test_voice_cli_integration.py::test_error_messages_use_force_in_run_agent assert 0 > 0 _vprint(force=True) call signature change

Happy to address any failure that's actually in scope here.

@briandevans briandevans force-pushed the fix/skill-view-runtime-skill-dir-sandbox-27491 branch 3 times, most recently from 0015883 to ea1ae33 Compare May 24, 2026 23:14
@briandevans briandevans force-pushed the fix/skill-view-runtime-skill-dir-sandbox-27491 branch 5 times, most recently from c5e9147 to 59f78df Compare June 1, 2026 04:14
When the terminal backend bind-mounts the skill tree (docker, singularity,
modal), the existing skill_view response returns the gateway-side host
path for ``skill_dir`` (e.g. ``/opt/data/skills/nextcloud``).  The agent
then references that path inside its execute_code sandbox, which sees the
same files mounted at ``/root/.hermes/skills/nextcloud`` — so any
bundled-script invocation against the host path fails to find the file
(NousResearch#27491).  ``${HERMES_SKILL_DIR}`` substitution in SKILL.md content has
the same problem.

This adds a ``runtime_skill_dir`` field to the skill_view response,
computed by mapping the host ``skill_dir`` through the active
``get_skills_directory_mount()`` entries.  The same path is now used for
``${HERMES_SKILL_DIR}`` template substitution so SKILL.md bodies render
with the sandbox path the agent will actually hit.  ``skill_dir`` is left
unchanged (host path) so Python callers like
``agent.skill_commands._load_skill_payload`` keep working — they read the
skill off the gateway-side filesystem.

For the ``local`` backend, ``runtime_skill_dir`` mirrors ``skill_dir``;
the fallthrough also covers backends that use file_sync rather than bind
mounts (ssh, daytona, vercel_sandbox), and the case where no mount maps
the skill_dir at all (e.g. a transient test path).

Fixes NousResearch#27491.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@briandevans briandevans force-pushed the fix/skill-view-runtime-skill-dir-sandbox-27491 branch from 59f78df to 19d4edc Compare June 1, 2026 12:13
@briandevans

Copy link
Copy Markdown
Contributor Author

Closing to focus the queue on security/file-safety work where civilian merges are landing. Happy to reopen if maintainers want this picked up.

@briandevans briandevans closed this Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend/docker Docker container execution backend/modal Modal.com cloud execution backend/singularity Singularity container execution P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: skill_dir returned from skill_view tool is incorrect when sandboxed

2 participants