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
34 changes: 16 additions & 18 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
# Credentials — never commit

__pycache__/
.certs/
.claude/
.gateway-env
*-token.txt
*.json.key
acs-ai-sa.json
secrets.yaml
kubeconfig
quay-pull-secret.yaml

# Local state
*.log
*.tmp
__pycache__/
.gateway-env
.certs/

# Build artifacts
# Credentials — never commit
# Local state
# Planning artifacts (local only)
acs-ai-sa.json
dashboard.html
harness
harness-openshell
infracluster/
.claude/

# Planning artifacts (local only)
plans/
openshell-arch/
strategy-dashboard.html
dashboard.html
kubeconfig
mlflow.db
openshell-arch/
plan_file_ga.md
plans/
quay-pull-secret.yaml
secrets.yaml
strategy-dashboard.html
109 changes: 53 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ harness apply -f agent.yaml
- **One-file agent definition** -- agent, providers, gateway, policy, and sandbox files in a single YAML
- **Multi-document YAML** -- `kind: agent/provider/gateway/payload/policy` composed in one file
- **Payload files** -- upload configs to sandbox paths without rebuilding the image
- **Multi-target deploy** -- same YAML works on local Podman, kind, and OpenShift
- **Headless task mode** -- `--task "do something"` runs the agent and outputs to stdout
- **Multi-target deploy** -- same YAML works on local Podman, kind, and OpenShell
- **Dry-run validation** -- `--dry-run` checks everything before deploying
- **Config inspection** -- `-o yaml` outputs the fully resolved config

Expand Down Expand Up @@ -49,20 +50,27 @@ Or build from source: `make cli`
# Set credentials (missing ones are skipped gracefully)
export GITHUB_TOKEN=ghp_...

# Deploy a sandbox
./harness apply
# Deploy a sandbox with the default agent config
harness apply -f profiles/agent-default.yaml

# Run a task headlessly (agent outputs to stdout)
harness apply -f profiles/agent-default.yaml --task "review this codebase for security issues"

# Run a task from a file
harness apply -f profiles/agent-default.yaml --task @tasks/review.md

# Interactive mode
./harness apply --attach
harness apply -f profiles/agent-default.yaml --attach

# Validate without deploying
./harness apply --dry-run
harness apply -f profiles/agent-default.yaml --dry-run

# See the fully resolved config
./harness apply -o yaml
```
harness apply -f profiles/agent-default.yaml -o yaml

The built-in config registers providers for GitHub, Jira, Vertex AI, and Google Workspace. Providers with missing credentials are skipped with an info message.
# Override the entrypoint
harness apply -f profiles/agent-default.yaml --entrypoint opencode
```

## The Agent YAML

Expand All @@ -84,6 +92,14 @@ providers:
env:
ANTHROPIC_BASE_URL: https://inference.local
ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed

payloads:
- sandbox_path: /sandbox/.claude/CLAUDE.md
local_path: profiles/images/sandbox-default/CLAUDE.md
- sandbox_path: /sandbox/.claude.json
local_path: profiles/images/sandbox-default/claude.json
- sandbox_path: /sandbox/.mcp.json
local_path: profiles/images/sandbox-default/mcp.json
```

### Multi-Document Harness YAML
Expand All @@ -95,20 +111,24 @@ Bundle everything in one file:
kind: agent
name: my-agent
entrypoint: claude
gateway: local
providers:
- profile: github
---
kind: provider
name: github
type: github
credentials: [GITHUB_TOKEN]
endpoints:
- { host: "api.github.com", port: 443 }
---
kind: gateway
name: local
type: local
kind: payload
sandbox_path: /sandbox/.claude/CLAUDE.md
content: |
You are a security review agent.
---
kind: policy
network_policies:
github:
endpoints:
- { host: "api.github.com", port: 443 }
```

