diff --git a/.agents/skills/validate/SKILL.md b/.agents/skills/validate/SKILL.md index 626d8da..980d9e5 100644 --- a/.agents/skills/validate/SKILL.md +++ b/.agents/skills/validate/SKILL.md @@ -83,7 +83,7 @@ gh run list --branch $(git branch --show-current) --limit 3 make test-suite ``` -Runs 27+ config parsing, output format, env resolution, and CLI flag tests. +Runs 33 config parsing, output format, env resolution, and CLI flag tests. No gateway needed for most tests. Skip if `harness` binary not built. ### 10. Config test suite (live) @@ -107,12 +107,12 @@ in both README.md and SPEC.md. No stale command references should exist. grep 'cmd.New.*Cmd' main.go | grep -v Hidden | grep -v Deprecated # Check README references all primary commands -for cmd in apply get describe deploy stop start; do +for cmd in apply get describe delete deploy doctor init migrate plan; do grep -q "harness $cmd" README.md && echo "README: $cmd OK" || echo "README: $cmd MISSING" done # Check SPEC references all primary commands -for cmd in apply get describe deploy stop start; do +for cmd in apply get describe delete deploy doctor init migrate plan; do grep -q "harness $cmd" SPEC.md && echo "SPEC: $cmd OK" || echo "SPEC: $cmd MISSING" done @@ -132,14 +132,14 @@ Report a summary table: Validation Results ------------------ Build: PASS - Unit tests: PASS (6 packages) + Unit tests: PASS (16 packages) Vet: PASS Local (full): PASS (22/22) Local (CI): PASS (14/14) OCP: PASS (10/10) Kind: SKIP (kind not installed) CI: GREEN (3/3 workflows) - Config suite: PASS (27/27, 3 skipped) + Config suite: PASS (33/33, 0 skipped) Config live: PASS (35/35, 3 skipped) Docs: PASS (all commands documented, no stale refs) ``` diff --git a/AGENTS.md b/AGENTS.md index 860cba8..78639ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -165,9 +165,10 @@ See `make help` for the full list. The test entry points: Or directly: ```bash -./test/test-flow.sh local # default mode -./test/test-flow.sh local --ci # ci mode -./test/test-flow.sh kind --ci # used in GitHub Actions +./test/test-flow.sh local-container # local Podman (see: make test-local) +./test/test-flow.sh local-container --ci # ci mode (no providers, ci-agent.yaml) +./test/test-flow.sh openshift # OCP (see: make test-remote) +./test/kind-lifecycle.sh # kind cluster (see: make test-kind, used in CI) ``` ### Default mode requirements diff --git a/CHANGELOG.md b/CHANGELOG.md index 5372013..20b3202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [0.3.0] - 2026-06-17 + +### Added +- Kubectl-style CLI: `harness apply` with `--dry-run` and `-o yaml`, plus `get`, `describe`, and `delete` commands +- Multi-document harness YAML (`---` separated agent/provider/gateway/policy docs) +- `harness init` and `harness doctor` commands +- `kind: config` embeds sandbox files directly in harness YAML +- `repo` field with `base_agent` inheritance and an inference warning +- Cloned repos cached in `~/.cache/harness-openshell/repos/` +- Configuration test suite with multi-config and free-API support +- Headless tasks, policy applied via the CLI, and multi-upload payloads +- Gateway profile auto-discovery and a configurable profile directory + +### Changed +- Deprecated commands removed; docs rewritten around the apply-first CLI +- Gateway profiles renamed +- Container registry switched to `quay.io/rcochran/openshell` +- Repository moved to `stackrox/harness-openshell`; CodeRabbit review config added + +### Fixed +- OpenCode config (`ANTHROPIC_BASE_URL`, MCP format, policy) + ## [0.2.0] - 2026-06-13 ### Added @@ -39,6 +61,6 @@ - `docs/proto-migration.md` (stale, never executed) - Stale TOML references and completed TODO items -## [0.1.2] - 2026-06-09 +## [0.1.2] - 2026-06-08 Initial Go rewrite release with full CLI, provider registration, and multi-target deployment. diff --git a/README.md b/README.md index 9837ebc..7d9dcd8 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,8 @@ Or build the harness from source: `make cli` | `harness get agents\|providers\|gateways` | List resources | | `harness describe ` | Sandbox details | | `harness delete [--all]` | Tear down | +| `harness plan -f FILE` | Read-only reconciliation plan (mutates nothing) | +| `harness migrate -f FILE` | Convert a legacy v1 config to v1alpha1 | ### Credentials @@ -229,9 +231,9 @@ Each provider discovers credentials from the host. Missing providers are skipped Tested on macOS (arm64) with Podman. Linux support is expected but not yet validated. ```bash -make test # vet + unit tests (5 packages) +make test # vet + unit tests (16 packages) make lint # golangci-lint -make test-suite # config parsing (23 tests, no gateway needed) +make test-suite # config parsing (33 tests, no gateway needed) make test-local # full e2e on local Podman (22 tests) make test-kind # self-contained kind cluster lifecycle make test-remote # full e2e on OCP (needs KUBECONFIG) diff --git a/SPEC.md b/SPEC.md index 3594098..ad6bce7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -9,9 +9,9 @@ The harness deploys and manages AI agent sandboxes on three targets: - **helm** -- Kubernetes pods via a k8s cluster (NodePort access) - **openshift** -- Kubernetes pods via an OpenShift-hosted OpenShell gateway (Route access) -Each sandbox is an isolated container running an agent entrypoint (Claude Code or OpenCode), with credential providers, network policies, and a rendered payload (run.sh, task.md). +Each sandbox is an isolated container running an agent entrypoint (e.g. Claude Code, OpenCode, or Codex; `bash` or any binary on PATH also works), with credential providers, network policies, and a rendered payload (`task.md` and a `bin/` directory). -Requires OpenShell v0.0.59+. +Requires OpenShell v0.0.110+. ## Agent Config @@ -38,8 +38,8 @@ env: Fields: - `name` (required) -- sandbox name, used for `openshell sandbox connect` - `base_agent` -- name of a base agent config to inherit from (e.g., `default` resolves `agent-default.yaml`). Providers, env, and payloads are merged additively; scalar fields (entrypoint, gateway, repo, task, image, policy) from the overlay win when non-empty. -- `image` -- container image for the sandbox (default: version-matched from ghcr.io, override with `HARNESS_OS_IMAGE` env) -- `entrypoint` -- command to run (default: `claude`). Supports `claude`, `opencode`, `bash`, or any binary on PATH. +- `image` -- container image for the sandbox (default: version-matched from `quay.io/rcochran/openshell`, override with `HARNESS_OS_IMAGE` env) +- `entrypoint` -- command to run (default: `claude`). Supports `claude`, `codex`, `opencode`, `bash`, or any binary on PATH. - `tty` -- enable TTY (default: true) - `repo` -- git URL to clone outside the sandbox and upload to `/sandbox/`. Shallow clone (`--depth 1`) with submodules. Git credentials never enter the sandbox unless needed. - `repo_ref` -- branch, tag, or ref to clone (default: HEAD). Passed as `--branch` to git clone. @@ -86,7 +86,7 @@ Primary command. Resolves an agent config, deploys the gateway and providers, cr 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. +3. **Check version** -- warn if openshell CLI is below v0.0.110. 4. **Resolve gateway** -- `--gateway` selects a profile by name; `--gateway-profile` loads from a file path. Default: `local-container`. `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). @@ -94,7 +94,7 @@ Primary command. Resolves an agent config, deploys the gateway and providers, cr - **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. -8. **Render payload** -- `run.sh` (entrypoint wrapper with PATH setup, entrypoint validation, `-p` task), `task.md` (if set). +8. **Render payload** -- `task.md` (if set) and a `bin/` directory. The in-sandbox command is built by the agent adapter (`internal/agent/adapter.go`) as a `bash -lc` invocation (PATH setup, entrypoint validation via `command -v`), not a `run.sh` file. Task dispatch depends on mode and entrypoint: headless (default) uses `opencode run "$(cat task.md)"` for OpenCode and `--print "$(cat task.md)"` for claude/codex/custom entrypoints; interactive (`--attach`) uses `-p "$(cat task.md)"`. 9. **Create sandbox** -- `openshell sandbox create` with `--env` (env vars), `--upload` (payload), and startup command. Retry up to 5 times. Default is non-interactive (headless). Use `--attach` for TTY mode. @@ -121,10 +121,26 @@ Show detailed status for a specific sandbox: phase, active gateway, and register Delete sandboxes by name, or use flags for bulk operations. `--all` deletes sandboxes, providers, and k8s resources. Reuses the same teardown functions as the old `teardown` command. +### `harness init [-o FILE] [--force] [--non-interactive]` + +Generate a `harness.yaml` config file. Interactive by default (prompts for entrypoint, providers, and gateway target); `--non-interactive` writes the embedded default. Writes to `harness.yaml` unless `-o` overrides the path. + +### `harness doctor [-f FILE] [--agent NAME] [--gateway NAME] [-o table|json|yaml]` + +Validate the environment for a configured sandbox. Phase 1 (offline) checks the openshell binary, target dependencies, and provider credentials without a running gateway; Phase 2 (online) checks provider registration when the gateway is reachable. + ### `harness deploy ` Deploy or verify the gateway for a target. Reads `profiles/gateways/.yaml`. +### `harness plan -f FILE [--gateway NAME] [-o table|json|yaml]` + +Read-only reconciliation plan. Shows the actions `harness apply` would take without mutating anything. Distinct from `apply --dry-run`, which is a separate legacy path. + +### `harness migrate -f FILE [-o FILE]` + +Convert a legacy v1 harness config to the v1alpha1 format. The input YAML is normalized and written as v1alpha1 to stdout (or `-o FILE`). Fields with no v1alpha1 home (`task`, `include`, inline policy documents, unresolved `base_agent`) are reported as warnings on stderr. + ### Deprecated Aliases These commands still work but will be removed in a future release: @@ -189,9 +205,8 @@ The harness renders agent config into a self-contained payload uploaded to `/san ``` openshell/ - run.sh -- validates entrypoint, execs it (with -p task if set) task.md -- task file with envsubst applied (if task: is set) - bin/ -- wrapper scripts + bin/ -- payload binaries prepended to PATH ``` -Environment variables are injected directly via `--env KEY=VALUE` flags on `openshell sandbox create` -- no file upload needed for env vars. `run.sh` is the entrypoint for interactive mode. +Environment variables are injected directly via `--env KEY=VALUE` flags on `openshell sandbox create` -- no file upload needed for env vars. The in-sandbox command (entrypoint validation and exec, with `-p`/`--print` task) is built by the agent adapter and wrapped in `bash -lc`; there is no `run.sh` file. diff --git a/docs/release-plan.md b/docs/release-plan.md index 51025ae..56bca7b 100644 --- a/docs/release-plan.md +++ b/docs/release-plan.md @@ -1,5 +1,15 @@ # Release Plan: CI → Embed → GoReleaser +> **Historical plan.** This records the original release roadmap; Phase 0 shipped. +> Some later details diverged from what shipped and are kept here for provenance, +> not as current behavior: +> - `harness init` generates a `harness.yaml` (interactive, or `--non-interactive` +> for the embedded default) at an output path — it does **not** extract the +> embedded FS to `~/.openshell/harness/`. See `cmd/init_cmd.go`. +> - The on-disk fallback harness directory is `~/.config/harness-openshell` +> (overridable via `HARNESS_OS_DIR` / `HARNESS_PROFILE_DIR`), not +> `~/.openshell/harness/`. See `detectHarnessDir()` in `main.go`. + ## Phase 0: CI (done) GitHub Actions for every PR and push to main. diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 9d3729f..68c2ee9 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -246,13 +246,13 @@ func ParseHarness(data []byte) (*Harness, error) { return nil, fmt.Errorf("document %d: parsing payload: %w", docIndex, err) } if doc.SandboxPath == "" { - return nil, fmt.Errorf("document %d: kind: payload requires a sandbox_path field", docIndex) + return nil, fmt.Errorf("document %d: kind: %s requires a sandbox_path field", docIndex, header.Kind) } if doc.Content == "" && doc.LocalPath == "" { - return nil, fmt.Errorf("document %d: kind: payload requires content or local_path field", docIndex) + return nil, fmt.Errorf("document %d: kind: %s requires content or local_path field", docIndex, header.Kind) } if doc.Content != "" && doc.LocalPath != "" { - return nil, fmt.Errorf("document %d: kind: payload cannot have both content and local_path", docIndex) + return nil, fmt.Errorf("document %d: kind: %s cannot have both content and local_path", docIndex, header.Kind) } h.Payloads = append(h.Payloads, PayloadEntry{ SandboxPath: doc.SandboxPath, diff --git a/internal/openshell/sdkclient/auth.go b/internal/openshell/sdkclient/auth.go index 1ac93bf..c700750 100644 --- a/internal/openshell/sdkclient/auth.go +++ b/internal/openshell/sdkclient/auth.go @@ -18,7 +18,7 @@ type EnvLookup func(string) string type connBranch int const ( - branchDefault connBranch = iota // none/plaintext/cloudflare_jwt/oidc-human → gateway.NewClient(name) + branchDefault connBranch = iota // none/plaintext/cloudflare_jwt/oidc (no client secret) → gateway.NewClient(name) branchMTLS // WithAuth(NoAuth()) + WithTLS(certs derived from cfg.Dir) branchSAOIDC // oidc + OPENSHELL_OIDC_CLIENT_SECRET present )