Skip to content

Feat/capabilities#895

Open
alcholiclg wants to merge 3 commits intomodelscope:mainfrom
alcholiclg:feat/capabilities
Open

Feat/capabilities#895
alcholiclg wants to merge 3 commits intomodelscope:mainfrom
alcholiclg:feat/capabilities

Conversation

@alcholiclg
Copy link
Copy Markdown
Collaborator

Change Summary

  • Built ms-agent-skills and ms-agent capabilities to support OpenClaw-like products in invoking existing ms-agent tools, components, and project-level functionalities;
  • Added example integrations for Nanobot.

Related issue number

Checklist

  • The pull request title is a good summary of the changes - it will be used in the changelog
  • Unit tests for the changes exist
  • Run pre-commit install and pre-commit run --all-files before git commit, and passed lint check.
  • Documentation reflects the changes where applicable

Copy link
Copy Markdown
Contributor

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

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 an integration between ms-agent and nanobot via the Model Context Protocol (MCP), exposing capabilities like deep research, web search, LSP code validation, and concurrent-safe file editing. The implementation includes a capability registry, an async task manager for long-running processes, and an MCP server adapter. Feedback focuses on critical safety and security improvements: addressing task-safety issues with global sys.stdout redirection to prevent protocol corruption, fixing a path traversal vulnerability in filesystem tools, ensuring background subprocesses are properly reaped upon cancellation to avoid zombie processes, and adding input validation for line-range operations.

else:
_register_cap(server, registry, cap, workspace)

server.run(transport=args.transport)
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 safely prevent stray library logs or print() calls from corrupting the MCP stdio JSON-RPC stream, it is recommended to call _protect_stdout() once globally during server startup. This is safer and more efficient than per-request redirection.

Suggested change
server.run(transport=args.transport)
_protect_stdout()
server.run(transport=args.transport)

Comment on lines +224 to +230
saved = sys.stdout
sys.stdout = sys.stderr
try:
result = await registry.invoke(cap_name, kw, workspace=workspace)
finally:
sys.stdout = saved
return json.dumps(result, ensure_ascii=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.

high

The per-request redirection of sys.stdout to sys.stderr is not task-safe in an asynchronous environment. If multiple tool calls are processed concurrently, they will interfere with each other's sys.stdout state, potentially leading to race conditions where the protocol stream is corrupted. If _protect_stdout() is called globally at startup, this local redirection is redundant and should be removed.

Suggested change
saved = sys.stdout
sys.stdout = sys.stderr
try:
result = await registry.invoke(cap_name, kw, workspace=workspace)
finally:
sys.stdout = saved
return json.dumps(result, ensure_ascii=False)
result = await registry.invoke(cap_name, kw, workspace=workspace)
return json.dumps(result, ensure_ascii=False)

Comment on lines +274 to +288
def _redirect_stdout():
"""Redirect stdout to stderr while running an in-process LLMAgent.

MCP stdio transport uses stdout for JSONRPC messages. LLMAgent.step()
writes streaming content and reasoning to sys.stdout, which would
corrupt the protocol. Redirecting to stderr keeps the channel clean
while still allowing the output to appear in server logs.
"""
old_stdout = sys.stdout
sys.stdout = sys.stderr
try:
yield
finally:
sys.stdout = old_stdout

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

The _redirect_stdout context manager modifies the global sys.stdout state, which is not task-safe in an async environment. Concurrent executions of agents will interfere with each other. It is better to rely on a global redirection at the MCP server level.

# Removed: global sys.stdout redirection is not task-safe.
# Use global redirection in mcp_server.py instead.

Comment on lines +119 to +125
def _resolve_path(path: str, workspace: str | None) -> str:
"""Resolve *path* against an optional workspace root."""
if os.path.isabs(path):
return path
if workspace:
return os.path.join(workspace, path)
return os.path.abspath(path)
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.

security-high high

The _resolve_path function is vulnerable to path traversal. It does not verify that the resolved path remains within the intended workspace. An agent could provide a path like ../../etc/passwd to access sensitive files. Additionally, allowing absolute paths (lines 121-122) completely bypasses the workspace restriction.

Suggested change
def _resolve_path(path: str, workspace: str | None) -> str:
"""Resolve *path* against an optional workspace root."""
if os.path.isabs(path):
return path
if workspace:
return os.path.join(workspace, path)
return os.path.abspath(path)
def _resolve_path(path: str, workspace: str | None) -> str:
"""Resolve *path* against an optional workspace root and prevent traversal."""
if workspace:
abs_workspace = os.path.abspath(workspace)
abs_path = os.path.abspath(os.path.join(abs_workspace, path))
if not abs_path.startswith(abs_workspace):
raise ValueError(f"Access denied: {path} is outside workspace {workspace}")
return abs_path
return os.path.abspath(path)

Comment on lines +214 to +218
if task._process and task._process.returncode is None:
try:
task._process.kill()
except ProcessLookupError:
pass
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

When a task is cancelled, the subprocess is killed using task._process.kill(), but it is never waited upon. This can lead to zombie processes accumulating on the system, as the process remains in the process table until the parent reaps it.

Suggested change
if task._process and task._process.returncode is None:
try:
task._process.kill()
except ProcessLookupError:
pass
if task._process and task._process.returncode is None:
try:
task._process.kill()
await task._process.wait()
except ProcessLookupError:
pass

Comment on lines +199 to +200
if end_line is None:
return {'error': 'end_line is required when start_line > 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.

medium

Validation is missing to ensure that end_line is greater than or equal to start_line. If start_line > end_line, the subsequent slicing logic will result in incorrect file content due to overlapping ranges.

Suggested change
if end_line is None:
return {'error': 'end_line is required when start_line > 0'}
if end_line is None:
return {'error': 'end_line is required when start_line > 0'}
if end_line < start_line:
return {'error': f'end_line ({end_line}) must be >= start_line ({start_line})'}

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