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
94 changes: 94 additions & 0 deletions .github/workflows/images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Images

on:
push:
branches: [main]
paths:
- 'sandbox/**'
- 'profiles/**'
- 'providers.toml'
tags: ["v*"]
pull_request:
paths:
- 'sandbox/**'
- 'profiles/**'
- 'providers.toml'

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
IMAGE_BASE: ghcr.io/${{ github.repository }}

jobs:
sandbox:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE_BASE }}
tags: |
type=raw,value=sandbox,enable={{is_default_branch}}
type=semver,pattern=sandbox-v{{version}}
type=ref,event=pr,prefix=sandbox-pr-
- uses: docker/build-push-action@v6
with:
context: sandbox
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: |
type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache
type=registry,ref=${{ env.IMAGE_BASE }}:sandbox
cache-to: type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache,mode=max

launcher:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build launcher binary
run: cd sandbox/launcher && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o launcher .
- name: Install openshell CLI
run: |
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
cp "$(which openshell)" sandbox/launcher/openshell
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE_BASE }}
tags: |
type=raw,value=launcher,enable={{is_default_branch}}
type=semver,pattern=launcher-v{{version}}
type=ref,event=pr,prefix=launcher-pr-
- uses: docker/build-push-action@v6
with:
context: sandbox/launcher
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: |
type=registry,ref=${{ env.IMAGE_BASE }}:launcher-cache
type=registry,ref=${{ env.IMAGE_BASE }}:launcher
cache-to: type=registry,ref=${{ env.IMAGE_BASE }}:launcher-cache,mode=max
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
# TODO: add `make embed-sync` step when embed package is implemented
- uses: goreleaser/goreleaser-action@v6
with:
args: release --clean
Expand Down
14 changes: 14 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 2

builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64

archives:
- format: tar.gz
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## make test-podman # build + test podman only
## make test-ocp # build + test OCP only

REGISTRY ?= quay.io/rcochran/openshell
REGISTRY ?= ghcr.io/robbycochran/harness-openshell
PLATFORM := linux/amd64

SANDBOX_IMAGE := $(REGISTRY):sandbox
Expand Down
2 changes: 1 addition & 1 deletion cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R

// Step 4: Helm install
status.Step(4, "Deploying gateway via Helm")
sandboxImage := envOr("SANDBOX_IMAGE", "quay.io/rcochran/openshell:sandbox")
sandboxImage := envOr("SANDBOX_IMAGE", "ghcr.io/robbycochran/harness-openshell:sandbox")

