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 @@ -15,7 +15,9 @@ __pycache__/

# Build artifacts
harness
harness-openshell
build/runner/harness
build/runner/openshell
infracluster/
.claude/

Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Current workarounds and their upstream tracking:

| Workaround | Why | Upstream |
|------------|-----|----------|
| Custom gateway image | `google-vertex-ai` provider not in released builds yet | Will ship in upstream release |
| Custom sandbox image | Adds mcp-atlassian and GWS CLI to community base | Upstreaming MCP integrations |
| `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` | Vertex AI rejects `context_management` beta header | Anthropic/Google to align APIs |
| Atlassian `JIRA_URL`/`JIRA_USERNAME` as `--config` material | Provider v2 config keys not injected as env vars yet | OpenShell roadmap |
| In-cluster runner Job | OpenShell doesn't have a native K8s-triggered sandbox creation | Potential future CRD |
Expand All @@ -79,7 +79,7 @@ Previously worked around, now resolved:

| Was | Resolution |
|-----|-----------|
| GWS credential export/upload to sandbox | GWS is now a native provider (`google-workspace`). Gateway manages OAuth refresh via `oauth2_refresh_token` strategy + `request_body_credential_rewrite` on `oauth2.googleapis.com`. Sandbox gets a proxy-resolved placeholder. |
| GWS credential export/upload to sandbox | GWS is now a native provider (`google-workspace`). Gateway manages OAuth refresh via `oauth2-refresh-token` strategy + `request_body_credential_rewrite` on `oauth2.googleapis.com`. Sandbox gets a proxy-resolved placeholder. |

## Validation

Expand All @@ -96,7 +96,7 @@ registration, credential injection, and the GWS OAuth token lifecycle.
Runs in GitHub Actions on every PR.

```
--ci flag = --no-providers --agent=ci --full
--ci flag = --no-providers --agent=ci
```

### Make targets
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Automates gateway deployment, provider registration, and sandbox creation across

