From 54c174f6d54b41d81237407f6e6a512bfce693ae Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 3 Sep 2026 21:25:14 -0700 Subject: [PATCH 1/3] docs: Add gateway context switching design document --- docs/designs/gateway-context-switching.md | 148 ++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/designs/gateway-context-switching.md diff --git a/docs/designs/gateway-context-switching.md b/docs/designs/gateway-context-switching.md new file mode 100644 index 0000000..dc1e7c4 --- /dev/null +++ b/docs/designs/gateway-context-switching.md @@ -0,0 +1,148 @@ +# Plan: Switch a Harness between OpenShell and HyperShell + +Status: proposed + +## Goal + +Run the same Harness file with three operating contexts: + +```text +local OpenShell +HyperShell with personally managed access +HyperShell with a constrained service account +``` + +The first version selects only the gateway, workspace, and connection method. +It does not move credentials or define security policy. HyperShell guardrails +come from the service account's workspace membership and gateway-managed +resources, not from Context behavior. + +CI is an execution environment. It may use the local Context with an ephemeral +gateway, or the service-account Context when a runner can reach HyperShell. + +## User experience + +```bash +harness apply -f test/ci-workflow.yaml --context contexts/local.yaml +harness apply -f test/ci-workflow.yaml --context contexts/hypershell-personal.yaml +harness apply -f test/ci-workflow.yaml --context contexts/hypershell-service-account.yaml +``` + +The local Context uses the active OpenShell gateway. The personal HyperShell +Context uses a gateway registration authenticated by the user through the +normal OpenShell login flow. The service-account Context uses the existing +direct OIDC client-credentials path with secret material supplied externally. + +Existing commands without `--context` continue to work. + +## Context file + +A Context contains one existing Harness `Target`. No new target model or +templating language is introduced. + +Personal HyperShell uses a normal named gateway registration: + +```yaml +apiVersion: harness.openshell.dev/v1alpha1 +kind: Context +metadata: + name: hypershell-personal +spec: + target: + gateway: hypershell + workspace: personal +``` + +The service-account Context uses direct, non-persistent connection metadata: + +```yaml +apiVersion: harness.openshell.dev/v1alpha1 +kind: Context +metadata: + name: hypershell-service-account +spec: + target: + workspace: controlled + registration: + endpoint: ${HYPERSHELL_GATEWAY} + oidc: + issuer: ${HYPERSHELL_OIDC_ISSUER} + clientId: ${HYPERSHELL_SANDBOX_SA_ID} + audience: ${HYPERSHELL_OIDC_AUDIENCE} +``` + +The workspace names above are examples. They are the main place to distinguish +personally managed provider state from a centrally managed, narrower service +account environment. + +`contexts/local.yaml` contains an empty target and therefore uses normal +OpenShell active-gateway resolution: + +```yaml +apiVersion: harness.openshell.dev/v1alpha1 +kind: Context +metadata: + name: local +spec: + target: {} +``` + +Environment interpolation uses the resolver already used by Harness files. +`OPENSHELL_OIDC_CLIENT_SECRET` stays in the process environment and is never +part of a Context, rendered Harness, or structured output. + +## Resolution + +When `--context` is present: + +1. Parse the Harness and Context strictly. +2. Replace the Harness `spec.target` with the Context target. +3. Expand environment references. +4. Apply existing target precedence: explicit CLI flag, then `OPENSHELL_*` + environment, then the resolved target, then the active/default gateway. +5. Use that one resolved object for dry-run output and execution. + +The Context target is replaced as a whole. Field-by-field merging is not +supported. + +## Implementation + +1. Add a strict `Context` config type containing only metadata and target. +2. Add `--context FILE` to `harness apply`. +3. Load the Context in `loadWorkflow` and replace `spec.target` before normal + environment and target resolution. +4. Add the three example Context files. +5. Run the existing smoke Harness through: + + - a developer's active local gateway; + - a user-authenticated HyperShell registration; + - HyperShell using the existing service-account VPN/OIDC setup. + +## Acceptance + +- The unchanged `test/ci-workflow.yaml` returns exactly + `canonical-sdk-ok` in all three contexts. +- `keep: false` removes the sandbox after every run. +- `harness apply -f file.yaml` behaves exactly as before. +- Invalid or incomplete Context files fail before gateway access. +- Dry-run structured output contains the resolved non-secret target and no + credential values. +- The personal Context relies on OpenShell's existing login and token refresh. +- The service-account Context contains no client secret and fails clearly + before sandbox creation when its OIDC issuer is unreachable off VPN. +- The selected service account cannot exceed the workspace role and gateway + resources assigned to it; Context does not claim to enforce those controls. + +## Not in this version + +- Provider or model aliases. +- Provider creation or credential bootstrap. +- Inference, image, policy, or agent overlays. +- Context discovery, inheritance, merging, or conditionals. +- Inline secrets. +- Managed HyperShell execution from public GitHub runners until network access + exists. +- NemoClaw integration. + +Those should be considered only after this target-only switch is useful in +practice. From e8d75b29d815536f3edcd7b7e068c7a63bd6a74f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 3 Sep 2026 21:26:09 -0700 Subject: [PATCH 2/3] docs: Document explicit sandbox image semantics and precedence rules Add comprehensive documentation to resolveSandboxImage and versionedImage functions, making the image selection precedence explicit: 1. HARNESS_OS_IMAGE environment variable (operator override) 2. agentImage parameter (workflow spec.sandbox.image) 3. versionedImage() default (version-stamped fallback) This clarifies the semantics for sandbox image resolution across all execution contexts: local OpenShell, HyperShell personal access, and HyperShell service-account modes. Documents the Agent Runtime Contract (ARC) requirements that resolved images must satisfy. --- cmd/sandbox_image.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cmd/sandbox_image.go b/cmd/sandbox_image.go index 4ffb94b..563047a 100644 --- a/cmd/sandbox_image.go +++ b/cmd/sandbox_image.go @@ -4,16 +4,45 @@ import "os" var Version = "dev" +// resolveSandboxImage determines which container image to use for sandbox execution. +// +// Image selection follows explicit precedence (highest to lowest): +// 1. HARNESS_OS_IMAGE environment variable - operator override for all contexts +// 2. agentImage parameter - workflow spec.sandbox.image explicit field +// 3. versionedImage("sandbox") - versioned default base image +// +// This precedence enables: +// - Local development and debugging via environment variable +// - Explicit per-workflow image overrides +// - Consistent versioned defaults across contexts (local OpenShell, HyperShell) +// +// The resolved image must satisfy the Agent Runtime Contract (ARC): +// - Run as unprivileged 'sandbox' user +// - Provide writable Python virtualenv at /opt/agent/venv +// - Maintain standard PATH conventions for agent tools +// - Support multi-context execution (local, HyperShell personal, service-account) +>>>>>>> 0de49bb (docs: Document explicit sandbox image semantics and precedence rules) func resolveSandboxImage(agentImage string) string { + // 1. Environment override (highest priority) if envImage := os.Getenv("HARNESS_OS_IMAGE"); envImage != "" { return envImage } + // 2. Explicit workflow image specification if agentImage != "" { return agentImage } + // 3. Versioned default base image (fallback) return versionedImage("sandbox") } +// versionedImage returns a fully qualified image reference for the named component. +// If Version is unset or "dev", returns the unversioned image (for local builds). +// Otherwise, appends the version tag to enable stable release references. +// +// Example outputs: +// versionedImage("sandbox") with Version="dev" → quay.io/rcochran/openshell:sandbox +// versionedImage("sandbox") with Version="0.1.0" → quay.io/rcochran/openshell:sandbox-0.1.0 +>>>>>>> 0de49bb (docs: Document explicit sandbox image semantics and precedence rules) func versionedImage(name string) string { base := "quay.io/rcochran/openshell" if Version == "" || Version == "dev" { From f0e15da69b04f04d47a517efbd9c055b0f073625 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 3 Sep 2026 21:26:24 -0700 Subject: [PATCH 3/3] feat(verify): Add make verify-fast and make verify targets Fast verification (vet + lint) without Docker/Kind/OpenShell. Full verification adds config test-suite. Validation: make verify-fast and make verify pass. --- Makefile | 19 +++++++++++++++++-- cmd/sandbox_image.go | 2 -- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4c92cc4..5204655 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,9 @@ ## OpenShell Harness — build, push, and test ## +## Verify (no Docker/Kind/OpenShell needed): +## make verify # vet + lint + test-suite +## make verify-fast # vet + lint (quick checks) +## ## Tests (CI mode auto-detects from CI env var): ## make test # vet + unit tests ## make test-local # local gateway integration @@ -19,13 +23,16 @@ VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev) LDFLAGS := -s -w -X main.version=$(VERSION) # Pinned OpenShell CLI/gateway version — single source of truth for `make -# openshell` and CI (.github/workflows/integration.yml). +# openshell`, CI (.github/workflows/integration.yml), and the runtime min-version +# check (internal/gateway.MinOpenShellVersion, enforced in lockstep by a test). OPENSHELL_VERSION := $(shell cat .openshell-version 2>/dev/null) IMAGE := $(REGISTRY):sandbox-$(VERSION) .PHONY: all cli openshell \ - vet lint test test-local test-kind test-remote test-vertex-gemini-opencode test-hypershell test-hypershell-haiku test-all \ + vet lint verify verify-fast \ + test test-local test-kind test-remote test-vertex-gemini-opencode test-hypershell test-hypershell-haiku test-all \ + test-suite test-suite-live \ dev-sandbox dev-push tag clean help ## ── CLI ────────────────────────────────────────────────────────────── @@ -66,6 +73,14 @@ lint: $(MAKE) vet; \ fi +## ── Verify targets (no Docker/Kind/OpenShell needed) ─────────────────── + +## Fast checks: vet + lint only +verify-fast: vet lint + +## Full verify: fast checks + config test suite +verify: verify-fast test-suite + ## ── Test targets ────────────────────────────────────────────────────── ## CI mode auto-detects from the CI env var (set by GitHub Actions). ## Locally: full tests with credentials. On GHA: no-credential mode. diff --git a/cmd/sandbox_image.go b/cmd/sandbox_image.go index 563047a..4e89b23 100644 --- a/cmd/sandbox_image.go +++ b/cmd/sandbox_image.go @@ -21,7 +21,6 @@ var Version = "dev" // - Provide writable Python virtualenv at /opt/agent/venv // - Maintain standard PATH conventions for agent tools // - Support multi-context execution (local, HyperShell personal, service-account) ->>>>>>> 0de49bb (docs: Document explicit sandbox image semantics and precedence rules) func resolveSandboxImage(agentImage string) string { // 1. Environment override (highest priority) if envImage := os.Getenv("HARNESS_OS_IMAGE"); envImage != "" { @@ -42,7 +41,6 @@ func resolveSandboxImage(agentImage string) string { // Example outputs: // versionedImage("sandbox") with Version="dev" → quay.io/rcochran/openshell:sandbox // versionedImage("sandbox") with Version="0.1.0" → quay.io/rcochran/openshell:sandbox-0.1.0 ->>>>>>> 0de49bb (docs: Document explicit sandbox image semantics and precedence rules) func versionedImage(name string) string { base := "quay.io/rcochran/openshell" if Version == "" || Version == "dev" {