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
7 changes: 6 additions & 1 deletion .github/workflows/images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- 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 }}
- name: Compute version tag
id: version
run: echo "version=$(git describe --tags --always 2>/dev/null || echo dev)" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE_BASE }}
tags: |
type=semver,pattern=sandbox-v{{version}}
type=sha,prefix=sandbox-,format=short
type=raw,value=sandbox-${{ steps.version.outputs.version }},enable=${{ github.ref_type != 'tag' }}
# Fork PRs can't write to the registry; same-repo PRs push so the
# integration (kind) job can pull the image without a local preload.
- uses: docker/build-push-action@v6
Expand Down
33 changes: 18 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
##
## Images (dev builds, tagged from git describe):
## make dev-sandbox # build sandbox image (native arch)
## make dev-runner # build runner image
## make dev-push # build + push both (sandbox multi-arch)
## make dev-push # build + push sandbox image (multi-arch)
## Release images are built and pushed by .github/workflows/images.yml.

REGISTRY ?= ghcr.io/robbycochran/harness-openshell
Expand All @@ -19,11 +18,11 @@ PLATFORM := linux/amd64
VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev)
LDFLAGS := -s -w -X main.version=$(VERSION)

DEV_SANDBOX_IMAGE := $(REGISTRY):sandbox-$(VERSION)
IMAGE := $(REGISTRY):sandbox-$(VERSION)

.PHONY: all cli \
vet lint test test-local test-kind test-remote test-all \
dev-sandbox dev-push clean help
dev-sandbox dev-push tag clean help

## ── CLI ──────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -66,15 +65,15 @@ test-local: cli
## Builds sandbox image locally and pre-loads into kind (no registry push needed).
## Use KEEP=1 to keep the cluster after tests (for debugging).
test-kind: cli
$(CONTAINER_CLI) build -t $(DEV_SANDBOX_IMAGE) sandbox/
$(CONTAINER_CLI) build -t $(IMAGE) sandbox/
@echo ""
SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) CONTAINER_CLI=$(CONTAINER_CLI) ./test/kind-lifecycle.sh $(if $(KEEP),--keep)
HARNESS_OS_IMAGE=$(IMAGE) CONTAINER_CLI=$(CONTAINER_CLI) ./test/kind-lifecycle.sh $(if $(KEEP),--keep)

## Remote (OCP): requires KUBECONFIG set
test-remote: cli dev-sandbox
@test -n "$${KUBECONFIG}" || { echo "ERROR: Set KUBECONFIG for OCP (e.g. export KUBECONFIG=infracluster/kubeconfig)"; exit 1; }
@echo ""
SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) ./test/test-flow.sh ocp
HARNESS_OS_IMAGE=$(IMAGE) ./test/test-flow.sh ocp

## All: unit + local + kind + remote
test-all: test test-local test-kind test-remote
Expand All @@ -83,20 +82,24 @@ test-all: test test-local test-kind test-remote

## Build dev sandbox image locally (native arch only)
dev-sandbox:
$(CONTAINER_CLI) build -t $(DEV_SANDBOX_IMAGE) sandbox/
@echo "Built: $(DEV_SANDBOX_IMAGE)"
$(CONTAINER_CLI) build -t $(IMAGE) sandbox/
@echo "Built: $(IMAGE)"

## Build and push dev sandbox image (multi-arch)
dev-push:
@$(CONTAINER_CLI) rmi --force $(DEV_SANDBOX_IMAGE) 2>/dev/null || true
@$(CONTAINER_CLI) manifest rm $(DEV_SANDBOX_IMAGE) 2>/dev/null || true
$(CONTAINER_CLI) build --platform linux/amd64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/
$(CONTAINER_CLI) build --platform linux/arm64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/
$(CONTAINER_CLI) manifest push $(DEV_SANDBOX_IMAGE)
@echo "Pushed: $(DEV_SANDBOX_IMAGE) (multi-arch)"
@$(CONTAINER_CLI) rmi --force $(IMAGE) 2>/dev/null || true
@$(CONTAINER_CLI) manifest rm $(IMAGE) 2>/dev/null || true
$(CONTAINER_CLI) build --platform linux/amd64 --manifest $(IMAGE) sandbox/
$(CONTAINER_CLI) build --platform linux/arm64 --manifest $(IMAGE) sandbox/
$(CONTAINER_CLI) manifest push $(IMAGE)
@echo "Pushed: $(IMAGE) (multi-arch)"

## ── Convenience targets ───────────────────────────────────────────────

## Show the current version tag (from git describe)
tag:
@echo $(VERSION)

## Clean built binaries
clean:
rm -f harness
Expand Down
18 changes: 10 additions & 8 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ env:

