diff --git a/cmd/harness.go b/cmd/harness.go index ec6f803..fe46b65 100644 --- a/cmd/harness.go +++ b/cmd/harness.go @@ -31,6 +31,9 @@ func runHarness(cmd *cobra.Command, args []string) error { return fmt.Errorf("coding harness is not installed and update checks are disabled — " + "unset KIMCHI_NO_UPDATE_CHECK or install the harness manually") } + if err := guardAgainstSelfExec(harnessPath); err != nil { + return err + } return launchHarness(cmd, harnessPath, args) } @@ -46,6 +49,10 @@ func runHarness(cmd *cobra.Command, args []string) error { return nil } + if err := guardAgainstSelfExec(harnessPath); err != nil { + return err + } + var props map[string]any if v, err := update.HarnessCurrentVersion(ctx); err == nil && v != nil { props = map[string]any{"version": v.String()} @@ -55,6 +62,33 @@ func runHarness(cmd *cobra.Command, args []string) error { return launchHarness(cmd, harnessPath, args) } +// guardAgainstSelfExec refuses to launch the harness if its resolved path +// points at the running CLI binary. Without this check a misconfigured path +// would cause syscall.Exec to replace the CLI with itself, producing an +// infinite launch loop instead of a clear failure. +func guardAgainstSelfExec(harnessPath string) error { + selfPath, err := update.ResolveExecutablePath() + if err != nil { + klog.V(1).ErrorS(err, "self-exec guard: could not resolve current executable; skipping check") + return nil + } + selfInfo, err := os.Stat(selfPath) + if err != nil { + klog.V(1).ErrorS(err, "self-exec guard: stat self failed; skipping check", "path", selfPath) + return nil + } + harnessInfo, err := os.Stat(harnessPath) + if err != nil { + klog.V(1).ErrorS(err, "self-exec guard: stat harness failed; skipping check", "path", harnessPath) + return nil + } + if os.SameFile(selfInfo, harnessInfo) { + return fmt.Errorf("refusing to launch harness: resolved harness path %q is the kimchi CLI itself "+ + "(this would loop forever). Remove the stray file and reinstall: rm %q && kimchi update", harnessPath, harnessPath) + } + return nil +} + // launchHarness resolves the API key (prompting if missing), ensures it is // saved in the config file, and execs into the harness binary. func launchHarness(cmd *cobra.Command, harnessPath string, args []string) error { diff --git a/internal/update/harness.go b/internal/update/harness.go index f8a2715..93e7b11 100644 --- a/internal/update/harness.go +++ b/internal/update/harness.go @@ -83,15 +83,11 @@ func HarnessPathInDir(dir string) string { return filepath.Join(dir, kimchiDevRepo.Binary) } -// ResolveHarnessPath derives the harness binary path from the kimchi executable's -// resolved directory. For example, if kimchi is at /usr/local/bin/kimchi, this -// returns /usr/local/bin/kimchi. +// ResolveHarnessPath returns the path where the harness binary lives. +// It is placed inside the harness data directory so it does not collide +// with the Go CLI binary (both are now named "kimchi" after the merge). func ResolveHarnessPath() (string, error) { - execPath, err := ResolveExecutablePath() - if err != nil { - return "", fmt.Errorf("resolve harness path: %w", err) - } - return HarnessPathInDir(filepath.Dir(execPath)), nil + return HarnessPathInDir(filepath.Join(harnessDataDir(), "bin")), nil } // HarnessInstalled reports whether the harness binary exists at the given path.