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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ plans/
openshell-arch/
strategy-dashboard.html
dashboard.html
mlflow.db
plan_file_ga.md
359 changes: 99 additions & 260 deletions README.md

Large diffs are not rendered by default.

72 changes: 49 additions & 23 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,43 +51,69 @@ Fields:

Provider profiles live in `profiles/providers/`. These are imported to the gateway during provider registration.

### Multi-document harness YAML

Agent configs support multi-document YAML (`---` separated) where provider, gateway, and policy definitions are co-located in one file:

```yaml
---
kind: agent
name: my-agent
entrypoint: claude
providers:
- profile: github
---
kind: provider
name: github
type: github
credentials: [GITHUB_TOKEN]
---
kind: gateway
name: local
type: local
```

Documents are dispatched by `kind` field. No `kind` field = agent (backwards compatible). Definitions in the harness file take priority over the `profiles/` tree.

## CLI

### `harness up [--gateway NAME] [--gateway-profile FILE] [--agent NAME] [--agent-profile|-f FILE] [--name SANDBOX] [--no-tty] [--provider-refresh]`
### `harness apply [-f FILE] [--agent NAME] [--gateway NAME] [--gateway-profile FILE] [--name SANDBOX] [--attach] [--provider-refresh] [--dry-run] [-o yaml|json]`

Full flow: deploy gateway, register providers, render agent config, create sandbox.
Primary command. Resolves an agent config, deploys the gateway and providers, creates a sandbox.

1. **Parse agent config** -- resolve `agent-<name>.yaml` from harness directory (default: `default`). `--agent-profile` (`-f`) overrides with a direct file path. Falls back to embedded `agent-basic.yaml` when `agent-default.yaml` is not found on disk.
2. **Check version** -- warn if openshell CLI is below v0.0.59.
3. **Ensure gateway** -- deploy if needed (local: Podman, remote: Helm to K8s/OCP). `--gateway` selects a profile by name; `--gateway-profile` loads from a file path. Gateway target can come from agent config's `gateway` field.
4. **Ensure providers** -- auto-register missing providers. Three registration flows:
1. **Parse agent config** -- resolve `agent-<name>.yaml` from harness directory (default: `default`). `-f` overrides with a direct file path. Falls back to embedded `agent-basic.yaml` when `agent-default.yaml` is not found on disk.
2. **Check output mode** -- if `-o yaml` or `-o json`, render the fully resolved config and exit. No gateway interaction needed.
3. **Check version** -- warn if openshell CLI is below v0.0.59.
4. **Resolve gateway** -- `--gateway` selects a profile by name; `--gateway-profile` loads from a file path. Default: `local`. `OPENSHELL_GATEWAY` env var is used as fallback.
5. **Dry-run check** -- if `--dry-run`, validate each step (gateway reachable, providers resolvable, env vars resolved, image available) and exit with pass/fail report.
6. **Ensure gateway** -- deploy if needed (local: Podman, remote: Helm to K8s/OCP).
7. **Ensure providers** -- auto-register missing providers. Three registration flows:
- **Standard** (`--from-existing`): GitHub, Atlassian -- OpenShell discovers credentials from local env.
- **ADC** (`--from-gcloud-adc`): Vertex AI -- reads ADC file, configures inference routing.
- **Custom**: GWS -- multi-step OAuth refresh flow (harness workaround until OpenShell adds native support).
5. **Render payload** -- `run.sh` (entrypoint wrapper with PATH setup, entrypoint validation, `-p` task), `task.md` (if set).
6. **Create sandbox** -- `openshell sandbox create` with `--env` (env vars), `--upload` (payload), and startup command. Retry up to 5 times.

`--provider-refresh` deletes and recreates all providers (replaces the old `harness providers --force`).
- **Custom**: GWS -- multi-step OAuth refresh flow.
8. **Render payload** -- `run.sh` (entrypoint wrapper with PATH setup, entrypoint validation, `-p` task), `task.md` (if set).
9. **Create sandbox** -- `openshell sandbox create` with `--env` (env vars), `--upload` (payload), and startup command. Retry up to 5 times.

### `harness create [--agent NAME] [--agent-profile|-f FILE] [--name SANDBOX]`
Default is non-interactive (headless). Use `--attach` for TTY mode.

Create a sandbox without deploying the gateway. Always non-interactive (no TTY). Assumes gateway is running. Auto-registers missing providers.
`--provider-refresh` deletes and recreates all providers.

### `harness deploy [local|ocp|kind]`

Deploy or verify the gateway for a target. Reads `profiles/gateways/<target>.yaml`.

### `harness status`

Show sandbox status. Read-only.

### `harness stop [NAME]` / `harness start [NAME]`

Stop or start a sandbox without deleting it.