appsDomain, err := clusterRunner.GetJSONPath(ctx, "ingresses.config.openshift.io/cluster", "{.spec.domain}")
if err != nil || appsDomain == "" {
Expand Down
2 changes: 1 addition & 1 deletion cmd/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func setupTestProfile(t *testing.T) string {
os.MkdirAll(filepath.Join(dir, "profiles"), 0o755)
os.WriteFile(filepath.Join(dir, "profiles", "default.toml"), []byte(`
name = "test-agent"
image = "quay.io/test:latest"
from = "quay.io/test:latest"
command = "claude --bare"
providers = ["github", "vertex-local", "atlassian"]

Expand Down
27 changes: 24 additions & 3 deletions cmd/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ func newRemote(harnessDir string, gw gateway.Gateway, profileName, sandboxName s
cfg.Name = sandboxName
}

// Remote can't build from a local Dockerfile — override with registry image
if cfg.From != "" {
fromPath := cfg.From
if !filepath.IsAbs(fromPath) {
fromPath = filepath.Join(harnessDir, fromPath)
}
if info, err := os.Stat(fromPath); err == nil && info.IsDir() {
sandboxImage := envOr("SANDBOX_IMAGE", "ghcr.io/robbycochran/harness-openshell:sandbox")
status.Infof("Remote: using image %s (profile 'from' is a local path)", sandboxImage)
cfg.From = sandboxImage
}
}

profilePath := filepath.Join(harnessDir, "profiles", profileName+".toml")

// 1. ConfigMap from profile
Expand Down Expand Up @@ -161,7 +174,7 @@ func newRemote(harnessDir string, gw gateway.Gateway, profileName, sandboxName s
"restartPolicy": "Never",
"containers": []map[string]any{{
"name": "launcher",
"image": "quay.io/rcochran/openshell:launcher",
"image": "ghcr.io/robbycochran/harness-openshell:launcher",
"imagePullPolicy": "Always",
"env": []map[string]any{
{"name": "GATEWAY_ENDPOINT", "value": "https://openshell.openshell.svc.cluster.local:8080"},
Expand Down Expand Up @@ -262,10 +275,18 @@ func newLocal(opts newLocalOpts) error {
cfg.Name = opts.sandboxName
}

// Resolve Dockerfile path relative to harnessDir
if cfg.From != "" && !filepath.IsAbs(cfg.From) {
candidate := filepath.Join(opts.harnessDir, cfg.From)
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
cfg.From = candidate
}
}

fmt.Println()
fmt.Println("=== Sandbox ===")
fmt.Printf(" Profile: %s\n", opts.profileName)
fmt.Printf(" Image: %s\n", cfg.Image)
fmt.Printf(" From: %s\n", cfg.From)

// 4. Validate providers against profile
status.Section("Providers")
Expand Down Expand Up @@ -323,7 +344,7 @@ func newLocal(opts newLocalOpts) error {
for attempt := 1; attempt <= 5; attempt++ {
err := gw.SandboxCreate(gateway.SandboxCreateOpts{
Name: cfg.Name,
Image: cfg.Image,
From: cfg.From,
Providers: registered,
TTY: !opts.noTTY,
Keep: cfg.KeepSandbox(),
Expand Down
4 changes: 2 additions & 2 deletions cmd/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ func TestNewLocal_SandboxCreateOpts(t *testing.T) {
if opts.Name != "custom-name" {
t.Errorf("Name = %q, want custom-name", opts.Name)
}
if opts.Image != "quay.io/test:latest" {
t.Errorf("Image = %q", opts.Image)
if opts.From != "quay.io/test:latest" {
t.Errorf("From = %q", opts.From)
}
if opts.TTY {
t.Error("TTY = true, want false (noTTY)")
Expand Down
4 changes: 2 additions & 2 deletions internal/gateway/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ func (c *CLI) SandboxCreate(opts SandboxCreateOpts) error {
} else {
args = append(args, "--no-tty")
}
if opts.Image != "" {
args = append(args, "--from", opts.Image)
if opts.From != "" {
args = append(args, "--from", opts.From)
}
for _, p := range opts.Providers {
args = append(args, "--provider", p)
Expand Down
2 changes: 1 addition & 1 deletion internal/gateway/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ echo "$@" > `+argsFile+`
gw := New(bin)
gw.SandboxCreate(SandboxCreateOpts{
Name: "my-agent",
Image: "quay.io/test:latest",
From: "quay.io/test:latest",
Providers: []string{"github", "vertex-local"},
TTY: true,
Keep: false,
Expand Down
2 changes: 1 addition & 1 deletion internal/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type GatewayInfo struct {

type SandboxCreateOpts struct {
Name string
Image string
From string
Providers []string
TTY bool
Keep bool
Expand Down
2 changes: 1 addition & 1 deletion internal/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type ProviderChecker interface {

type Config struct {
Name string `toml:"name"`
Image string `toml:"image"`
From string `toml:"from"`
Command string `toml:"command"`
Keep *bool `toml:"keep"`
Providers []string `toml:"providers"`
Expand Down
6 changes: 3 additions & 3 deletions internal/profile/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestParseFile_Full(t *testing.T) {
path := filepath.Join(dir, "config.toml")
os.WriteFile(path, []byte(`
name = "research"
image = "quay.io/test/sandbox:latest"
from = "quay.io/test/sandbox:latest"
command = "claude --bare --model opus"
keep = false
providers = ["github", "vertex-local"]
Expand All @@ -42,8 +42,8 @@ JIRA_URL = "https://example.atlassian.net"
if cfg.Name != "research" {
t.Errorf("Name = %q, want %q", cfg.Name, "research")
}
if cfg.Image != "quay.io/test/sandbox:latest" {
t.Errorf("Image = %q", cfg.Image)
if cfg.From != "quay.io/test/sandbox:latest" {
t.Errorf("From = %q", cfg.From)
}
if cfg.Command != "claude --bare --model opus" {
t.Errorf("Command = %q", cfg.Command)
Expand Down
2 changes: 1 addition & 1 deletion profiles/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# harness create --profile research # uses profiles/research.toml

name = "agent"
image = "quay.io/rcochran/openshell:sandbox"
from = "quay.io/rcochran/openshell:sandbox"
command = "claude --bare"
keep = true

Expand Down
8 changes: 4 additions & 4 deletions sandbox/launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

type Config struct {
Name string `toml:"name"`
Image string `toml:"image"`
From string `toml:"from"`
Command string `toml:"command"`
Keep *bool `toml:"keep"`
Providers []string `toml:"providers"`
Expand Down Expand Up @@ -160,8 +160,8 @@ func createSandbox(cfg *Config, providers []string, cli string) error {
fmt.Println("\n=== Creating sandbox ===")
for attempt := 1; attempt <= 5; attempt++ {
args := []string{"sandbox", "create", "--name", cfg.Name, "--no-tty"}
if cfg.Image != "" {
args = append(args, "--from", cfg.Image)
if cfg.From != "" {
args = append(args, "--from", cfg.From)
}
for _, p := range providers {
args = append(args, "--provider", p)
Expand Down Expand Up @@ -261,7 +261,7 @@ func main() {

fmt.Println("=== Sandbox Launcher ===")
fmt.Printf(" Name: %s\n", cfg.Name)
fmt.Printf(" Image: %s\n", cfg.Image)
fmt.Printf(" From: %s\n", cfg.From)
fmt.Printf(" Providers: %s\n", strings.Join(cfg.Providers, " "))
fmt.Printf(" Command: %s\n", cfg.Command)
fmt.Printf(" Gateway: %s\n", endpoint)
Expand Down
8 changes: 4 additions & 4 deletions sandbox/launcher/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestParseConfig_Full(t *testing.T) {
path := filepath.Join(dir, "config.toml")
os.WriteFile(path, []byte(`
name = "research"
image = "quay.io/test/sandbox:latest"
from = "quay.io/test/sandbox:latest"
command = "claude --bare --model opus"
keep = false
providers = ["github", "vertex-local"]
Expand All @@ -28,8 +28,8 @@ JIRA_URL = "https://example.atlassian.net"
if cfg.Name != "research" {
t.Errorf("Name = %q, want %q", cfg.Name, "research")
}
if cfg.Image != "quay.io/test/sandbox:latest" {
t.Errorf("Image = %q, want %q", cfg.Image, "quay.io/test/sandbox:latest")
if cfg.From != "quay.io/test/sandbox:latest" {
t.Errorf("From = %q, want %q", cfg.From, "quay.io/test/sandbox:latest")
}
if cfg.Command != "claude --bare --model opus" {
t.Errorf("Command = %q, want %q", cfg.Command, "claude --bare --model opus")
Expand All @@ -55,7 +55,7 @@ func TestParseConfig_Defaults(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.toml")
os.WriteFile(path, []byte(`
image = "quay.io/test/sandbox:latest"
from = "quay.io/test/sandbox:latest"
`), 0o644)

cfg, err := parseConfig(path)
Expand Down
Loading