```bash
Expand All @@ -118,21 +138,12 @@ harness apply -f harness.yaml
## Targets

```bash
harness apply # local Podman (default)
harness apply --gateway ocp # deploy to OpenShift
harness deploy ocp # deploy gateway only
harness apply -f profiles/agent-default.yaml # local Podman
harness apply -f profiles/agent-default.yaml --gateway ocp # deploy to OpenShift
harness apply -f profiles/agent-opencode.yaml # OpenCode agent
harness deploy ocp # deploy gateway only
```

The `gateway:` field in the agent YAML or `--gateway` flag selects the target. Gateway profiles live in `profiles/gateways/`.

### OpenCode

```bash
harness apply --agent opencode
```

Same providers and gateway, different agent binary. See `profiles/agent-opencode.yaml`.

## How It Works

```
Expand All @@ -159,57 +170,43 @@ See the [OpenShell docs](https://github.com/NVIDIA/OpenShell) for the full secur
### Commands

```
harness apply [-f FILE] [--agent NAME] [--gateway NAME] [--attach] [--dry-run] [-o yaml|json]
Deploy a sandboxed agent. Primary command.
-f loads a harness/agent YAML file directly.
--agent selects from profiles/ by name (default: "default").
harness apply -f FILE [--task TEXT|@FILE] [--entrypoint NAME] [--gateway NAME] [--attach] [--dry-run] [-o yaml|json]
Deploy a sandboxed agent from a config file.
-f is required -- always specify which config to apply.
--task runs the agent headlessly with a task (inline text or @filepath).
--entrypoint overrides the agent entrypoint (claude, opencode, bash).
--attach enables interactive TTY mode.
--dry-run validates without deploying.
-o yaml outputs the fully resolved config.

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

harness get agents [-o table|json|yaml]
List running sandboxes. Wraps openshell sandbox list with
consistent structured output. Aliases: sandboxes, sandbox.

harness get providers [-o table|json|yaml]
List registered providers. Credentials never included in output.

harness get gateways [-o table|json|yaml]
List gateways. Aliases: gateway, gw.

harness describe <name>
Detailed status for a specific sandbox (phase, gateway, providers).

harness delete <name> [<name>...]
Delete specific sandboxes by name.

harness delete --all
Delete all sandboxes, providers, and k8s resources.
harness get agents|providers|gateways [-o table|json|yaml]
List resources with consistent structured output.

harness delete --providers / --k8s
Delete providers or k8s resources selectively.
harness describe <name> [-o table|json|yaml]
Detailed status for a specific sandbox.

harness stop [NAME] / harness start [NAME]
Stop or start a sandbox without deleting it.
harness delete <name> [--all] [--providers] [--k8s]
Delete sandboxes or other resources.
```

For sandbox connect/logs, use openshell directly:
For runtime operations, use openshell directly:
```
openshell sandbox connect [NAME]
openshell sandbox logs [NAME] [--tail]
openshell sandbox exec [NAME] -- ...
```

### Config Files

| File | Purpose |
|------|---------|
| `profiles/agent-*.yaml` | Agent config: image, entrypoint, providers, env, optional task file |
| `profiles/agent-*.yaml` | Agent config: image, entrypoint, providers, env, payloads, task |
| `profiles/providers/` | OpenShell provider profiles (imported to gateway on registration) |
| `profiles/gateways/*.yaml` | Gateway profiles: `local.yaml`, `kind.yaml`, `ocp.yaml` |
| `profiles/images/sandbox-default/` | Sandbox image: Dockerfile, policy, MCP configs, Claude settings |
| `profiles/images/sandbox-default/` | Sandbox image defaults (overridable via payloads) |

### Credentials

Expand Down
2 changes: 0 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,7 @@ Delete sandboxes by name, or use flags for bulk operations. `--all` deletes sand

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

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

Stop or start a sandbox without deleting it.

### Deprecated Aliases

Expand Down
7 changes: 3 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ as strongest OTel backend for full signal coverage.
- [ ] `harness runs list/show` queries traces from the backend
- [ ] Headless mode (`harness run --task '...'`) records automatically

## Deferred (post-0.1)
## Deferred

- [ ] Multi-agent workflow support (fleet.yaml / workflow.yaml)
- [ ] `harness policy suggest` (DenialEvent stream -> policy proposals)
- [ ] Fleet management (multi-gateway kubectl-context style)
- [ ] `harness init` -- generate default harness.yaml, print next steps (highest-impact missing feature)
- [ ] Pre-flight checks in apply (openshell exists, podman running, credentials present)
38 changes: 37 additions & 1 deletion cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/robbycochran/harness-openshell/internal/agent"
Expand All @@ -19,6 +20,8 @@ func NewApplyCmd(harnessDir, cli string) *cobra.Command {
gatewayName string
gatewayProfile string
sandboxName string
task string
entrypoint string
attach bool
providerRefresh bool
dryRun bool
Expand Down Expand Up @@ -46,7 +49,38 @@ then deploy a sandbox. Use --dry-run to validate without deploying, or
agentCfg := harness.Agent
agentPath := resolveAgentPath(harnessDir, agentName, file)

// Print config path (skip for structured output -- goes to stderr)
// CLI overrides
if entrypoint != "" {
agentCfg.Entrypoint = entrypoint
}
if task != "" && !attach {
// Headless task: set TTY=false so BuildRunSh generates --print
f := false
agentCfg.TTY = &f
}
if task != "" {
if strings.HasPrefix(task, "@") {
path := task[1:]
if path == "" {
return fmt.Errorf("--task @: missing file path after @")
}
agentCfg.Task = path
} else {
tmpTask, err := os.CreateTemp("", "harness-task-*.md")
if err != nil {
return fmt.Errorf("creating task file: %w", err)
}
defer os.Remove(tmpTask.Name())
if _, err := tmpTask.WriteString(task); err != nil {
tmpTask.Close()
return fmt.Errorf("writing task: %w", err)
}
tmpTask.Close()
agentCfg.Task = tmpTask.Name()
}
}

// Print config path (skip for structured output)
if output == "" {
status.Infof("Config: %s", agentPath)
}
Expand Down Expand Up @@ -107,6 +141,8 @@ then deploy a sandbox. Use --dry-run to validate without deploying, or
cmd.Flags().StringVar(&gatewayName, "gateway", envOr("OPENSHELL_GATEWAY", ""), "Gateway profile name (local, kind, ocp)")
cmd.Flags().StringVar(&gatewayProfile, "gateway-profile", "", "Path to gateway profile YAML")
cmd.Flags().StringVar(&sandboxName, "name", "", "Sandbox name (overrides agent config)")
cmd.Flags().StringVar(&task, "task", "", "Task to pass to the agent (inline text or @filepath)")
cmd.Flags().StringVar(&entrypoint, "entrypoint", "", "Override agent entrypoint (claude, opencode, bash)")
cmd.Flags().BoolVar(&attach, "attach", false, "Attach TTY after creation (interactive mode)")
cmd.Flags().BoolVar(&providerRefresh, "provider-refresh", false, "Delete and recreate all providers")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Validate configuration without deploying")
Expand Down
20 changes: 3 additions & 17 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,15 @@ import (
)

func NewDeployCmd(harnessDir, cli string) *cobra.Command {
var (
local bool
remote bool
kubeconfig string
)
var kubeconfig string

cmd := &cobra.Command{
Use: "deploy [gateway]",
Short: "Deploy or verify the gateway",
Long: "Deploy a gateway by name (e.g., local, ocp, kind). Reads configuration from profiles/gateways/<name>.yaml.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
gatewayName, err := resolveGatewayName(args, local, remote)
gatewayName, err := resolveGatewayName(args)
if err != nil {
return err
}
Expand All @@ -50,25 +46,15 @@ func NewDeployCmd(harnessDir, cli string) *cobra.Command {
},
}

cmd.Flags().BoolVar(&local, "local", false, "Alias for 'harness deploy local'")
cmd.Flags().BoolVar(&remote, "remote", false, "Alias for 'harness deploy ocp'")
cmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig (remote only)")
cmd.Flags().MarkHidden("local")
cmd.Flags().MarkHidden("remote")

return cmd
}

func resolveGatewayName(args []string, local, remote bool) (string, error) {
func resolveGatewayName(args []string) (string, error) {
if len(args) > 0 {
return args[0], nil
}
if local {
return "local", nil
}
if remote {
return "ocp", nil
}
return "", fmt.Errorf("specify a gateway: harness deploy <local|ocp|kind>")
}

Expand Down
Loading
Loading