Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
53a8aba
feat: add workspace isolation and global proxy
dbpolito Aug 11, 2026
a3b92a0
fix: restore CI validation
dbpolito Aug 11, 2026
0532007
fix: keep workspace features opt-in
dbpolito Aug 12, 2026
20d78bd
fix: repair proxy network and protocol transitions
dbpolito Aug 12, 2026
1abc959
fix: isolate recursive workspace context
dbpolito Aug 12, 2026
4deb159
fix: preserve existing routes on proxy failure
dbpolito Aug 12, 2026
2e3e0dc
fix: make proxy preparation transactional
dbpolito Aug 12, 2026
22024db
fix: harden workspace and proxy identity
dbpolito Aug 12, 2026
4f0e6c0
fix: close remaining proxy identity gaps
dbpolito Aug 12, 2026
296e9c9
fix: secure and rollback proxy preparation
dbpolito Aug 12, 2026
44dbd2c
fix: isolate proxy admin on management network
dbpolito Aug 12, 2026
8d62e4f
fix: reserve proxy management resources
dbpolito Aug 12, 2026
2bc0e98
fix: disable proxy autosave resume
dbpolito Aug 12, 2026
a9ad8db
fix: migrate legacy proxy commands
dbpolito Aug 12, 2026
ef25316
fix: reconcile proxy after concurrent starts
dbpolito Aug 12, 2026
691c898
fix: propagate final proxy reconciliation
dbpolito Aug 12, 2026
f489ca6
fix: scope proxy rollback to start generation
dbpolito Aug 12, 2026
bf7951d
fix: restore prior routes during concurrent rollback
dbpolito Aug 12, 2026
d609074
fix: preserve complete Caddy state on rollback
dbpolito Aug 12, 2026
093c58f
fix: preserve concurrent proxy protocol state
dbpolito Aug 12, 2026
e59bc40
fix: distinguish newer proxy route generations
dbpolito Aug 12, 2026
e381e9e
fix: expand global proxy without losing projects
dbpolito Aug 12, 2026
a09ef17
fix: rollback failed proxy listener expansion
dbpolito Aug 12, 2026
13df64c
fix: preserve projects during proxy migration
dbpolito Aug 12, 2026
b5c34d8
fix: preserve exact proxy port bindings
dbpolito Aug 12, 2026
b873fd9
fix: sanitize proxy recreation bindings
dbpolito Aug 12, 2026
8023282
fix: stabilize CI environment isolation
dbpolito Aug 12, 2026
b8696f0
fix: restore reachable proxy administration
dbpolito Aug 12, 2026
2279dc8
fix: clean workspace hosts and stop unused proxy
dbpolito Aug 12, 2026
9f8cdf5
fix: restart stopped proxy before inspection
dbpolito Aug 12, 2026
b8a1bff
fix: disambiguate duplicate workspace hosts
dbpolito Aug 12, 2026
c2f56e3
Merge branch 'main' into feature/workspace-proxy
fabriciojs Aug 15, 2026
4430923
minor language tweak
fabriciojs Aug 15, 2026
701399c
refactor(parser): consolidate kool.yml discovery into a single source…
fabriciojs Aug 15, 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
4 changes: 4 additions & 0 deletions commands/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ func (d *KoolDocker) Execute(args []string) (err error) {
d.dockerRun.AppendArgs("--env", "ASUSER="+asuser)
}

for _, envVar := range environment.AgentEnvVars(d.envStorage) {
d.dockerRun.AppendArgs("--env", envVar)
}

if len(d.Flags.EnvVariables) > 0 {
for _, envVar := range d.Flags.EnvVariables {
d.dockerRun.AppendArgs("--env", envVar)
Expand Down
17 changes: 17 additions & 0 deletions commands/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,23 @@ func TestEnvFlagNewDockerCommand(t *testing.T) {
}
}

func TestAgentEnvNewDockerCommand(t *testing.T) {
f := newFakeKoolDocker()
f.shell.(*shell.FakeShell).MockIsTerminal = false
f.envStorage.(*environment.FakeEnvStorage).Envs["CLAUDECODE"] = "1"
cmd := NewDockerCommand(f)
cmd.SetArgs([]string{"image"})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing docker command; error: %v", err)
}

argsAppend := f.dockerRun.(*builder.FakeCommand).ArgsAppend
if len(argsAppend) != 4 || argsAppend[0] != "--env" || argsAppend[1] != "CLAUDECODE=1" {
t.Errorf("bad arguments to KoolDocker.dockerRun Command with detected agent: %v", argsAppend)
}
}

func TestVolumesFlagNewDockerCommand(t *testing.T) {
f := newFakeKoolDocker()
f.shell.(*shell.FakeShell).MockIsTerminal = false
Expand Down
4 changes: 4 additions & 0 deletions commands/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func (e *KoolExec) Execute(args []string) (err error) {

e.checkUser(args[0])

for _, envVar := range environment.AgentEnvVars(e.env) {
e.composeExec.AppendArgs("--env", envVar)
}

if len(e.Flags.EnvVariables) > 0 {
for _, envVar := range e.Flags.EnvVariables {
e.composeExec.AppendArgs("--env", envVar)
Expand Down
16 changes: 16 additions & 0 deletions commands/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ func TestEnvFlagNewExecCommand(t *testing.T) {
}
}

func TestAgentEnvNewExecCommand(t *testing.T) {
f := newFakeKoolExec()
f.env.(*environment.FakeEnvStorage).Envs["OPENCODE"] = "1"
cmd := NewExecCommand(f)
cmd.SetArgs([]string{"service", "command"})

if err := cmd.Execute(); err != nil {
t.Errorf("unexpected error executing exec command; error: %v", err)
}

argsAppend := f.composeExec.(*builder.FakeCommand).ArgsAppend
if len(argsAppend) != 2 || argsAppend[0] != "--env" || argsAppend[1] != "OPENCODE=1" {
t.Errorf("bad arguments to KoolExec.composeExec Command with detected agent: %v", argsAppend)
}
}

func TestDetachFlagNewExecCommand(t *testing.T) {
f := newFakeKoolExec()
cmd := NewExecCommand(f)
Expand Down
30 changes: 30 additions & 0 deletions commands/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package commands

import (
"kool-dev/kool/core/environment"
"kool-dev/kool/core/shell"
"kool-dev/kool/services/proxy"

"github.com/spf13/cobra"
)

// AddKoolProxy adds commands for managing Kool's local proxy.
func AddKoolProxy(root *cobra.Command) {
proxyCommand := &cobra.Command{
Use: "proxy",
Short: "Manage Kool's local proxy",
}
proxyCommand.AddCommand(&cobra.Command{
Use: "trust",
Short: "Trust the local proxy certificate authority",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
sh := shell.NewShell()
sh.SetInStream(cmd.InOrStdin())
sh.SetOutStream(cmd.OutOrStdout())
sh.SetErrStream(cmd.ErrOrStderr())
return proxy.NewManager(sh, environment.NewEnvStorage()).Trust()
},
})
root.AddCommand(proxyCommand)
}
116 changes: 95 additions & 21 deletions commands/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"errors"
"fmt"
"io"
"kool-dev/kool/core/environment"
Expand Down Expand Up @@ -32,6 +33,7 @@ var AddCommands AddCommandsFN = func(root *cobra.Command) {
AddKoolInfo(root)
AddKoolLogs(root)
AddKoolPreset(root)
AddKoolProxy(root)
AddKoolRestart(root)
AddKoolRun(root)
AddKoolSelfUpdate(root)
Expand All @@ -47,7 +49,7 @@ const DEV_VERSION = "0.0.0-dev"

var version string = DEV_VERSION

var rootCmd = NewRootCmd(environment.NewEnvStorage())
var rootCmd = newRootCmd(environment.NewEnvStorage(), true)

var originalWorkingDir = ""

Expand All @@ -60,6 +62,11 @@ func init() {

// NewRootCmd creates the root command
func NewRootCmd(env environment.EnvStorage) (cmd *cobra.Command) {
return newRootCmd(env, false)
}

func newRootCmd(env environment.EnvStorage, initializeEnvironment bool) (cmd *cobra.Command) {
environmentInitialized := false
cmd = &cobra.Command{
Args: cobra.ArbitraryArgs,
Use: "kool",
Expand All @@ -75,20 +82,17 @@ Complete documentation is available at https://kool.dev/docs`,
DisableFlagsInUseLine: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
cmd.SilenceUsage = true

if verbose := cmd.Flags().Lookup("verbose"); verbose != nil && verbose.Value.String() == "true" {
env.Set("KOOL_VERBOSE", verbose.Value.String())
}

if !hasWarnedDevelopmentVersion && version == DEV_VERSION && shell.NewTerminalChecker().IsTerminal(cmd.OutOrStdout()) {
shell.NewShell().Warning("Warning: you are executing a development version of kool.")
hasWarnedDevelopmentVersion = true
}

workDirFlag := cmd.Flags().Lookup("working_dir")
if workDirFlag != nil && workDirFlag.Value.String() != "" {
workDir := workDirFlag.Value.String()

currentWorkingDir, getwdErr := os.Getwd()
if getwdErr != nil {
return getwdErr
}
if originalWorkingDir == "" {
originalWorkingDir = currentWorkingDir
}
if originalWorkingDir != "" {
// having an original working dir set means we have
// already changed the working dir before and we are in
Expand All @@ -108,17 +112,27 @@ Complete documentation is available at https://kool.dev/docs`,
if err = os.Chdir(workDir); err != nil {
return
}

if originalWorkingDir == "" {
// we only set the original working dir if it is not set
// yet. This is to avoid overriding the original working
// dir in recursive calls.
if originalWorkingDir, err = os.Getwd(); err != nil {
return
}
if workDir, err = os.Getwd(); err != nil {
return
}
env.Set("PWD", workDir)

}
if initializeEnvironment && !environmentInitialized {
environment.InitEnvironmentVariables(env)
environmentInitialized = true
}
if workspaceError := env.Get("KOOL_WORKSPACE_ERROR"); workspaceError != "" {
return errors.New(workspaceError)
}

if verbose := cmd.Flags().Lookup("verbose"); verbose != nil && verbose.Value.String() == "true" {
env.Set("KOOL_VERBOSE", verbose.Value.String())
}

environment.NewEnvStorage().Set("PWD", workDir)
if !hasWarnedDevelopmentVersion && version == DEV_VERSION && shell.NewTerminalChecker().IsTerminal(cmd.OutOrStdout()) {
shell.NewShell().Warning("Warning: you are executing a development version of kool.")
hasWarnedDevelopmentVersion = true
}

return
Expand Down Expand Up @@ -165,7 +179,27 @@ func Execute() error {

func setRecursiveCall(root *cobra.Command) {
shell.RecursiveCall = func(args []string, in io.Reader, out, err io.Writer) error {
childRoot := NewRootCmd(environment.NewEnvStorage())
currentDirectory, getwdErr := os.Getwd()
if getwdErr != nil {
return getwdErr
}
currentPWD := os.Getenv("PWD")
currentOriginalWorkingDir := originalWorkingDir
defer func() {
_ = os.Chdir(currentDirectory)
_ = os.Setenv("PWD", currentPWD)
originalWorkingDir = currentOriginalWorkingDir
}()

initializeEnvironment := hasWorkingDirArg(args)
if initializeEnvironment {
restoreEnvironment := clearDirectoryEnvironment(currentDirectory)
defer restoreEnvironment()
restoreWorkspace := environment.IsolateWorkspace()
defer restoreWorkspace()
originalWorkingDir = currentDirectory
}
childRoot := newRootCmd(environment.NewEnvStorage(), initializeEnvironment)

childRoot.SetArgs(args)

Expand All @@ -179,6 +213,46 @@ func setRecursiveCall(root *cobra.Command) {
}
}

func hasWorkingDirArg(args []string) bool {
for _, arg := range args {
if arg == "-w" || arg == "--working_dir" || strings.HasPrefix(arg, "--working_dir=") || strings.HasPrefix(arg, "-w=") || strings.HasPrefix(arg, "-w") && len(arg) > 2 {
return true
}
}
return false
}

func clearDirectoryEnvironment(directory string) func() {
previous := make(map[string]string)
for _, entry := range os.Environ() {
parts := strings.SplitN(entry, "=", 2)
previous[parts[0]] = parts[1]
}
keys := []string{
"PWD", "KOOL_NAME", "KOOL_WORKSPACES_ENABLED", "KOOL_PROXY_ENABLED",
"KOOL_WORKSPACE", "KOOL_WORKSPACE_PROVIDER", "KOOL_WORKSPACE_ERROR",
"KOOL_WORKSPACE_SOURCE", "KOOL_WORKSPACE_SOURCE_PROJECT", "KOOL_WORKSPACE_NAME",
"KOOL_WORKSPACE_PATH", "KOOL_WORKSPACE_PROJECT", "KOOL_WORKSPACE_SERVICES",
"KOOL_PROXY_DOMAIN", "KOOL_PROXY_HOST", "COMPOSE_PROJECT_NAME", "COMPOSE_FILE",
"KOOL_GLOBAL_NETWORK",
}
keys = append(keys, environment.LoadedEnvKeys(directory)...)
for _, key := range keys {
_ = os.Unsetenv(key)
}
return func() {
for _, entry := range os.Environ() {
key := strings.SplitN(entry, "=", 2)[0]
if _, exists := previous[key]; !exists {
_ = os.Unsetenv(key)
}
}
for key, value := range previous {
_ = os.Setenv(key, value)
}
}
}

// RootCmd exposes the root command
func RootCmd() *cobra.Command {
return rootCmd
Expand Down
Loading
Loading