Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 13 additions & 17 deletions container/src/libcfcontainer/cuttlefish_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package libcfcontainer

import (
"bytes"
"context"
"errors"
"fmt"
Expand All @@ -40,7 +39,7 @@ type CuttlefishContainerManager interface {
// Create and start a container instance
CreateAndStartContainer(ctx context.Context, additionalConfig *container.Config, additionalHostConfig *container.HostConfig, name string) (string, error)
// Execute a command on a running container instance
ExecOnContainer(ctx context.Context, ctr string, interact bool, cmd []string) (string, error)
ExecOnContainer(ctx context.Context, ctr string, cmd []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error
// Stop and remove a container instance
StopAndRemoveContainer(ctx context.Context, ctr string) error
}
Expand Down Expand Up @@ -156,37 +155,34 @@ func (m *CuttlefishContainerManagerImpl) CreateAndStartContainer(ctx context.Con
return createRes.ID, nil
}

func (m *CuttlefishContainerManagerImpl) ExecOnContainer(ctx context.Context, ctr string, interact bool, cmd []string) (string, error) {
func (m *CuttlefishContainerManagerImpl) ExecOnContainer(ctx context.Context, ctr string, cmd []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
execConfig := container.ExecOptions{
AttachStderr: interact,
AttachStdin: interact,
AttachStdout: true,
AttachStderr: stderr != nil,
AttachStdin: stdin != nil,
AttachStdout: stdout != nil,
Cmd: cmd,
Tty: false,
}
createRes, err := m.cli.ContainerExecCreate(ctx, ctr, execConfig)
if err != nil {
return "", fmt.Errorf("failed to create container execution %q: %w", strings.Join(cmd, " "), err)
return fmt.Errorf("failed to create container execution %q: %w", strings.Join(cmd, " "), err)
}
attachRes, err := m.cli.ContainerExecAttach(ctx, createRes.ID, container.ExecStartOptions{})
if err != nil {
return "", fmt.Errorf("failed to attach container execution %q: %w", strings.Join(cmd, " "), err)
return fmt.Errorf("failed to attach container execution %q: %w", strings.Join(cmd, " "), err)
}
defer attachRes.Close()
var stdoutBuf bytes.Buffer
stdout := io.Writer(&stdoutBuf)
if interact {
stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
if stdin != nil {
go io.Copy(attachRes.Conn, stdin)
}
go io.Copy(attachRes.Conn, os.Stdin)
stdcopy.StdCopy(stdout, os.Stderr, attachRes.Reader)
stdcopy.StdCopy(stdout, stderr, attachRes.Reader)

if result, err := m.cli.ContainerExecInspect(ctx, createRes.ID); err != nil {
return "", fmt.Errorf("failed to run command on the container: %w", err)
return fmt.Errorf("failed to run command on the container: %w", err)
} else if result.ExitCode != 0 {
return "", fmt.Errorf("failed to run command on the container with exit code %d", result.ExitCode)
return fmt.Errorf("failed to run command on the container with exit code %d", result.ExitCode)
}
return stdoutBuf.String(), nil
return nil
}

func (m *CuttlefishContainerManagerImpl) StopAndRemoveContainer(ctx context.Context, ctr string) error {
Expand Down
31 changes: 14 additions & 17 deletions container/src/podcvd/cmd/cuttlefish_mcp_server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ import (
"context"
"fmt"
"log"
"os"
"os/exec"
"strings"

"github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

func runPodcvd(args []string) (string, error) {
func runPodcvd(clientID string, args []string) (string, error) {
cmd := exec.Command("podcvd", args...)
cmd.Env = append(os.Environ(), fmt.Sprintf("PODCVD_CLIENT_ID=%s", clientID))
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
Expand All @@ -38,16 +41,14 @@ func runPodcvd(args []string) (string, error) {
}

type toolHandler struct {
// TODO(seungjaeyoo): Add client ID to distinguish AI agents.
clientID string
}

type groupNameArg struct {
GroupName string `json:"group_name,omitempty"`
}

type createArgs struct {
groupNameArg
}
type createArgs struct{}

type fleetArgs struct{}

Expand All @@ -58,11 +59,7 @@ type removeArgs struct {
type clearArgs struct{}

func (h *toolHandler) Create(ctx context.Context, req *mcp.CallToolRequest, args createArgs) (*mcp.CallToolResult, any, error) {
cmd := []string{"create", "--vhost_user_vsock=true", "--report_anonymous_usage_stats=n"}
if args.GroupName != "" {
cmd = append([]string{fmt.Sprintf("--group_name=%s", args.GroupName)}, cmd...)
}
output, err := runPodcvd(cmd)
output, err := runPodcvd(h.clientID, []string{"create", "--vhost_user_vsock=true", "--report_anonymous_usage_stats=n"})
if err != nil {
return nil, nil, err
}
Expand All @@ -74,7 +71,7 @@ func (h *toolHandler) Create(ctx context.Context, req *mcp.CallToolRequest, args
}

func (h *toolHandler) Fleet(ctx context.Context, req *mcp.CallToolRequest, args fleetArgs) (*mcp.CallToolResult, any, error) {
output, err := runPodcvd([]string{"fleet"})
output, err := runPodcvd(h.clientID, []string{"fleet"})
if err != nil {
return nil, nil, err
}
Expand All @@ -90,7 +87,7 @@ func (h *toolHandler) Remove(ctx context.Context, req *mcp.CallToolRequest, args
if args.GroupName != "" {
cmd = append([]string{fmt.Sprintf("--group_name=%s", args.GroupName)}, cmd...)
}
if _, err := runPodcvd(cmd); err != nil {
if _, err := runPodcvd(h.clientID, cmd); err != nil {
return nil, nil, err
}
return &mcp.CallToolResult{
Expand All @@ -101,7 +98,7 @@ func (h *toolHandler) Remove(ctx context.Context, req *mcp.CallToolRequest, args
}

func (h *toolHandler) Clear(ctx context.Context, req *mcp.CallToolRequest, args clearArgs) (*mcp.CallToolResult, any, error) {
if _, err := runPodcvd([]string{"clear"}); err != nil {
if _, err := runPodcvd(h.clientID, []string{"clear"}); err != nil {
return nil, nil, err
}
return &mcp.CallToolResult{
Expand All @@ -120,7 +117,7 @@ component for orchestrating lifecycle of Cuttlefish instances and their groups.
`

func main() {
handlers := &toolHandler{}
handlers := &toolHandler{clientID: uuid.New().String()}
server := mcp.NewServer(
&mcp.Implementation{
Name: "cuttlefish-mcp-server",
Expand All @@ -137,15 +134,15 @@ func main() {
}, handlers.Create)
mcp.AddTool(server, &mcp.Tool{
Name: "fleet",
Description: "List all Cuttlefish instance groups",
Description: "List all Cuttlefish instance groups created by this agent or the human user",
}, handlers.Fleet)
mcp.AddTool(server, &mcp.Tool{
Name: "remove",
Description: "Remove a Cuttlefish instance group and also destroy corresponding ADB connections",
Description: "Remove a Cuttlefish instance group created by this agent or the human user and also destroy corresponding ADB connections",
}, handlers.Remove)
mcp.AddTool(server, &mcp.Tool{
Name: "clear",
Description: "Clear all Cuttlefish instance groups and also destroy corresponding ADB connections",
Description: "Clear all Cuttlefish instance groups created by this agent or the human user and also destroy corresponding ADB connections",
}, handlers.Clear)

if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
Expand Down
1 change: 1 addition & 0 deletions container/src/podcvd/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions container/src/podcvd/internal/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const (
)

const (
envClientID = "PODCVD_CLIENT_ID"
labelClientID = "client_id"
labelGroupName = "group_name"
labelCreatedBy = "created_by"
valueCreatedBy = "podcvd"
ToolingContainerName = "tooling"
)
33 changes: 33 additions & 0 deletions container/src/podcvd/internal/cvd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package internal
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"

"github.com/go-playground/validator/v10"
)
Expand Down Expand Up @@ -67,3 +69,34 @@ func ParseInstanceGroups(jsonStr, groupName string) (*InstanceGroup, error) {
}
return &instanceGroup, nil
}

func UpdateCvdGroupJsonRaw(data any, containerName, ipAddr string) {
switch v := data.(type) {
case map[string]any:
for k, val := range v {
if s, ok := val.(string); ok {
v[k] = updateStringOnCvdGroupJsonRaw(s, containerName, ipAddr)
} else {
UpdateCvdGroupJsonRaw(val, containerName, ipAddr)
}
}
case []any:
for k, val := range v {
if s, ok := val.(string); ok {
v[k] = updateStringOnCvdGroupJsonRaw(s, containerName, ipAddr)
} else {
UpdateCvdGroupJsonRaw(val, containerName, ipAddr)
}
}
}
}

func updateStringOnCvdGroupJsonRaw(data, containerName, ipAddr string) string {
data = strings.ReplaceAll(data, "0.0.0.0", ipAddr)
data = strings.ReplaceAll(data, "localhost", ipAddr)
data = strings.ReplaceAll(data, "127.0.0.1", ipAddr)
if filepath.IsAbs(data) {
data = fmt.Sprintf("%s:%s", containerName, data)
}
return data
}
23 changes: 19 additions & 4 deletions container/src/podcvd/internal/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func ExecFetchCmdOnDisposableHost(ccm libcfcontainer.CuttlefishContainerManager,
}
containerCfg := &container.Config{
Image: imageName,
Labels: map[string]string{
labelCreatedBy: valueCreatedBy,
},
}
cvdDataHome, err := cvdDataHome()
if err != nil {
Expand Down Expand Up @@ -111,7 +114,7 @@ func ExecFetchCmdOnDisposableHost(ccm libcfcontainer.CuttlefishContainerManager,
}
args := append([]string{"cvd"}, cvdArgs.SerializeCommonArgs()...)
args = append(args, cvdArgs.SubCommandArgs...)
if _, err := ccm.ExecOnContainer(context.Background(), containerID, true, args); err != nil {
if err := ccm.ExecOnContainer(context.Background(), containerID, args, os.Stdin, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("failed to execute fetch command on the container: %w", err)
}
if err := ccm.StopAndRemoveContainer(context.Background(), containerID); err != nil {
Expand All @@ -129,6 +132,9 @@ func ExecHelpCmdOnDisposableHost(ccm libcfcontainer.CuttlefishContainerManager,
"ANDROID_HOST_OUT=/host_out",
},
Image: imageName,
Labels: map[string]string{
labelCreatedBy: valueCreatedBy,
},
}
currentDir, err := os.Getwd()
if err != nil {
Expand All @@ -149,7 +155,7 @@ func ExecHelpCmdOnDisposableHost(ccm libcfcontainer.CuttlefishContainerManager,
}
args := append([]string{"cvd"}, cvdArgs.SerializeCommonArgs()...)
args = append(args, cvdArgs.SubCommandArgs...)
if _, err := ccm.ExecOnContainer(context.Background(), containerID, true, args); err != nil {
if err := ccm.ExecOnContainer(context.Background(), containerID, args, os.Stdin, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("failed to execute help command on the container: %w", err)
}
if err := ccm.StopAndRemoveContainer(context.Background(), containerID); err != nil {
Expand Down Expand Up @@ -256,8 +262,14 @@ func createAndStartContainer(ccm libcfcontainer.CuttlefishContainerManager, comm
"ANDROID_HOST_OUT=/host_out",
"ANDROID_PRODUCT_OUT=/product_out",
},
Image: imageName,
Labels: map[string]string{},
Image: imageName,
Labels: map[string]string{
labelCreatedBy: valueCreatedBy,
},
}
clientID := os.Getenv(envClientID)
if clientID != "" {
containerCfg.Labels[labelClientID] = clientID
}
cvdDataHome, err := cvdDataHome()
if err != nil {
Expand Down Expand Up @@ -358,6 +370,9 @@ func ensureOperatorHealthy(ip string) error {
func createAndStartToolingContainer(ccm libcfcontainer.CuttlefishContainerManager) error {
containerCfg := &container.Config{
Image: imageName,
Labels: map[string]string{
labelCreatedBy: valueCreatedBy,
},
}
cvdDataHome, err := cvdDataHome()
if err != nil {
Expand Down
Loading
Loading