diff --git a/.gitignore b/.gitignore index 0af33bf..201e825 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ plans/ openshell-arch/ strategy-dashboard.html dashboard.html +mlflow.db +plan_file_ga.md diff --git a/README.md b/README.md index 9704ed6..4b55dd8 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,61 @@ -# OpenShell Harness +# harness -Orchestration CLI for [OpenShell](https://github.com/NVIDIA/OpenShell) AI agent sandboxes. -Automates gateway deployment, provider registration, and sandbox creation across local Podman and remote Kubernetes/OpenShift targets. +Deploy a fully sandboxed AI agent in one command. -## Quick Start +```bash +harness apply -f agent.yaml +``` + +One file defines your agent, providers, gateway, and policy. The harness resolves credentials, deploys the gateway, registers providers, and launches a sandbox with deny-by-default L7 egress. Works on local Podman and remote Kubernetes/OpenShift with the same config. -**Prerequisites:** [OpenShell v0.0.59+](https://github.com/NVIDIA/OpenShell) installed and running, Podman. +## What You Get + +- **Sandbox isolation** -- every agent runs in a container with Landlock filesystem restrictions and deny-by-default network policy +- **Credential proxy** -- secrets are resolved at the gateway boundary, never exposed inside the sandbox +- **Multi-target** -- same agent YAML deploys to local Podman, kind, or OpenShift +- **Declarative config** -- multi-document YAML bundles agent + providers + gateway + policy in one file +- **Dry-run validation** -- `--dry-run` checks gateway, providers, env vars, and image before deploying +- **Config inspection** -- `-o yaml` outputs the fully resolved harness config + +## Install ```bash # macOS brew tap nvidia/openshell && brew install openshell && brew services start openshell -# Download the harness binary (macOS ARM64 shown -- see Releases for other platforms) +# Download the harness binary curl -L https://github.com/robbycochran/harness-openshell/releases/latest/download/harness_darwin_arm64 -o harness chmod +x harness - -# Set credentials (any combination -- missing ones are skipped gracefully) -export GITHUB_TOKEN=ghp_... # GitHub (gh CLI in sandbox) -export JIRA_API_TOKEN=... # Jira (mcp-atlassian MCP server) -export JIRA_URL=https://your-org.atlassian.net -export JIRA_USERNAME=you@company.com - -# Launch a sandbox -./harness up ``` -The built-in config registers three providers: GitHub, Jira, and Vertex AI. Providers with missing credentials are skipped with an info message -- you don't need all three to get started. The sandbox runs Claude Code with whatever providers are available. - -To customize providers or add GWS, create an `profiles/agent-default.yaml` in your project directory -- it takes precedence over the builtin. See [Agent Configs](#agent-configs) below. - -## Where This Fits - -[OpenShell](https://github.com/NVIDIA/OpenShell) provides the runtime: gateway, sandboxes, L7 network policy, and credential proxy. It handles sandbox lifecycle and credential injection once providers are registered, but leaves gateway deployment orchestration, credential validation, and multi-target configuration to the user. +Or build from source: `make cli` -This harness fills a different gap: multi-provider credential management (inline validation, registration, health checks) across deployment targets (local Podman, kind, OpenShift) with declarative agent configs. It is model-agnostic -- the agent config chooses the entrypoint and inference backend. The harness orchestrates the infrastructure around it. - -## What `harness up` Replaces - -One command: +## Quick Start ```bash -harness up -``` - -reads `profiles/agent-default.yaml`: +# Set credentials (missing ones are skipped gracefully) +export GITHUB_TOKEN=ghp_... -```yaml -# profiles/agent-default.yaml -name: agent -entrypoint: claude -tty: true - -providers: - - profile: github - - profile: vertex-local - - profile: atlassian - env: - JIRA_URL: ${JIRA_URL} - JIRA_USERNAME: ${JIRA_USERNAME} - - profile: gws +# Deploy a sandbox +./harness apply -env: - ANTHROPIC_BASE_URL: https://inference.local - ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed -``` +# Interactive mode +./harness apply --attach -and replaces this sequence of `openshell` commands (captured from `harness up --show-commands`): +# Validate without deploying +./harness apply --dry-run -```bash -# 1. Enable providers v2 -openshell settings set --global --key providers_v2_enabled --value true --yes - -# 2. Import custom provider profiles (atlassian, gws) -openshell provider profile import --from profiles/providers/ - -# 3. Register GitHub (discovers GITHUB_TOKEN from environment) -openshell provider create --name github --type github --from-existing - -# 4. Register Vertex AI (reads ADC, configures inference routing) -openshell provider create --name vertex-local --type google-vertex-ai \ - --from-gcloud-adc \ - --config VERTEX_AI_PROJECT_ID=my-project \ - --config VERTEX_AI_REGION=global -openshell inference set --provider vertex-local --model claude-sonnet-4-6 --no-verify - -# 5. Register Atlassian (discovers JIRA_API_TOKEN from environment) -openshell provider create --name atlassian --type atlassian --from-existing - -# 6. Register GWS (export OAuth creds, create provider, configure refresh) -gws auth export --unmasked -openshell provider create --name gws --type google-workspace \ - --credential GOOGLE_WORKSPACE_CLI_TOKEN=pending -openshell provider refresh configure gws \ - --credential-key GOOGLE_WORKSPACE_CLI_TOKEN \ - --strategy oauth2-refresh-token \ - --material client_id=... --material client_secret=... \ - --material refresh_token=... --material scopes=... \ - --secret-material-key client_secret --secret-material-key refresh_token -openshell provider refresh rotate gws \ - --credential-key GOOGLE_WORKSPACE_CLI_TOKEN - -# 7. Create sandbox with all providers and env vars -openshell sandbox create --name agent \ - --from ghcr.io/robbycochran/harness-openshell:sandbox \ - --provider github --provider vertex-local --provider atlassian --provider gws \ - --env ANTHROPIC_API_KEY=sk-ant-openshell-proxy-managed \ - --env ANTHROPIC_BASE_URL=https://inference.local \ - --env CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 \ - --env JIRA_URL=https://your-org.atlassian.net \ - --env JIRA_USERNAME=you@company.com \ - --upload payload:/sandbox/.config --no-git-ignore \ - --tty -- bash /sandbox/.config/openshell/run.sh +# See the fully resolved config +./harness apply -o yaml ``` -The harness also handles: local gateway deployment, version checking (openshell >= v0.0.59), payload rendering, retry logic, and graceful skipping of providers whose credentials are not available. - -Credentials are proxy-managed -- the sandbox holds placeholder tokens; real secrets are substituted by the gateway at the network boundary. +The built-in config registers providers for GitHub, Jira, Vertex AI, and Google Workspace. Providers with missing credentials are skipped with an info message. -### OCP Deployment - -The `gateway:` field tells the harness which deployment target to use: +## The Agent YAML ```yaml -# profiles/agent-ocp.yaml +# profiles/agent-default.yaml name: agent -gateway: ocp entrypoint: claude tty: true @@ -140,81 +71,11 @@ providers: env: ANTHROPIC_BASE_URL: https://inference.local ANTHROPIC_API_KEY: sk-ant-openshell-proxy-managed - CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" -``` - -```bash -harness up -f profiles/agent-ocp.yaml # deploys to OCP (reads profiles/gateways/ocp.yaml) -harness up # defaults to local Podman ``` -`--gateway local` and `--gateway ocp` flags override the `gateway:` field. +### Multi-Document Harness YAML -For OCP, the harness first deploys the gateway via Helm, then runs the same provider registration and sandbox creation. The full sequence: - -```bash -# 1. Deploy gateway to OCP -kubectl create ns openshell -kubectl label ns openshell pod-security.kubernetes.io/enforce=privileged -kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/.../manifest.yaml -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 -kubectl rollout status statefulset/openshell -n openshell --timeout=300s - -# 2. Register gateway with CLI (mTLS certs extracted from K8s secrets) -openshell gateway add https://openshell-route.apps.cluster.example.com:443 \ - --name openshell-remote-ocp --local -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 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 -openshell provider create --name atlassian --type atlassian --from-existing -# ... GWS multi-step registration (same as local) -openshell inference set --provider vertex-local --model claude-sonnet-4-6 --no-verify -openshell sandbox create --name agent \ - --from ghcr.io/robbycochran/harness-openshell:sandbox \ - --provider github --provider vertex-local --provider atlassian --provider gws \ - --env ANTHROPIC_BASE_URL=https://inference.local \ - --env ANTHROPIC_API_KEY=sk-ant-openshell-proxy-managed \ - --env CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 \ - --upload payload:/sandbox/.config --no-git-ignore \ - --tty \ - -- bash /sandbox/.config/openshell/run.sh -``` - -### Task Agents - -For non-interactive task agents, set `task:` and `tty: false`: - -```yaml -name: standup -entrypoint: claude -task: tasks/daily-standup.md -tty: false -# ... same providers and env -``` - -When `task:` is set, the harness passes its content to the entrypoint via `-p`. - -### OpenCode - -[OpenCode](https://github.com/opencode-ai/opencode) is supported as an alternative entrypoint: - -```bash -harness up --agent opencode -``` - -Same providers and gateway -- just a different agent binary. See `profiles/agent-opencode.yaml`. - -## Multi-Document Harness YAML - -A harness YAML can bundle all definitions into a single file using `---` separators and a `kind` field (similar to Kubernetes manifests). This enables fully declarative, self-contained agent configurations: +Bundle everything in one file: ```yaml --- @@ -224,9 +85,6 @@ entrypoint: claude gateway: local providers: - profile: github - - profile: vertex -env: - ANTHROPIC_BASE_URL: https://inference.local --- kind: provider name: github @@ -241,126 +99,107 @@ type: local ``` ```bash -harness up -f harness.yaml # everything in one file +harness apply -f harness.yaml ``` -Definitions in the harness file take priority over the `profiles/` tree. Single-document agent YAMLs (without `kind`) continue to work unchanged. - -Use `harness render` to export an existing agent config as a complete harness YAML: +## Targets ```bash -harness render --include-defaults # outputs agent + gateway + all providers -harness render -o harness.yaml # write to file +harness apply # local Podman (default) +harness apply --gateway ocp # deploy to OpenShift +harness deploy ocp # deploy gateway only ``` -## Local Setup - -### Prerequisites - -- [OpenShell CLI v0.0.59+](https://github.com/NVIDIA/OpenShell) (`brew tap nvidia/openshell && brew install openshell && brew services start openshell` on macOS) -- Podman/Docker -- Go 1.23+ (only needed for building from source) - -### Credentials - -Each provider requires credentials on the host. The harness validates these inline during registration. Providers with missing credentials are skipped with an info message. +The `gateway:` field in the agent YAML or `--gateway` flag selects the target. Gateway profiles live in `profiles/gateways/`. -| Provider | Required | -|----------|----------| -| `github` | `GITHUB_TOKEN` env var | -| `vertex-local` | `gcloud auth application-default login --project ` + `ANTHROPIC_VERTEX_PROJECT_ID` + `CLOUD_ML_REGION` env vars | -| `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 `profiles/providers/` and validated inline during registration. - -### Build from Source +### OpenCode ```bash -make cli -./harness up +harness apply --agent opencode ``` -For remote OpenShift: `./harness up --gateway ocp` (requires `kubectl`, `helm`, cluster access). +Same providers and gateway, different agent binary. See `profiles/agent-opencode.yaml`. ## 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 `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 `profiles/agent-*.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. - ``` -harness CLI ──> openshell CLI ──> Gateway (Podman or K8s) - |── Provider credentials - |── L7 network policy - |── inference.local proxy - └── Sandbox container - |── claude / opencode - |── gh, mcp-atlassian, gws - └── placeholder tokens +harness CLI --> openshell CLI --> Gateway (Podman or K8s) + |-- Provider credentials + |-- L7 network policy + |-- inference.local proxy + +-- Sandbox container + |-- claude / opencode + |-- gh, mcp-atlassian, gws + +-- placeholder tokens ``` -See the [OpenShell docs](https://github.com/NVIDIA/OpenShell) for the full security model (L7 policy, Landlock, proxy credential resolution). +The harness orchestrates three OpenShell components: -## Config Files +- **Gateway** -- credential proxy and L7 network policy engine. Runs as Podman container (local) or K8s StatefulSet (remote). +- **Providers** -- credential registrations. Provider profiles in `profiles/providers/` are imported to the gateway. Missing credentials are skipped. +- **Sandbox** -- isolated container running the agent entrypoint. Credentials are proxy-managed placeholder tokens. Network egress is deny-by-default at L7. -| File | Purpose | -|------|---------| -| `profiles/agent-*.yaml` | Agent config: image, entrypoint, providers, env, optional task file | -| `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/Dockerfile` | Sandbox image: OpenShell base + MCP servers + CLI tools | -| `profiles/images/sandbox-default/policy.yaml` | Network egress rules applied to sandboxes | -| `profiles/images/sandbox-default/opencode.json` | MCP server config for OpenCode agent | +See the [OpenShell docs](https://github.com/NVIDIA/OpenShell) for the full security model. + +## Reference -## Commands +### Commands ``` -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 --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 profiles/agent-default.yaml). - --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] [--agent-profile|-f FILE] [--name SANDBOX] - Create a sandbox without deploying the gateway. - Assumes gateway is running. Auto-registers missing providers. +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"). + --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 status - Show sandbox status. - harness stop [NAME] / harness start [NAME] Stop or start a sandbox without deleting it. -harness render [--agent NAME] [--agent-profile|-f FILE] [--output|-o FILE] [--include-defaults] - Render a complete multi-document harness YAML from an agent config. - Includes all referenced providers and gateways in one file. - Built-in OpenShell providers are labeled separately from custom ones. - --include-defaults adds the effective gateway even if not set in the agent config. - harness teardown [--sandboxes] [--providers] [--k8s] Tear down resources. At least one flag required. + (Deprecated: will be replaced by 'harness delete') + +harness status + Show sandbox status. + (Deprecated: will be replaced by 'harness get agents') +``` For sandbox connect/logs, use openshell directly: - openshell sandbox connect [NAME] - openshell sandbox logs [NAME] [--follow] ``` +openshell sandbox connect [NAME] +openshell sandbox logs [NAME] [--tail] +``` + +### Config Files + +| File | Purpose | +|------|---------| +| `profiles/agent-*.yaml` | Agent config: image, entrypoint, providers, env, optional task file | +| `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 | + +### Credentials + +Each provider requires credentials on the host. Missing providers are skipped. + +| Provider | Required | +|----------|----------| +| `github` | `GITHUB_TOKEN` env var | +| `vertex-local` | `gcloud auth application-default login` + `ANTHROPIC_VERTEX_PROJECT_ID` + `CLOUD_ML_REGION` | +| `atlassian` | `JIRA_API_TOKEN` + `JIRA_URL` + `JIRA_USERNAME` | +| `gws` | `gws auth login` (OAuth via [gws CLI](https://github.com/googleworkspace/cli)) | ## Documentation Map | Document | What it is | |----------|------------| -| [SPEC.md](SPEC.md) | **Authoritative** behavior spec for the CLI -- commands, configs, payload | -| [AGENTS.md](AGENTS.md) | Contributor guide: coding principles, workaround tracking, validation modes | +| [SPEC.md](SPEC.md) | Authoritative behavior spec for the CLI | +| [AGENTS.md](AGENTS.md) | Contributor guide: coding principles, upstream conventions, validation | | [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/archive/](docs/archive/README.md) | Historical design docs | diff --git a/SPEC.md b/SPEC.md index 3905c2c..1ca9f3b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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-.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-.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/.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 @@ -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. @@ -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`) | diff --git a/cmd/create.go b/cmd/create.go deleted file mode 100644 index 4f6ef23..0000000 --- a/cmd/create.go +++ /dev/null @@ -1,111 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "time" - - "github.com/robbycochran/harness-openshell/internal/agent" - "github.com/robbycochran/harness-openshell/internal/gateway" - "github.com/robbycochran/harness-openshell/internal/status" - "github.com/spf13/cobra" -) - -func NewCreateCmd(harnessDir, cli string) *cobra.Command { - var ( - agentName 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 'openshell sandbox connect' afterward.", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 && sandboxName == "" { - sandboxName = args[0] - } - - harness, err := resolveHarness(harnessDir, agentName, agentProfile) - if err != nil { - return err - } - agentCfg := harness.Agent - - gw := gateway.New(cli) - - // 1. Check which gateway is active. - activeGW, err := activeGatewayInfo(gw) - if err != nil { - return err - } - - status.Header("Gateway") - status.OKf("%s (%s)", activeGW.Name, activeGW.Endpoint) - name := agentCfg.Name - if sandboxName != "" { - name = sandboxName - } - - sandboxImage := resolveSandboxImage(agentCfg.Image) - - status.Header("Agent") - status.Infof("Name: %s", name) - status.Infof("Image: %s", sandboxImage) - - // 3. Ensure providers are registered - registered := ensureProviders(harnessDir, gw, agentCfg, false, harness) - - // 4. Deploy the sandbox - status.Header("Sandbox") - 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, harnessDir, payloadDir); err != nil { - return fmt.Errorf("rendering payload: %w", err) - } - - return createSandbox(sandboxOpts{ - harnessDir: harnessDir, - gw: gw, - name: name, - image: sandboxImage, - providers: registered, - noTTY: true, - retrySleep: 5 * time.Second, - sandboxCmd: []string{"true"}, - payloadDir: payloadDir, - env: agentCfg.BuildEnvMap(), - onSuccess: func(n string) { - fmt.Println() - status.OKf("Sandbox created: %s — connect with: openshell sandbox connect %s", n, n) - }, - }) - }, - } - - cmd.Flags().StringVar(&agentName, "agent", "default", "Agent config name (from profiles/agent-.yaml)") - 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 -} - -func activeGatewayInfo(gw gateway.Gateway) (*gateway.GatewayInfo, error) { - gateways, err := gw.GatewayList() - if err != nil { - return nil, fmt.Errorf("could not list gateways: %w — deploy one first: harness deploy", err) - } - for _, g := range gateways { - if g.Active { - return &g, nil - } - } - return nil, fmt.Errorf("no active gateway — deploy one first: harness deploy") -} - - diff --git a/cmd/create_test.go b/cmd/create_test.go deleted file mode 100644 index 3d0736a..0000000 --- a/cmd/create_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "strings" - "testing" - - "github.com/robbycochran/harness-openshell/internal/gateway" -) - -func TestActiveGatewayInfo_ListError(t *testing.T) { - gw := &mockGW{} - - _, err := activeGatewayInfo(gw) - if err == nil { - t.Fatal("expected error when no active gateway") - } - if !strings.Contains(err.Error(), "no active gateway") { - t.Errorf("error = %q, want 'no active gateway'", err) - } -} - -func TestActiveGatewayInfo_RemoteGateway(t *testing.T) { - gw := &mockGW{ - gatewayListResult: []gateway.GatewayInfo{ - {Name: "openshell-remote-ocp", Endpoint: "https://gateway.apps.ocp.example.com:443", Active: true}, - }, - } - - info, err := activeGatewayInfo(gw) - if err != nil { - t.Fatalf("activeGatewayInfo: %v", err) - } - if info.Name != "openshell-remote-ocp" { - t.Errorf("Name = %q, want openshell-remote-ocp", info.Name) - } -} - - diff --git a/cmd/executor.go b/cmd/executor.go new file mode 100644 index 0000000..0b0d306 --- /dev/null +++ b/cmd/executor.go @@ -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(), + }) +} diff --git a/cmd/up_test.go b/cmd/executor_test.go similarity index 79% rename from cmd/up_test.go rename to cmd/executor_test.go index bf1e6d6..dd470e3 100644 --- a/cmd/up_test.go +++ b/cmd/executor_test.go @@ -135,37 +135,6 @@ func TestUpLocal_SandboxCreateRetry(t *testing.T) { } } -func TestActiveGatewayInfo_NoGateway(t *testing.T) { - gw := &mockGW{} - - _, err := activeGatewayInfo(gw) - if err == nil { - t.Fatal("expected error when no gateway is active") - } - if !strings.Contains(err.Error(), "no active gateway") { - t.Errorf("error = %q, want 'no active gateway'", err) - } -} - -func TestActiveGatewayInfo_LocalGateway(t *testing.T) { - gw := &mockGW{ - gatewayListResult: []gateway.GatewayInfo{ - {Name: "local", Endpoint: "127.0.0.1:17670", Active: true}, - }, - } - - info, err := activeGatewayInfo(gw) - if err != nil { - t.Fatalf("activeGatewayInfo: %v", err) - } - if info.Name != "local" { - t.Errorf("Name = %q, want local", info.Name) - } - if !strings.Contains(info.Endpoint, "127.0.0.1") { - t.Errorf("Endpoint = %q, want 127.0.0.1", info.Endpoint) - } -} - func TestUpLocal_SandboxCreateOpts(t *testing.T) { t.Setenv("HARNESS_OS_IMAGE", "") dir := setupTestAgent(t) @@ -224,7 +193,7 @@ func TestUpLocal_EnsureLocal_DeploysGateway(t *testing.T) { } } -func TestResolveAgentConfig_EmbeddedFallback(t *testing.T) { +func TestResolveHarness_EmbeddedFallback(t *testing.T) { dir := t.TempDir() DefaultAgentConfig = []byte(`name: embedded-default entrypoint: claude @@ -233,16 +202,16 @@ providers: `) t.Cleanup(func() { DefaultAgentConfig = nil }) - cfg, err := resolveAgentConfig(dir, "default", "") + h, err := resolveHarness(dir, "default", "") if err != nil { - t.Fatalf("resolveAgentConfig: %v", err) + t.Fatalf("resolveHarness: %v", err) } - if cfg.Name != "embedded-default" { - t.Errorf("Name = %q, want embedded-default", cfg.Name) + if h.Agent.Name != "embedded-default" { + t.Errorf("Name = %q, want embedded-default", h.Agent.Name) } } -func TestResolveAgentConfig_DiskOverridesEmbedded(t *testing.T) { +func TestResolveHarness_DiskOverridesEmbedded(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "agent-default.yaml"), []byte(`name: disk-agent entrypoint: claude @@ -257,16 +226,16 @@ providers: `) t.Cleanup(func() { DefaultAgentConfig = nil }) - cfg, err := resolveAgentConfig(dir, "default", "") + h, err := resolveHarness(dir, "default", "") if err != nil { - t.Fatalf("resolveAgentConfig: %v", err) + t.Fatalf("resolveHarness: %v", err) } - if cfg.Name != "disk-agent" { - t.Errorf("Name = %q, want disk-agent (disk should override embedded)", cfg.Name) + if h.Agent.Name != "disk-agent" { + t.Errorf("Name = %q, want disk-agent (disk should override embedded)", h.Agent.Name) } } -func TestResolveAgentConfig_ExplicitFileNoFallback(t *testing.T) { +func TestResolveHarness_ExplicitFileNoFallback(t *testing.T) { dir := t.TempDir() DefaultAgentConfig = []byte(`name: embedded-default entrypoint: claude @@ -275,13 +244,13 @@ providers: `) t.Cleanup(func() { DefaultAgentConfig = nil }) - _, err := resolveAgentConfig(dir, "default", "/nonexistent/agent.yaml") + _, err := resolveHarness(dir, "default", "/nonexistent/agent.yaml") if err == nil { t.Fatal("expected error for explicit nonexistent --file, should not fall back to embedded") } } -func TestResolveAgentConfig_NonDefaultNameNoFallback(t *testing.T) { +func TestResolveHarness_NonDefaultNameNoFallback(t *testing.T) { dir := t.TempDir() DefaultAgentConfig = []byte(`name: embedded-default entrypoint: claude @@ -290,7 +259,7 @@ providers: `) t.Cleanup(func() { DefaultAgentConfig = nil }) - _, err := resolveAgentConfig(dir, "research", "") + _, err := resolveHarness(dir, "research", "") if err == nil { t.Fatal("expected error for --agent research when file doesn't exist, should not fall back to embedded") } diff --git a/cmd/render.go b/cmd/render.go deleted file mode 100644 index da37f9e..0000000 --- a/cmd/render.go +++ /dev/null @@ -1,103 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/robbycochran/harness-openshell/internal/agent" - "github.com/robbycochran/harness-openshell/internal/status" - "github.com/spf13/cobra" -) - -func NewRenderCmd(harnessDir, cli string) *cobra.Command { - var ( - agentName string - agentProfile string - outputFile string - includeDefaults bool - ) - - cmd := &cobra.Command{ - Use: "render [flags]", - Short: "Render a complete harness YAML from an agent config", - Long: "Reads an agent config and its referenced providers/gateways, then outputs a single multi-document YAML with all definitions included. Built-in OpenShell provider profiles are labeled separately from custom ones.", - RunE: func(cmd *cobra.Command, args []string) error { - h, err := resolveHarness(harnessDir, agentName, agentProfile) - if err != nil { - return err - } - - // Collect built-in provider profiles from the profiles/providers/ directory - builtinProviders := loadProviderProfiles(harnessDir) - - // Collect gateway profiles referenced by the agent - gwName := h.Agent.Gateway - if gwName == "" && includeDefaults { - gwName = "local" - } - if gwName != "" && len(h.Gateways) == 0 { - gwData := loadGatewayProfile(harnessDir, gwName) - if gwData != nil { - h.Gateways[gwName] = gwData - } - } - - out, err := agent.RenderHarness(h, builtinProviders) - if err != nil { - return fmt.Errorf("rendering harness: %w", err) - } - - if outputFile != "" { - if err := os.WriteFile(outputFile, out, 0o644); err != nil { - return fmt.Errorf("writing output: %w", err) - } - status.OKf("Rendered to %s", outputFile) - } else { - fmt.Print(string(out)) - } - return nil - }, - } - - cmd.Flags().StringVar(&agentName, "agent", "default", "Agent config name") - cmd.Flags().StringVarP(&agentProfile, "agent-profile", "f", "", "Path to agent YAML file") - cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Write to file instead of stdout") - cmd.Flags().BoolVar(&includeDefaults, "include-defaults", false, "Include default gateway even if not set in agent config") - - return cmd -} - -func loadProviderProfiles(harnessDir string) map[string][]byte { - profiles := make(map[string][]byte) - dir := filepath.Join(harnessDir, "profiles", "providers") - entries, err := os.ReadDir(dir) - if err != nil { - return profiles - } - for _, e := range entries { - if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { - continue - } - name := e.Name()[:len(e.Name())-5] // strip .yaml - data, err := os.ReadFile(filepath.Join(dir, e.Name())) - if err == nil { - profiles[name] = data - } - } - return profiles -} - -func loadGatewayProfile(harnessDir, name string) []byte { - // Try profiles/gateways/.yaml - path := filepath.Join(harnessDir, "profiles", "gateways", name+".yaml") - data, err := os.ReadFile(path) - if err == nil { - return data - } - // Try embedded - if d, ok := EmbeddedGatewayProfiles[name]; ok { - return d - } - return nil -} diff --git a/cmd/resolve.go b/cmd/resolve.go index 9c040e5..2be1871 100644 --- a/cmd/resolve.go +++ b/cmd/resolve.go @@ -73,14 +73,6 @@ func resolveHarness(harnessDir, agentName, agentFile string) (*agent.Harness, er return agent.ParseHarness(DefaultAgentConfig) } -func resolveAgentConfig(harnessDir, agentName, agentFile string) (*agent.AgentConfig, error) { - h, err := resolveHarness(harnessDir, agentName, agentFile) - if err != nil { - return nil, err - } - return h.Agent, nil -} - func resolveGatewayConfigWithHarness(harnessDir, name string, h *agent.Harness) (*gateway.GatewayConfig, error) { if h != nil { if data, ok := h.Gateways[name]; ok { @@ -135,3 +127,35 @@ func resolveGatewayConfigFromFile(path string) (*gateway.GatewayConfig, error) { cfg.Dir = filepath.Dir(path) return cfg, nil } + +func loadProviderProfiles(harnessDir string) map[string][]byte { + profiles := make(map[string][]byte) + dir := filepath.Join(harnessDir, "profiles", "providers") + entries, err := os.ReadDir(dir) + if err != nil { + return profiles + } + for _, e := range entries { + if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { + continue + } + name := e.Name()[:len(e.Name())-5] + data, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err == nil { + profiles[name] = data + } + } + return profiles +} + +func loadGatewayProfile(harnessDir, name string) []byte { + path := filepath.Join(harnessDir, "profiles", "gateways", name+".yaml") + data, err := os.ReadFile(path) + if err == nil { + return data + } + if d, ok := EmbeddedGatewayProfiles[name]; ok { + return d + } + return nil +} diff --git a/cmd/up.go b/cmd/up.go deleted file mode 100644 index bc88d83..0000000 --- a/cmd/up.go +++ /dev/null @@ -1,196 +0,0 @@ -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" - "github.com/spf13/cobra" -) - -func NewUpCmd(harnessDir, cli string) *cobra.Command { - var ( - gatewayName string - gatewayProfile string - agentName string - agentProfile string - sandboxName string - noTTY bool - providerRefresh bool - ) - - cmd := &cobra.Command{ - Use: "up [flags]", - Short: "Deploy gateway, register providers, and create a sandbox", - Long: "Deploy gateway and register providers if needed, then render an agent config into a running sandbox.", - RunE: func(cmd *cobra.Command, args []string) error { - if gatewayName != "" && gatewayProfile != "" { - return fmt.Errorf("--gateway and --gateway-profile are mutually exclusive") - } - if len(args) > 0 && sandboxName == "" { - sandboxName = args[0] - } - - harness, err := resolveHarness(harnessDir, agentName, agentProfile) - if err != nil { - return err - } - agentCfg := harness.Agent - agentPath := resolveAgentPath(harnessDir, agentName, agentProfile) - - gw := gateway.New(cli) - if err := gw.CheckMinVersion("0.0.59"); err != nil { - status.Warn(fmt.Sprintf("OpenShell version: %v", err)) - } - - var gwCfg *gateway.GatewayConfig - gwTarget := gatewayName - if gatewayProfile != "" { - gwCfg, err = resolveGatewayConfigFromFile(gatewayProfile) - if err != nil { - return err - } - gwTarget = gwCfg.Gateway.Type - } else { - if gwTarget == "" { - if agentCfg.Gateway != "" { - gwTarget = agentCfg.Gateway - } else { - gwTarget = "local" - } - } - gwCfg, _ = resolveGatewayConfigWithHarness(harnessDir, gwTarget, harness) - } - isRemote := gwTarget != "local" - - return upLocal(upLocalOpts{ - harnessDir: harnessDir, - gw: gw, - gwCfg: gwCfg, - ensureLocal: !isRemote, - agentCfg: agentCfg, - agentPath: agentPath, - sandboxName: sandboxName, - noTTY: noTTY, - providerRefresh: providerRefresh, - harness: harness, - retrySleep: 5 * time.Second, - }) - }, - } - - cmd.Flags().StringVar(&gatewayName, "gateway", "", "Gateway profile name (local, kind, ocp)") - cmd.Flags().StringVar(&gatewayProfile, "gateway-profile", "", "Path to gateway profile YAML (overrides --gateway)") - cmd.Flags().StringVar(&agentName, "agent", "default", "Agent config name (from profiles/agent-.yaml)") - cmd.Flags().StringVarP(&agentProfile, "agent-profile", "f", "", "Path to agent YAML file (overrides --agent)") - cmd.Flags().StringVar(&sandboxName, "name", "", "Sandbox name (overrides agent config)") - cmd.Flags().BoolVar(&noTTY, "no-tty", false, "Non-interactive mode (for testing)") - cmd.Flags().BoolVar(&providerRefresh, "provider-refresh", false, "Delete and recreate all providers") - - return cmd -} - -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 - - // 1. Parse agent config - 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) - - // Top-level context - status.Infof("Agent: %s (%s)", sandboxName, filepath.Base(opts.agentPath)) - status.Infof("Image: %s", sandboxImage) - if agentCfg.Task != "" { - status.Infof("Task: %s", agentCfg.Task) - } - - // 2. Ensure gateway - 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) - } - } - - // 3. Ensure providers needed by the agent are registered - registered := ensureProviders(opts.harnessDir, gw, agentCfg, opts.providerRefresh, opts.harness) - - // 4. Render payload - 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) - } - - // 5. Create sandbox - 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(), - }) -} - -var Version = "dev" - -// DefaultAgentConfig holds the embedded default agent YAML, set from main.go. -var DefaultAgentConfig []byte - diff --git a/main.go b/main.go index 567e5d3..4c02cf7 100644 --- a/main.go +++ b/main.go @@ -60,7 +60,6 @@ func main() { } root.CompletionOptions.HiddenDefaultCmd = true - // Primary commands root.AddCommand( cmd.NewApplyCmd(harnessDir, cli), cmd.NewDeployCmd(harnessDir, cli), @@ -68,23 +67,14 @@ func main() { cmd.NewStartCmd(harnessDir, cli), ) - // Deprecated aliases (hidden, print warning) - upCmd := cmd.NewUpCmd(harnessDir, cli) - upCmd.Hidden = true - upCmd.Deprecated = "use 'harness apply --attach' instead" - createCmd := cmd.NewCreateCmd(harnessDir, cli) - createCmd.Hidden = true - createCmd.Deprecated = "use 'harness apply' instead" + // Deprecated aliases (kept until get/delete commands ship) teardownCmd := cmd.NewTeardownCmd(harnessDir, cli) teardownCmd.Hidden = true - teardownCmd.Deprecated = "use 'harness delete' instead" + teardownCmd.Deprecated = "will be replaced by 'harness delete' in a future release" statusCmd := cmd.NewStatusCmd(harnessDir, cli) statusCmd.Hidden = true - statusCmd.Deprecated = "use 'harness get agents' instead" - renderCmd := cmd.NewRenderCmd(harnessDir, cli) - renderCmd.Hidden = true - renderCmd.Deprecated = "use 'harness apply -o yaml' instead" - root.AddCommand(upCmd, createCmd, teardownCmd, statusCmd, renderCmd) + statusCmd.Deprecated = "will be replaced by 'harness get agents' in a future release" + root.AddCommand(teardownCmd, statusCmd) if err := root.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err)