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
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ cli:
## Sandbox image (Claude Code + mcp-atlassian + gws, multi-arch)
sandbox: sandbox/Dockerfile sandbox/startup.sh \
sandbox/policy.yaml sandbox/CLAUDE.md sandbox/settings.json
-$(CONTAINER_CLI) manifest rm $(SANDBOX_IMAGE) 2>/dev/null
@$(CONTAINER_CLI) manifest rm $(SANDBOX_IMAGE) 2>/dev/null || true
$(CONTAINER_CLI) build --platform linux/amd64 --manifest $(SANDBOX_IMAGE) sandbox/
$(CONTAINER_CLI) build --platform linux/arm64 --manifest $(SANDBOX_IMAGE) sandbox/
@echo "Built: $(SANDBOX_IMAGE) (multi-arch)"
Expand Down Expand Up @@ -119,8 +119,8 @@ dev-runner: cli-runner

## Build and push dev images (sandbox: multi-arch, runner: amd64)
dev-push: cli-runner
-$(CONTAINER_CLI) rmi $(DEV_SANDBOX_IMAGE) 2>/dev/null
-$(CONTAINER_CLI) manifest rm $(DEV_SANDBOX_IMAGE) 2>/dev/null
@$(CONTAINER_CLI) rmi --force $(DEV_SANDBOX_IMAGE) 2>/dev/null || true
@$(CONTAINER_CLI) manifest rm $(DEV_SANDBOX_IMAGE) 2>/dev/null || true
$(CONTAINER_CLI) build --platform linux/amd64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/
$(CONTAINER_CLI) build --platform linux/arm64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/
$(CONTAINER_CLI) manifest push $(DEV_SANDBOX_IMAGE)
Expand Down
14 changes: 7 additions & 7 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
}
isLocal := strings.Contains(activeGW.Endpoint, "127.0.0.1")

status.Section("Gateway")
status.Header("Gateway")
status.OKf("%s (%s)", activeGW.Name, activeGW.Endpoint)
agentCfg, err := agent.ParseFile(agentPath)
if err != nil {
Expand All @@ -60,12 +60,12 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
sandboxImage = envImage
}

status.Section("Agent")
fmt.Printf(" Name: %s\n", name)
fmt.Printf(" Image: %s\n", sandboxImage)
status.Header("Agent")
status.Infof("Name: %s", name)
status.Infof("Image: %s", sandboxImage)

