Skip to content

Support remote sandbox skill script staging#918

Merged
rainsonGain merged 19 commits into
mainfrom
feat/agent-runtime-skill-script-sandbox-bridge
Jun 9, 2026
Merged

Support remote sandbox skill script staging#918
rainsonGain merged 19 commits into
mainfrom
feat/agent-runtime-skill-script-sandbox-bridge

Conversation

@wuman001

@wuman001 wuman001 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add remote skill execution asset staging and command rewriting for remote sandboxes
  • cover legacy skill aliases, active-skill relative commands, binary assets, and Windows-specific path handling
  • extend execution asset discovery and compatibility tests for staged skill helpers

Test Plan

  • pytest -q tests/sandbox/test_skill_sync.py tests/sandbox/test_mcp_servers_skill_paths.py tests/sandbox/test_mcp_servers_content.py tests/sandbox/test_terminal_namespace.py tests/skills/test_execution_assets.py tests/skills/test_filesystem_skill_provider.py tests/skills/test_plugin_skill_provider.py tests/skills/test_skill_registry.py tests/core/test_skill_activation_resolver.py tests/core/test_skill_runtime_resolution.py tests/core/context/amni/services/test_skill_service.py tests/config/test_task_loader.py tests/core/test_markdown_agent_loader_skills.py

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to stage and execute skill execution assets in remote sandboxes. It includes utility modules to discover, manifest, and hash execution assets, a synchronization module to copy these assets to remote sandboxes (handling binary files via base64 and preserving POSIX permissions), and path-rewriting logic in the MCP server runner to map local or virtual skill paths to remote locations during terminal tool execution. The reviewer's feedback highlights critical cross-platform compatibility issues on Windows hosts due to backslash/forward-slash mismatches, memory safety concerns when hashing large binary assets, and several defensive type-checking opportunities to prevent runtime crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +366 to +369
host_file_paths = {
str((Path(asset_root) / relative_path).resolve()): relative_path
for relative_path in relative_paths
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

On Windows hosts, Path.resolve() resolves paths using backslashes (e.g., C:\path\to\skill\run.py). However, LLM-generated commands or tool parameters often use forward slashes (e.g., C:/path/to/skill/run.py). This mismatch causes host_path in rewritten to fail, completely skipping the path rewriting for those files. Registering both forward-slash and backward-slash normalized versions of the host paths ensures robust cross-platform path rewriting.

            host_file_paths = {}
            for relative_path in relative_paths:
                resolved_path = str((Path(asset_root) / relative_path).resolve())
                host_file_paths[resolved_path] = relative_path
                resolved_path_forward = resolved_path.replace("\\", "/")
                if resolved_path_forward != resolved_path:
                    host_file_paths[resolved_path_forward] = relative_path

Comment on lines +396 to +400
root_referenced = asset_root in rewritten or any(
self._skill_root_occurs_in_command(rewritten, alias)
for alias in path_aliases
)
file_referenced = any(host_path in rewritten for host_path in host_file_paths)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similarly to the file path mismatch, asset_root can contain backslashes on Windows, whereas the command text might use forward slashes. Checking both the native and forward-slash normalized versions of asset_root ensures that root references are correctly detected.

            asset_root_forward = asset_root.replace("\\", "/")
            root_referenced = (
                asset_root in rewritten
                or (asset_root_forward != asset_root and asset_root_forward in rewritten)
                or any(
                    self._skill_root_occurs_in_command(rewritten, alias)
                    for alias in path_aliases
                )
            )
            file_referenced = any(host_path in rewritten for host_path in host_file_paths)

Comment on lines +431 to +435
rewritten = self._rewrite_cd_command_root(
rewritten,
asset_root,
remote_root,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To ensure that cd commands targeting the asset root are correctly rewritten on Windows when forward slashes are used, we should also rewrite the forward-slash normalized version of asset_root.

Suggested change
rewritten = self._rewrite_cd_command_root(
rewritten,
asset_root,
remote_root,
)
rewritten = self._rewrite_cd_command_root(
rewritten,
asset_root,
remote_root,
)
if asset_root_forward != asset_root:
rewritten = self._rewrite_cd_command_root(
rewritten,
asset_root_forward,
remote_root,
)

Comment on lines +38 to +40
asset_root = Path(str(skill_config.get("asset_root", "") or "")).resolve()
if not asset_root.exists():
raise RuntimeError(f"Skill '{skill_name}' asset root does not exist: {asset_root}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If asset_root is empty or None in skill_config, Path("") resolves to the current working directory (Path.cwd()). Since the current working directory always exists, asset_root.exists() will evaluate to True, leading to a silent fallback and potentially syncing unintended files from the host's working directory. We should explicitly validate that asset_root is configured and non-empty.

Suggested change
asset_root = Path(str(skill_config.get("asset_root", "") or "")).resolve()
if not asset_root.exists():
raise RuntimeError(f"Skill '{skill_name}' asset root does not exist: {asset_root}")
asset_root_str = skill_config.get("asset_root")
if not asset_root_str:
raise RuntimeError(f"Skill '{skill_name}' asset_root is not configured")
asset_root = Path(asset_root_str).resolve()
if not asset_root.exists():
raise RuntimeError(f"Skill '{skill_name}' asset root does not exist: {asset_root}")

Comment on lines +101 to +109
for rel_path in manifest.relative_paths:
asset_path = manifest.root / rel_path
mode_bits = asset_path.stat().st_mode & 0o777
hasher.update(rel_path.encode("utf-8"))
hasher.update(b"\0")
hasher.update(f"{mode_bits:o}".encode("ascii"))
hasher.update(b"\0")
hasher.update(asset_path.read_bytes())
hasher.update(b"\0")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

asset_path.read_bytes() reads the entire file into memory. If a skill contains large binary assets (such as models, datasets, or large images), this can cause high memory usage or Out-Of-Memory (OOM) errors. Reading and hashing the file in chunks (e.g., 64KB) is much more memory-efficient and safer.

Suggested change
for rel_path in manifest.relative_paths:
asset_path = manifest.root / rel_path
mode_bits = asset_path.stat().st_mode & 0o777
hasher.update(rel_path.encode("utf-8"))
hasher.update(b"\0")
hasher.update(f"{mode_bits:o}".encode("ascii"))
hasher.update(b"\0")
hasher.update(asset_path.read_bytes())
hasher.update(b"\0")
for rel_path in manifest.relative_paths:
asset_path = manifest.root / rel_path
mode_bits = asset_path.stat().st_mode & 0o777
hasher.update(rel_path.encode("utf-8"))
hasher.update(b"\0")
hasher.update(f"{mode_bits:o}".encode("ascii"))
hasher.update(b"\0")
with asset_path.open("rb") as f:
while chunk := f.read(65536):
hasher.update(chunk)
hasher.update(b"\0")

Comment on lines 257 to 259
# Ensure tool_list is loaded
if not self.tool_list or not context:
if not self.tool_list:
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If parameter is explicitly passed as None or is not a dictionary (e.g., due to an invalid tool call payload), attempting to assign keys like parameter["session_id"] will raise a TypeError. Adding a defensive type check prevents potential runtime crashes.

Suggested change
# Ensure tool_list is loaded
if not self.tool_list or not context:
if not self.tool_list:
return False
# Ensure tool_list is loaded
if not self.tool_list or not isinstance(parameter, dict):
return False

Comment on lines +127 to +129
async def _require_success(result: dict[str, Any], phase: str) -> None:
if result and result.get("success", True):
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

result is assumed to be a dictionary when calling result.get("success", True). If result is a string, list, or other non-dict type (e.g., due to an unexpected response or test mock), this will raise an AttributeError. Checking isinstance(result, dict) first makes this helper much more robust.

Suggested change
async def _require_success(result: dict[str, Any], phase: str) -> None:
if result and result.get("success", True):
return
async def _require_success(result: Any, phase: str) -> None:
if isinstance(result, dict) and result.get("success", True):
return

Comment on lines +45 to +50
def _resolve_entrypoint_metadata(self, skill_id: str) -> dict[str, Any]:
entrypoint_id = skill_id.split(":", 1)[1] if ":" in skill_id else skill_id
for entrypoint in self._plugin.manifest.entrypoints.get("skills", ()):
if entrypoint.entrypoint_id == entrypoint_id:
return dict(entrypoint.metadata or {})
return {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If entrypoints is missing or None in the plugin manifest, calling self._plugin.manifest.entrypoints.get will raise an AttributeError. Using getattr with a default value prevents potential crashes.

Suggested change
def _resolve_entrypoint_metadata(self, skill_id: str) -> dict[str, Any]:
entrypoint_id = skill_id.split(":", 1)[1] if ":" in skill_id else skill_id
for entrypoint in self._plugin.manifest.entrypoints.get("skills", ()):
if entrypoint.entrypoint_id == entrypoint_id:
return dict(entrypoint.metadata or {})
return {}
def _resolve_entrypoint_metadata(self, skill_id: str) -> dict[str, Any]:
entrypoint_id = skill_id.split(":", 1)[1] if ":" in skill_id else skill_id
entrypoints = getattr(self._plugin.manifest, "entrypoints", None) or {}
for entrypoint in entrypoints.get("skills", ()):
if entrypoint.entrypoint_id == entrypoint_id:
return dict(entrypoint.metadata or {})
return {}

@rainsonGain rainsonGain merged commit dce0078 into main Jun 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants