| name | add-runtime |
| description | Add a new runtime implementation to the kdn runtime system |
| argument-hint | <runtime-name> |
Add Runtime Skill
This skill guides you through adding a new runtime implementation to the kdn runtime system.
What are Runtimes?
Runtimes provide the execution environment for workspaces on different container/VM platforms:
- Podman: Container-based workspaces
- MicroVM: Lightweight VM-based workspaces
- Kubernetes: Kubernetes pod-based workspaces
- fake: Test runtime for development
Steps to Add a New Runtime
1. Create Runtime Package
Create a new directory: pkg/runtime/<runtime-name>/
Example: pkg/runtime/podman/
2. Implement the Runtime Interface
Create pkg/runtime/<runtime-name>/<runtime-name>.go with:
package <runtime-name>
import (
"context"
"fmt"
"github.com/openkaiden/kdn/pkg/runtime"
"github.com/openkaiden/kdn/pkg/logger"
"github.com/openkaiden/kdn/pkg/steplogger"
api "github.com/openkaiden/kdn-api/cli/go"
)
type <runtime-name>Runtime struct {
storageDir string
}
var _ runtime.Runtime = (*<runtime-name>Runtime)(nil)
var _ runtime.StorageAware = (*<runtime-name>Runtime)(nil)
func New() runtime.Runtime {
return &<runtime-name>Runtime{}
}
func (r *<runtime-name>Runtime) Type() string {
return "<runtime-name>"
}
func (r *<runtime-name>Runtime) DisplayName() string {
return "<Pretty Runtime Name>"
}
func (r *<runtime-name>Runtime) Description() string {
return "<short description of what this runtime provides>"
}
func (r *<runtime-name>Runtime) Local() bool {
return true
}
func (r *<runtime-name>Runtime) Initialize(storageDir string) error {
r.storageDir = storageDir
return nil
}
func (r *<runtime-name>Runtime) Available() bool {
_, err := exec.LookPath("<runtime-cli-tool>")
return err == nil
}
func (r *<runtime-name>Runtime) WorkspaceSourcesPath() string {
return "/workspace/sources"
}
func (r *<runtime-name>Runtime) Create(ctx context.Context, params runtime.CreateParams) (runtime.RuntimeInfo, error) {
stepLogger := steplogger.FromContext(ctx)
defer stepLogger.Complete()
stepLogger.Start("Preparing workspace environment", "Workspace environment prepared")
if err := r.prepareEnvironment(params); err != nil {
stepLogger.Fail(err)
return runtime.RuntimeInfo{}, err
}
stepLogger.Start("Creating workspace instance", "Workspace instance created")
info, err := r.createInstance(ctx, params)
if err != nil {
stepLogger.Fail(err)
return runtime.RuntimeInfo{}, err
}
if len(params.AgentSettings) > 0 {
stepLogger.Start("Copying agent settings to workspace", "Agent settings copied")
if err := r.copyAgentSettings(ctx, info.ID, params.AgentSettings); err != nil {
stepLogger.Fail(err)
return runtime.RuntimeInfo{}, err
}
}
return info, nil
}
func (r *<runtime-name>Runtime) Start(ctx context.Context, id string) (runtime.RuntimeInfo, error) {
stepLogger := steplogger.FromContext(ctx)
defer stepLogger.Complete()
stepLogger.Start(fmt.Sprintf("Starting workspace: %s", id), "Workspace started")
if err := r.startInstance(ctx, id); err != nil {
stepLogger.Fail(err)
return runtime.RuntimeInfo{}, err
}
stepLogger.Start("Verifying workspace status", "Workspace status verified")
info, err := r.getInfo(ctx, id)
if err != nil {
stepLogger.Fail(err)
return runtime.RuntimeInfo{}, err
}
return info, nil
}
func (r *<runtime-name>Runtime) Stop(ctx context.Context, id string) error {
stepLogger := steplogger.FromContext(ctx)
defer stepLogger.Complete()
stepLogger.Start(fmt.Sprintf("Stopping workspace: %s", id), "Workspace stopped")
if err := r.stopInstance(ctx, id); err != nil {
stepLogger.Fail(err)
return err
}
return nil
}
func (r *<runtime-name>Runtime) Remove(ctx context.Context, id string) error {
stepLogger := steplogger.FromContext(ctx)
defer stepLogger.Complete()
stepLogger.Start("Checking workspace state", "Workspace state checked")
state, err := r.checkState(ctx, id)
if err != nil {
stepLogger.Fail(err)
return err
}
if state == "running" {
err := fmt.Errorf("workspace is still running, stop it first")
stepLogger.Fail(err)
return err
}
stepLogger.Start(fmt.Sprintf("Removing workspace: %s", id), "Workspace removed")
if err := r.removeInstance(ctx, id); err != nil {
stepLogger.Fail(err)
return err
}
return nil
}
func (r *<runtime-name>Runtime) Info(ctx context.Context, id string) (runtime.RuntimeInfo, error) {
platformState := "running"
state := r.mapState(platformState)
return runtime.RuntimeInfo{
ID: id,
State: state,
Info: info,
}, nil
}
func (r *<runtime-name>Runtime) mapState(platformState string) api.WorkspaceState {
switch platformState {
case "running", "active":
return api.WorkspaceStateRunning
case "created", "exited", "stopped", "paused":
return api.WorkspaceStateStopped
case "failed", "dead":
return api.WorkspaceStateError
default:
return api.WorkspaceStateUnknown
}
}
3. Register the Runtime
Edit pkg/runtimesetup/register.go:
- Add import:
import (
"github.com/openkaiden/kdn/pkg/runtime"
"github.com/openkaiden/kdn/pkg/runtime/fake"
"github.com/openkaiden/kdn/pkg/runtime/<runtime-name>"
)
- Add to
availableRuntimes slice:
var availableRuntimes = []runtimeFactory{
fake.New,
<runtime-name>.New,
}
3.5. StepLogger Integration
IMPORTANT: All runtime methods that accept context.Context MUST use StepLogger for user feedback.
Required imports:
import (
"github.com/openkaiden/kdn/pkg/steplogger"
)
Pattern:
- Retrieve logger from context:
stepLogger := steplogger.FromContext(ctx)
- Defer completion:
defer stepLogger.Complete()
- Start each step:
stepLogger.Start("In progress message", "Completion message")
- Fail on errors:
stepLogger.Fail(err) before returning the error
Benefits:
- Users see progress during long-running operations
- Clear feedback on which step failed
- Automatic silence in JSON mode
- No changes needed for JSON vs text output
See AGENTS.md for complete StepLogger documentation and best practices.
3.6. Logger Integration (for CLI Command Execution)
If your runtime executes external CLI commands (e.g., via exec.Command), use pkg/logger to route stdout/stderr to the user when --show-logs is passed.
Required imports:
import (
"github.com/openkaiden/kdn/pkg/logger"
)
Pattern — retrieve from context and pass to exec:
func (r *<runtime-name>Runtime) runSomething(ctx context.Context, args ...string) error {
l := logger.FromContext(ctx)
cmd := exec.CommandContext(ctx, "<tool>", args...)
cmd.Stdout = l.Stdout()
cmd.Stderr = l.Stderr()
return cmd.Run()
}
Variable naming convention:
- Use
stepLogger for steplogger.StepLogger
- Use
l for logger.Logger
When --show-logs is not set, logger.FromContext returns a no-op logger that discards all output, so the pattern is safe to use unconditionally.
See AGENTS.md for the --show-logs flag pattern and complete Logger documentation.
4. Add Tests
Create pkg/runtime/<runtime-name>/<runtime-name>_test.go:
package <runtime-name>
import (
"context"
"testing"
"github.com/openkaiden/kdn/pkg/steplogger"
)
func TestNew(t *testing.T) {
t.Parallel()
rt := New()
if rt == nil {
t.Fatal("New() returned nil")
}
if rt.Type() != "<runtime-name>" {
t.Errorf("Expected type '<runtime-name>', got %s", rt.Type())
}
if rt.DisplayName() != "<Pretty Runtime Name>" {
t.Errorf("DisplayName() = %q, want %q", rt.DisplayName(), "<Pretty Runtime Name>")
}
}
func TestCreate(t *testing.T) {
t.Parallel()
rt := New()
_, err := rt.Create(context.Background(), params)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
}
func TestCreate_StepLogger(t *testing.T) {
t.Parallel()
fakeLogger := &fakeStepLogger{}
ctx := steplogger.WithLogger(context.Background(), fakeLogger)
rt := New()
_, err := rt.Create(ctx, params)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
if len(fakeLogger.startCalls) == 0 {
t.Error("Expected Start() to be called")
}
if fakeLogger.completeCalls != 1 {
t.Errorf("Expected Complete() to be called once, got %d", fakeLogger.completeCalls)
}
}
type fakeStepLogger struct {
startCalls []stepCall
failCalls []error
completeCalls int
}
type stepCall struct {
inProgress string
completed string
}
func (f *fakeStepLogger) Start(inProgress, completed string) {
f.startCalls = append(f.startCalls, stepCall{inProgress, completed})
}
func (f *fakeStepLogger) Fail(err error) {
f.failCalls = append(f.failCalls, err)
}
func (f *fakeStepLogger) Complete() {
f.completeCalls++
}
Reference: See pkg/runtime/podman/steplogger_test.go and related test files for complete examples.
5. Update Copyright Headers
Run the copyright headers skill:
/copyright-headers
6. Test the Runtime
make test
make build
./kdn init --runtime <runtime-name>
Required Interfaces
Runtime Interface (required)
All runtimes MUST implement:
type Runtime interface {
Type() string
DisplayName() string
Description() string
Local() bool
WorkspaceSourcesPath() string
Create(ctx context.Context, params CreateParams) (RuntimeInfo, error)
Start(ctx context.Context, id string) (RuntimeInfo, error)
Stop(ctx context.Context, id string) error
Remove(ctx context.Context, id string) error
Info(ctx context.Context, id string) (RuntimeInfo, error)
}
StorageAware Interface (optional)
Implement if the runtime needs persistent storage:
type StorageAware interface {
Initialize(storageDir string) error
}
When implemented, the registry will:
- Create a directory at
REGISTRY_STORAGE/<runtime-type>
- Call
Initialize() with the path
- The runtime can use this directory to persist data
AgentLister Interface (optional)
Implement if the runtime can report which agents it supports:
type AgentLister interface {
ListAgents() ([]string, error)
}
Use this to:
- Report agents discovered from configuration files
- Enable the
info command to display available agents
- Allow agent discovery without runtime-specific knowledge
FlagProvider Interface (optional)
Implement if the runtime needs to expose CLI flags on the init command:
type FlagProvider interface {
Flags() []FlagDef
}
Use this to:
- Declare runtime-specific flags (e.g.,
--openshell-driver)
- Provide shell completion values for those flags
- Keep the init command runtime-agnostic — flag values flow through
CreateParams.RuntimeOptions as map[string]string
The runtimesetup.ListFlags() bridge discovers and deduplicates flags from all available FlagProvider runtimes.
HostResolver Interface (optional)
Implement if the runtime uses a different hostname than host.containers.internal to reach the host machine:
type HostResolver interface {
ContainerHostname() string
}
Use this to:
- Declare the hostname used inside the workspace to reach the host (e.g.,
host.openshell.internal)
- Ensure
agent.SetModel() rewrites localhost URLs to the correct hostname
- The manager automatically detects this interface and passes the hostname to
SetModel()
ConfigTransformer Interface (optional)
Implement if the runtime needs to transform the merged workspace configuration before it is applied:
type ConfigTransformer interface {
TransformConfig(cfg *workspace.WorkspaceConfiguration) error
}
Use this to:
- Rewrite localhost URLs in MCP command args to the runtime's container hostname
- Apply runtime-specific transformations to the workspace config after merging
Available Interface (optional)
Implement to control runtime availability:
type Available interface {
Available() bool
}
Use this to:
- Check if required CLI tools are installed
- Check OS compatibility
- Check configuration prerequisites
- Check license/permission requirements
Reference Implementation
See pkg/runtime/fake/ for a complete reference implementation that demonstrates:
- All required Runtime interface methods
- StorageAware implementation for persistence
- Proper error handling and state management
- Comprehensive tests
Common Patterns
State Validation
All runtimes MUST return valid WorkspaceState values in RuntimeInfo.State. Valid states are:
running - The instance is actively running
stopped - The instance is created but not running
error - The instance encountered an error
unknown - The instance state cannot be determined
Validation is enforced at the manager boundary (fail-fast approach):
The instances manager validates all RuntimeInfo returned from runtime methods. If a runtime returns an invalid state, the manager immediately returns an error identifying which runtime failed. This means:
✅ You don't need to call runtime.ValidateState() in your runtime implementation
✅ Invalid states are caught automatically during development
✅ Clear error messages identify the problematic runtime
Required: Map platform-specific states to valid WorkspaceState values
Your runtime must map platform-specific states to the four valid states:
func (r *myRuntime) Create(ctx context.Context, params runtime.CreateParams) (runtime.RuntimeInfo, error) {
return runtime.RuntimeInfo{
ID: id,
State: api.WorkspaceStateStopped,
Info: info,
}, nil
}
func (r *myRuntime) Start(ctx context.Context, id string) (runtime.RuntimeInfo, error) {
return runtime.RuntimeInfo{
ID: id,
State: api.WorkspaceStateRunning,
Info: info,
}, nil
}
func (r *myRuntime) Info(ctx context.Context, id string) (runtime.RuntimeInfo, error) {
platformState := r.getPlatformState(id)
state := r.mapState(platformState)
return runtime.RuntimeInfo{
ID: id,
State: state,
Info: info,
}, nil
}
func (r *myRuntime) mapState(platformState string) api.WorkspaceState {
switch platformState {
case "running", "active":
return api.WorkspaceStateRunning
case "created", "exited", "stopped", "paused":
return api.WorkspaceStateStopped
case "failed", "dead":
return api.WorkspaceStateError
default:
return api.WorkspaceStateUnknown
}
}
Important notes:
- Newly created instances should return state
"stopped", not platform-specific values like "created"
- Platform-specific states must be mapped to the four valid states in your runtime
- The manager validates all states at the boundary - you don't need to validate yourself
- If you return an invalid state, you'll get a clear error during development:
runtime "my-runtime" returned invalid state: invalid runtime state: "created"
(must be one of: running, stopped, error, unknown)
- This fail-fast approach catches bugs early without requiring validation in every runtime
Error Handling
Use the predefined errors from pkg/runtime:
import "github.com/openkaiden/kdn/pkg/runtime"
return runtime.RuntimeInfo{}, fmt.Errorf("%w: %s", runtime.ErrInstanceNotFound, id)
return runtime.RuntimeInfo{}, fmt.Errorf("%w: name is required", runtime.ErrInvalidParams)
Persistence
If using StorageAware:
func (r *myRuntime) Initialize(storageDir string) error {
r.storageDir = storageDir
r.storageFile = filepath.Join(storageDir, "instances.json")
return r.loadFromDisk()
}
func (r *myRuntime) Create(...) {
if err := r.saveToDisk(); err != nil {
return runtime.RuntimeInfo{}, fmt.Errorf("failed to persist instance: %w", err)
}
}
Usage Example
After implementing a Podman runtime:
./kdn init --runtime podman
./kdn workspace start <workspace-id>
./kdn workspace stop <workspace-id>
./kdn workspace remove <workspace-id>
Notes
- Runtime names should be lowercase (e.g.,
podman, microvm, k8s)
- Use the
fake runtime as a reference implementation
- All runtimes are registered automatically via
runtimesetup.RegisterAll()
- Commands don't need to be modified when adding new runtimes
- Only available runtimes (those with
Available() == true) will be registered