Skip to content
This repository was archived by the owner on Jul 22, 2026. It is now read-only.

Commit b776a3e

Browse files
benoitfclaude
andcommitted
feat(openshell): switch to official release binaries with version override
Replace rolling dev builds with official GitHub releases (v0.0.37). Add --openshell-version flag to override the version at init time. Cache binaries per version at <storageDir>/bin/<version>/ to avoid conflicts. Remove --no-bootstrap flag unsupported in official CLI. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Florent Benoit <fbenoit@redhat.com>
1 parent 6a2a92e commit b776a3e

11 files changed

Lines changed: 124 additions & 24 deletions

File tree

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ The runtime system provides a pluggable architecture for managing workspaces on
180180

181181
**To add a new runtime, use:** `/add-runtime`
182182

183+
### OpenShell Runtime — Version Management
184+
185+
The OpenShell runtime downloads three binaries (`openshell`, `openshell-gateway`, `openshell-driver-vm`) from official GitHub releases at `https://github.com/NVIDIA/OpenShell/releases`.
186+
187+
**Default version constant:** `pkg/runtime/openshell/version.go` defines `DefaultVersion` (currently `v0.0.37`). To bump the default, edit this single constant.
188+
189+
**`--openshell-version` flag:** Users can override the version at `kdn init` time (e.g., `kdn init --openshell-version v0.1.0`). The flag value flows through `RuntimeOptions["openshell-version"]` and is read in `Create()` before binaries are downloaded.
190+
191+
**Binary caching:** Binaries are cached per version at `<storageDir>/bin/<version>/`. Different versions coexist without conflict.
192+
183193
### Podman Runtime — Deny-mode Networking
184194

185195
When a workspace has `network.mode = deny`, the Podman runtime enforces outbound traffic filtering on every `Start()` using two layers. Allowed hosts come from `network.hosts` and are automatically augmented by host patterns derived from configured secrets. With no allowed hosts at all, the approval-handler denies every request (fully-isolated workspace).

pkg/runtime/openshell/create.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ func (r *openshellRuntime) Create(ctx context.Context, params runtime.CreatePara
3939
return runtime.RuntimeInfo{}, err
4040
}
4141

42+
if v := params.RuntimeOptions["openshell-version"]; v != "" {
43+
r.version = v
44+
}
45+
4246
driver := params.RuntimeOptions["openshell-driver"]
4347

