with one click
with one click
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | add-runtime |
| description | Add a new runtime implementation to the kdn runtime system |
| argument-hint | <runtime-name> |
This skill guides you through adding a new runtime implementation to the kdn runtime system.
Runtimes provide the execution environment for workspaces on different container/VM platforms:
Create a new directory: pkg/runtime/<runtime-name>/
Example: pkg/runtime/podman/
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" // Optional: only if executing external commands
"github.com/openkaiden/kdn/pkg/steplogger"
api "github.com/openkaiden/kdn-api/cli/go"
)
type <runtime-name>Runtime struct {
storageDir string
}
// Ensure implementation of runtime.Runtime at compile time
var _ runtime.Runtime = (*<runtime-name>Runtime)(nil)
// Ensure implementation of runtime.StorageAware at compile time (optional)
var _ runtime.StorageAware = (*<runtime-name>Runtime)(nil)
// Uncomment only if this runtime implements runtime.FlagProvider.
// var _ runtime.FlagProvider = (*<runtime-name>Runtime)(nil)
// New creates a new runtime instance
func New() runtime.Runtime {
return &<runtime-name>Runtime{}
}
// Type returns the runtime type identifier
func (r *<runtime-name>Runtime) Type() string {
return "<runtime-name>"
}
// DisplayName returns the human-readable display name (e.g., "Podman", "OpenShell").
func (r *<runtime-name>Runtime) DisplayName() string {
return "<Pretty Runtime Name>"
}
// Description returns a human-readable description of the runtime.
func (r *<runtime-name>Runtime) Description() string {
return "<short description of what this runtime provides>"
}
// Local reports whether the runtime executes workspaces on the local machine.
func (r *<runtime-name>Runtime) Local() bool {
return true // set to false for remote runtimes (e.g., Kubernetes)
}
// Initialize implements runtime.StorageAware (optional)
func (r *<runtime-name>Runtime) Initialize(storageDir string) error {
r.storageDir = storageDir
// Optional: create subdirectories, load state, etc.
return nil
}
// Available implements runtimesetup.Available (optional)
func (r *<runtime-name>Runtime) Available() bool {
// Check if the runtime is available on this system
// Example: check if CLI tool is installed
_, err := exec.LookPath("<runtime-cli-tool>")
return err == nil
}
// WorkspaceSourcesPath returns the path where sources are mounted inside the workspace.
// This is a constant for each runtime type.
func (r *<runtime-name>Runtime) WorkspaceSourcesPath() string {
return "/workspace/sources" // Adjust based on your runtime's mount point
}
// Create creates a new runtime instance
func (r *<runtime-name>Runtime) Create(ctx context.Context, params runtime.CreateParams) (runtime.RuntimeInfo, error) {
stepLogger := steplogger.FromContext(ctx)
defer stepLogger.Complete()
// Step 1: Prepare environment
stepLogger.Start("Preparing workspace environment", "Workspace environment prepared")
if err := r.prepareEnvironment(params); err != nil {
stepLogger.Fail(err)
return runtime.RuntimeInfo{}, err
}
// Step 2: Create instance
stepLogger.Start("Creating workspace instance", "Workspace instance created")
info, err := r.createInstance(ctx, params)
if err != nil {
stepLogger.Fail(err)
return runtime.RuntimeInfo{}, err
}
// Step 3: Copy agent default settings files into the workspace home directory.
// params.AgentSettings is a map[string]agent.SettingsFile (relative forward-slash path ā settings file)
// populated from <storage-dir>/config/<agent>/ by the instances manager.
// The manager automatically modifies these settings via agent.SkipOnboarding() if the
// agent is registered (e.g., Claude agent adds hasCompletedOnboarding and trust flags).
// For image-based runtimes (e.g., Podman), embed these files BEFORE install RUN commands
// so agent install scripts can read and build upon the defaults.
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
}
// Start starts a runtime instance
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
}
// Stop stops a runtime instance
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
}
// Remove removes a runtime instance
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
}
// Info retrieves information about a runtime instance
func (r *<runtime-name>Runtime) Info(ctx context.Context, id string) (runtime.RuntimeInfo, error) {
// Implementation: get workspace info
// Map platform-specific state to valid WorkspaceState
platformState := "running" // Get actual state from platform
state := r.mapState(platformState)
return runtime.RuntimeInfo{
ID: id,
State: state,
Info: info,
}, nil
}
// mapState maps platform-specific states to valid WorkspaceState values
func (r *<runtime-name>Runtime) mapState(platformState string) api.WorkspaceState {
// Example mapping - adjust for your platform
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
}
}
Edit pkg/runtimesetup/register.go:
import (
"github.com/openkaiden/kdn/pkg/runtime"
"github.com/openkaiden/kdn/pkg/runtime/fake"
"github.com/openkaiden/kdn/pkg/runtime/<runtime-name>" // Add this
)
availableRuntimes slice:var availableRuntimes = []runtimeFactory{
fake.New,
<runtime-name>.New, // Add this
}
IMPORTANT: All runtime methods that accept context.Context MUST use StepLogger for user feedback.
Required imports:
import (
"github.com/openkaiden/kdn/pkg/steplogger"
)
Pattern:
stepLogger := steplogger.FromContext(ctx)defer stepLogger.Complete()stepLogger.Start("In progress message", "Completion message")stepLogger.Fail(err) before returning the errorBenefits:
See AGENTS.md for complete StepLogger documentation and best practices.
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:
stepLogger for steplogger.StepLoggerl for logger.LoggerWhen --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.
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()
// Test basic functionality
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()
// Create fake step logger to track calls
fakeLogger := &fakeStepLogger{}
ctx := steplogger.WithLogger(context.Background(), fakeLogger)
rt := New()
_, err := rt.Create(ctx, params)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
// Verify step logger was used correctly
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)
}
}
// Fake step logger for testing
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++
}
// Add tests for other methods (Start, Stop, Remove)...
// Each should include both functional and StepLogger tests
Reference: See pkg/runtime/podman/steplogger_test.go and related test files for complete examples.
Run the copyright headers skill:
/copyright-headers
# Run tests
make test
# Build
make build
# Test with CLI (if runtime is available on your system)
./kdn init --runtime <runtime-name>
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)
}
Implement if the runtime needs persistent storage:
type StorageAware interface {
Initialize(storageDir string) error
}
When implemented, the registry will:
REGISTRY_STORAGE/<runtime-type>Initialize() with the pathImplement if the runtime can report which agents it supports:
type AgentLister interface {
ListAgents() ([]string, error)
}
Use this to:
info command to display available agentsImplement if the runtime needs to expose CLI flags on the init command:
type FlagProvider interface {
Flags() []FlagDef
}
Use this to:
--openshell-driver)CreateParams.RuntimeOptions as map[string]stringThe runtimesetup.ListFlags() bridge discovers and deduplicates flags from all available FlagProvider runtimes.
Implement to control runtime availability:
type Available interface {
Available() bool
}
Use this to:
See pkg/runtime/fake/ for a complete reference implementation that demonstrates:
All runtimes MUST return valid WorkspaceState values in RuntimeInfo.State. Valid states are:
running - The instance is actively runningstopped - The instance is created but not runningerror - The instance encountered an errorunknown - The instance state cannot be determinedValidation 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:
// Create() - return "stopped" for newly created instances
func (r *myRuntime) Create(ctx context.Context, params runtime.CreateParams) (runtime.RuntimeInfo, error) {
// ... create instance logic ...
return runtime.RuntimeInfo{
ID: id,
State: api.WorkspaceStateStopped, // New instances are "stopped", not "created"
Info: info,
}, nil
}
// Start() - return "running" after starting
func (r *myRuntime) Start(ctx context.Context, id string) (runtime.RuntimeInfo, error) {
// ... start instance logic ...
return runtime.RuntimeInfo{
ID: id,
State: api.WorkspaceStateRunning, // Instance is now running
Info: info,
}, nil
}
// Info() - map platform state to valid WorkspaceState
func (r *myRuntime) Info(ctx context.Context, id string) (runtime.RuntimeInfo, error) {
platformState := r.getPlatformState(id) // e.g., "created", "exited", "paused"
// Map platform state to valid WorkspaceState
state := r.mapState(platformState)
return runtime.RuntimeInfo{
ID: id,
State: state,
Info: info,
}, nil
}
// mapState maps platform-specific states to valid WorkspaceState values
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:
"stopped", not platform-specific values like "created"runtime "my-runtime" returned invalid state: invalid runtime state: "created"
(must be one of: running, stopped, error, unknown)
Use the predefined errors from pkg/runtime:
import "github.com/openkaiden/kdn/pkg/runtime"
// Instance not found
return runtime.RuntimeInfo{}, fmt.Errorf("%w: %s", runtime.ErrInstanceNotFound, id)
// Invalid parameters
return runtime.RuntimeInfo{}, fmt.Errorf("%w: name is required", runtime.ErrInvalidParams)
If using StorageAware:
func (r *myRuntime) Initialize(storageDir string) error {
r.storageDir = storageDir
r.storageFile = filepath.Join(storageDir, "instances.json")
// Load existing state
return r.loadFromDisk()
}
func (r *myRuntime) Create(...) {
// ... create instance
// Save to disk
if err := r.saveToDisk(); err != nil {
return runtime.RuntimeInfo{}, fmt.Errorf("failed to persist instance: %w", err)
}
}
After implementing a Podman runtime:
# Initialize workspace with Podman runtime
./kdn init --runtime podman
# Start workspace
./kdn workspace start <workspace-id>
# Stop workspace
./kdn workspace stop <workspace-id>
# Remove workspace
./kdn workspace remove <workspace-id>
podman, microvm, k8s)fake runtime as a reference implementationruntimesetup.RegisterAll()Available() == true) will be registered