Fields:
- `name` (required) -- sandbox name, used for `openshell sandbox connect`
- `image` -- container image for the sandbox (default: version-matched from ghcr.io, override with `SANDBOX_IMAGE` env)
- `image` -- container image for the sandbox (default: version-matched from ghcr.io, override with `HARNESS_OS_IMAGE` env)
- `entrypoint` -- command to run (default: `claude`). Supports `claude`, `opencode`, `bash`, or any binary on PATH.
- `tty` -- enable TTY (default: false)
- `task` -- path to a task.md file, passed to entrypoint via `-p "$(cat task.md)"`
Expand Down Expand Up @@ -115,23 +115,25 @@ The CLI resolves images from its embedded version (set via `-ldflags` at build t
- `v0.1.2-5-gabc1234` → `:sandbox-v0.1.2-5-gabc1234` (dev build, matches `make dev-sandbox`)
- `dev` → `:sandbox` (bare `go build` without ldflags)

`SANDBOX_IMAGE` env var overrides the version-based resolution.
`HARNESS_OS_IMAGE` env var overrides the version-based resolution.

## Environment Variables

Harness-specific variables use the `HARNESS_OS_` prefix. OpenShell runtime variables use `OPENSHELL_`.

| Variable | Purpose |
|----------|---------|
| `SANDBOX_IMAGE` | Override sandbox image (dev/CI builds) |
| `HARNESS_DIR` | Override harness directory detection |
| `OPENSHELL_NAMESPACE` | Override K8s namespace (default: `openshell`) |
| `HARNESS_OS_DIR` | Override harness directory detection |
| `HARNESS_OS_IMAGE` | Override sandbox image (dev/CI builds) |
| `HARNESS_OS_GATEWAY` | Override gateway name in gateway config |
| `HARNESS_OS_PULL_SECRET` | Image pull secret name passed to Helm install |
| `HARNESS_OS_SANDBOX_PULL_SECRET` | Sandbox image pull secret name passed to Helm install |
| `OPENSHELL_CLI` | Override openshell binary path |
| `OPENSHELL_NAMESPACE` | Override K8s namespace (default: `openshell`) |
| `OPENSHELL_MODEL` | Inference model for provider registration (default: `claude-sonnet-4-6`) |
| `OPENSHELL_CHART_VERSION` | Override Helm chart version (beats `gateway.yaml`) |
| `PULL_SECRET` / `SANDBOX_PULL_SECRET` | Image pull secret names passed to the Helm install |
| `KUBECONFIG` | K8s cluster config for remote targets |

`GATEWAY_NAME` is internal -- used by env override in gateway config, not typically set by users.

## Payload

The harness renders agent config into a self-contained payload uploaded to `/sandbox/.config/openshell/`:
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- Prerequisite: proto files stabilize (OpenShell is alpha)

### Image registry as gateway config vs env override
- `SANDBOX_IMAGE` env var overrides the version-based image resolution (for dev/CI)
- `HARNESS_OS_IMAGE` env var overrides the version-based image resolution (for dev/CI)
- Consider: gateway.yaml uses a `registry` field and images are relative to it

### registerProviders should filter by agent's provider list
Expand Down
6 changes: 3 additions & 3 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,16 @@ func deployFromConfig(harnessDir string, gwCfg *gateway.GatewayConfig, gw gatewa
} else if valuesPath != "" {
helmArgs = append(helmArgs, "--values", valuesPath)
}
if sandboxImage := os.Getenv("SANDBOX_IMAGE"); sandboxImage != "" {
if sandboxImage := os.Getenv("HARNESS_OS_IMAGE"); sandboxImage != "" {
helmArgs = append(helmArgs, "--set", "server.sandboxImage="+sandboxImage)
}
if routeHost != "" {
helmArgs = append(helmArgs, "--set", "pkiInitJob.serverDnsNames[0]="+routeHost)
}
if ps := os.Getenv("PULL_SECRET"); ps != "" {
if ps := os.Getenv("HARNESS_OS_PULL_SECRET"); ps != "" {
helmArgs = append(helmArgs, "--set", "imagePullSecrets[0].name="+ps)
}
if sps := os.Getenv("SANDBOX_PULL_SECRET"); sps != "" {
if sps := os.Getenv("HARNESS_OS_SANDBOX_PULL_SECRET"); sps != "" {
helmArgs = append(helmArgs, "--set", "server.sandboxImagePullSecrets[0].name="+sps)
}
if err := kc.RunHelm(ctx, helmArgs...); err != nil {
Expand Down
4 changes: 1 addition & 3 deletions cmd/sandbox_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package cmd

import "os"

// resolveSandboxImage returns the final sandbox image path following
// the precedence: SANDBOX_IMAGE env var > agentImage > version-derived default.
func resolveSandboxImage(agentImage string) string {
if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" {
if envImage := os.Getenv("HARNESS_OS_IMAGE"); envImage != "" {
return envImage
}
if agentImage != "" {
Expand Down
2 changes: 1 addition & 1 deletion cmd/up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestActiveGatewayInfo_LocalGateway(t *testing.T) {
}

func TestUpLocal_SandboxCreateOpts(t *testing.T) {
t.Setenv("SANDBOX_IMAGE", "")
t.Setenv("HARNESS_OS_IMAGE", "")
dir := setupTestAgent(t)
gw := &mockGW{
providerList: []string{"github", "vertex-local"},
Expand Down
2 changes: 1 addition & 1 deletion dev-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ else
make -C "$REPO_ROOT" cli dev-sandbox
fi

export SANDBOX_IMAGE="${REGISTRY}:sandbox-${VERSION}"
export HARNESS_OS_IMAGE="${REGISTRY}:sandbox-${VERSION}"
exec "$REPO_ROOT/harness" "$@"
2 changes: 1 addition & 1 deletion internal/gateway/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (c *GatewayConfig) applyDefaults() {
}

func (c *GatewayConfig) applyEnvOverrides() {
if v := os.Getenv("GATEWAY_NAME"); v != "" {
if v := os.Getenv("HARNESS_OS_GATEWAY"); v != "" {
c.Gateway.Name = v
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/gateway/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,15 @@ gateway:
name: original-name
`)

t.Setenv("GATEWAY_NAME", "env-gw-name")
t.Setenv("HARNESS_OS_GATEWAY", "env-gw-name")

cfg, err := LoadConfig(dir)
if err != nil {
t.Fatal(err)
}

if cfg.Gateway.Name != "env-gw-name" {
t.Errorf("GATEWAY_NAME override: got %q", cfg.Gateway.Name)
t.Errorf("HARNESS_OS_GATEWAY override: got %q", cfg.Gateway.Name)
}
}

Expand All @@ -187,7 +187,7 @@ gateway:
name: original-name
`)

t.Setenv("GATEWAY_NAME", "")
t.Setenv("HARNESS_OS_GATEWAY", "")

cfg, err := LoadConfig(dir)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func main() {
}

func detectHarnessDir() string {
if d := os.Getenv("HARNESS_DIR"); d != "" {
if d := os.Getenv("HARNESS_OS_DIR"); d != "" {
return d
}
var roots []string
Expand Down
18 changes: 7 additions & 11 deletions test/kind-lifecycle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,23 @@ kubectl create namespace openshell --dry-run=client -o yaml | kubectl apply -f -

# Pre-load dev sandbox image into kind.
# In CI (CI=true), images.yml has already pushed the image to the registry so
# kind can pull it directly — skip the 60-90s local preload that disrupts the
# local openshell service. Locally, preload from the container daemon so
# tests work without a registry push.
# Pre-load the sandbox image into kind from the local container daemon
# so tests work without a registry push.
CONTAINER_CLI=${CONTAINER_CLI:-podman}
if [[ -n "${SANDBOX_IMAGE:-}" ]] && [[ -z "${CI:-}" ]]; then
if "$CONTAINER_CLI" image inspect "$SANDBOX_IMAGE" &>/dev/null; then
echo " Pre-loading image: $SANDBOX_IMAGE"
if [[ -n "${HARNESS_OS_IMAGE:-}" ]]; then
if "$CONTAINER_CLI" image inspect "$HARNESS_OS_IMAGE" &>/dev/null; then
echo " Pre-loading image: $HARNESS_OS_IMAGE"
if [[ "$CONTAINER_CLI" == "docker" ]]; then
kind load docker-image "$SANDBOX_IMAGE" --name "$CLUSTER_NAME"
kind load docker-image "$HARNESS_OS_IMAGE" --name "$CLUSTER_NAME"
else
# kind only loads from the docker daemon directly; podman goes via archive
IMAGE_ARCHIVE=$(mktemp /tmp/kind-sandbox-image-XXXXXX.tar)
"$CONTAINER_CLI" save "$SANDBOX_IMAGE" -o "$IMAGE_ARCHIVE"
"$CONTAINER_CLI" save "$HARNESS_OS_IMAGE" -o "$IMAGE_ARCHIVE"
kind load image-archive "$IMAGE_ARCHIVE" --name "$CLUSTER_NAME"
rm -f "$IMAGE_ARCHIVE"
fi
else
echo " Image not found locally — kind will pull from registry at sandbox create time"
fi
elif [[ -n "${SANDBOX_IMAGE:-}" ]]; then
echo " CI mode — kind will pull image from registry at sandbox create time"
fi

# ── Run tests ───────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion test/test-flow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fi
LOG_FILE="${TEST_LOG_FILE:-}"
if [[ -n "$LOG_FILE" ]]; then
mkdir -p "$(dirname "$LOG_FILE")"
exec > >(tee -a "$LOG_FILE") 2>&1
exec > >(sed -u 's/\x1b\[[0-9;]*m//g' | tee -a "$LOG_FILE") 2>&1
fi

harness() {
Expand Down
Loading