diff --git a/experimental/apps-mcp/README.md b/experimental/apps-mcp/README.md index a15d8bb496..aedbb84cfe 100644 --- a/experimental/apps-mcp/README.md +++ b/experimental/apps-mcp/README.md @@ -12,12 +12,13 @@ extensive validation to ensure high-quality outputs. 1. **Explore your data** - Query Databricks catalogs, schemas, and tables to understand your data 2. **Generate the app** - Scaffold a full-stack TypeScript application (tRPC + React) with proper structure 3. **Customize with AI** - Use workspace tools to read, write, and edit files naturally through conversation -4. **Validate rigorously** - Run builds, type checks, and tests to ensure quality +4. **Validate rigorously** - Run builds, type checks, and tests in isolated containers (Dagger) 5. **Deploy confidently** - Push validated apps directly to Databricks Apps platform **Why use it:** - **Speed**: Go from concept to deployed Databricks app in minutes, not hours or days - **Quality**: Extensive validation ensures your app builds, passes tests, and is production-ready +- **Safety**: Containerized validation prevents breaking changes from reaching production - **Simplicity**: One natural language conversation handles the entire workflow Perfect for data engineers and developers who want to build Databricks apps without the manual overhead of project setup, configuration, testing infrastructure, and deployment pipelines. @@ -103,10 +104,11 @@ Create the application structure: Ensure production-readiness before deployment: -- **`validate_data_app`** - Comprehensive validation +- **`validate_data_app`** - Comprehensive validation in isolated containers - Build verification (npm build) - Type checking (TypeScript compiler) - Test execution (full test suite) + - Containerized with Dagger (Docker) *This step guarantees your application is tested and ready for production before deployment.* @@ -258,12 +260,17 @@ Deploy the app to Databricks as "orders-dashboard" - Every change is validated before deployment - No broken builds reach production -**2. Natural Language = Productivity** +**2. Containerized Validation = Safety** +- Dagger containers ensure reproducible builds +- Isolated testing prevents environment issues +- Consistent behavior from development to production + +**3. Natural Language = Productivity** - Describe what you want, not how to build it - AI handles implementation details - Focus on requirements, not configuration -**3. End-to-End Workflow = Simplicity** +**4. End-to-End Workflow = Simplicity** - Single tool for entire lifecycle - No context switching between tools - Seamless progression from idea to deployment @@ -276,6 +283,7 @@ The Databricks MCP server doesn't just generate code—it ensures quality: - ✅ **Build verification** - Ensures code compiles - ✅ **Test suite** - Validates functionality - ✅ **Linting** - Enforces code quality +- ✅ **Containerization** - Reproducible environments - ✅ **Databricks integration** - Native SDK usage --- @@ -293,6 +301,15 @@ databricks experimental apps-mcp --warehouse-id --with-workspace- # Enable deployment databricks experimental apps-mcp --warehouse-id --allow-deployment + +# With custom Docker image +databricks experimental apps-mcp --warehouse-id --docker-image node:20-alpine + +# Without containerized validation +databricks experimental apps-mcp --warehouse-id --use-dagger=false + +# Check configuration +databricks experimental apps-mcp check ``` ### CLI Flags @@ -302,6 +319,8 @@ databricks experimental apps-mcp --warehouse-id --allow-deploymen | `--warehouse-id` | Databricks SQL Warehouse ID (required) | - | | `--with-workspace-tools` | Enable workspace file operations | `false` | | `--allow-deployment` | Enable deployment operations | `false` | +| `--docker-image` | Docker image for validation | `node:20-alpine` | +| `--use-dagger` | Use Dagger for containerized validation | `true` | | `--help` | Show help | - | ### Environment Variables @@ -314,6 +333,7 @@ databricks experimental apps-mcp --warehouse-id --allow-deploymen | `DATABRICKS_WAREHOUSE_ID` | Alternative name for warehouse ID | `abc123def456` | | `ALLOW_DEPLOYMENT` | Enable deployment operations | `true` or `false` | | `WITH_WORKSPACE_TOOLS` | Enable workspace tools | `true` or `false` | +| `USE_DAGGER` | Enable Dagger sandbox for validation | `true` or `false` | ### Authentication @@ -328,6 +348,7 @@ For more details, see the [Databricks authentication documentation](https://docs ### Requirements - **Databricks CLI** (this package) +- **Docker** (optional, for Dagger-based validation) - **Databricks workspace** with a SQL warehouse - **MCP-compatible client** (Claude Desktop, Continue, etc.) diff --git a/experimental/apps-mcp/cmd/apps_mcp.go b/experimental/apps-mcp/cmd/apps_mcp.go index fcaee1f588..e6062ecd24 100644 --- a/experimental/apps-mcp/cmd/apps_mcp.go +++ b/experimental/apps-mcp/cmd/apps_mcp.go @@ -13,6 +13,8 @@ func NewMcpCmd() *cobra.Command { var warehouseID string var allowDeployment bool var withWorkspaceTools bool + var dockerImage string + var useDagger bool cmd := &cobra.Command{ Use: "apps-mcp", @@ -33,7 +35,13 @@ The server communicates via stdio using the Model Context Protocol.`, databricks experimental apps-mcp --warehouse-id abc123 --with-workspace-tools # Start with deployment tools enabled - databricks experimental apps-mcp --warehouse-id abc123 --allow-deployment`, + databricks experimental apps-mcp --warehouse-id abc123 --allow-deployment + + # Start with custom Docker image for validation + databricks experimental apps-mcp --warehouse-id abc123 --docker-image node:20-alpine + + # Start without containerized validation + databricks experimental apps-mcp --warehouse-id abc123 --use-dagger=false`, PreRunE: root.MustWorkspaceClient, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -47,7 +55,10 @@ The server communicates via stdio using the Model Context Protocol.`, WarehouseID: warehouseID, DatabricksHost: w.Config.Host, IoConfig: &mcplib.IoConfig{ - Validation: &mcplib.ValidationConfig{}, + Validation: &mcplib.ValidationConfig{ + DockerImage: dockerImage, + UseDagger: useDagger, + }, }, } @@ -71,6 +82,10 @@ The server communicates via stdio using the Model Context Protocol.`, cmd.Flags().StringVar(&warehouseID, "warehouse-id", "", "Databricks SQL Warehouse ID") cmd.Flags().BoolVar(&allowDeployment, "allow-deployment", false, "Enable deployment tools") cmd.Flags().BoolVar(&withWorkspaceTools, "with-workspace-tools", false, "Enable workspace tools (file operations, bash, grep, glob)") + cmd.Flags().StringVar(&dockerImage, "docker-image", "node:20-alpine", "Docker image for validation") + cmd.Flags().BoolVar(&useDagger, "use-dagger", true, "Use Dagger for containerized validation") + + cmd.AddCommand(newCheckCmd()) return cmd } diff --git a/experimental/apps-mcp/cmd/check.go b/experimental/apps-mcp/cmd/check.go new file mode 100644 index 0000000000..c39bfedd5b --- /dev/null +++ b/experimental/apps-mcp/cmd/check.go @@ -0,0 +1,51 @@ +package mcp + +import ( + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/log" + "github.com/spf13/cobra" +) + +func newCheckCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "check", + Short: "Check MCP server environment", + Long: `Verify that the environment is correctly configured for running the MCP server. + +This command checks: +- Databricks authentication (API token, profile, or other auth methods) +- Workspace connectivity + +Use this command to troubleshoot connection issues before starting the MCP server.`, + Example: ` # Check environment configuration + databricks experimental apps-mcp check + + # Check with specific profile + databricks experimental apps-mcp check --profile production`, + PreRunE: root.MustWorkspaceClient, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + log.Info(ctx, "Checking MCP server environment") + + // Check Databricks authentication + w := cmdctx.WorkspaceClient(ctx) + me, err := w.CurrentUser.Me(ctx) + if err != nil { + return err + } + + cmdio.LogString(ctx, "✓ Databricks authentication: OK") + cmdio.LogString(ctx, " User: "+me.UserName) + cmdio.LogString(ctx, " Host: "+w.Config.Host) + + cmdio.LogString(ctx, "\nEnvironment is ready for MCP server") + + return nil + }, + } + + return cmd +} diff --git a/experimental/apps-mcp/lib/config.go b/experimental/apps-mcp/lib/config.go index afeddd655e..9687a16127 100644 --- a/experimental/apps-mcp/lib/config.go +++ b/experimental/apps-mcp/lib/config.go @@ -16,6 +16,7 @@ type Config struct { type IoConfig struct { Template *TemplateConfig Validation *ValidationConfig + Dagger *DaggerConfig } // TemplateConfig specifies which template to use for scaffolding new projects. @@ -24,19 +25,30 @@ type TemplateConfig struct { Path string } -// ValidationConfig defines custom validation commands for project validation. +// ValidationConfig defines custom validation commands and docker images for project validation. type ValidationConfig struct { - Command string - Timeout int + Command string + DockerImage string + UseDagger bool + Timeout int } // SetDefaults applies default values to ValidationConfig if not explicitly set. func (v *ValidationConfig) SetDefaults() { + if v.DockerImage == "" { + v.DockerImage = "node:20-alpine" + } if v.Timeout == 0 { v.Timeout = 600 } } +// DaggerConfig configures the Dagger sandbox when use_dagger is enabled. +type DaggerConfig struct { + Image string + ExecuteTimeout int +} + // DefaultConfig returns a Config with sensible default values. func DefaultConfig() *Config { validationCfg := &ValidationConfig{} @@ -51,6 +63,10 @@ func DefaultConfig() *Config { Path: "", }, Validation: validationCfg, + Dagger: &DaggerConfig{ + Image: "node:20-alpine", + ExecuteTimeout: 600, + }, }, WarehouseID: "", } diff --git a/experimental/apps-mcp/lib/providers/io/validate.go b/experimental/apps-mcp/lib/providers/io/validate.go index 2a763ec968..ea815b4068 100644 --- a/experimental/apps-mcp/lib/providers/io/validate.go +++ b/experimental/apps-mcp/lib/providers/io/validate.go @@ -7,6 +7,9 @@ import ( "os" "path/filepath" + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox" + "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox/dagger" "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox/local" "github.com/databricks/cli/libs/log" ) @@ -44,7 +47,7 @@ func (p *Provider) Validate(ctx context.Context, args *ValidateArgs) (*ValidateR valConfig := p.config.Validation if valConfig.Command != "" { log.Infof(ctx, "using custom validation command: command=%s", valConfig.Command) - validation = NewValidationCmd(valConfig.Command, "") + validation = NewValidationCmd(valConfig.Command, valConfig.DockerImage) } } @@ -53,12 +56,45 @@ func (p *Provider) Validate(ctx context.Context, args *ValidateArgs) (*ValidateR validation = NewValidationTRPC() } - log.Info(ctx, "using local sandbox for validation") - sb, err := p.createLocalSandbox(workDir) - if err != nil { - return nil, fmt.Errorf("failed to create local sandbox: %w", err) + validationCfg := p.config.Validation + if validationCfg == nil { + validationCfg = &mcp.ValidationConfig{} + validationCfg.SetDefaults() + } else { + validationCfg.SetDefaults() + } + + var sb sandbox.Sandbox + var sandboxType string + if validationCfg.UseDagger { + log.Info(ctx, "attempting to create Dagger sandbox") + daggerSb, err := p.createDaggerSandbox(ctx, workDir, validationCfg) + if err != nil { + log.Warnf(ctx, "failed to create Dagger sandbox, falling back to local: error=%s", err.Error()) + sb, err = p.createLocalSandbox(workDir) + if err != nil { + return nil, fmt.Errorf("failed to create local sandbox: %w", err) + } + sandboxType = "local" + } else { + sb = daggerSb + sandboxType = "dagger" + } + } else { + log.Info(ctx, "using local sandbox") + sb, err = p.createLocalSandbox(workDir) + if err != nil { + return nil, fmt.Errorf("failed to create local sandbox: %w", err) + } + sandboxType = "local" + } + + // Log which sandbox is being used for transparency + if sandboxType == "dagger" { + log.Info(ctx, "✓ Using Dagger sandbox for validation (containerized, isolated environment)") + } else { + log.Info(ctx, "Using local sandbox for validation (host filesystem)") } - sandboxType := "local" defer func() { if closeErr := sb.Close(); closeErr != nil { @@ -101,7 +137,62 @@ func (p *Provider) Validate(ctx context.Context, args *ValidateArgs) (*ValidateR return result, nil } -func (p *Provider) createLocalSandbox(workDir string) (*local.LocalSandbox, error) { +func (p *Provider) createDaggerSandbox(ctx context.Context, workDir string, cfg *mcp.ValidationConfig) (sandbox.Sandbox, error) { + log.Infof(ctx, "creating Dagger sandbox: image=%s, timeout=%d, workDir=%s", + cfg.DockerImage, cfg.Timeout, workDir) + + sb, err := dagger.NewDaggerSandbox(ctx, dagger.Config{ + Image: cfg.DockerImage, + ExecuteTimeout: cfg.Timeout, + BaseDir: "/workspace", + }) + if err != nil { + log.Errorf(ctx, "failed to create Dagger sandbox: error=%s, image=%s", + err.Error(), cfg.DockerImage) + return nil, err + } + + log.Debug(ctx, "propagating environment variables") + if err := p.propagateEnvironment(sb); err != nil { + log.Errorf(ctx, "failed to propagate environment: error=%s", err.Error()) + sb.Close() + return nil, fmt.Errorf("failed to set environment: %w", err) + } + + log.Debugf(ctx, "syncing files from host to container: workDir=%s", workDir) + if err := sb.RefreshFromHost(ctx, workDir, "/workspace"); err != nil { + log.Errorf(ctx, "failed to sync files: error=%s", err.Error()) + sb.Close() + return nil, fmt.Errorf("failed to sync files: %w", err) + } + + log.Info(ctx, "Dagger sandbox created successfully") + return sb, nil +} + +func (p *Provider) createLocalSandbox(workDir string) (sandbox.Sandbox, error) { log.Infof(p.ctx, "creating local sandbox: workDir=%s", workDir) return local.NewLocalSandbox(workDir) } + +func (p *Provider) propagateEnvironment(sb sandbox.Sandbox) error { + daggerSb, ok := sb.(*dagger.DaggerSandbox) + if !ok { + return nil + } + + envVars := []string{ + "DATABRICKS_HOST", + "DATABRICKS_TOKEN", + "DATABRICKS_WAREHOUSE_ID", + } + + for _, key := range envVars { + if value := os.Getenv(key); value != "" { + daggerSb.WithEnv(key, value) + log.Debugf(p.ctx, "propagated environment variable: key=%s", key) + } + } + + return nil +} diff --git a/experimental/apps-mcp/lib/sandbox/dagger/dagger.go b/experimental/apps-mcp/lib/sandbox/dagger/dagger.go new file mode 100644 index 0000000000..aa5bbded36 --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/dagger/dagger.go @@ -0,0 +1,349 @@ +// Package dagger provides a Dagger-based sandbox implementation for +// containerized execution and file operations. +package dagger + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "dagger.io/dagger" + "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox" +) + +var ( + globalClient *dagger.Client + clientMu sync.Mutex +) + +func init() { + sandbox.Register(sandbox.TypeDagger, func(cfg *sandbox.Config) (sandbox.Sandbox, error) { + return NewDaggerSandbox(context.Background(), Config{ + Image: "node:20-alpine", + ExecuteTimeout: int(cfg.Timeout.Seconds()), + BaseDir: cfg.BaseDir, + }) + }) +} + +// GetGlobalClient returns a singleton Dagger client, creating it if necessary. +// This enables connection pooling across multiple sandbox instances. +func GetGlobalClient(ctx context.Context) (*dagger.Client, error) { + clientMu.Lock() + defer clientMu.Unlock() + + if globalClient == nil { + var err error + globalClient, err = dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr)) + if err != nil { + return nil, fmt.Errorf("failed to connect to Dagger: %w", err) + } + } + + return globalClient, nil +} + +// CloseGlobalClient closes the global Dagger client if it exists. +// Should be called during application shutdown. +func CloseGlobalClient() error { + clientMu.Lock() + defer clientMu.Unlock() + + if globalClient != nil { + err := globalClient.Close() + globalClient = nil + return err + } + + return nil +} + +// DaggerSandbox implements the Sandbox interface using Dagger containers. +// It provides isolated execution environments with container-based operations. +type DaggerSandbox struct { + client *dagger.Client + container *dagger.Container + workdir string + baseDir string + mu sync.RWMutex + + image string + executeTimeout int +} + +// Config holds configuration options for creating a DaggerSandbox. +type Config struct { + // Image is the Docker image to use (default: node:20-alpine) + Image string + // ExecuteTimeout is the execution timeout in seconds (default: 600) + ExecuteTimeout int + // BaseDir is the base directory for operations + BaseDir string +} + +// NewDaggerSandbox creates a new DaggerSandbox with the specified configuration. +// It uses the global Dagger client for connection pooling and initializes a container +// with the specified image. +func NewDaggerSandbox(ctx context.Context, cfg Config) (*DaggerSandbox, error) { + if cfg.Image == "" { + cfg.Image = "node:20-alpine" + } + if cfg.ExecuteTimeout == 0 { + cfg.ExecuteTimeout = 600 + } + + client, err := GetGlobalClient(ctx) + if err != nil { + return nil, err + } + + container := client.Container(). + From(cfg.Image). + WithWorkdir("/workspace") + + return &DaggerSandbox{ + client: client, + container: container, + workdir: "/workspace", + baseDir: cfg.BaseDir, + image: cfg.Image, + executeTimeout: cfg.ExecuteTimeout, + }, nil +} + +// resolvePath resolves a path relative to the working directory. +// If the path is absolute, it returns it as-is. +// If the path is relative, it joins it with the working directory. +func (d *DaggerSandbox) resolvePath(path string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(d.workdir, path) +} + +// Exec executes a command in the Dagger container. +// The command is executed using "sh -c" to support shell features. +func (d *DaggerSandbox) Exec(ctx context.Context, command string) (*sandbox.ExecResult, error) { + d.mu.RLock() + container := d.container.WithExec([]string{"sh", "-c", command}, dagger.ContainerWithExecOpts{ + Expect: dagger.ReturnTypeAny, + }) + d.mu.RUnlock() + + exitCode, exitErr := container.ExitCode(ctx) + if exitErr != nil { + return nil, fmt.Errorf("failed to get exit code: %w", exitErr) + } + + stdout, stdoutErr := container.Stdout(ctx) + if stdoutErr != nil { + return nil, fmt.Errorf("failed to get stdout: %w", stdoutErr) + } + + stderr, stderrErr := container.Stderr(ctx) + if stderrErr != nil { + return nil, fmt.Errorf("failed to get stderr: %w", stderrErr) + } + + result := &sandbox.ExecResult{ + Stdout: stdout, + Stderr: stderr, + ExitCode: exitCode, + } + + d.mu.Lock() + d.container = container + d.mu.Unlock() + + if exitCode != 0 { + return result, fmt.Errorf("command exited with code %d", exitCode) + } + + return result, nil +} + +// WriteFile writes content to a file in the container. +// Parent directories are created automatically if they don't exist. +func (d *DaggerSandbox) WriteFile(ctx context.Context, path, content string) error { + d.mu.Lock() + defer d.mu.Unlock() + + fullPath := d.resolvePath(path) + dir := filepath.Dir(fullPath) + + d.container = d.container.WithExec([]string{"mkdir", "-p", dir}) + d.container = d.container.WithNewFile(fullPath, content, dagger.ContainerWithNewFileOpts{ + Permissions: 0o644, + }) + + _, err := d.container.Sync(ctx) + return err +} + +// WriteFiles writes multiple files to the container in a single operation. +// This is much more efficient than individual WriteFile calls as it prevents +// deep query chains in Dagger. +func (d *DaggerSandbox) WriteFiles(ctx context.Context, files map[string]string) error { + if len(files) == 0 { + return nil + } + + d.mu.Lock() + defer d.mu.Unlock() + + tmpDir := d.client.Directory() + + for path, content := range files { + tmpDir = tmpDir.WithNewFile(path, content) + } + + d.container = d.container.WithDirectory(d.workdir, tmpDir) + + _, err := d.container.Sync(ctx) + return err +} + +// ReadFile reads the content of a file from the container. +func (d *DaggerSandbox) ReadFile(ctx context.Context, path string) (string, error) { + d.mu.RLock() + defer d.mu.RUnlock() + + fullPath := d.resolvePath(path) + + file := d.container.File(fullPath) + contents, err := file.Contents(ctx) + if err != nil { + return "", fmt.Errorf("failed to read file %s: %w", path, err) + } + + return contents, nil +} + +// DeleteFile deletes a file from the container. +func (d *DaggerSandbox) DeleteFile(ctx context.Context, path string) error { + d.mu.Lock() + defer d.mu.Unlock() + + fullPath := d.resolvePath(path) + d.container = d.container.WithExec([]string{"rm", "-f", fullPath}) + + _, err := d.container.Sync(ctx) + return err +} + +// ListDirectory lists all files and directories in the specified path. +// Returns a list of entry names (not full paths). +func (d *DaggerSandbox) ListDirectory(ctx context.Context, path string) ([]string, error) { + d.mu.RLock() + defer d.mu.RUnlock() + + fullPath := d.resolvePath(path) + + dir := d.container.Directory(fullPath) + entries, err := dir.Entries(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list directory %s: %w", path, err) + } + + return entries, nil +} + +// SetWorkdir changes the working directory for future operations. +func (d *DaggerSandbox) SetWorkdir(ctx context.Context, path string) error { + d.mu.Lock() + defer d.mu.Unlock() + + d.workdir = path + d.container = d.container.WithWorkdir(path) + + return nil +} + +// ExportDirectory exports a directory from the container to the host filesystem. +// Returns the absolute path to the exported directory on the host. +func (d *DaggerSandbox) ExportDirectory(ctx context.Context, containerPath, hostPath string) (string, error) { + d.mu.RLock() + dir := d.container.Directory(containerPath) + d.mu.RUnlock() + + exportedPath, err := dir.Export(ctx, hostPath) + if err != nil { + return "", fmt.Errorf("failed to export directory: %w", err) + } + + return exportedPath, nil +} + +// RefreshFromHost imports files from the host filesystem into the container. +// This is useful for incremental updates without recreating the sandbox. +func (d *DaggerSandbox) RefreshFromHost(ctx context.Context, hostPath, containerPath string) error { + d.mu.Lock() + defer d.mu.Unlock() + + hostDir := d.client.Host().Directory(hostPath) + d.container = d.container.WithDirectory(containerPath, hostDir) + + _, err := d.container.Sync(ctx) + return err +} + +// Close releases the Dagger client resources. +// After calling Close, the sandbox should not be used. +func (d *DaggerSandbox) Close() error { + // With connection pooling, individual sandbox instances do not close + // the global client. Use CloseGlobalClient() during application shutdown. + // We only clear the container reference, not the client. + d.mu.Lock() + d.container = nil + d.mu.Unlock() + return nil +} + +// Fork creates a copy of the sandbox with the same state. +// This is lightweight due to Dagger's immutability model - containers +// are just references that can be safely shared. +func (d *DaggerSandbox) Fork() sandbox.Sandbox { + d.mu.RLock() + defer d.mu.RUnlock() + + return &DaggerSandbox{ + client: d.client, + container: d.container, + workdir: d.workdir, + baseDir: d.baseDir, + image: d.image, + executeTimeout: d.executeTimeout, + } +} + +// WithEnv sets a single environment variable in the container. +// This modifies the container state, so subsequent operations will have access to it. +func (d *DaggerSandbox) WithEnv(key, value string) { + d.mu.Lock() + defer d.mu.Unlock() + + d.container = d.container.WithEnvVariable(key, value) +} + +// WithEnvs sets multiple environment variables in the container. +// This is more efficient than calling WithEnv multiple times separately. +func (d *DaggerSandbox) WithEnvs(envs map[string]string) { + d.mu.Lock() + defer d.mu.Unlock() + + for key, value := range envs { + d.container = d.container.WithEnvVariable(key, value) + } +} + +// ExecWithTimeout executes a command with a timeout. +// If the timeout is exceeded, it returns context.DeadlineExceeded. +func (d *DaggerSandbox) ExecWithTimeout(ctx context.Context, command string) (*sandbox.ExecResult, error) { + timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(d.executeTimeout)*time.Second) + defer cancel() + + return d.Exec(timeoutCtx, command) +} diff --git a/experimental/apps-mcp/lib/sandbox/dagger/dagger_test.go b/experimental/apps-mcp/lib/sandbox/dagger/dagger_test.go new file mode 100644 index 0000000000..fe0a47667c --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/dagger/dagger_test.go @@ -0,0 +1,609 @@ +package dagger + +import ( + "context" + "os" + "strings" + "testing" + "time" +) + +// TestMain handles setup and teardown for all tests in this package. +func TestMain(m *testing.M) { + code := m.Run() + + // Cleanup global client after all tests + _ = CloseGlobalClient() + + os.Exit(code) +} + +// resetGlobalClient closes and resets the global client for test isolation. +// Call this in tests that need a fresh client. +func resetGlobalClient() { + clientMu.Lock() + defer clientMu.Unlock() + + if globalClient != nil { + globalClient.Close() + globalClient = nil + } +} + +func TestNewDaggerSandbox(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + tests := []struct { + name string + cfg Config + wantErr bool + }{ + { + name: "default config", + cfg: Config{}, + wantErr: false, + }, + { + name: "custom image", + cfg: Config{ + Image: "alpine:latest", + ExecuteTimeout: 300, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sb, err := NewDaggerSandbox(ctx, tt.cfg) + if (err != nil) != tt.wantErr { + t.Errorf("NewDaggerSandbox() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && sb != nil { + defer sb.Close() + + if tt.cfg.Image == "" && sb.image != "node:20-alpine" { + t.Errorf("expected default image 'node:20-alpine', got %s", sb.image) + } + if tt.cfg.ExecuteTimeout == 0 && sb.executeTimeout != 600 { + t.Errorf("expected default timeout 600, got %d", sb.executeTimeout) + } + } + }) + } +} + +func TestDaggerSandbox_Exec(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + tests := []struct { + name string + command string + wantExitCode int + wantStdout string + wantErr bool + }{ + { + name: "simple echo", + command: "echo 'hello world'", + wantExitCode: 0, + wantStdout: "hello world", + wantErr: false, + }, + { + name: "print working directory", + command: "pwd", + wantExitCode: 0, + wantStdout: "/workspace", + wantErr: false, + }, + { + name: "exit code non-zero", + command: "exit 1", + wantExitCode: 1, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := sb.Exec(ctx, tt.command) + if (err != nil) != tt.wantErr { + t.Errorf("Exec() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if result == nil { + t.Fatal("expected result, got nil") + } + + if result.ExitCode != tt.wantExitCode { + t.Errorf("expected exit code %d, got %d", tt.wantExitCode, result.ExitCode) + } + + if tt.wantStdout != "" && !strings.Contains(result.Stdout, tt.wantStdout) { + t.Errorf("expected stdout to contain %q, got: %s", tt.wantStdout, result.Stdout) + } + }) + } +} + +func TestDaggerSandbox_WriteReadFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + tests := []struct { + name string + path string + content string + }{ + { + name: "simple file", + path: "test.txt", + content: "test content", + }, + { + name: "file in subdirectory", + path: "subdir/nested.txt", + content: "nested content", + }, + { + name: "file with special characters", + path: "special.txt", + content: "content with\nnewlines\tand\ttabs", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := sb.WriteFile(ctx, tt.path, tt.content); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + read, err := sb.ReadFile(ctx, tt.path) + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + + if read != tt.content { + t.Errorf("expected content %q, got %q", tt.content, read) + } + }) + } +} + +func TestDaggerSandbox_WriteFiles_BulkOperation(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + files := map[string]string{ + "file1.txt": "content1", + "file2.txt": "content2", + "dir/file3.txt": "content3", + } + + if err := sb.WriteFiles(ctx, files); err != nil { + t.Fatalf("WriteFiles() error = %v", err) + } + + for path, expected := range files { + content, err := sb.ReadFile(ctx, path) + if err != nil { + t.Errorf("failed to read %s: %v", path, err) + continue + } + if content != expected { + t.Errorf("%s: expected %q, got %q", path, expected, content) + } + } +} + +func TestDaggerSandbox_DeleteFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + path := "test.txt" + content := "test content" + + if err := sb.WriteFile(ctx, path, content); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + if err := sb.DeleteFile(ctx, path); err != nil { + t.Fatalf("DeleteFile() error = %v", err) + } + + _, err = sb.ReadFile(ctx, path) + if err == nil { + t.Error("expected error when reading deleted file, got nil") + } +} + +func TestDaggerSandbox_ListDirectory(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + files := map[string]string{ + "file1.txt": "content1", + "file2.txt": "content2", + "file3.txt": "content3", + } + + if err := sb.WriteFiles(ctx, files); err != nil { + t.Fatalf("WriteFiles() error = %v", err) + } + + entries, err := sb.ListDirectory(ctx, ".") + if err != nil { + t.Fatalf("ListDirectory() error = %v", err) + } + + if len(entries) != len(files) { + t.Errorf("expected %d entries, got %d", len(files), len(entries)) + } + + for _, entry := range entries { + if _, ok := files[entry]; !ok { + t.Errorf("unexpected entry: %s", entry) + } + } +} + +func TestDaggerSandbox_SetWorkdir(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + newWorkdir := "/tmp" + if err := sb.SetWorkdir(ctx, newWorkdir); err != nil { + t.Fatalf("SetWorkdir() error = %v", err) + } + + result, err := sb.Exec(ctx, "pwd") + if err != nil { + t.Fatalf("Exec() error = %v", err) + } + + if !strings.Contains(result.Stdout, newWorkdir) { + t.Errorf("expected working directory %s, got: %s", newWorkdir, result.Stdout) + } +} + +func TestDaggerSandbox_ExportDirectory(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + files := map[string]string{ + "export/file1.txt": "content1", + "export/file2.txt": "content2", + } + + if err := sb.WriteFiles(ctx, files); err != nil { + t.Fatalf("WriteFiles() error = %v", err) + } + + tmpDir := t.TempDir() + exportedPath, err := sb.ExportDirectory(ctx, "export", tmpDir+"/exported") + if err != nil { + t.Fatalf("ExportDirectory() error = %v", err) + } + + if exportedPath == "" { + t.Error("expected non-empty exported path") + } +} + +func TestDaggerSandbox_RefreshFromHost(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + tmpDir := t.TempDir() + testFile := tmpDir + "/test.txt" + testContent := "host content" + + if err := writeHostFile(testFile, testContent); err != nil { + t.Fatalf("failed to write host file: %v", err) + } + + if err := sb.RefreshFromHost(ctx, tmpDir, "/imported"); err != nil { + t.Fatalf("RefreshFromHost() error = %v", err) + } + + content, err := sb.ReadFile(ctx, "/imported/test.txt") + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + + if content != testContent { + t.Errorf("expected content %q, got %q", testContent, content) + } +} + +func TestDaggerSandbox_Close(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + + if err := sb.Close(); err != nil { + t.Errorf("Close() error = %v", err) + } + + if err := sb.Close(); err != nil { + t.Errorf("second Close() error = %v", err) + } +} + +func TestDaggerSandbox_Fork(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + + original, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer original.Close() + + if err := original.WriteFile(ctx, "base.txt", "base content"); err != nil { + t.Fatalf("write to original failed: %v", err) + } + + forked := original.Fork() + defer forked.Close() + + content, err := forked.ReadFile(ctx, "base.txt") + if err != nil { + t.Fatalf("read from fork failed: %v", err) + } + + if content != "base content" { + t.Errorf("expected 'base content', got %q", content) + } + + if err := forked.WriteFile(ctx, "forked.txt", "forked content"); err != nil { + t.Fatalf("write to fork failed: %v", err) + } + + _, err = original.ReadFile(ctx, "forked.txt") + if err != nil { + t.Log("original does not have forked file (expected due to Dagger immutability)") + } +} + +func TestDaggerSandbox_WithEnv(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + sb.WithEnv("TEST_VAR", "test_value") + + result, err := sb.Exec(ctx, "echo $TEST_VAR") + if err != nil { + t.Fatalf("exec failed: %v", err) + } + + if !strings.Contains(result.Stdout, "test_value") { + t.Errorf("expected 'test_value' in stdout, got: %s", result.Stdout) + } +} + +func TestDaggerSandbox_WithEnvs(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + sb, err := NewDaggerSandbox(ctx, Config{}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + envs := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + "VAR3": "value3", + } + + sb.WithEnvs(envs) + + for key, expected := range envs { + result, err := sb.Exec(ctx, "echo $"+key) + if err != nil { + t.Fatalf("exec failed for %s: %v", key, err) + } + + if !strings.Contains(result.Stdout, expected) { + t.Errorf("expected %q in stdout for %s, got: %s", expected, key, result.Stdout) + } + } +} + +func TestDaggerSandbox_ExecWithTimeout(t *testing.T) { + if testing.Short() { + t.Skip("skipping Dagger integration test") + } + + t.Cleanup(resetGlobalClient) + + ctx := context.Background() + + t.Run("timeout exceeded", func(t *testing.T) { + sb, err := NewDaggerSandbox(ctx, Config{ExecuteTimeout: 2}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + _, err = sb.ExecWithTimeout(ctx, "sleep 5") + if err == nil { + t.Error("expected timeout error") + } + + if !strings.Contains(err.Error(), "deadline") && !strings.Contains(err.Error(), "timeout") && !strings.Contains(err.Error(), "canceled") { + t.Errorf("expected timeout/deadline/canceled error, got: %v", err) + } + }) + + t.Run("quick command succeeds", func(t *testing.T) { + sb, err := NewDaggerSandbox(ctx, Config{ExecuteTimeout: 10}) + if err != nil { + t.Fatalf("failed to create sandbox: %v", err) + } + defer sb.Close() + + result, err := sb.ExecWithTimeout(ctx, "echo 'quick command'") + if err != nil { + t.Fatalf("expected success for quick command: %v", err) + } + + if !strings.Contains(result.Stdout, "quick command") { + t.Errorf("expected 'quick command' in stdout, got: %s", result.Stdout) + } + }) +} + +func writeHostFile(path, content string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(content) + return err +} diff --git a/experimental/apps-mcp/lib/sandbox/dagger/metrics.go b/experimental/apps-mcp/lib/sandbox/dagger/metrics.go new file mode 100644 index 0000000000..3df9ebb839 --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/dagger/metrics.go @@ -0,0 +1,64 @@ +package dagger + +import ( + "context" + "sync/atomic" + "time" + + "github.com/databricks/cli/libs/log" +) + +// DaggerMetrics tracks usage statistics for Dagger sandboxes. +type DaggerMetrics struct { + ValidationCount atomic.Int64 + SuccessCount atomic.Int64 + FallbackCount atomic.Int64 + TotalDurationMs atomic.Int64 +} + +// GlobalMetrics provides global metrics tracking for all Dagger operations. +var GlobalMetrics = &DaggerMetrics{} + +// RecordValidation records metrics for a validation operation. +// This should be called after each validation completes. +func RecordValidation(ctx context.Context, success bool, duration time.Duration) { + GlobalMetrics.ValidationCount.Add(1) + GlobalMetrics.TotalDurationMs.Add(duration.Milliseconds()) + + if success { + GlobalMetrics.SuccessCount.Add(1) + } + + count := GlobalMetrics.ValidationCount.Load() + avgDuration := float64(GlobalMetrics.TotalDurationMs.Load()) / float64(count) + + log.Infof(ctx, "Validation completed (success: %v, duration_ms: %d, sandbox: dagger, total_validations: %d, avg_duration_ms: %.2f)", + success, duration.Milliseconds(), count, avgDuration) +} + +// RecordFallback records when Dagger fails and falls back to local sandbox. +func RecordFallback(ctx context.Context, reason string) { + GlobalMetrics.FallbackCount.Add(1) + + log.Warnf(ctx, "Dagger fallback to local sandbox (reason: %s, fallback_count: %d)", + reason, GlobalMetrics.FallbackCount.Load()) +} + +// GetMetrics returns a snapshot of current metrics. +func GetMetrics() map[string]any { + validations := GlobalMetrics.ValidationCount.Load() + totalDuration := GlobalMetrics.TotalDurationMs.Load() + + avgDuration := float64(0) + if validations > 0 { + avgDuration = float64(totalDuration) / float64(validations) + } + + return map[string]any{ + "validation_count": validations, + "success_count": GlobalMetrics.SuccessCount.Load(), + "fallback_count": GlobalMetrics.FallbackCount.Load(), + "avg_duration_ms": avgDuration, + "total_duration_ms": totalDuration, + } +} diff --git a/experimental/apps-mcp/lib/sandbox/dagger/stub.go b/experimental/apps-mcp/lib/sandbox/dagger/stub.go deleted file mode 100644 index 8bd49e2d1d..0000000000 --- a/experimental/apps-mcp/lib/sandbox/dagger/stub.go +++ /dev/null @@ -1,69 +0,0 @@ -// Package dagger provides a stub implementation for Dagger-based sandbox. -// This is a placeholder for future containerized execution support. -package dagger - -import ( - "context" - "errors" - - "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox" -) - -func init() { - sandbox.Register(sandbox.TypeDagger, func(cfg *sandbox.Config) (sandbox.Sandbox, error) { - return nil, errors.New("dagger sandbox is not implemented") - }) -} - -// DaggerSandbox is a stub implementation that always returns errors. -type DaggerSandbox struct{} - -// Exec is not implemented. -func (d *DaggerSandbox) Exec(ctx context.Context, command string) (*sandbox.ExecResult, error) { - return nil, errors.New("dagger sandbox is not implemented") -} - -// WriteFile is not implemented. -func (d *DaggerSandbox) WriteFile(ctx context.Context, path, content string) error { - return errors.New("dagger sandbox is not implemented") -} - -// WriteFiles is not implemented. -func (d *DaggerSandbox) WriteFiles(ctx context.Context, files map[string]string) error { - return errors.New("dagger sandbox is not implemented") -} - -// ReadFile is not implemented. -func (d *DaggerSandbox) ReadFile(ctx context.Context, path string) (string, error) { - return "", errors.New("dagger sandbox is not implemented") -} - -// DeleteFile is not implemented. -func (d *DaggerSandbox) DeleteFile(ctx context.Context, path string) error { - return errors.New("dagger sandbox is not implemented") -} - -// ListDirectory is not implemented. -func (d *DaggerSandbox) ListDirectory(ctx context.Context, path string) ([]string, error) { - return nil, errors.New("dagger sandbox is not implemented") -} - -// SetWorkdir is not implemented. -func (d *DaggerSandbox) SetWorkdir(ctx context.Context, path string) error { - return errors.New("dagger sandbox is not implemented") -} - -// ExportDirectory is not implemented. -func (d *DaggerSandbox) ExportDirectory(ctx context.Context, containerPath, hostPath string) (string, error) { - return "", errors.New("dagger sandbox is not implemented") -} - -// RefreshFromHost is not implemented. -func (d *DaggerSandbox) RefreshFromHost(ctx context.Context, hostPath, containerPath string) error { - return errors.New("dagger sandbox is not implemented") -} - -// Close is not implemented. -func (d *DaggerSandbox) Close() error { - return nil -} diff --git a/experimental/apps-mcp/lib/sandbox/doc.go b/experimental/apps-mcp/lib/sandbox/doc.go index 0b91548f2f..fb12a0c245 100644 --- a/experimental/apps-mcp/lib/sandbox/doc.go +++ b/experimental/apps-mcp/lib/sandbox/doc.go @@ -16,7 +16,7 @@ Interface: Implementations: - local: Direct filesystem and shell access with security constraints -- dagger: Not implemented (stub only) +- dagger: Containerized execution (stub, future implementation) Security: diff --git a/experimental/apps-mcp/lib/sandbox/sandbox.go b/experimental/apps-mcp/lib/sandbox/sandbox.go index 63997edf38..1febe889cf 100644 --- a/experimental/apps-mcp/lib/sandbox/sandbox.go +++ b/experimental/apps-mcp/lib/sandbox/sandbox.go @@ -12,8 +12,8 @@ type ExecResult struct { } // Sandbox defines the interface for executing commands and managing files -// in an isolated environment. Implementations may use local filesystem -// or other isolation mechanisms. +// in an isolated environment. Implementations may use local filesystem, +// containers (e.g., Dagger), or other isolation mechanisms. type Sandbox interface { // Exec executes a command in the sandbox and returns the result. // The command is executed in the current working directory. diff --git a/experimental/apps-mcp/lib/server/doc.go b/experimental/apps-mcp/lib/server/doc.go index 71f469c715..2f9ef2b34b 100644 --- a/experimental/apps-mcp/lib/server/doc.go +++ b/experimental/apps-mcp/lib/server/doc.go @@ -34,6 +34,6 @@ Sandbox: Tools execute in a sandbox abstraction that can be: - Local: Direct filesystem and shell access -- Dagger: Not implemented (stub only) +- Dagger: Containerized execution (future) */ package server diff --git a/go.mod b/go.mod index 4820f0b25a..c86d47c825 100644 --- a/go.mod +++ b/go.mod @@ -41,14 +41,21 @@ require ( ) // Dependencies for experimental MCP commands -require github.com/google/jsonschema-go v0.3.0 // MIT +require ( + dagger.io/dagger v0.19.6 // Apache 2.0 + github.com/google/jsonschema-go v0.3.0 // BSD-3-Clause +) require ( cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.8.4 // indirect + github.com/99designs/gqlgen v0.17.81 // indirect + github.com/Khan/genqlient v0.8.1 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -59,22 +66,39 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/vektah/gqlparser/v2 v2.5.30 // indirect github.com/zclconf/go-cty v1.16.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.8.0 // indirect golang.org/x/net v0.45.0 // indirect golang.org/x/time v0.13.0 // indirect google.golang.org/api v0.249.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/grpc v1.75.1 // indirect + google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index 16ecc9ff0e..384fc803b0 100644 --- a/go.sum +++ b/go.sum @@ -4,20 +4,32 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.8.4 h1:oXMa1VMQBVCyewMIOm3WQsnVd9FbKBtm8reqWRaXnHQ= cloud.google.com/go/compute/metadata v0.8.4/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +dagger.io/dagger v0.19.6 h1:rxnAe7JSXqnNFGIGt9w+xX+dEDsmAYLpiOVAkWnOVPQ= +dagger.io/dagger v0.19.6/go.mod h1:BjAJWl4Lx7XRW7nooNjBi0ZAC5Ici2pkthkdBIZdbTI= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/99designs/gqlgen v0.17.81 h1:kCkN/xVyRb5rEQpuwOHRTYq83i0IuTQg9vdIiwEerTs= +github.com/99designs/gqlgen v0.17.81/go.mod h1:vgNcZlLwemsUhYim4dC1pvFP5FX0pr2Y+uYUoHFb1ig= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= +github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -75,6 +87,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -104,13 +118,14 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMhHQ= github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= @@ -130,6 +145,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -141,6 +158,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= @@ -153,14 +172,38 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= +go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= @@ -176,7 +219,6 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= @@ -192,10 +234,12 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w= google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=