Files
zeekayandHanzo Dev a386271c80 isolation is a value the org sets, and an empty one is not runc
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]>
2026-09-11 13:47:31 -07:00

143 lines
5.1 KiB
Go

// Copyright 2025 Hanzo Industries Inc.
// SPDX-License-Identifier: AGPL-3.0
package tools
import (
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"github.com/hanzoai/runtime/apps/cli/apiclient"
"github.com/mark3labs/mcp-go/mcp"
log "github.com/sirupsen/logrus"
)
type FileUploadArgs struct {
Id *string `json:"id,omitempty"`
FilePath *string `json:"filePath,omitempty"`
Content *string `json:"content,omitempty"`
Encoding *string `json:"encoding,omitempty"`
Overwrite *bool `json:"overwrite,omitempty"`
}
func GetFileUploadTool() mcp.Tool {
return mcp.NewTool("file_upload",
mcp.WithDescription("Upload files to the Runtime sandbox from text or base64-encoded binary content. Creates necessary parent directories automatically and verifies successful writes. Files persist during the session and have appropriate permissions for further tool operations. Supports overwrite controls and maintains original file formats."),
mcp.WithString("filePath", mcp.Required(), mcp.Description("Path to the file to upload. Files should always be uploaded to the /tmp directory if user doesn't specify otherwise.")),
mcp.WithString("content", mcp.Required(), mcp.Description("Content of the file to upload.")),
mcp.WithString("encoding", mcp.Required(), mcp.Description("Encoding of the file to upload.")),
mcp.WithBoolean("overwrite", mcp.Required(), mcp.Description("Overwrite the file if it already exists.")),
mcp.WithString("id", mcp.Required(), mcp.Description("ID of the sandbox to upload the file to.")),
)
}
func FileUpload(ctx context.Context, request mcp.CallToolRequest, args FileUploadArgs) (*mcp.CallToolResult, error) {
apiClient, err := apiclient.GetApiClient(nil, runtimeMCPHeaders)
if err != nil {
return nil, err
}
if args.Id == nil || *args.Id == "" {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("sandbox ID is required")
}
if args.FilePath == nil || *args.FilePath == "" {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("filePath parameter is required")
}
if args.Content == nil || *args.Content == "" {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("content parameter is required")
}
if args.Encoding == nil || *args.Encoding == "" {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("encoding parameter is required")
}
overwrite := false
if args.Overwrite != nil && *args.Overwrite {
overwrite = *args.Overwrite
}
// Get the sandbox using sandbox ID
sandbox, _, err := apiClient.SandboxAPI.GetSandbox(ctx, *args.Id).Execute()
if err != nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("failed to get sandbox: %v", err)
}
if sandbox == nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("no sandbox available")
}
// Check if file exists and handle overwrite
if !overwrite {
fileInfo, _, err := apiClient.ToolboxAPI.GetFileInfo(ctx, *args.Id).Path(*args.FilePath).Execute()
if err == nil && fileInfo != nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("file '%s' already exists and overwrite=false", *args.FilePath)
}
}
// Prepare content based on encoding
var binaryContent []byte
if *args.Encoding == "base64" {
var err error
binaryContent, err = base64.StdEncoding.DecodeString(*args.Content)
if err != nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("invalid base64 encoding: %v", err)
}
} else {
// Default is text encoding
binaryContent = []byte(*args.Content)
}
// Create parent directories if they don't exist
parentDir := filepath.Dir(*args.FilePath)
if parentDir != "" {
_, err := apiClient.ToolboxAPI.CreateFolder(ctx, *args.Id).Path(parentDir).Mode("0755").Execute()
if err != nil {
log.Errorf("Error creating parent directory: %v", err)
// Continue anyway as upload might handle this
}
}
// Upload the file
tempFile, err := os.CreateTemp("", "upload-*")
if err != nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("error creating temp file: %v", err)
}
defer os.Remove(tempFile.Name()) // Clean up temp file when done
defer tempFile.Close()
// Write content to temp file
if _, err := tempFile.Write(binaryContent); err != nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("error writing to temp file: %v", err)
}
// Reset file pointer to beginning
if _, err := tempFile.Seek(0, 0); err != nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("error seeking temp file: %v", err)
}
// Upload the file
_, err = apiClient.ToolboxAPI.UploadFile(ctx, *args.Id).Path(*args.FilePath).File(tempFile).Execute()
if err != nil {
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("error uploading file: %v", err)
}
// Get file info for size
fileInfo, _, err := apiClient.ToolboxAPI.GetFileInfo(ctx, *args.Id).Path(*args.FilePath).Execute()
if err != nil {
log.Errorf("Error getting file info after upload: %v", err)
return &mcp.CallToolResult{IsError: true}, fmt.Errorf("error getting file info after upload: %v", err)
}
fileSizeKB := float64(fileInfo.Size) / 1024
log.Infof("File uploaded successfully: %s, size: %.2fKB", *args.FilePath, fileSizeKB)
return mcp.NewToolResultText(fmt.Sprintf("File uploaded successfully: %s", *args.FilePath)), nil
}