4448
// Update driver in memory so ensureGatewayRunning uses the requested driver.
@@ -190,7 +194,7 @@ func (r *openshellRuntime) createSandbox(ctx context.Context, name string, agent
190194
for _, p := range providers {
191195
args = append(args, "--provider", p)
192196
}
193-
args = append(args, "--no-tty", "--no-bootstrap", "--", "true")
197+
args = append(args, "--no-tty", "--", "true")
194198
err := r.executor.Run(ctx, l.Stdout(), l.Stderr(), args...)
195199
if err == nil {
196200
return nil

pkg/runtime/openshell/download.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ import (
2727
)
2828

2929
const (
30-
openshellGatewayRelease = "dev"
31-
openshellRelease = "dev"
32-
openshellDriverVMRelease = "vm-dev"
33-
githubRepo = "NVIDIA/OpenShell"
30+
githubRepo = "NVIDIA/OpenShell"
3431
)
3532

3633
// platformAsset returns the asset name for the current platform.

pkg/runtime/openshell/gateway.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (r *openshellRuntime) hasActiveSandboxes(ctx context.Context) bool {
117117
func (r *openshellRuntime) ensureGatewayRunning(ctx context.Context) error {
118118
// Download binaries on first use (deferred from Initialize to avoid
119119
// network calls during runtime registration).
120-
if err := r.ensureBinaries(); err != nil {
120+
if err := r.ensureBinaries(ctx); err != nil {
121121
return err
122122
}
123123

pkg/runtime/openshell/info.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func (r *openshellRuntime) Info(ctx context.Context, id string) (runtime.Runtime
3030
return runtime.RuntimeInfo{}, fmt.Errorf("%w: sandbox ID is required", runtime.ErrInvalidParams)
3131
}
3232

33-
if err := r.ensureBinaries(); err != nil {
33+
if err := r.ensureBinaries(ctx); err != nil {
3434
return runtime.RuntimeInfo{}, err
3535
}
3636

pkg/runtime/openshell/openshell.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package openshell
1717

1818
import (
19+
"context"
1920
"fmt"
2021
"path/filepath"
2122
"sync"
@@ -24,6 +25,7 @@ import (
2425
"github.com/openkaiden/kdn/pkg/runtime/openshell/exec"
2526
"github.com/openkaiden/kdn/pkg/secret"
2627
"github.com/openkaiden/kdn/pkg/secretservice"
28+
"github.com/openkaiden/kdn/pkg/steplogger"
2729
)
2830

2931
const (
@@ -44,6 +46,7 @@ type openshellRuntime struct {
4446
binariesErr error
4547
secretStore secret.Store
4648
secretServiceRegistry secretservice.Registry
49+
version string
4750
}
4851

4952
// Ensure openshellRuntime implements runtime.Runtime at compile time.
@@ -100,6 +103,10 @@ func (r *openshellRuntime) Flags() []runtime.FlagDef {
100103
Usage: "OpenShell driver to use (podman, vm)",
101104
Completions: []string{"podman", "vm"},
102105
},
106+
{
107+
Name: "openshell-version",
108+
Usage: fmt.Sprintf("OpenShell version tag to download (default: %s)", DefaultVersion),
109+
},
103110
}
104111
}
105112

@@ -128,30 +135,41 @@ func (r *openshellRuntime) Initialize(storageDir string) error {
128135
// openshell-driver-vm binaries if they are not already present.
129136
// It is safe to call from multiple entry points — the download
130137
// runs at most once per runtime instance.
131-
func (r *openshellRuntime) ensureBinaries() error {
138+
func (r *openshellRuntime) ensureBinaries(ctx context.Context) error {
132139
r.binariesOnce.Do(func() {
133-
r.binariesErr = r.downloadBinaries()
140+
r.binariesErr = r.downloadBinaries(ctx)
134141
})
135142
return r.binariesErr
136143
}
137144

138-
func (r *openshellRuntime) downloadBinaries() error {
139-
binDir := filepath.Join(r.storageDir, "bin")
145+
func (r *openshellRuntime) resolveVersion() string {
146+
if r.version != "" {
147+
return r.version
148+
}
149+
return DefaultVersion
150+
}
151+
152+
func (r *openshellRuntime) downloadBinaries(ctx context.Context) error {
153+
version := r.resolveVersion()
154+
binDir := filepath.Join(r.storageDir, "bin", version)
155+
156+
step := steplogger.FromContext(ctx)
157+
step.Start(fmt.Sprintf("Downloading OpenShell %s binaries", version), fmt.Sprintf("OpenShell %s binaries ready", version))
140158

141-
gatewayPath, err := ensureBinary(binDir, "openshell-gateway", openshellGatewayRelease)
159+
gatewayPath, err := ensureBinary(binDir, "openshell-gateway", version)
142160
if err != nil {
143161
return fmt.Errorf("failed to ensure openshell-gateway binary: %w", err)
144162
}
145163
r.gatewayBinaryPath = gatewayPath
146164

147-
openshellPath, err := ensureBinary(binDir, "openshell", openshellRelease)
165+
openshellPath, err := ensureBinary(binDir, "openshell", version)
148166
if err != nil {
149167
return fmt.Errorf("failed to ensure openshell binary: %w", err)
150168
}
151169
r.executor = exec.New(openshellPath)
152170

153171
if _, assetErr := platformAsset("openshell-driver-vm"); assetErr == nil {
154-
vmDriverPath, dlErr := ensureBinary(binDir, "openshell-driver-vm", openshellDriverVMRelease)
172+
vmDriverPath, dlErr := ensureBinary(binDir, "openshell-driver-vm", version)
155173
if dlErr != nil {
156174
return fmt.Errorf("failed to ensure openshell-driver-vm binary: %w", dlErr)
157175
}

pkg/runtime/openshell/openshell_test.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package openshell
1616

1717
import (
18+
"context"
1819
"os"
1920
"path/filepath"
2021
"testing"
@@ -99,8 +100,8 @@ func TestOpenshellRuntime_Flags(t *testing.T) {
99100
rt := &openshellRuntime{}
100101
flags := rt.Flags()
101102

102-
if len(flags) != 1 {
103-
t.Fatalf("Expected 1 flag, got %d", len(flags))
103+
if len(flags) != 2 {
104+
t.Fatalf("Expected 2 flags, got %d", len(flags))
104105
}
105106

106107
if flags[0].Name != "openshell-driver" {
@@ -109,6 +110,10 @@ func TestOpenshellRuntime_Flags(t *testing.T) {
109110
if len(flags[0].Completions) != 2 {
110111
t.Errorf("Expected 2 completions for openshell-driver, got %d", len(flags[0].Completions))
111112
}
113+
114+
if flags[1].Name != "openshell-version" {
115+
t.Errorf("Expected second flag name 'openshell-version', got %q", flags[1].Name)
116+
}
112117
}
113118

114119
func TestOpenshellRuntime_SetSecretServiceRegistry(t *testing.T) {
@@ -183,7 +188,7 @@ func TestOpenshellRuntime_EnsureBinaries_SkipsWhenDepsInjected(t *testing.T) {
183188
fakeExec := exec.NewFake()
184189
rt := newWithDeps(fakeExec, "/fake/openshell-gateway", t.TempDir())
185190

186-
err := rt.ensureBinaries()
191+
err := rt.ensureBinaries(context.Background())
187192
if err != nil {
188193
t.Fatalf("ensureBinaries() should be no-op for test deps: %v", err)
189194
}
@@ -197,7 +202,7 @@ func TestDownloadBinaries_WithPreExistingBinaries(t *testing.T) {
197202
t.Parallel()
198203

199204
storageDir := t.TempDir()
200-
binDir := filepath.Join(storageDir, "bin")
205+
binDir := filepath.Join(storageDir, "bin", DefaultVersion)
201206
if err := os.MkdirAll(binDir, 0755); err != nil {
202207
t.Fatalf("Failed to create bin dir: %v", err)
203208
}
@@ -214,7 +219,7 @@ func TestDownloadBinaries_WithPreExistingBinaries(t *testing.T) {
214219
}
215220

216221
rt := &openshellRuntime{storageDir: storageDir}
217-
err := rt.downloadBinaries()
222+
err := rt.downloadBinaries(context.Background())
218223
if err != nil {
219224
t.Fatalf("downloadBinaries() with existing binaries: %v", err)
220225
}
@@ -231,7 +236,7 @@ func TestEnsureBinaries_RunsOnce(t *testing.T) {
231236
t.Parallel()
232237

233238
storageDir := t.TempDir()
234-
binDir := filepath.Join(storageDir, "bin")
239+
binDir := filepath.Join(storageDir, "bin", DefaultVersion)
235240
if err := os.MkdirAll(binDir, 0755); err != nil {
236241
t.Fatalf("Failed to create bin dir: %v", err)
237242
}
@@ -250,12 +255,13 @@ func TestEnsureBinaries_RunsOnce(t *testing.T) {
250255
rt := &openshellRuntime{storageDir: storageDir}
251256

252257
// Call ensureBinaries twice — downloadBinaries should only execute once
253-
if err := rt.ensureBinaries(); err != nil {
258+
ctx := context.Background()
259+
if err := rt.ensureBinaries(ctx); err != nil {
254260
t.Fatalf("First ensureBinaries() call: %v", err)
255261
}
256262
firstExecutor := rt.executor
257263

258-
if err := rt.ensureBinaries(); err != nil {
264+
if err := rt.ensureBinaries(ctx); err != nil {
259265
t.Fatalf("Second ensureBinaries() call: %v", err)
260266
}
261267

pkg/runtime/openshell/remove.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (r *openshellRuntime) Remove(ctx context.Context, id string) error {
3333
return fmt.Errorf("%w: sandbox ID is required", runtime.ErrInvalidParams)
3434
}
3535

36-
if err := r.ensureBinaries(); err != nil {
36+
if err := r.ensureBinaries(ctx); err != nil {
3737
return err
3838
}
3939

pkg/runtime/openshell/terminal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func (r *openshellRuntime) Terminal(ctx context.Context, instanceID string, _ st
2828
return fmt.Errorf("%w: instance ID is required", runtime.ErrInvalidParams)
2929
}
3030

31-
if err := r.ensureBinaries(); err != nil {
31+
if err := r.ensureBinaries(ctx); err != nil {
3232
return err
3333
}
3434

pkg/runtime/openshell/version.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2026 Red Hat, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package openshell
16+
17+
// DefaultVersion is the default OpenShell release tag used to download binaries.
18+
// Override at runtime with the --openshell-version flag on kdn init.
19+
const DefaultVersion = "v0.0.37"

0 commit comments

Comments
 (0)