Skip to content
Open
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
2 changes: 2 additions & 0 deletions extensions/ty-sandbox/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ty-sandbox
*.exe
23 changes: 23 additions & 0 deletions extensions/ty-sandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM golang:1.24-alpine AS builder

WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /ty-sandbox ./cmd/main.go

FROM alpine:3.21

RUN apk add --no-cache git nodejs npm

# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code

COPY --from=builder /ty-sandbox /usr/local/bin/ty-sandbox

WORKDIR /workspace
EXPOSE 8080

ENTRYPOINT ["ty-sandbox"]
CMD ["--addr", ":8080"]
171 changes: 171 additions & 0 deletions extensions/ty-sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# ty-sandbox

HTTP/SSE server for running coding agents in cloud sandboxes. Port of [rivet-dev/sandbox-agent](https://github.com/rivet-dev/sandbox-agent) to Go, integrated with TaskYou.

## Install

```sh
go build -o ty-sandbox ./cmd/main.go
```

Or with Docker:

```sh
docker build -t ty-sandbox .
docker run -p 8080:8080 -e ANTHROPIC_API_KEY=sk-... ty-sandbox
```

## Usage

Start the server:

```sh
ty-sandbox --addr :8080
```

With authentication:

```sh
ty-sandbox --addr :8080 --auth-token your-secret
```

## API

### Health

```
GET /v1/health
```

### Agents

List agents:

```
GET /v1/agents
```

Install an agent:

```
POST /v1/agents/{agent}/install
```

### Sessions

Create a session:

```
POST /v1/sessions/{session_id}

{
"agent": "claude-code",
"prompt": "Fix the login bug",
"work_dir": "/workspace"
}
```

List sessions:

```
GET /v1/sessions
```

Send a message:

```
POST /v1/sessions/{session_id}/messages

{"message": "Also update the tests"}
```

Get events (polling):

```
GET /v1/sessions/{session_id}/events
GET /v1/sessions/{session_id}/events?after_sequence=5
```

Stream events (SSE):

```
GET /v1/sessions/{session_id}/events/sse
```

Terminate a session:

```
POST /v1/sessions/{session_id}/terminate
```

### Human-in-the-Loop

Reply to a question:

```
POST /v1/sessions/{session_id}/questions/{question_id}/reply

{"answer": "Yes, proceed"}
```

Reject a question:

```
POST /v1/sessions/{session_id}/questions/{question_id}/reject
```

Reply to a permission request:

```
POST /v1/sessions/{session_id}/permissions/{permission_id}/reply

{"allow": true}
```

## Event Schema

Events follow the universal schema from sandbox-agent. Each event has:

- `event_id` - Unique identifier
- `session_id` - Session this event belongs to
- `type` - Event type (see below)
- `source` - `"agent"` or `"daemon"`
- `sequence` - Monotonically increasing counter
- `time` - ISO 8601 timestamp
- `data` - Type-specific payload

Event types:

| Type | Description |
|------|-------------|
| `session.started` | Session initialized |
| `session.ended` | Session terminated |
| `turn.started` | Conversation turn began |
| `turn.ended` | Conversation turn ended |
| `item.started` | Message or tool call began |
| `item.delta` | Streaming content update |
| `item.completed` | Message or tool call finished |
| `question.requested` | Agent needs user input |
| `question.resolved` | Question was answered |
| `permission.requested` | Tool execution needs approval |
| `permission.resolved` | Permission was granted/denied |
| `error` | Error occurred |

## Supported Agents

| Agent | ID | Status |
|-------|----|--------|
| Claude Code | `claude-code` | Supported |
| Mock | `mock` | For testing |

## TaskYou Integration

When the `ty` CLI is available, sessions can create/update TaskYou tasks. Set `--ty-path` or have `ty` on your PATH.

## Environment Variables

| Variable | Description |
|----------|-------------|
| `SANDBOX_AUTH_TOKEN` | Bearer token for API auth |
| `SANDBOX_WORK_DIR` | Default working directory |
| `ANTHROPIC_API_KEY` | API key for Claude Code |
94 changes: 94 additions & 0 deletions extensions/ty-sandbox/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// ty-sandbox is an HTTP/SSE server that enables remote control of coding agents
// (Claude Code, Codex, etc.) running in isolated sandbox environments.
//
// It exposes a unified REST + SSE API modeled after rivet-dev/sandbox-agent,
// and integrates with TaskYou for task tracking.
//
// Usage:
//
// ty-sandbox [flags]
// ty-sandbox --addr :8080 --auth-token secret123
// ty-sandbox --work-dir /workspace
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"

"github.com/bborn/workflow/extensions/ty-sandbox/internal/agent"
"github.com/bborn/workflow/extensions/ty-sandbox/internal/bridge"
"github.com/bborn/workflow/extensions/ty-sandbox/internal/server"
"github.com/bborn/workflow/extensions/ty-sandbox/internal/session"
)

var (
version = "0.1.0"
)

func main() {
addr := flag.String("addr", ":8080", "listen address")
authToken := flag.String("auth-token", "", "bearer token for API authentication (or SANDBOX_AUTH_TOKEN env)")
workDir := flag.String("work-dir", "", "default working directory for agent sessions")
tyPath := flag.String("ty-path", "", "path to ty CLI for TaskYou integration")
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()

if *showVersion {
fmt.Printf("ty-sandbox %s\n", version)
os.Exit(0)
}

// Auth token from env if not set via flag
if *authToken == "" {
*authToken = os.Getenv("SANDBOX_AUTH_TOKEN")
}

// Work dir from env if not set
if *workDir == "" {
*workDir = os.Getenv("SANDBOX_WORK_DIR")
}

log.Printf("ty-sandbox %s starting", version)

// Initialize components
registry := agent.NewRegistry()
mgr := session.NewManager(registry)
br := bridge.New(*tyPath)

if br.IsAvailable() {
log.Printf("TaskYou bridge: available")
} else {
log.Printf("TaskYou bridge: not available (ty CLI not found)")
}

cfg := server.Config{
Addr: *addr,
AuthToken: *authToken,
}

srv := server.New(cfg, registry, mgr)

// Graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

_ = ctx

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
log.Printf("shutting down...")
cancel()
os.Exit(0)
}()

if err := srv.Start(); err != nil {
log.Fatalf("server error: %v", err)
}
}
15 changes: 15 additions & 0 deletions extensions/ty-sandbox/config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ty-sandbox configuration
#
# All values can also be set via flags or environment variables.

# Listen address
addr: ":8080"

# Bearer token for API authentication (env: SANDBOX_AUTH_TOKEN)
# auth_token: "your-secret-token"

# Default working directory for agent sessions (env: SANDBOX_WORK_DIR)
# work_dir: "/workspace"

# Path to ty CLI for TaskYou integration (auto-detected if on PATH)
# ty_path: "/usr/local/bin/ty"
5 changes: 5 additions & 0 deletions extensions/ty-sandbox/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/bborn/workflow/extensions/ty-sandbox

go 1.24.4

require github.com/google/uuid v1.6.0
2 changes: 2 additions & 0 deletions extensions/ty-sandbox/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Loading