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
30 changes: 17 additions & 13 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,22 @@ jobs:

- name: Run local integration
run: make test-local
env:
TEST_LOG_FILE: /tmp/test-logs/test-flow-local.log

- name: Export gateway logs
- name: Export logs
if: always()
run: |
mkdir -p /tmp/gateway-logs
journalctl --user -u openshell-gateway --no-pager > /tmp/gateway-logs/gateway.log 2>/dev/null || true
openshell status > /tmp/gateway-logs/status.txt 2>&1 || true
openshell gateway list > /tmp/gateway-logs/gateways.txt 2>&1 || true
mkdir -p /tmp/test-logs
journalctl --user -u openshell-gateway --no-pager > /tmp/test-logs/gateway.log 2>/dev/null || true
openshell status > /tmp/test-logs/status.txt 2>&1 || true
openshell gateway list > /tmp/test-logs/gateways.txt 2>&1 || true

- uses: actions/upload-artifact@v4
if: always()
with:
name: gateway-logs-local
path: /tmp/gateway-logs/
name: test-logs-local
path: /tmp/test-logs/

kind:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -75,17 +77,19 @@ jobs:

- name: Run kind integration
run: make test-kind
env:
TEST_LOG_FILE: /tmp/test-logs/test-flow-kind.log

- name: Export logs
if: always()
run: |
mkdir -p /tmp/kind-logs
kubectl -n openshell logs statefulset/openshell > /tmp/kind-logs/gateway.log 2>/dev/null || true
kubectl -n openshell get all > /tmp/kind-logs/resources.txt 2>&1 || true
openshell gateway list > /tmp/kind-logs/gateways.txt 2>&1 || true
mkdir -p /tmp/test-logs
kubectl -n openshell logs statefulset/openshell > /tmp/test-logs/gateway.log 2>/dev/null || true
kubectl -n openshell get all > /tmp/test-logs/resources.txt 2>&1 || true
openshell gateway list > /tmp/test-logs/gateways.txt 2>&1 || true

