-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add Sprites integration for cloud-based Claude execution #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Explores using Sprites (sprites.dev) as isolated cloud execution environments for Claude instances. This could simplify or replace the current `taskd` cloud deployment approach. Key findings: - Sprites provide VM-level isolation with persistent filesystems - Designed specifically for AI agent workloads - ~$0.46 for a 4-hour Claude session - Could eliminate need for dedicated cloud server - Hook callbacks would work via HTTP Proposes phased implementation from PoC to full cloud mode. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for running Claude tasks on Fly.io Sprites - isolated cloud VMs that enable dangerous mode safely. Key features: - `task sprite init <project>` - Create sprite, clone repo, setup deps - `task sprite status` - Show sprite status for projects - `task sprite destroy <project>` - Delete a project's sprite - `task sprite attach <project>` - SSH into sprite's tmux session - `task sprite sync <project>` - Pull latest code, update deps - `task sprite checkpoint <project>` - Save sprite state Architecture: - One sprite per project (persistent dev environment) - Claude runs in dangerous mode inside isolated VM - Hook events stream via tail -f over WebSocket - User input sent via tmux send-keys Database: - Added sprite_name and sprite_status columns to projects table Executor: - Automatically uses sprite if project has one configured - Hook streaming via tail -f /tmp/task-hooks.log - Polls for completion, respects status from hooks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify the Sprites integration from per-project sprites to a single shared sprite that runs the entire task daemon + TUI. Changes: - Auto-connect to sprite when SPRITES_TOKEN is set - Add --local flag to force local execution - Remove per-project sprite complexity (executor_sprite.go, client wrapper) - Sprite runs `task -l --dangerous` with TTY attached - User's local machine acts as thin client to cloud sprite This approach is simpler and more cost-effective: one sprite handles all projects, dangerous mode is safe inside the isolated VM. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify sprites architecture: sprites are now only used as the execution environment for Claude, not for running the entire task app. Key changes: - Task app runs locally (or on remote server), database stays local - When SPRITES_TOKEN is set, executor runs Claude on sprite instead of local tmux - Hooks stream back from sprite via tail -f on /tmp/task-hooks.jsonl - No database syncing needed - sprite is just a sandbox for Claude New files: - internal/sprites/sprites.go - Shared sprite client/token logic - internal/executor/executor_sprite.go - Sprite execution + hook streaming Flow: 1. User runs `task` locally (normal) 2. Executor checks SPRITES_TOKEN at startup 3. If set, creates SpriteRunner and starts hook listener 4. When task runs, Claude executes on sprite in tmux session 5. Hooks write to file on sprite, executor tails and updates local DB Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Review the new Sprites exec API and document how it can improve the existing sprites integration in PR #103 and PR #160. Key findings: - Exec sessions replace tmux entirely (persistent, reconnectable) - Filesystem API replaces shell-based file operations - Services API replaces nohup+polling for long-running processes - Network Policy API enables security restrictions - Port notifications enable dev server URL forwarding Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sprites Exec API — opportunities for this PRI reviewed the new Sprites exec API ( 1. Replace tmux with native exec sessionsThe exec API provides persistent sessions that survive client disconnections — exactly what tmux does here, but natively. Current approach ( // Start Claude in tmux on sprite
cmd := r.sprite.CommandContext(ctx, "tmux", "new-session", "-d", "-s", sessionName,
"-c", workDir, "sh", "-c", claudeCmd)
cmd.CombinedOutput()
// Poll for completion every 2 seconds
ticker := time.NewTicker(2 * time.Second)
for {
checkCmd := r.sprite.CommandContext(ctx, "tmux", "has-session", "-t", sessionName)
if err := checkCmd.Run(); err != nil {
break // session ended
}
}With exec API directly: cmd := r.sprite.CommandContext(ctx, "claude",
"--dangerously-skip-permissions", "-p", prompt)
cmd.Dir = workDir
cmd.Env = []string{
fmt.Sprintf("TASK_ID=%d", task.ID),
fmt.Sprintf("WORKTREE_PORT=%d", task.Port),
}
// Sessions persist after disconnect — no tmux needed
// cmd.SessionID() gives us the ID for reconnection later
stdout, _ := cmd.StdoutPipe()
cmd.Start()
// Store session ID for crash recovery
sessionID := cmd.SessionID()
db.SetTaskSessionID(task.ID, sessionID)
// Block until Claude exits — no polling
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
db.AppendTaskLog(task.ID, "claude", scanner.Text())
}
cmd.Wait() // ExitMessage gives exact exit codeBenefits:
2. Crash recovery via session reconnectionIf the local // On executor restart, check for existing sessions
sessions, _ := client.ListSessions(ctx, spriteName)
for _, session := range sessions {
if strings.HasPrefix(session.Command, "claude") && session.IsActive {
// Reattach to running Claude session
cmd := sprite.AttachSession(ctx, session.ID)
stdout, _ := cmd.StdoutPipe()
// Resume streaming logs to database...
}
}The 3. Replace file-tailing hooks with direct streaming or FS WatchCurrent approach ( // Tail hooks file on sprite via exec
cmd := r.sprite.CommandContext(r.hookCtx, "tail", "-n", "0", "-f", "/tmp/task-hooks.jsonl")
stdout, _ := cmd.StdoutPipe()
cmd.Start()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
// parse JSON hook events
}Option A — FS Watch WebSocket (recommended): // Watch for file changes without running a command
watcher := sprite.Filesystem().Watch(ctx, "/tmp/task-hooks.jsonl")Option B — Parse Claude's stdout directly: Option C — Read via Filesystem API: // Periodic read without exec
data, _ := sprite.Filesystem().ReadFile("/tmp/task-hooks.jsonl")4. Use Filesystem API for sprite setupCurrent approach ( installHookCmd := fmt.Sprintf(`cat > /usr/local/bin/task-sprite-hook << 'HOOKEOF'\n%s\nHOOKEOF\nchmod +x ...`, hookScript)
cmd := sprite.CommandContext(ctx, "sh", "-c", installHookCmd)With Filesystem API: fs := sprite.Filesystem()
fs.WriteFile("/usr/local/bin/task-sprite-hook", []byte(hookScript), 0755)
fs.WriteFile("/root/.claude/settings.json", []byte(claudeSettings), 0644)
fs.MkdirAll("/workspace", 0755)No shell escaping, no heredocs, no 5. Network Policy for sprite securityThe design doc mentions network restrictions but doesn't implement them. The Policy API makes this trivial: client.SetPolicy(ctx, spriteName, &sprites.Policy{
AllowedDomains: []string{
"github.com",
"api.anthropic.com",
"rubygems.org",
"registry.npmjs.org",
},
})This is one of the main security benefits of sprites — worth implementing. 6. Port notifications for dev serversWhen Claude starts a dev server inside the sprite, the exec API sends a cmd.TextMessageHandler = func(data []byte) {
var notification sprites.PortNotificationMessage
json.Unmarshal(data, ¬ification)
if notification.Type == "port_opened" {
db.AppendTaskLog(task.ID, "system",
fmt.Sprintf("Dev server available at %s", notification.ProxyURL))
}
}This would let the TUI show clickable URLs when Claude starts servers — useful for web dev tasks. SDK NoteThe Full analysis with all code examples: #286 |
Implement PR #103 feedback to use the Sprites exec API directly instead of tmux-based execution. Key improvements: 1. **Native exec sessions instead of tmux** - Sessions persist after client disconnect - cmd.Wait() blocks until completion (no polling) - No tmux dependency needed on sprite 2. **Direct stdout streaming** - Stream Claude's output to task logs in real-time - Parse output for status changes - No file-tailing hooks needed 3. **Filesystem API for setup** - Use sprite.Filesystem().WriteFile() and MkdirAll() - No shell escaping or heredocs required 4. **Port notifications** - Handle PortNotificationMessage for dev servers - Automatically log proxy URLs when Claude starts servers 5. **Crash recovery foundation** - Track active tasks for graceful shutdown - Idle watcher for automatic checkpointing New files: - cmd/task/sprite.go - CLI commands for sprite management - internal/sprites/sprites.go - Shared token/client logic - internal/executor/executor_sprite.go - SpriteRunner with exec API - docs/sprites-design.md - Architecture documentation - docs/sprites-discussion.md - Design discussion Usage: export SPRITES_TOKEN=<token> # or: task sprite token <token> task # Claude now runs on sprite Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Add Sprites (Fly.io managed sandboxes) as an execution environment for Claude tasks. Sprites provide isolated VMs where Claude can run with
--dangerously-skip-permissionssafely.Architecture: Sprite as execution environment only
Key changes:
SPRITES_TOKENis set, Claude executes on sprite instead of local tmux/tmp/task-hooks.jsonl)New files:
internal/sprites/sprites.go- Shared token/client logicinternal/executor/executor_sprite.go- SpriteRunner + hook streamingcmd/task/sprite.go- CLI commands for sprite managementUsage:
Test plan
task spritesubcommands🤖 Generated with Claude Code