Skip to content
Merged
56 changes: 55 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,60 @@
# Contributing

## Principles
## Coding Guidelines

**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.

### Think Before Coding

Don't assume. Don't hide confusion. Surface tradeoffs.

- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.

### Simplicity First

Minimum code that solves the problem. Nothing speculative.

- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.

### Surgical Changes

Touch only what you must. Clean up only your own mess.

When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.

When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.

The test: Every changed line should trace directly to the user's request.

### Goal-Driven Execution

Define success criteria. Loop until verified.

- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"

For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```

## Project Principles

1. **Simplicity** — fewer scripts, fewer moving parts, less code. If something can be a single command, don't wrap it in a script.

Expand Down
178 changes: 89 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,78 @@
# OpenShell Harness

Deploy AI agent sandboxes on Podman (local) or OpenShift using [OpenShell](https://github.com/NVIDIA/OpenShell). Each sandbox gets:
A gateway harness for [OpenShell](https://github.com/NVIDIA/OpenShell). One command to a working AI agent sandbox — gateway deployed, providers registered, credentials validated, sandbox running.

- **Claude Code** via Google Vertex AI (`inference.local` routing)
- **Jira/Confluence** via mcp-atlassian MCP server
- **Gmail, Calendar, Drive** via gws CLI
- **GitHub** via gh CLI
- Network policy enforcement per sandbox
Without this tool, setting up a sandbox means manually deploying an OpenShell gateway (Helm install, mTLS cert extraction, SCC grants), manually registering providers with credentials scattered across env vars and files, and debugging when credentials expire or proxies misconfigure. The harness handles all of that.

## Prerequisites
## What it does

- [OpenShell CLI](https://github.com/NVIDIA/OpenShell) (`openshell`)
- Python 3.11+ (for TOML parsing)
- Podman (local) or kubectl + helm (OCP)
- `gcloud auth application-default login` (for Vertex AI)
The harness wraps `openshell` — it doesn't replace it. It adds orchestration, validation, and configuration management across three domains:

Optional: `gws` CLI (Google Workspace), `bats` (for unit tests)
| Domain | What | Config |
|--------|------|--------|
| **Infrastructure** | Deploy the gateway (local Podman or remote OpenShift), Helm, mTLS, RBAC | `openshell.toml` |
| **Providers** | Register credential providers (Vertex AI, GitHub, Atlassian), validate inputs | `providers.toml` |
| **Sandbox** | Create sandboxes from profiles, stage files, connect | `profiles/*.toml` |

## Setup
Each sandbox gets:
- Claude Code via Vertex AI (`inference.local` gateway routing)
- Jira and Confluence via mcp-atlassian MCP server
- Gmail, Calendar, Drive via gws CLI
- GitHub via gh CLI
- Network policy enforcement per sandbox

```bash
# Add harness CLI to PATH
export PATH="$PWD/bin:$PATH"
## Prerequisites

# See available commands
harness
```
- [OpenShell CLI](https://github.com/NVIDIA/OpenShell) (`openshell`)
- Podman (local) or kubectl + helm (OpenShift)
- `gcloud auth application-default login` (Vertex AI)

## Quick Start (Local)
Optional: `gws` CLI (Google Workspace), `bats` (tests)

```bash
# Install OpenShell if you haven't
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
## Quick Start

```bash
# Set credentials
export GITHUB_TOKEN="ghp_..."
export JIRA_API_TOKEN="..."
export ANTHROPIC_VERTEX_PROJECT_ID="my-project"
export CLOUD_ML_REGION="us-east5"

# Create a sandbox (deploys gateway + registers providers if needed)
# Local — deploy gateway, register providers, create sandbox
harness new --local
```

## Quick Start (OpenShift)

```bash
# Create a sandbox on the cluster
# OpenShift — same flow, remote cluster
harness new --remote

# Reconnect to a running sandbox
harness connect
```

## CLI Reference
## Commands

```
harness new [--local|--remote] [--profile NAME] [SANDBOX_NAME]
Create a new sandbox. Auto-deploys gateway and registers providers if needed.
harness new [--local|--remote] [--profile NAME]
Full flow: deploy gateway + register providers + create sandbox.

harness connect [SANDBOX_NAME]
harness connect [NAME]
Reconnect to a running sandbox.

harness deploy [--local|--remote]
Deploy or verify the gateway without creating a sandbox.

harness teardown [--sandboxes] [--providers] [--k8s]
Tear down sandboxes, providers, or k8s resources.

harness preflight
Check environment prerequisites.

harness providers
harness providers [--force]
Register providers with the gateway.

harness test [podman|ocp|all] [--full]
End-to-end validation.
harness preflight [--strict]
Check environment prerequisites (credentials, CLI tools, gateway).

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

## Profiles

Sandboxes are configured via `profiles/*.toml`:
Sandboxes are configured via TOML profiles:

```toml
# profiles/default.toml
Expand All @@ -91,58 +86,63 @@ ANTHROPIC_BASE_URL = "https://inference.local"
JIRA_URL = "https://mysite.atlassian.net"
```

Use a specific profile: `harness new --profile coder`
Use a specific profile: `harness new --profile research`

## Testing
## Why a sandbox

```bash
harness test podman --full # full local validation
harness test ocp --full # full OCP validation
bats test/preflight.bats # unit tests (29 tests)
make test # build images + test both
Compared to running Claude Code locally:

- **Credential isolation** — the sandbox never sees real API tokens (proxy-resolved placeholders)
- **Network policy** — per-binary egress rules control which processes reach which hosts
- **Reproducible environment** — pinned tool versions, same setup across machines and team members
- **Team sharing** — OpenShift deployment with mTLS, shared gateway, per-user sandboxes

## Architecture

```
Your machine OpenShift cluster
┌──────────┐ ┌──────────────────────────────┐
│ harness │ Route (mTLS) │ Gateway (StatefulSet) │
│ CLI ├───────────────────►│ ├─ gRPC API │
│ │ │ ├─ inference.local proxy │
└──────────┘ │ ├─ Provider credential store │
│ └─ OAuth token refresh │
│ │
│ Sandbox Pods │
│ ├─ Claude Code → Vertex AI │
│ ├─ mcp-atlassian │
│ ├─ gws CLI │
│ ├─ gh CLI │
│ └─ L7 network proxy │
└──────────────────────────────┘
```

## Files
## Project Layout

| Path | Purpose |
|------|---------|
| `bin/harness` | CLI entry point |
| `bin/scripts/` | Subcommand scripts (new, deploy, teardown, etc.) |
| `bin/scripts/lib/` | Shared libraries (profile parsing, providers, common) |
| `profiles/default.toml` | Default sandbox profile |
| `providers.toml` | Provider definitions (env/file/check inputs) |
| `openshell.toml` | Which providers to enable, upstream version pin |
| `sandbox/` | Sandbox image (Dockerfile, startup.sh, policy.yaml, CLAUDE.md) |
| `sandbox/launcher/` | In-cluster launcher image (for OCP sandboxes) |
| `test/` | Tests (preflight.bats, test-flow.sh) |
| `values-ocp.yaml` | Helm values for OpenShift deployment |
| `AGENTS.md` | Project principles and workaround tracking |

## Why Use a Sandbox?
| `main.go`, `cmd/` | CLI commands (Go) |
| `internal/gateway/` | OpenShell CLI wrapper (Gateway interface) |
| `internal/k8s/` | kubectl/helm/oc runner with retry |
| `internal/profile/` | Profile TOML parsing |
| `internal/preflight/` | Provider prerequisite checks |
| `profiles/` | Sandbox profiles (TOML) |
| `providers.toml` | Provider catalog (inputs, prerequisites) |
| `sandbox/` | Sandbox image (Dockerfile, startup, policy, CLAUDE.md) |
| `sandbox/launcher/` | In-cluster launcher for OCP sandboxes |
| `sandbox/profiles/` | OpenShell provider type profiles (YAML) |
| `deploy/` | K8s manifests (RBAC, route) |
| `test/` | Tests (bats preflight, test-flow.sh integration) |

Compared to running Claude Code locally:
- **Credential isolation** — sandbox never sees real API tokens (proxy-resolved placeholders)
- **Network policy** — per-binary egress rules (policy.yaml controls which processes reach which hosts)
- **Reproducible environment** — pinned tool versions in Dockerfile
- **Team sharing** — OCP deployment with mTLS, shared gateway, per-user sandboxes

## Architecture
## Testing

```bash
make validate # full matrix: {bash,go} x {podman,ocp}
make test # build images + test
bats test/preflight.bats # 29 preflight unit tests
go test ./... # Go unit tests
```
Your Mac OpenShift Cluster
┌──────────┐ ┌──────────────────────────────┐
│ harness │ OpenShift Route │ Gateway (StatefulSet) │
│ CLI ├──────────────────►│ ├─ gRPC API │
│ │ TLS passthrough │ ├─ inference.local proxy │
│ │ mTLS :443 │ ├─ Provider credential store │
└──────────┘ │ └─ OAuth token refresh │
│ │
│ Sandbox Pods │
│ ├─ Claude Code → inference │
│ │ .local → Vertex AI │
│ ├─ mcp-atlassian │
│ ├─ gws CLI │
│ ├─ gh CLI │
│ └─ Network proxy │
└──────────────────────────────┘
```

## Design

The harness is a thin wrapper that should shrink over time. As OpenShell adds native support for features the harness currently bridges (GWS credentials, provider config injection, in-cluster sandbox creation), the custom code gets replaced by upstream. See [AGENTS.md](AGENTS.md) for workaround tracking and [docs/design.md](docs/design.md) for the full design document.
25 changes: 11 additions & 14 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import (
"github.com/spf13/cobra"
)

var (
sccPrivilegedSAs = []string{"openshell", "openshell-sandbox", "default"}
secretNames = []string{"openshell-gws", "openshell-atlassian"}
)

func NewDeployCmd(harnessDir, cli string) *cobra.Command {
var (
local bool
Expand Down Expand Up @@ -61,7 +66,7 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
if chartVersion == "" {
cfg, _ := preflight.LoadConfig(filepath.Join(harnessDir, "openshell.toml"))
if cfg != nil {
chartVersion = cfg.ChartVersion
chartVersion = cfg.Upstream.ChartVersion
}
}
if chartVersion == "" {
Expand Down Expand Up @@ -94,7 +99,7 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R

// Step 3: OpenShift SCCs (best-effort — oc may not exist on non-OpenShift)
status.Step(3, "Granting OpenShift SCCs")
for _, sa := range []string{"openshell", "openshell-sandbox", "default"} {
for _, sa := range sccPrivilegedSAs {
kc.RunOC(ctx, "adm", "policy", "add-scc-to-user", "privileged", "-z", sa, "-n", namespace)
}
kc.RunOC(ctx, "adm", "policy", "add-scc-to-user", "anyuid", "-z", "openshell", "-n", namespace)
Expand All @@ -109,10 +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 := os.Getenv("SANDBOX_IMAGE")
if sandboxImage == "" {
sandboxImage = "quay.io/rcochran/openshell:sandbox"
}
sandboxImage := envOr("SANDBOX_IMAGE", "quay.io/rcochran/openshell:sandbox")

appsDomain, err := clusterRunner.GetJSONPath(ctx, "ingresses.config.openshift.io/cluster", "{.spec.domain}")
if err != nil || appsDomain == "" {
Expand Down Expand Up @@ -152,10 +154,7 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R

// Step 6: CLI gateway config
status.Step(6, "Configuring CLI gateway")
gatewayName := os.Getenv("GATEWAY_NAME")
if gatewayName == "" {
gatewayName = "openshell-remote-ocp"
}
gatewayName := envOr("GATEWAY_NAME", "openshell-remote-ocp")
gatewayURL := fmt.Sprintf("https://%s:443", routeHost)

// Remove existing gateways for this host
Expand Down Expand Up @@ -223,13 +222,11 @@ func deployLocal(gw gateway.Gateway) error {
}

status.Section("Container Runtime")
podmanPath, _ := exec.LookPath("podman")
if podmanPath == "" {
if _, err := exec.LookPath("podman"); err != nil {
status.Fail("Podman not found")
return fmt.Errorf("podman is required")
}
cmd := exec.Command("podman", "--version")
out, _ := cmd.Output()
out, _ := exec.Command("podman", "--version").Output()
status.OKf("Podman: %s", strings.TrimSpace(string(out)))

status.Section("Gateway")
Expand Down
Loading
Loading