### `harness teardown [--sandboxes] [--providers] [--k8s]`
### Deprecated Aliases

These commands still work but will be removed in a future release:

Tear down resources. At least one flag required.
| Old command | Replacement | Notes |
|-------------|-------------|-------|
| `harness teardown` | `harness delete` (planned) | Flags: `--sandboxes`, `--providers`, `--k8s` |
| `harness status` | `harness get agents` (planned) | |

## Config Files

Expand Down Expand Up @@ -115,9 +141,9 @@ All images are published to `ghcr.io/robbycochran/harness-openshell`. CI never p

The CLI resolves images from its embedded version (set via `-ldflags` at build time):

- `v0.1.2` `:sandbox-v0.1.2` (tagged release)
- `v0.1.2-5-gabc1234` `:sandbox-v0.1.2-5-gabc1234` (dev build, matches `make dev-sandbox`)
- `dev` `:sandbox` (bare `go build` without ldflags)
- `v0.1.2` -> `:sandbox-v0.1.2` (tagged release)
- `v0.1.2-5-gabc1234` -> `:sandbox-v0.1.2-5-gabc1234` (dev build, matches `make dev-sandbox`)
- `dev` -> `:sandbox` (bare `go build` without ldflags)

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

Expand All @@ -129,10 +155,10 @@ Harness-specific variables use the `HARNESS_OS_` prefix. OpenShell runtime varia
|----------|---------|
| `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_GATEWAY` | Override gateway name (used by apply, plugin-compatible) |
| `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`) |
Expand Down
111 changes: 0 additions & 111 deletions cmd/create.go

This file was deleted.

38 changes: 0 additions & 38 deletions cmd/create_test.go

This file was deleted.

105 changes: 105 additions & 0 deletions cmd/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package cmd

import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/robbycochran/harness-openshell/internal/agent"
"github.com/robbycochran/harness-openshell/internal/gateway"
"github.com/robbycochran/harness-openshell/internal/k8s"
"github.com/robbycochran/harness-openshell/internal/status"
)

var Version = "dev"

var DefaultAgentConfig []byte

type upLocalOpts struct {
harnessDir string
gw gateway.Gateway
gwCfg *gateway.GatewayConfig
ensureLocal bool
agentCfg *agent.AgentConfig
agentPath string
sandboxName string
noTTY bool
providerRefresh bool
harness *agent.Harness
retrySleep time.Duration
}

func upLocal(opts upLocalOpts) error {
gw := opts.gw

agentCfg := opts.agentCfg
if agentCfg == nil {
var err error
agentCfg, err = agent.ParseFile(opts.agentPath)
if err != nil {
return err
}
}
sandboxName := agentCfg.Name
if opts.sandboxName != "" {
sandboxName = opts.sandboxName
}
noTTY := opts.noTTY || agentCfg.NoTTY()

sandboxImage := resolveSandboxImage(agentCfg.Image)

status.Infof("Agent: %s (%s)", sandboxName, filepath.Base(opts.agentPath))
status.Infof("Image: %s", sandboxImage)
if agentCfg.Task != "" {
status.Infof("Task: %s", agentCfg.Task)
}

if opts.ensureLocal {
if err := deployLocal(gw); err != nil {
return fmt.Errorf("deploy failed: %w", err)
}
} else if gw.InferenceGet() != nil {
if opts.gwCfg == nil {
return fmt.Errorf("no active gateway -- use --gateway local or: harness deploy ocp")
}
kc := k8s.New("", k8s.DefaultNamespace())
clusterRunner := k8s.New("", "")
if err := deployFromConfig(opts.harnessDir, opts.gwCfg, gw, kc, clusterRunner); err != nil {
return fmt.Errorf("deploy failed: %w", err)
}
}

registered := ensureProviders(opts.harnessDir, gw, agentCfg, opts.providerRefresh, opts.harness)

payloadDir, err := os.MkdirTemp("", "harness-payload-")
if err != nil {
return fmt.Errorf("creating payload dir: %w", err)
}
defer os.RemoveAll(payloadDir)

if err := agent.RenderPayload(agentCfg, opts.harnessDir, payloadDir); err != nil {
return fmt.Errorf("rendering payload: %w", err)
}

status.Header("Sandbox")
var sandboxCmd []string
if noTTY {
sandboxCmd = []string{"true"}
} else {
sandboxCmd = []string{"bash", "/sandbox/.config/openshell/run.sh"}
}

return createSandbox(sandboxOpts{
harnessDir: opts.harnessDir,
gw: gw,
name: sandboxName,
image: sandboxImage,
providers: registered,
noTTY: noTTY,
retrySleep: opts.retrySleep,
sandboxCmd: sandboxCmd,
payloadDir: payloadDir,
env: agentCfg.BuildEnvMap(),
})
}
Loading
Loading