feat(devops): add Claude sandbox session
Starts Claude in immutable dev environment with auth forwarding. Auto-boots VM, mounts project, forwards credentials. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
86ba93edd3
commit
c00b374147
1 changed files with 141 additions and 0 deletions
141
pkg/devops/claude.go
Normal file
141
pkg/devops/claude.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package devops
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ClaudeOptions configures the Claude sandbox session.
|
||||
type ClaudeOptions struct {
|
||||
NoAuth bool // Don't forward any auth
|
||||
Auth []string // Selective auth: "gh", "anthropic", "ssh", "git"
|
||||
Model string // Model to use: opus, sonnet
|
||||
}
|
||||
|
||||
// Claude starts a sandboxed Claude session in the dev environment.
|
||||
func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptions) error {
|
||||
// Auto-boot if not running
|
||||
running, err := d.IsRunning(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !running {
|
||||
fmt.Println("Dev environment not running, booting...")
|
||||
if err := d.Boot(ctx, DefaultBootOptions()); err != nil {
|
||||
return fmt.Errorf("failed to boot: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Mount project
|
||||
if err := d.mountProject(ctx, projectDir); err != nil {
|
||||
return fmt.Errorf("failed to mount project: %w", err)
|
||||
}
|
||||
|
||||
// Prepare environment variables to forward
|
||||
envVars := []string{}
|
||||
|
||||
if !opts.NoAuth {
|
||||
authTypes := opts.Auth
|
||||
if len(authTypes) == 0 {
|
||||
authTypes = []string{"gh", "anthropic", "ssh", "git"}
|
||||
}
|
||||
|
||||
for _, auth := range authTypes {
|
||||
switch auth {
|
||||
case "anthropic":
|
||||
if key := os.Getenv("ANTHROPIC_API_KEY"); key != "" {
|
||||
envVars = append(envVars, "ANTHROPIC_API_KEY="+key)
|
||||
}
|
||||
case "git":
|
||||
// Forward git config
|
||||
name, _ := exec.Command("git", "config", "user.name").Output()
|
||||
email, _ := exec.Command("git", "config", "user.email").Output()
|
||||
if len(name) > 0 {
|
||||
envVars = append(envVars, "GIT_AUTHOR_NAME="+strings.TrimSpace(string(name)))
|
||||
envVars = append(envVars, "GIT_COMMITTER_NAME="+strings.TrimSpace(string(name)))
|
||||
}
|
||||
if len(email) > 0 {
|
||||
envVars = append(envVars, "GIT_AUTHOR_EMAIL="+strings.TrimSpace(string(email)))
|
||||
envVars = append(envVars, "GIT_COMMITTER_EMAIL="+strings.TrimSpace(string(email)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build SSH command with agent forwarding
|
||||
args := []string{
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-A", // SSH agent forwarding
|
||||
"-p", "2222",
|
||||
}
|
||||
|
||||
args = append(args, "root@localhost")
|
||||
|
||||
// Build command to run inside
|
||||
claudeCmd := "cd /app && claude"
|
||||
if opts.Model != "" {
|
||||
claudeCmd += " --model " + opts.Model
|
||||
}
|
||||
args = append(args, claudeCmd)
|
||||
|
||||
// Set environment for SSH
|
||||
cmd := exec.CommandContext(ctx, "ssh", args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Pass environment variables through SSH
|
||||
for _, env := range envVars {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
cmd.Env = append(os.Environ(), env)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Starting Claude in sandboxed environment...")
|
||||
fmt.Println("Project mounted at /app")
|
||||
fmt.Println("Auth forwarded: SSH agent" + formatAuthList(opts))
|
||||
fmt.Println()
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func formatAuthList(opts ClaudeOptions) string {
|
||||
if opts.NoAuth {
|
||||
return " (none)"
|
||||
}
|
||||
if len(opts.Auth) == 0 {
|
||||
return ", gh, anthropic, git"
|
||||
}
|
||||
return ", " + strings.Join(opts.Auth, ", ")
|
||||
}
|
||||
|
||||
// CopyGHAuth copies GitHub CLI auth to the VM.
|
||||
func (d *DevOps) CopyGHAuth(ctx context.Context) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ghConfigDir := filepath.Join(home, ".config", "gh")
|
||||
if _, err := os.Stat(ghConfigDir); os.IsNotExist(err) {
|
||||
return nil // No gh config to copy
|
||||
}
|
||||
|
||||
// Use scp to copy gh config
|
||||
cmd := exec.CommandContext(ctx, "scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-P", "2222",
|
||||
"-r", ghConfigDir,
|
||||
"root@localhost:/root/.config/",
|
||||
)
|
||||
return cmd.Run()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue