Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2c432cd
feat(runtime): add podman runtime with system utilities module
feloy Mar 19, 2026
cc96f30
ci: allow codecov upload to fail without failing CI
feloy Mar 19, 2026
47c4068
Merge pull request #1 from feloy/podman-runtime-1
feloy Mar 19, 2026
3e067c3
feat(runtime/podman): implement Create method with container provisio…
feloy Mar 19, 2026
a81231a
fix(runtime/podman): suppress command output during Create
feloy Mar 19, 2026
c9fe16a
fix(runtime/podman): fix Windows path separator handling
feloy Mar 19, 2026
71647e3
Merge pull request #2 from feloy/podman-runtime-2
feloy Mar 19, 2026
010a572
feat(runtime/podman): implement Start method with exec abstraction
feloy Mar 19, 2026
da546bb
Merge pull request #3 from feloy/podman-runtime-3
feloy Mar 19, 2026
b5ba531
feat(runtime/podman): implement Stop method
feloy Mar 19, 2026
3019bd6
feat(runtime/podman): implement Info method
feloy Mar 19, 2026
197e19f
Merge pull request #4 from feloy/podman-runtime-stop
feloy Mar 19, 2026
6e07294
feat(runtime/podman): implement Remove method
feloy Mar 20, 2026
bee815a
Merge pull request #5 from feloy/podman-runtime-remove
feloy Mar 20, 2026
a8cf819
fix(instances): handle runtime Remove errors in Delete method
feloy Mar 20, 2026
e4c3492
fix: container not found detection
feloy Mar 20, 2026
d4625e7
Merge pull request #6 from feloy/podman-runtime-doc
feloy Mar 20, 2026
5ccd400
docs(runtime/podman): document container image, packages, and permiss…
feloy Mar 20, 2026
d2bd851
Merge pull request #7 from feloy/podman-runtime-doc2
feloy Mar 20, 2026
1bc3199
Revert "ci: allow codecov upload to fail without failing CI"
feloy Mar 20, 2026
9df97b0
fix: group creation when already exists
feloy Mar 20, 2026
6e21e2b
fix(runtime/podman): uid/gid on Windows
feloy Mar 20, 2026
be26a72
fix: remove unused code
feloy Mar 20, 2026
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
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,90 @@ kortex-cli list
kortex-cli list --storage /tmp/kortex-storage
```

## Runtimes

### Podman Runtime

The Podman runtime provides a container-based development environment for workspaces. It creates an isolated environment with all necessary tools pre-installed and configured.

#### Container Image

**Base Image:** `registry.fedoraproject.org/fedora:latest`

The Podman runtime builds a custom container image based on Fedora Linux, providing a stable and up-to-date foundation for development work.

#### Installed Packages

The runtime includes a comprehensive development toolchain:

- **Core Utilities:**
- `which` - Command location utility
- `procps-ng` - Process management utilities
- `wget2` - Advanced file downloader

- **Development Tools:**
- `@development-tools` - Complete development toolchain (gcc, make, etc.)
- `jq` - JSON processor
- `gh` - GitHub CLI

- **Language Support:**
- `golang` - Go programming language
- `golangci-lint` - Go linter
- `python3` - Python 3 interpreter
- `python3-pip` - Python package manager

#### User and Permissions

The container runs as a non-root user named `claude` with the following configuration:

- **User:** `claude`
- **UID/GID:** Matches the host user's UID and GID for seamless file permissions
- **Home Directory:** `/home/claude`

**Sudo Permissions:**

The `claude` user has limited sudo access with no password required (`NOPASSWD`) for:

- **Package Management:**
- `/usr/bin/dnf` - Install, update, and manage packages

- **Process Management:**
- `/bin/nice` - Run programs with modified scheduling priority
- `/bin/kill`, `/usr/bin/kill` - Send signals to processes
- `/usr/bin/killall` - Kill processes by name

All other sudo commands are explicitly denied for security.

#### AI Agent

**Claude Code** is installed as the default AI agent using the official installation script from `claude.ai/install.sh`. This provides:

- Full Claude Code CLI capabilities
- Integrated development assistance
- Access to Claude's latest features

The agent runs within the container environment and has access to the mounted workspace sources and dependencies.

#### Working Directory

The container's working directory is set to `/workspace/sources`, which is where your project source code is mounted. This ensures that the agent and all tools operate within your project context.

#### Example Usage

```bash
# Register a workspace with the Podman runtime
kortex-cli init /path/to/project --runtime podman

