Skip to content

Commit dce0078

Browse files
authored
Support remote sandbox skill script staging (#918)
* feat: sync skill scripts into remote sandbox * fix: fail remote skill sync before execution * fix: support aliased terminal skill sync * fix: mark successful sandbox tool calls * test: cover remote skill sync boundaries * fix: cover remote skill sync edge cases * fix: rewrite legacy claude skill paths in remote sandbox * refactor: derive remote skill path aliases from skill configs * fix: preserve skill config and binary asset staging * fix: stage binary skill assets safely in remote sandbox * fix: cover relative skill command staging paths * fix: support aliased sandbox services for skill staging * fix: avoid hijacking workspace commands in remote skill sync * fix: always expose legacy claude skill aliases * fix: preserve default skill aliases across custom config * fix: stage ts and js skill helper modules * Fix Windows remote skill path quoting * Support tilde Claude skill aliases * Update skill registry alias expectations
1 parent 2fcf4fd commit dce0078

27 files changed

Lines changed: 3423 additions & 61 deletions

aworld/config/task_loader.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,11 @@ async def _load_skill_agent(
319319
f"missing agent {agent_id}: failed to load skill content for '{skill_name}' "
320320
f"from {skills_path}: {e}"
321321
)
322+
323+
runtime_skill_config = registry.build_skill_config(
324+
descriptor.skill_id,
325+
active_override=True,
326+
)
322327

323328
# Build Agent from skill (consistent with skill_service logic)
324329
agent_config_dict = agent_def.get("config", {})
@@ -345,6 +350,17 @@ async def _load_skill_agent(
345350
agent_def.get("mcp_config")
346351
)
347352
)
353+
354+
# Preserve framework-loaded skill metadata for downstream runtime features
355+
# such as remote sandbox execution-asset staging without re-enabling the
356+
# legacy skill tool path on these agentic skills.
357+
merged_skill_configs = dict(agent.conf.skill_configs or {})
358+
merged_skill_configs[skill_name] = runtime_skill_config
359+
360+
agent.conf.skill_configs = merged_skill_configs
361+
agent.skill_configs = merged_skill_configs
362+
if agent.sandbox is not None:
363+
agent.sandbox.skill_configs = merged_skill_configs
348364

349365
return agent
350366

aworld/sandbox/builtin/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
FILESYSTEM_TOOL_MAPPING = {
1212
"read_file": "read_file",
1313
"write_file": "write_file",
14+
"write_file_base64": "write_file_base64",
1415
"edit_file": "replace_in_file",
1516
"replace_in_file": "replace_in_file",
1617
"edit_file_range": "edit_file_range",
@@ -88,4 +89,3 @@ def decorator(func: Callable) -> Callable:
8889
func._fallback = fallback_to_builtin
8990
return func
9091
return decorator
91-

aworld/sandbox/implementations/sandbox.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def __init__(
8888
self._initialized = False
8989
self._file_namespace = None
9090
self._terminal_namespace = None
91+
self._remote_skill_execution_roots: dict[tuple[str, str], str] = {}
92+
self._remote_skill_execution_base_dir: str | None = None
9193

9294
user_mcp_config = copy.deepcopy(mcp_config) if mcp_config else {}
9395
user_mcp_servers = mcp_servers or []
@@ -1315,5 +1317,14 @@ def get_skill_list(self) -> Optional[Any]:
13151317
return None
13161318
return self._skill_configs
13171319

1320+
async def ensure_skill_execution_assets_ready(
1321+
self,
1322+
skill_name: str,
1323+
skill_config: Dict[str, Any],
1324+
) -> str:
1325+
from aworld.sandbox.skill_sync import ensure_remote_skill_assets_ready
1326+
1327+
return await ensure_remote_skill_assets_ready(self, skill_name, skill_config)
1328+
13181329
def __del__(self):
13191330
super().__del__()

aworld/sandbox/namespaces/base.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ def resolve_service_name(sandbox: Any, logical_name: str) -> str:
3333
(comma-separated) contains logical_name. Fallback: return logical_name.
3434
"""
3535
mcp_config = getattr(sandbox, "mcp_config", None) or getattr(sandbox, "_mcp_config", None)
36+
return resolve_service_name_from_config(mcp_config, logical_name)
37+
38+
39+
def resolve_service_name_from_config(mcp_config: Any, logical_name: str) -> str:
40+
"""Resolve a logical service name against an MCP config dict."""
3641
if not mcp_config:
3742
return logical_name
3843
servers = mcp_config.get("mcpServers") or {}
@@ -48,9 +53,41 @@ def resolve_service_name(sandbox: Any, logical_name: str) -> str:
4853
names = [n.strip() for n in mcp_servers_header.split(",") if n.strip()]
4954
if logical_name in names:
5055
return key
56+
server_suffix_alias = f"{logical_name}-server"
57+
if server_suffix_alias in servers:
58+
return server_suffix_alias
59+
for key in servers:
60+
if str(key).strip().lower().endswith(f"-{server_suffix_alias}"):
61+
return key
5162
return logical_name
5263

5364

65+
def service_matches_logical_name(
66+
mcp_config: Any,
67+
server_name: str,
68+
logical_name: str,
69+
) -> bool:
70+
"""Return whether a concrete server name should be treated as a logical service."""
71+
if not server_name:
72+
return False
73+
normalized_server_name = str(server_name).strip()
74+
if normalized_server_name == logical_name:
75+
return True
76+
77+
servers = (mcp_config or {}).get("mcpServers") or {}
78+
server_config = servers.get(normalized_server_name, {})
79+
headers = server_config.get("headers") or {}
80+
mcp_servers_header = (headers.get("MCP_SERVERS") or "").strip()
81+
if mcp_servers_header:
82+
names = [n.strip() for n in mcp_servers_header.split(",") if n.strip()]
83+
if logical_name in names:
84+
return True
85+
86+
server_suffix_alias = f"{logical_name}-server"
87+
normalized_lower = normalized_server_name.lower()
88+
return normalized_lower == server_suffix_alias or normalized_lower.endswith(f"-{server_suffix_alias}")
89+
90+
5491
def _parse_action_results(results: List[Any]) -> Dict[str, Any]:
5592
"""Parse List[ActionResult] from call_tool into normalized dict."""
5693
if not results:

aworld/sandbox/namespaces/file.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ async def write_file(self, path: str, content: str) -> Dict[str, Any]:
3232
"""Write content to file."""
3333
return await self._call_tool("write_file", path=path, content=content)
3434

35+
async def write_file_base64(self, path: str, content_base64: str) -> Dict[str, Any]:
36+
"""Write base64-decoded binary content to file."""
37+
return await self._call_tool("write_file_base64", path=path, content_base64=content_base64)
38+
3539
async def edit_file(
3640
self,
3741
path: str,

0 commit comments

Comments
 (0)