mirror of
https://github.com/hanzoai/runtime.git
synced 2026-10-30 18:23:13 +00:00
Every sandbox was created privileged under the daemon's default runtime, and the setting that would have changed it defaulted to an empty string that silently meant runc. For a service whose whole premise is running code somebody else wrote, that is not a boundary. An org sets its isolation and the create request carries it, so a sandbox records what it was made with: changing the policy moves new sandboxes and leaves running ones alone. gvisor is the default. firecracker is delivered through kata's shim, which is why it is one name rather than two. runc is refused unless the runner names that org, and an empty list names nobody. Absent means the default, unknown is a refusal, and a runtime the daemon does not have is a refusal. Nothing substitutes quietly. The runner took an organization in a field called UserId with a note beside it saying so. Deciding who may have runc through a field named after something else is not reviewable. Privileged is replaced by dropping everything and granting the eight the daemon uses. Nothing the sandbox does broke; what stopped working is mount, mknod, raw sockets, chroot, writing to proc, and seeing the host's disks. AUDIT_WRITE is among them because without it sudo works and complains every time. The creation histogram's lowest bucket was a tenth of a second, so the metric could not observe the claim the product makes about its own speed. It can now, and it carries the isolation, so comparing them is a query. Co-authored-by: Hanzo Dev <[email protected]>
131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
// Copyright 2025 Hanzo Industries Inc.
|
|
// SPDX-License-Identifier: AGPL-3.0
|
|
|
|
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
apiclient "github.com/hanzoai/runtime/libs/api-client-go"
|
|
apiclient_cli "github.com/hanzoai/runtime/apps/cli/apiclient"
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type ExecuteCommandArgs struct {
|
|
Id *string `json:"id,omitempty"`
|
|
Command *string `json:"command,omitempty"`
|
|
}
|
|
|
|
type CommandResult struct {
|
|
Stdout string `json:"stdout"`
|
|
Stderr string `json:"stderr"`
|
|
ExitCode int `json:"exitCode"`
|
|
ErrorType string `json:"errorType,omitempty"`
|
|
}
|
|
|
|
func GetExecuteCommandTool() mcp.Tool {
|
|
return mcp.NewTool("execute_command",
|
|
mcp.WithDescription("Execute shell commands in the ephemeral Runtime Linux environment. Returns full stdout and stderr output with exit codes. Commands have sandbox user permissions and can install packages, modify files, and interact with running services. Always use /tmp directory. Use verbose flags where available for better output."),
|
|
mcp.WithString("command", mcp.Required(), mcp.Description("Command to execute.")),
|
|
mcp.WithString("id", mcp.Required(), mcp.Description("ID of the sandbox to execute the command in.")),
|
|
)
|
|
}
|
|
|
|
func ExecuteCommand(ctx context.Context, request mcp.CallToolRequest, args ExecuteCommandArgs) (*mcp.CallToolResult, error) {
|
|
apiClient, err := apiclient_cli.GetApiClient(nil, runtimeMCPHeaders)
|
|
if err != nil {
|
|
return &mcp.CallToolResult{IsError: true}, err
|
|
}
|
|
|
|
if args.Id == nil || *args.Id == "" {
|
|
return returnCommandError("Sandbox ID is required", "SandboxError")
|
|
}
|
|
|
|
if args.Command == nil || *args.Command == "" {
|
|
return returnCommandError("Command must be a non-empty string", "ValueError")
|
|
}
|
|
|
|
// Process the command
|
|
command := strings.TrimSpace(*args.Command)
|
|
if strings.Contains(command, "&&") || strings.HasPrefix(command, "cd ") {
|
|
// Wrap complex commands in /bin/sh -c
|
|
command = fmt.Sprintf("/bin/sh -c %s", shellQuote(command))
|
|
}
|
|
|
|
log.Infof("Executing command: %s", command)
|
|
|
|
// Execute the command
|
|
result, _, err := apiClient.ToolboxAPI.ExecuteCommand(ctx, *args.Id).
|
|
ExecuteRequest(*apiclient.NewExecuteRequest(command)).
|
|
Execute()
|
|
|
|
if err != nil {
|
|
// Classify error types
|
|
errStr := err.Error()
|
|
switch {
|
|
case strings.Contains(errStr, "Connection") || strings.Contains(errStr, "Timeout"):
|
|
return returnCommandError(fmt.Sprintf("Network error during command execution: %s", errStr), "NetworkError")
|
|
case strings.Contains(errStr, "Unauthorized") || strings.Contains(errStr, "401"):
|
|
return returnCommandError("Authentication failed during command execution. Please check your API key", "NetworkError")
|
|
default:
|
|
return returnCommandError(fmt.Sprintf("Command execution failed: %s", errStr), "CommandExecutionError")
|
|
}
|
|
}
|
|
|
|
// Process command output
|
|
cmdResult := CommandResult{
|
|
Stdout: strings.TrimSpace(result.Result),
|
|
ExitCode: int(result.ExitCode),
|
|
}
|
|
|
|
// Log truncated output
|
|
outputLen := len(cmdResult.Stdout)
|
|
logOutput := cmdResult.Stdout
|
|
if outputLen > 500 {
|
|
logOutput = cmdResult.Stdout[:500] + "..."
|
|
}
|
|
|
|
log.Infof("Command completed - exit code: %d, output length: %d", cmdResult.ExitCode, outputLen)
|
|
|
|
log.Debugf("Command output (truncated): %s", logOutput)
|
|
|
|
// Check for non-zero exit code
|
|
if cmdResult.ExitCode > 0 {
|
|
log.Infof("Command exited with non-zero status - exit code: %d", cmdResult.ExitCode)
|
|
}
|
|
|
|
// Convert result to JSON
|
|
resultJSON, err := json.MarshalIndent(cmdResult, "", " ")
|
|
if err != nil {
|
|
return returnCommandError(fmt.Sprintf("Error marshaling result: %v", err), "CommandExecutionError")
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(resultJSON)), nil
|
|
}
|
|
|
|
// Helper function to return command errors in a consistent format
|
|
func returnCommandError(message, errorType string) (*mcp.CallToolResult, error) {
|
|
return &mcp.CallToolResult{
|
|
IsError: true,
|
|
Result: mcp.Result{
|
|
Meta: map[string]interface{}{
|
|
"Stdout": "",
|
|
"Stderr": message,
|
|
"ExitCode": -1,
|
|
"ErrorType": errorType,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Helper function to quote shell commands
|
|
func shellQuote(s string) string {
|
|
// Simple shell quoting - wrap in single quotes and escape existing single quotes
|
|
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
|
|
}
|