# Start the workspace (builds image and starts container)
kortex-cli start <workspace-id>
```

The first time you start a workspace, the Podman runtime will:
1. Create a Containerfile with the configuration above
2. Build a custom image (tagged as `kortex-cli-<workspace-name>`)
3. Create a container with your source code mounted
4. Start the container and make it ready for use

## Workspace Configuration

Each workspace can optionally include a configuration file that customizes the environment and mount behavior for that specific workspace. The configuration is stored in a `workspace.json` file within the workspace's configuration directory (typically `.kortex` in the sources directory).
Expand Down
15 changes: 9 additions & 6 deletions pkg/instances/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,8 @@ func (m *manager) Get(id string) (Instance, error) {
}

// Delete unregisters an instance by ID.
// Before removing from storage, it attempts to remove the runtime instance.
// Runtime cleanup is best-effort: if the runtime is unavailable, deletion proceeds anyway.
// Before removing from storage, it removes the runtime instance.
// If runtime removal fails, the instance is NOT removed from storage and an error is returned.
func (m *manager) Delete(ctx context.Context, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
Expand All @@ -396,13 +396,16 @@ func (m *manager) Delete(ctx context.Context, id string) error {
return ErrInstanceNotFound
}

// Runtime cleanup (best effort)
// Runtime cleanup
runtimeInfo := instanceToDelete.GetRuntimeData()
if runtimeInfo.Type != "" && runtimeInfo.InstanceID != "" {
rt, err := m.runtimeRegistry.Get(runtimeInfo.Type)
if err == nil {
// Runtime is available, try to clean up (ignore errors)
_ = rt.Remove(ctx, runtimeInfo.InstanceID)
if err != nil {
return fmt.Errorf("failed to get runtime: %w", err)
}
// Remove runtime instance (must succeed before removing from storage)
if err := rt.Remove(ctx, runtimeInfo.InstanceID); err != nil {
return fmt.Errorf("failed to remove runtime instance: %w", err)
}
}

Expand Down
38 changes: 38 additions & 0 deletions pkg/instances/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,44 @@ func TestManager_Delete(t *testing.T) {
t.Errorf("Get() from new manager error = %v, want %v", err, ErrInstanceNotFound)
}
})

t.Run("returns error when runtime Remove fails", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
tmpDir := t.TempDir()
manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir))

instanceTmpDir := t.TempDir()
sourceDir := filepath.Join(instanceTmpDir, "source")
configDir := filepath.Join(instanceTmpDir, "config")
inst := newFakeInstance(newFakeInstanceParams{
SourceDir: sourceDir,
ConfigDir: configDir,
Accessible: true,
})
added, _ := manager.Add(ctx, AddOptions{Instance: inst, RuntimeType: "fake"})

generatedID := added.GetID()

// Start the instance (fake runtime doesn't allow removing running instances)
err := manager.Start(ctx, generatedID)
if err != nil {
t.Fatalf("Start() unexpected error = %v", err)
}

// Try to delete while running - should fail
err = manager.Delete(ctx, generatedID)
if err == nil {
t.Fatal("Delete() expected error when runtime Remove fails, got nil")
}

// Verify instance was NOT deleted from manager storage
_, err = manager.Get(generatedID)
if err != nil {
t.Errorf("Get() after failed Delete() unexpected error = %v, instance should still exist", err)
}
})
}

func TestManager_Start(t *testing.T) {
Expand Down
Loading
Loading