// 3. Validate providers are registered
status.Section("Providers")
status.Header("Providers")
providerNames := agentCfg.ProviderNames()
registered, missing := profile.ValidateProviders(providerNames, gw)
for _, n := range registered {
Expand All @@ -84,7 +84,7 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {

// 5. Run preflight checks (only for unregistered providers)
if len(missing) > 0 && allProviders != nil {
status.Section("Preflight")
status.Header("Preflight")
preflightOK := true
for _, p := range allProviders {
if !providerInList(p.Name, missing) {
Expand Down Expand Up @@ -115,7 +115,7 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
}

// 6. Deploy the sandbox
status.Section("Creating sandbox")
status.Header("Creating sandbox")
if needsRunner {
status.Info("Custom providers detected — using in-cluster runner")
gwCfg := loadGatewayConfigForActive(harnessDir, activeGW)
Expand Down
56 changes: 21 additions & 35 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,13 @@ func deployLocal(gw gateway.Gateway) error {
return fmt.Errorf("openshell CLI not found. Install it first:\n curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh")
}

status.Section("Container Runtime")
status.Header("Deploy")
if _, err := exec.LookPath("podman"); err != nil {
status.Fail("Podman not found")
return fmt.Errorf("podman is required")
}
out, _ := exec.Command("podman", "--version").Output()
status.OKf("Podman: %s", strings.TrimSpace(string(out)))

status.Section("Gateway")
gateways, err := gw.GatewayList()
if err != nil {
return fmt.Errorf("listing gateways: %w", err)
Expand All @@ -104,9 +102,8 @@ func deployLocal(gw gateway.Gateway) error {

if localGW == "" {
status.Fail("No local gateway found")
fmt.Println()
fmt.Println(" Install OpenShell (auto-registers the gateway):")
fmt.Println(" curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh")
status.Detail("Install OpenShell (auto-registers the gateway):")
status.Sub("curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh")
return fmt.Errorf("no local gateway")
}

Expand All @@ -118,15 +115,11 @@ func deployLocal(gw gateway.Gateway) error {
status.OKf("%s (active, reachable)", localGW)
} else {
status.Failf("%s (not responding)", localGW)
fmt.Println()
fmt.Println(" Start the gateway:")
fmt.Println(" macOS: brew services start openshell")
fmt.Println(" Linux: systemctl --user start openshell")
status.Detail("Start the gateway:")
status.Sub("macOS: brew services start openshell")
status.Sub("Linux: systemctl --user start openshell")
return fmt.Errorf("gateway not responding")
}

fmt.Println()
status.Done("Done.")
return nil
}

Expand All @@ -150,14 +143,14 @@ func deployFromConfig(harnessDir string, gwCfg *gateway.GatewayConfig, gw gatewa
chartVersion = gwCfg.Chart.Version
}

fmt.Printf("OpenShell chart: %s\n", chartVersion)
status.Header("Deploy")
status.Infof("Chart: %s", chartVersion)
if kbcfg := os.Getenv("KUBECONFIG"); kbcfg != "" {
fmt.Printf("KUBECONFIG: %s\n", kbcfg)
status.Infof("KUBECONFIG: %s", kbcfg)
}
fmt.Println()

// Step 1: Namespace
status.Step(1, "Creating namespace")
status.Step(1, "Namespace")
clusterRunner.RunKubectl(ctx, "create", "ns", namespace)
if _, err := clusterRunner.RunKubectl(ctx, "label", "ns", namespace,
"pod-security.kubernetes.io/enforce=privileged",
Expand All @@ -167,33 +160,33 @@ func deployFromConfig(harnessDir string, gwCfg *gateway.GatewayConfig, gw gatewa
}

// Step 2: Sandbox CRD
status.Step(2, "Installing Sandbox CRD")
if err := clusterRunner.RunKubectlPassthrough(ctx, "apply", "-f", gwCfg.Chart.CRD.URL); err != nil {
status.Step(2, "Sandbox CRD")
if _, err := clusterRunner.RunKubectl(ctx, "apply", "-f", gwCfg.Chart.CRD.URL); err != nil {
return fmt.Errorf("installing sandbox CRD: %w", err)
}
status.OK("Installed")

// Step 3: Platform-specific setup
if gwCfg.IsOCP() {
status.Step(3, "Granting OpenShift SCCs")
status.Step(3, "OpenShift SCCs")
for _, sa := range gwCfg.OCP.SCCPrivileged {
kc.RunOC(ctx, "adm", "policy", "add-scc-to-user", "privileged", "-z", sa, "-n", namespace)
}
for _, sa := range gwCfg.OCP.SCCAnyuid {
kc.RunOC(ctx, "adm", "policy", "add-scc-to-user", "anyuid", "-z", sa, "-n", namespace)
}
// The sandbox controller ClusterRole and ClusterRoleBinding are
// included in the upstream manifest.yaml applied in step 2.
status.OK("Granted")
}

// Addon manifests (RBAC, etc.)
for _, manifestPath := range gwCfg.ManifestPaths() {
if err := kc.RunKubectlPassthrough(ctx, "apply", "-f", manifestPath); err != nil {
if _, err := kc.RunKubectl(ctx, "apply", "-f", manifestPath); err != nil {
return fmt.Errorf("applying %s: %w", filepath.Base(manifestPath), err)
}
}

// Step 4: Helm install
status.Step(4, "Deploying gateway via Helm")
status.Step(4, "Helm install")

// routeHost is needed before Helm (for OCP PKI cert SAN).
// gatewayURL is resolved after Helm for nodeport (service doesn't exist yet).
Expand Down Expand Up @@ -229,14 +222,13 @@ func deployFromConfig(harnessDir string, gwCfg *gateway.GatewayConfig, gw gatewa
return fmt.Errorf("helm install failed: %w", err)
}

status.Section("Waiting for gateway")
if err := kc.RunKubectlPassthrough(ctx, "rollout", "status", "statefulset/openshell", "--timeout=300s"); err != nil {
if _, err := kc.RunKubectl(ctx, "rollout", "status", "statefulset/openshell", "--timeout=300s"); err != nil {
return fmt.Errorf("gateway rollout failed: %w", err)
}
status.OK("Gateway ready")

// Step 5: CLI gateway config
// Resolve the gateway URL now that the service exists.
status.Step(5, "Configuring CLI gateway")
status.Step(5, "CLI gateway")
gatewayName := gwCfg.Gateway.Name

var gatewayURL string
Expand Down Expand Up @@ -305,23 +297,17 @@ func deployFromConfig(harnessDir string, gwCfg *gateway.GatewayConfig, gw gatewa
status.OKf("%s registered", gatewayName)
}

fmt.Print(" Waiting for gateway...")
var gwReachable bool
for range 30 {
if gw.InferenceGet() == nil {
gwReachable = true
status.OK("reachable")
break
}
time.Sleep(2 * time.Second)
fmt.Print(".")
}
if !gwReachable {
fmt.Println()
return fmt.Errorf("gateway not reachable after 60s (try: openshell inference get)")
}

fmt.Println()
status.Done("Done.")
status.OK("Reachable")
return nil
}
4 changes: 4 additions & 0 deletions cmd/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ func (m *mockGW) ProviderProfileImport(string) error
func (m *mockGW) ProviderProfileDelete(string) error { return nil }
func (m *mockGW) SettingsSet(string, string) error { return nil }
func (m *mockGW) SandboxList() ([]string, error) { return nil, nil }
func (m *mockGW) SandboxStatus() ([]gateway.SandboxInfo, error) { return nil, nil }
func (m *mockGW) SandboxConnect(string) error { return nil }
func (m *mockGW) SandboxLogs(string, bool) error { return nil }
func (m *mockGW) SandboxStop(string) error { return nil }
func (m *mockGW) SandboxStart(string) error { return nil }
func (m *mockGW) GatewayAdd(string, string, bool, bool) error { return nil }
func (m *mockGW) GatewayRemove(name string) error {
if m.onGatewayRemove != nil {
Expand Down
27 changes: 27 additions & 0 deletions cmd/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cmd

import (
"github.com/robbycochran/harness-openshell/internal/gateway"
"github.com/spf13/cobra"
)

func NewLogsCmd(harnessDir, cli string) *cobra.Command {
var follow bool

cmd := &cobra.Command{
Use: "logs [SANDBOX_NAME]",
Short: "Stream sandbox logs",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
gw := gateway.New(cli)
name := ""
if len(args) > 0 {
name = args[0]
}
return gw.SandboxLogs(name, follow)
},
}

cmd.Flags().BoolVarP(&follow, "follow", "f", false, "Follow log output")
return cmd
}
46 changes: 19 additions & 27 deletions cmd/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func NewProvidersCmd(harnessDir, cli string) *cobra.Command {
Short: "Register providers with the gateway",
RunE: func(cmd *cobra.Command, args []string) error {
gw := gateway.New(cli)
return registerProviders(harnessDir, gw, force, nil)
return registerProviders(harnessDir, gw, force, nil, true)
},
}

Expand All @@ -34,7 +34,7 @@ func NewProvidersCmd(harnessDir, cli string) *cobra.Command {
// registerProviders registers providers with the gateway. If gwCfg is non-nil
// and has a [providers] section, only providers in that list are registered.
// Otherwise all providers are registered (backward-compatible behavior).
func registerProviders(harnessDir string, gw gateway.Gateway, force bool, gwCfg *gateway.GatewayConfig) error {
func registerProviders(harnessDir string, gw gateway.Gateway, force bool, gwCfg *gateway.GatewayConfig, standalone bool) error {
model := envOr("OPENSHELL_MODEL", "claude-sonnet-4-6")

// Build the set of enabled provider names from gateway config (if available)
Expand Down Expand Up @@ -71,21 +71,16 @@ func registerProviders(harnessDir string, gw gateway.Gateway, force bool, gwCfg
status.Info("Deleted existing providers")
}

status.Header("Providers")

// Enable providers v2
status.Section("Enabling providers v2")
if err := gw.SettingsSet("providers_v2_enabled", "true"); err != nil {
return fmt.Errorf("enabling providers v2: %w", err)
}

// Import custom profiles
status.Section("Importing custom profiles")
profilesDir := filepath.Join(harnessDir, "agents", "providers", "profiles")
if err := gw.ProviderProfileImport(profilesDir); err != nil {
status.Info("already imported")
}

// Register providers
status.Section("Registering providers")
gw.ProviderProfileImport(profilesDir)

if err := registerGitHub(gw, providerEnabled); err != nil {
return err
Expand All @@ -100,24 +95,21 @@ func registerProviders(harnessDir string, gw gateway.Gateway, force bool, gwCfg
return err
}

// Show results
status.Section("Providers")
names, err := gw.ProviderList()
if err != nil {
return fmt.Errorf("listing providers: %w", err)
}
for _, n := range names {
status.OK(n)
}

status.Section("Inference")
m := gw.InferenceModel()
if m != "" {
status.OKf("Model: %s", m)
if standalone {
names, err := gw.ProviderList()
if err != nil {
return fmt.Errorf("listing providers: %w", err)
}
fmt.Println()
for _, n := range names {
status.OK(n)
}
m := gw.InferenceModel()
if m != "" {
status.OKf("Inference: %s", m)
}
status.Done("Done. Launch a sandbox with: harness up --local")
}

fmt.Println()
status.Done("Done. Launch a sandbox with: harness up --local")
return nil
}

Expand Down
14 changes: 7 additions & 7 deletions cmd/providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestRegisterProviders_GitHubWhenTokenSet(t *testing.T) {
providers: map[string]bool{},
}

err := registerProviders(dir, gw, false, nil)
err := registerProviders(dir, gw, false, nil, true)
if err != nil {
t.Fatalf("registerProviders: %v", err)
}
Expand All @@ -41,7 +41,7 @@ func TestRegisterProviders_SkipsWhenTokenMissing(t *testing.T) {
providers: map[string]bool{},
}

err := registerProviders(dir, gw, false, nil)
err := registerProviders(dir, gw, false, nil, true)
if err != nil {
t.Fatalf("registerProviders: %v", err)
}
Expand All @@ -56,7 +56,7 @@ func TestRegisterProviders_SkipsExistingProvider(t *testing.T) {
providers: map[string]bool{"github": true},
}

err := registerProviders(dir, gw, false, nil)
err := registerProviders(dir, gw, false, nil, true)
if err != nil {
t.Fatalf("registerProviders: %v", err)
}
Expand All @@ -72,7 +72,7 @@ func TestRegisterProviders_ForceWithRunningSandboxes(t *testing.T) {
sandboxes: []string{"test-sandbox"},
}

err := registerProviders(dir, gw, true, nil)
err := registerProviders(dir, gw, true, nil, true)
if err == nil {
t.Fatal("expected error with --force and running sandboxes")
}
Expand All @@ -90,7 +90,7 @@ func TestRegisterProviders_ForceDeletesAndRecreates(t *testing.T) {
providers: map[string]bool{},
}

err := registerProviders(dir, gw, true, nil)
err := registerProviders(dir, gw, true, nil, true)
if err != nil {
t.Fatalf("registerProviders: %v", err)
}
Expand All @@ -108,7 +108,7 @@ func TestRegisterProviders_RespectsGatewayConfig(t *testing.T) {
gwCfg := &gateway.GatewayConfig{}
gwCfg.Providers.Enabled = []string{"github"}

err := registerProviders(dir, gw, false, gwCfg)
err := registerProviders(dir, gw, false, gwCfg, true)
if err != nil {
t.Fatalf("registerProviders: %v", err)
}
Expand All @@ -122,7 +122,7 @@ func TestRegisterProviders_ListError(t *testing.T) {
providerErr: fmt.Errorf("gateway unreachable"),
}

err := registerProviders(dir, gw, false, nil)
err := registerProviders(dir, gw, false, nil, true)
if err == nil {
t.Fatal("expected error when provider list fails")
}
Expand Down
Loading
Loading