- uses: actions/upload-artifact@v4
if: always()
with:
name: gateway-logs-kind
path: /tmp/kind-logs/
name: test-logs-kind
path: /tmp/test-logs/
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ and replaces this sequence of `openshell` commands (captured from `harness up --
openshell settings set --global --key providers_v2_enabled --value true --yes

# 2. Import custom provider profiles (atlassian, gws)
openshell provider profile import --from agents/providers/profiles/
openshell provider profile import --from profiles/providers/

# 3. Register GitHub (discovers GITHUB_TOKEN from environment)
openshell provider create --name github --type github --from-existing
Expand Down Expand Up @@ -144,11 +144,11 @@ env:
```

```bash
harness up -f agents/ocp.yaml # deploys to OCP (reads gateways/ocp/gateway.yaml)
harness up -f agents/ocp.yaml # deploys to OCP (reads profiles/gateways/ocp.yaml)
harness up # defaults to local Podman
```

`--local` and `--remote` flags override the `gateway:` field.
`--gateway local` and `--gateway ocp` flags override the `gateway:` field.

For OCP, the harness first deploys the gateway via Helm, then runs the same provider registration and sandbox creation. The full sequence:

Expand All @@ -160,7 +160,7 @@ kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/.../m
oc adm policy add-scc-to-user privileged -z openshell -n openshell
oc adm policy add-scc-to-user anyuid -z openshell -n openshell
helm upgrade --install openshell oci://ghcr.io/nvidia/openshell/helm-chart \
--version 0.0.59 -n openshell -f gateways/ocp/helm/values.yaml
--version 0.0.59 -n openshell -f <inline-values-from-profile>
kubectl rollout status statefulset/openshell -n openshell --timeout=300s

# 2. Register gateway with CLI (mTLS certs extracted from K8s secrets)
Expand All @@ -170,7 +170,7 @@ openshell gateway select openshell-remote-ocp

# 3-9. Same as local: settings, profiles, providers, inference, sandbox create
openshell settings set --global --key providers_v2_enabled --value true
openshell provider profile import --from agents/providers/profiles/
openshell provider profile import --from profiles/providers/
openshell provider create --name github --type github --from-existing
openshell provider create --name vertex-local --type google-vertex-ai \
--from-gcloud-adc --config VERTEX_AI_PROJECT_ID=... --config VERTEX_AI_REGION=global
Expand Down Expand Up @@ -231,7 +231,7 @@ Each provider requires credentials on the host. The harness validates these inli
| `atlassian` | `JIRA_API_TOKEN` + `JIRA_URL` + `JIRA_USERNAME` env vars |
| `gws` | `gws auth login` (OAuth via [gws CLI](https://github.com/googleworkspace/cli)) |

Provider profiles are defined in `agents/providers/profiles/` and validated inline during registration.
Provider profiles are defined in `profiles/providers/` and validated inline during registration.

### Build from Source

Expand All @@ -240,14 +240,14 @@ make cli
./harness up
```

For remote OpenShift: `./harness up --remote` (requires `kubectl`, `helm`, cluster access).
For remote OpenShift: `./harness up --gateway ocp` (requires `kubectl`, `helm`, cluster access).

## How It Works

The harness orchestrates three OpenShell components via the `openshell` CLI:

- **Gateway** -- OpenShell's credential proxy and L7 network policy engine. Runs as a Podman container (local) or Kubernetes StatefulSet (remote). Manages provider credentials, inference routing, and sandbox lifecycle.
- **Providers** -- Credential registrations on the gateway. Provider profiles are declared in `agents/providers/profiles/`. The harness validates credentials inline during registration -- providers with missing credentials are skipped.
- **Providers** -- Credential registrations on the gateway. Provider profiles are declared in `profiles/providers/`. The harness validates credentials inline during registration -- providers with missing credentials are skipped.
- **Sandbox** -- Container running the agent entrypoint (Claude Code or OpenCode), configured by `agents/*.yaml`. The gateway injects credentials at the network boundary -- the sandbox process sees proxy-managed placeholder tokens. Network egress is deny-by-default at L7.

```
Expand All @@ -268,43 +268,44 @@ See the [OpenShell docs](https://github.com/NVIDIA/OpenShell) for the full secur
| File | Purpose |
|------|---------|
| `agents/*.yaml` | Agent config: image, entrypoint, providers, env, optional task file |
| `agents/providers/profiles/` | OpenShell provider profiles (imported to gateway on registration) |
| `gateways/*/gateway.yaml` | Deployment target config: `local/` (Podman), `kind/`, `ocp/` (OpenShift) |
| `profiles/providers/` | OpenShell provider profiles (imported to gateway on registration) |
| `profiles/gateways/*.yaml` | Gateway profiles: `local.yaml`, `kind.yaml`, `ocp.yaml` |
| `sandbox/Dockerfile` | Sandbox image: OpenShell base + MCP servers + CLI tools |
| `sandbox/policy.yaml` | Network egress rules applied to sandboxes |
| `sandbox/opencode.json` | MCP server config for OpenCode agent |

## Commands

```
harness up [--remote] [--agent NAME] [-f FILE] [--name SANDBOX] [--provider-refresh]
harness up [--gateway NAME] [--gateway-profile FILE] [--agent NAME] [--agent-profile|-f FILE] [--name SANDBOX] [--no-tty] [--provider-refresh]
Deploy gateway + register providers + create sandbox.
Defaults to local gateway (use --remote for OCP).
Defaults to local gateway (use --gateway ocp for OCP).
--gateway selects a gateway profile by name (local, kind, ocp).
--gateway-profile loads a gateway profile from a file path.
--agent defaults to "default" (embedded or agents/default.yaml).
-f renders any agent YAML file directly.
--agent-profile (-f) renders any agent YAML file directly.
--no-tty disables TTY allocation.
--provider-refresh deletes and recreates all providers.

harness create [--agent NAME] [-f FILE] [--name SANDBOX]
harness create [--agent NAME] [--agent-profile|-f FILE] [--name SANDBOX]
Create a sandbox without deploying the gateway.
Assumes gateway is running. Auto-registers missing providers.

harness connect [NAME]
Reconnect to a running sandbox.

harness deploy [local|ocp|kind]
Deploy or verify the gateway for a target.

harness status
Show sandbox status.

harness logs [NAME] [-f]
Stream sandbox logs (-f to follow).

harness stop [NAME] / harness start [NAME]
Stop or start a sandbox without deleting it.

harness teardown [--sandboxes] [--providers] [--k8s]
Tear down resources. At least one flag required.

For sandbox connect/logs, use openshell directly:
openshell sandbox connect [NAME]
openshell sandbox logs [NAME] [--follow]
```

## Documentation Map
Expand All @@ -316,4 +317,3 @@ harness teardown [--sandboxes] [--providers] [--k8s]
| [TODO.md](TODO.md) | Roadmap and known gaps |
| [docs/archive/](docs/archive/README.md) | Historical design docs (e.g. the June 2026 design-v1 proposal) -- outdated, kept for context |
| [docs/release-plan.md](docs/release-plan.md) | Release phases: CI (done), embed + `harness init`, GoReleaser |
| [docs/proto-migration.md](docs/proto-migration.md) | Deferred plan to adopt proto-generated config types |
25 changes: 9 additions & 16 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,19 @@ Fields:
- `env` -- additional environment variables injected via `--env` on sandbox create (empty values read from host env)
- `include` -- extra files to include in the payload
- `policy` -- path to a network policy YAML
- `gateway` -- target gateway name (overrides active gateway)

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

## CLI

### `harness up [--local|--remote] [--agent NAME] [-f FILE] [--name SANDBOX] [--no-tty] [--provider-refresh]`
### `harness up [--gateway NAME] [--gateway-profile FILE] [--agent NAME] [--agent-profile|-f FILE] [--name SANDBOX] [--no-tty] [--provider-refresh]`

Full flow: deploy gateway, register providers, render agent config, create sandbox.

1. **Check version** -- warn if openshell CLI is below v0.0.59.
2. **Ensure gateway** -- deploy if needed (local: Podman, remote: Helm to K8s/OCP).
3. **Parse agent config** -- read `agents/<name>.yaml` (default: `default`). `-f` overrides with a direct file path.
2. **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.
3. **Parse agent config** -- read `agents/<name>.yaml` (default: `default`). `--agent-profile` (`-f`) overrides with a direct file path.
4. **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.
Expand All @@ -68,26 +69,18 @@ Full flow: deploy gateway, register providers, render agent config, create sandb

`--provider-refresh` deletes and recreates all providers (replaces the old `harness providers --force`).

### `harness create [--agent NAME] [-f FILE] [--name SANDBOX]`
### `harness create [--agent NAME] [--agent-profile|-f FILE] [--name SANDBOX]`

Create a sandbox without deploying the gateway. Assumes gateway is running. Auto-registers missing providers.

### `harness connect [NAME]`

Reconnect to a running sandbox via `openshell sandbox connect`.

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

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

### `harness status`

Show sandbox status. Read-only.

### `harness logs [NAME] [-f|--follow]`

Stream logs for a sandbox.

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

Stop or start a sandbox without deleting it.
Expand All @@ -101,8 +94,8 @@ Tear down resources. At least one flag required.
| File | Purpose |
|------|---------|
| `agents/*.yaml` | Agent config: image, entrypoint, providers, env, task |
| `agents/providers/profiles/` | OpenShell provider profile YAMLs |
| `gateways/*/gateway.yaml` | Deployment target config with Helm, images, RBAC |
| `profiles/providers/` | OpenShell provider profile YAMLs |
| `profiles/gateways/*.yaml` | Gateway profiles: deployment target config with inline Helm values |
| `sandbox/Dockerfile` | Sandbox image: OpenShell base + MCP servers + CLI tools |
| `sandbox/policy.yaml` | Network egress rules applied to sandboxes |
| `sandbox/opencode.json` | MCP server config for OpenCode agent |
Expand Down
13 changes: 5 additions & 8 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

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

### registerProviders should filter by agent's provider list
- `registerProviders()` in `cmd/providers.go` uses the gateway config's provider
Expand All @@ -26,10 +26,8 @@

## Config Format

- [ ] Remove `providers.toml`; add provider profile validation in its place
- [ ] Convert gateway configs from TOML to YAML
- [ ] Specify/document the YAML formats (agent config, provider profiles)
- [ ] Document non-secret provider env vars (what `providers[].config` captures
- [ ] Document non-secret provider env vars (what `providers[].env` captures
and why it exists alongside secret credentials)

## CLI
Expand All @@ -47,12 +45,11 @@
## Testing

### Current coverage
- Go unit tests across cmd/ and all internal/ packages
- 29 bats preflight tests (run in CI via `.github/workflows/ci.yml`)
- Go unit tests across cmd/ and all internal/ packages (run in CI via `.github/workflows/ci.yml`)
- Integration: local + kind + OCP via `make test-all`

### Gaps
- [ ] Integration test for `providers --force`
- [ ] Integration test for `harness up --provider-refresh`

## Release

Expand All @@ -62,7 +59,7 @@

## Deferred (post-0.1)

- [ ] Gateway-level LLM proxy/logging (gateway.toml `[proxy]` section)
- [ ] Gateway-level LLM proxy/logging (gateway.yaml proxy section)
- [ ] Multi-agent workflow support (fleet.yaml / workflow.yaml)
- [ ] `harness policy suggest` (DenialEvent stream -> policy proposals)
- [ ] Fleet management (multi-gateway kubectl-context style)
22 changes: 0 additions & 22 deletions cmd/connect.go

This file was deleted.

28 changes: 7 additions & 21 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ import (
func NewCreateCmd(harnessDir, cli string) *cobra.Command {
var (
agentName string
agentFile string
agentProfile string
sandboxName string
)

cmd := &cobra.Command{
Use: "create [flags]",
Short: "Create a sandbox without attaching",
Long: "Validate gateway readiness, run preflight checks, and deploy a sandbox. Does not attach interactively — use 'harness connect' afterward.",
Long: "Validate gateway readiness, run preflight checks, and deploy a sandbox. Does not attach interactively — use 'openshell sandbox connect' afterward.",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 && sandboxName == "" {
sandboxName = args[0]
}

agentCfg, err := resolveAgentConfig(harnessDir, agentName, agentFile)
agentCfg, err := resolveAgentConfig(harnessDir, agentName, agentProfile)
if err != nil {
return err
}
Expand All @@ -54,24 +54,10 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
status.Infof("Image: %s", sandboxImage)

// 3. Ensure providers are registered
status.Header("Providers")
providerNames := agentCfg.ProviderNames()
registered, missing := gateway.ValidateProviders(providerNames, gw)
if len(missing) > 0 {
if err := registerProviders(harnessDir, gw, false, agentCfg.Providers); err != nil {
status.Warn(fmt.Sprintf("provider registration: %v", err))
}
registered, missing = gateway.ValidateProviders(providerNames, gw)
}
for _, n := range registered {
status.OKf("%s: attached", n)
}
for _, n := range missing {
status.Failf("%s: not registered", n)
}
registered := ensureProviders(harnessDir, gw, agentCfg, false)

// 4. Deploy the sandbox
status.Header("Creating sandbox")
status.Header("Sandbox")
payloadDir, err := os.MkdirTemp("", "harness-payload-")
if err != nil {
return fmt.Errorf("creating payload dir: %w", err)
Expand All @@ -95,14 +81,14 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
env: agentCfg.BuildEnvMap(),
onSuccess: func(n string) {
fmt.Println()
status.OKf("Sandbox created: %s — connect with: harness connect %s", n, n)
status.OKf("Sandbox created: %s — connect with: openshell sandbox connect %s", n, n)
},
})
},
}

cmd.Flags().StringVar(&agentName, "agent", "default", "Agent config name (from agents/)")
cmd.Flags().StringVarP(&agentFile, "file", "f", "", "Path to agent YAML file (overrides --agent)")
cmd.Flags().StringVarP(&agentProfile, "agent-profile", "f", "", "Path to agent YAML file (overrides --agent)")
cmd.Flags().StringVar(&sandboxName, "name", "", "Sandbox name (overrides agent config)")

return cmd
Expand Down
Loading
Loading