```bash
# macOS
brew install openshell && brew services start openshell
brew tap nvidia/openshell && brew install openshell && brew services start openshell

# Download the harness binary (macOS ARM64 shown -- see Releases for other platforms)
curl -L https://github.com/robbycochran/harness-openshell/releases/latest/download/harness_darwin_arm64 -o harness
Expand Down Expand Up @@ -43,7 +43,7 @@ An agent config declares the sandbox image, entrypoint, and which providers to a
# agents/default.yaml
name: agent
image: ghcr.io/robbycochran/harness-openshell:sandbox
entrypoint: claude --bare
entrypoint: claude
tty: true

providers:
Expand Down Expand Up @@ -71,7 +71,7 @@ For non-interactive task agents, set `task:` and `tty: false`:
```yaml
# agents/demo.yaml
name: demo
entrypoint: claude --bare -p
entrypoint: claude -p
task: demo/DEMO-TASK.md
tty: false
# ... same providers and env
Expand Down Expand Up @@ -121,7 +121,7 @@ harness CLI ──→ openshell CLI ──→ Gateway (Podman or K8s)
├── L7 network policy
├── inference.local proxy
└── Sandbox container
├── claude --bare
├── claude
├── gh, mcp-atlassian, gws
└── placeholder tokens
```
Expand Down
8 changes: 4 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
- Consider: gateway.toml uses a `registry` field and images are relative to it

### Consolidate internal/profile into internal/agent
- `internal/profile/` has dead code: `Parse()`, `ParseFile()`, `BuildSandboxEnv()`
- Only `Config` struct, `ValidateProviders`, and `StageHarnessDir` still used
- Move `ValidateProviders` to agent or gateway package, inline `Config` into sandbox.go
- Remove TOML dependency
- ✅ Removed dead code: `Parse()`, `ParseFile()`, `BuildSandboxEnv()`
- Only `Config` struct, `ValidateProviders`, and `StageHarnessDir` remain
- TODO: Move `ValidateProviders` to agent or gateway package, inline `Config` into sandbox.go
- TODO: Remove TOML dependency (github.com/BurntSushi/toml) if no longer used

## Agent Config

Expand Down
8 changes: 8 additions & 0 deletions TODOS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# TODOs

## remove providers.tooml, add a todo for provider profile validation in thefture
## convert toml to yaml for gateways
## specify the yaml formats

## for flows that supports agent.yaml (create and up..) should also support --provider-profile and config

## document that we need a way to specify non secret env vars in providers to capture like secrets, thats what provider config captures

## registerProviders should filter by agent's provider list

**What:** `registerProviders()` in `cmd/providers.go` uses the gateway config's provider
Expand Down
2 changes: 1 addition & 1 deletion agents/providers/profiles/atlassian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Only JIRA_API_TOKEN is a provider credential (proxy resolves it in
# Basic auth headers). JIRA_URL and JIRA_USERNAME are non-secret config
# uploaded to the sandbox by sandbox-ocp.sh.
# passed via agent.yaml config field.

id: atlassian
display_name: Atlassian (Jira + Confluence)
Expand Down
18 changes: 3 additions & 15 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -52,12 +53,7 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
name = sandboxName
}

sandboxImage := agentCfg.Image
if sandboxImage == "" {
sandboxImage = defaultSandboxImage()
} else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" {
sandboxImage = envImage
}
sandboxImage := resolveSandboxImage(agentCfg.Image)

status.Header("Agent")
status.Infof("Name: %s", name)
Expand Down Expand Up @@ -86,7 +82,7 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
status.Header("Preflight")
preflightOK := true
for _, p := range allProviders {
if !providerInList(p.Name, missing) {
if !slices.Contains(missing, p.Name) {
continue
}
ok, details := preflight.CheckProvider(p)
Expand Down Expand Up @@ -222,11 +218,3 @@ func createViaRunner(harnessDir string, gwCfg *gateway.GatewayConfig, gw gateway
return upRemote(harnessDir, gwCfg, gw, agentPath, sandboxName)
}

func providerInList(name string, providers []string) bool {
for _, p := range providers {
if p == name {
return true
}
}
return false
}
11 changes: 0 additions & 11 deletions cmd/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,3 @@ func TestProfileHasCustomProviders_WithCustom(t *testing.T) {
}
}

func TestProviderInList(t *testing.T) {
if !providerInList("github", []string{"github", "vertex-local"}) {
t.Error("github should be in list")
}
if providerInList("atlassian", []string{"github", "vertex-local"}) {
t.Error("atlassian should not be in list")
}
if providerInList("github", nil) {
t.Error("nil list should return false")
}
}
6 changes: 1 addition & 5 deletions cmd/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ func runLaunch(cli string) error {
return err
}

if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" {
agentCfg.Image = envImage
} else if agentCfg.Image == "" {
agentCfg.Image = defaultSandboxImage()
}
agentCfg.Image = resolveSandboxImage(agentCfg.Image)

fmt.Println("=== Sandbox Runner ===")
fmt.Printf(" Name: %s\n", agentCfg.Name)
Expand Down
2 changes: 1 addition & 1 deletion cmd/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ func registerGWS(harnessDir string, gw gateway.Gateway, enabled func(string) boo
// gwsProfileScopes reads the refresh.scopes list from agents/providers/profiles/gws.yaml
// and returns them as a space-separated string for use as OAuth scope material.
func gwsProfileScopes(harnessDir string) string {
profilePath := filepath.Join(harnessDir, "sandbox", "profiles", "gws.yaml")
profilePath := filepath.Join(harnessDir, "agents", "providers", "profiles", "gws.yaml")
data, err := os.ReadFile(profilePath)
if err != nil {
return ""
Expand Down
2 changes: 1 addition & 1 deletion cmd/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func createSandbox(opts sandboxOpts) error {
return fmt.Errorf("staging payload: %w", err)
}
} else {
if err := profile.StageHarnessDir(cfg, uploadDir); err != nil {
if err := profile.StageHarnessDir(uploadDir); err != nil {
return fmt.Errorf("staging files: %w", err)
}
}
Expand Down
15 changes: 15 additions & 0 deletions cmd/sandbox_image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cmd

import "os"

// resolveSandboxImage returns the final sandbox image path following
// the precedence: SANDBOX_IMAGE env var > agentImage > defaultSandboxImage().
func resolveSandboxImage(agentImage string) string {
if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" {
return envImage
}
if agentImage != "" {
return agentImage
}
return defaultSandboxImage()
}
14 changes: 2 additions & 12 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,7 @@ func upRemote(harnessDir string, gwCfg *gateway.GatewayConfig, gw gateway.Gatewa
name = sandboxName
}

sandboxImage := agentCfg.Image
if sandboxImage == "" {
sandboxImage = defaultSandboxImage()
} else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" {
sandboxImage = envImage
}
sandboxImage := resolveSandboxImage(agentCfg.Image)

// Top-level context
status.Infof("Agent: %s (%s)", name, filepath.Base(agentPath))
Expand Down Expand Up @@ -296,12 +291,7 @@ func upLocal(opts upLocalOpts) error {
}
noTTY := opts.noTTY || agentCfg.NoTTY()

sandboxImage := agentCfg.Image
if sandboxImage == "" {
sandboxImage = defaultSandboxImage()
} else if envImage := os.Getenv("SANDBOX_IMAGE"); envImage != "" {
sandboxImage = envImage
}
sandboxImage := resolveSandboxImage(agentCfg.Image)

// Top-level context
status.Infof("Agent: %s (%s)", sandboxName, filepath.Base(opts.agentPath))
Expand Down
14 changes: 14 additions & 0 deletions docs/archive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Archived Documentation

This directory contains historical design documents that are no longer current but may have valuable context.

## design-v1-2026-06-08.md

Original design proposal from early development. **Outdated** — uses old naming (`harness.toml` vs `openshell.toml`, `profiles/*.toml` vs `agents/*.yaml`). Current implementation diverged from this design on 2026-06-08.

**Still valuable sections:**
- Provider health two-level model (lines 208-244)
- OAuth-proxy auth roadmap for OCP (lines 435-476)
- OCP vs local k8s comparison (lines 397-421)

For current architecture, see: README.md, SPEC.md, and AGENTS.md in the repo root.
File renamed without changes.
61 changes: 3 additions & 58 deletions internal/profile/profile.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
package profile

import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/BurntSushi/toml"
)

// ProviderChecker checks if a provider is registered.
Expand All @@ -32,42 +26,6 @@ func (c *Config) KeepSandbox() bool {
return *c.Keep
}

func (c *Config) BuildSandboxEnv() string {
if len(c.Env) == 0 {
return ""
}
keys := make([]string, 0, len(c.Env))
for k := range c.Env {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
fmt.Fprintf(&b, "export %s=%q\n", k, c.Env[k])
}
return b.String()
}

// Parse reads a profile TOML file and returns a Config with defaults applied.
func Parse(harnessDir, name string) (*Config, error) {
path := filepath.Join(harnessDir, "profiles", name+".toml")
return ParseFile(path)
}

// ParseFile reads a profile TOML file by path.
func ParseFile(path string) (*Config, error) {
var cfg Config
if _, err := toml.DecodeFile(path, &cfg); err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
if cfg.Name == "" {
cfg.Name = "agent"
}
if cfg.Command == "" {
cfg.Command = "claude"
}
return &cfg, nil
}

// ValidateProviders checks which profile providers are registered on the
// gateway. Returns the list of registered providers and a list of missing ones.
Expand All @@ -82,20 +40,7 @@ func ValidateProviders(providers []string, gw ProviderChecker) (registered, miss
return
}

// StageHarnessDir writes sandbox.env to harnessDir.
func StageHarnessDir(cfg *Config, harnessDir string) error {
if err := os.MkdirAll(harnessDir, 0o755); err != nil {
return err
}

envContent := cfg.BuildSandboxEnv()
if envContent != "" {
if err := os.WriteFile(filepath.Join(harnessDir, "sandbox.env"), []byte(envContent), 0o600); err != nil {
return fmt.Errorf("writing sandbox.env: %w", err)
}
lines := strings.Count(envContent, "\n")
fmt.Printf(" Env: %d vars staged\n", lines)
}

return nil
// StageHarnessDir creates the harness directory.
func StageHarnessDir(harnessDir string) error {
return os.MkdirAll(harnessDir, 0o755)
}
Loading
Loading