diff --git a/README.md b/README.md index c247ea6..0ec9d93 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ go get github.com/activadee/godex ``` -`godex` automatically downloads the Codex CLI into your user cache the first time it is needed. The cached build is keyed by platform and release tag so upgrades are seamless. Advanced users can override the cache directory or release tag via the `GODEX_CLI_CACHE` and `GODEX_CLI_RELEASE_TAG` environment variables. If you prefer to use a self-managed binary, set `CodexOptions.CodexPathOverride` or ensure the CLI is already available on your `PATH` (e.g. `which codex`). Authentication is handled entirely by the CLI; reuse whichever credentials you already configured (environment variables, `codex auth login`, etc.) or set `CodexOptions.APIKey` to override the API key programmatically: +`godex` automatically downloads the Codex CLI into your user cache the first time it is needed. The cached build is keyed by platform and release tag so upgrades are seamless. Advanced users can override the cache directory or release tag via `CodexOptions.CLICacheDir` / `CodexOptions.CLIReleaseTag` (or the `GODEX_CLI_CACHE` / `GODEX_CLI_RELEASE_TAG` environment variables) and enforce SHA-256 verification with `CodexOptions.CLIChecksum` or `GODEX_CLI_CHECKSUM`. If you prefer to use a self-managed binary, set `CodexOptions.CodexPathOverride` or ensure the CLI is already available on your `PATH` (e.g. `which codex`). Authentication is handled entirely by the CLI; reuse whichever credentials you already configured (environment variables, `codex auth login`, etc.) or set `CodexOptions.APIKey` to override the API key programmatically: ```bash export CODEX_API_KEY=sk-... @@ -18,6 +18,40 @@ codex auth login Override service endpoints (for self-hosted deployments) with `CodexOptions.BaseURL`. +### CLI bootstrap controls + +`CodexOptions` exposes the same knobs that previously required environment variables when +you need deterministic CLI bootstrapping: + +- `CLICacheDir` overrides where downloaded binaries are stored. It takes precedence over + `GODEX_CLI_CACHE` and falls back to the user cache or `os.TempDir()`. +- `CLIReleaseTag` pins the release asset fetched from `github.com/openai/codex`. It overrides + `GODEX_CLI_RELEASE_TAG` and defaults to the SDK's bundled tag. +- `CLIChecksum` enforces integrity by verifying the SHA-256 checksum of the extracted binary. + Supply the expected digest (hex encoded) from the official release notes or your + distribution channel. The environment variable equivalent is `GODEX_CLI_CHECKSUM`. + +```go +import ( + "os" + "path/filepath" + + "github.com/activadee/godex" +) + +client, err := godex.New(godex.CodexOptions{ + CLICacheDir: filepath.Join(os.TempDir(), "codex-cache"), + CLIReleaseTag: "rust-v0.55.0", + CLIChecksum: "f8a1...", +}) +``` + +When a checksum is configured, `godex` verifies both cached binaries and freshly downloaded +ones, forcing a re-download or returning an error if the digest does not match. This allows +you to gate Codex upgrades on an allowlisted fingerprint without writing custom bootstrap +code. The checksum is calculated over the extracted `codex` executable for the detected +platform/architecture. + ## Quick start ```go diff --git a/codex.go b/codex.go index f19789d..6eda394 100644 --- a/codex.go +++ b/codex.go @@ -11,7 +11,12 @@ type Codex struct { // New constructs a Codex SDK instance. The Codex binary is discovered automatically unless // CodexOptions.CodexPathOverride is provided. func New(options CodexOptions) (*Codex, error) { - exec, err := codexexec.New(options.CodexPathOverride) + exec, err := codexexec.New(codexexec.RunnerOptions{ + PathOverride: options.CodexPathOverride, + CacheDir: options.CLICacheDir, + ReleaseTag: options.CLIReleaseTag, + ChecksumHex: options.CLIChecksum, + }) if err != nil { return nil, err } diff --git a/internal/codexexec/bundle.go b/internal/codexexec/bundle.go index c09c34f..77baa2b 100644 --- a/internal/codexexec/bundle.go +++ b/internal/codexexec/bundle.go @@ -5,6 +5,8 @@ import ( "archive/zip" "bytes" "compress/gzip" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" @@ -12,6 +14,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "time" ) @@ -24,6 +27,81 @@ const ( const defaultCodexReleaseTag = "rust-v0.55.0" +var ErrChecksumMismatch = errors.New("codex bundle checksum mismatch") + +type bundleConfig struct { + cacheDir string + releaseTag string + checksumHex string +} + +func (cfg bundleConfig) cacheDirPath() (string, error) { + if dir := strings.TrimSpace(cfg.cacheDir); dir != "" { + return dir, nil + } + if override := strings.TrimSpace(os.Getenv("GODEX_CLI_CACHE")); override != "" { + return override, nil + } + if dir, err := os.UserCacheDir(); err == nil && dir != "" { + return filepath.Join(dir, "godex", "codex"), nil + } + return filepath.Join(os.TempDir(), "godex", "codex"), nil +} + +func (cfg bundleConfig) releaseTagName() string { + if tag := strings.TrimSpace(cfg.releaseTag); tag != "" { + return tag + } + if env := strings.TrimSpace(os.Getenv("GODEX_CLI_RELEASE_TAG")); env != "" { + return env + } + return defaultCodexReleaseTag +} + +func (cfg bundleConfig) checksumValue() (string, error) { + value := strings.TrimSpace(cfg.checksumHex) + if value == "" { + value = strings.TrimSpace(os.Getenv("GODEX_CLI_CHECKSUM")) + } + if value == "" { + return "", nil + } + normalized, err := normalizeChecksum(value) + if err != nil { + return "", err + } + return normalized, nil +} + +func normalizeChecksum(value string) (string, error) { + cleaned := strings.ToLower(strings.TrimSpace(value)) + if cleaned == "" { + return "", nil + } + if _, err := hex.DecodeString(cleaned); err != nil { + return "", fmt.Errorf("invalid checksum value %q: %w", value, err) + } + return cleaned, nil +} + +func (cfg bundleConfig) releasePinned() bool { + if strings.TrimSpace(cfg.releaseTag) != "" { + return true + } + return strings.TrimSpace(os.Getenv("GODEX_CLI_RELEASE_TAG")) != "" +} + +func (cfg bundleConfig) checksumRequired() bool { + if strings.TrimSpace(cfg.checksumHex) != "" { + return true + } + return strings.TrimSpace(os.Getenv("GODEX_CLI_CHECKSUM")) != "" +} + +func (cfg bundleConfig) requireBundledBinary() bool { + return cfg.releasePinned() || cfg.checksumRequired() +} + var downloadBinaryFunc = downloadBinaryFromRelease var runtimeGOOS = runtime.GOOS var runtimeGOARCH = runtime.GOARCH @@ -36,13 +114,6 @@ type targetInfo struct { exeName string } -func releaseTag() string { - if v := os.Getenv("GODEX_CLI_RELEASE_TAG"); v != "" { - return v - } - return defaultCodexReleaseTag -} - func detectTarget(goos, goarch string) (targetInfo, bool) { switch goos { case "linux": @@ -106,18 +177,22 @@ func detectTarget(goos, goarch string) (targetInfo, bool) { return targetInfo{}, false } -func ensureBundledBinary() (string, error) { +func ensureBundledBinary(cfg bundleConfig) (string, error) { info, ok := detectTarget(runtimeGOOS, runtimeGOARCH) if !ok { return "", fmt.Errorf("unsupported platform: %s/%s", runtimeGOOS, runtimeGOARCH) } - cacheDir, err := bundleCacheDir() + cacheDir, err := cfg.cacheDirPath() if err != nil { return "", err } - release := releaseTag() + release := cfg.releaseTagName() + checksumHex, err := cfg.checksumValue() + if err != nil { + return "", fmt.Errorf("resolve checksum: %w", err) + } targetDir := filepath.Join(cacheDir, release, info.triple) if err := os.MkdirAll(targetDir, 0o755); err != nil { return "", fmt.Errorf("create bundle directory: %w", err) @@ -125,7 +200,16 @@ func ensureBundledBinary() (string, error) { destPath := filepath.Join(targetDir, info.exeName) if statErr := ensureBinaryState(destPath); statErr == nil { - return destPath, nil + if checksumHex == "" { + return destPath, nil + } + if err := verifyChecksum(destPath, checksumHex); err == nil { + return destPath, nil + } else if errors.Is(err, ErrChecksumMismatch) { + _ = os.Remove(destPath) + } else { + return "", fmt.Errorf("verify cached binary: %w", err) + } } else if !errors.Is(statErr, os.ErrNotExist) { return "", fmt.Errorf("stat bundled binary: %w", statErr) } @@ -133,6 +217,12 @@ func ensureBundledBinary() (string, error) { if err := downloadBinaryFunc(info, release, destPath); err != nil { return "", err } + if checksumHex != "" { + if err := verifyChecksum(destPath, checksumHex); err != nil { + _ = os.Remove(destPath) + return "", fmt.Errorf("verify downloaded binary: %w", err) + } + } return destPath, nil } @@ -141,16 +231,6 @@ func ensureBinaryState(path string) error { return err } -func bundleCacheDir() (string, error) { - if override := os.Getenv("GODEX_CLI_CACHE"); override != "" { - return override, nil - } - if dir, err := os.UserCacheDir(); err == nil && dir != "" { - return filepath.Join(dir, "godex", "codex"), nil - } - return filepath.Join(os.TempDir(), "godex", "codex"), nil -} - func downloadBinaryFromRelease(info targetInfo, release, destPath string) error { url := fmt.Sprintf("https://github.com/openai/codex/releases/download/%s/%s", release, info.assetName) @@ -226,6 +306,24 @@ func extractZipBinary(data []byte, info targetInfo, destPath string) error { return fmt.Errorf("binary %s not found in archive", info.binaryName) } +func verifyChecksum(path, expectedHex string) error { + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("open binary for checksum: %w", err) + } + defer file.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return fmt.Errorf("hash binary: %w", err) + } + actual := hex.EncodeToString(hasher.Sum(nil)) + if actual != expectedHex { + return fmt.Errorf("%w: expected %s, got %s", ErrChecksumMismatch, expectedHex, actual) + } + return nil +} + func writeBinary(r io.Reader, destPath string) error { tmpFile, err := os.CreateTemp(filepath.Dir(destPath), filepath.Base(destPath)+".tmp-*") if err != nil { diff --git a/internal/codexexec/bundle_test.go b/internal/codexexec/bundle_test.go index 37a469d..0b25fa8 100644 --- a/internal/codexexec/bundle_test.go +++ b/internal/codexexec/bundle_test.go @@ -1,6 +1,9 @@ package codexexec import ( + "crypto/sha256" + "encoding/hex" + "errors" "fmt" "os" "path/filepath" @@ -41,7 +44,7 @@ func TestDetectTargetSupportsKnownCombinations(t *testing.T) { func TestEnsureBundledBinaryDownloadsWhenMissing(t *testing.T) { tmp := t.TempDir() - t.Setenv("GODEX_CLI_CACHE", tmp) + cfg := bundleConfig{cacheDir: tmp} originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH runtimeGOOS, runtimeGOARCH = "linux", "amd64" @@ -60,7 +63,7 @@ func TestEnsureBundledBinaryDownloadsWhenMissing(t *testing.T) { } t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) - path, err := ensureBundledBinary() + path, err := ensureBundledBinary(cfg) if err != nil { t.Fatalf("ensureBundledBinary returned error: %v", err) } @@ -74,7 +77,7 @@ func TestEnsureBundledBinaryDownloadsWhenMissing(t *testing.T) { func TestEnsureBundledBinarySkipsDownloadWhenPresent(t *testing.T) { tmp := t.TempDir() - t.Setenv("GODEX_CLI_CACHE", tmp) + cfg := bundleConfig{cacheDir: tmp} originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH runtimeGOOS, runtimeGOARCH = "linux", "amd64" @@ -83,7 +86,7 @@ func TestEnsureBundledBinarySkipsDownloadWhenPresent(t *testing.T) { }) info, _ := detectTarget(runtimeGOOS, runtimeGOARCH) - release := releaseTag() + release := cfg.releaseTagName() targetDir := filepath.Join(tmp, release, info.triple) if err := os.MkdirAll(targetDir, 0o755); err != nil { t.Fatalf("mkdir: %v", err) @@ -100,7 +103,7 @@ func TestEnsureBundledBinarySkipsDownloadWhenPresent(t *testing.T) { } t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) - path, err := ensureBundledBinary() + path, err := ensureBundledBinary(cfg) if err != nil { t.Fatalf("ensureBundledBinary returned error: %v", err) } @@ -109,6 +112,144 @@ func TestEnsureBundledBinarySkipsDownloadWhenPresent(t *testing.T) { } } +func TestBundleCacheDirPrefersOptionOverEnv(t *testing.T) { + envDir := filepath.Join(t.TempDir(), "env-cache") + t.Setenv("GODEX_CLI_CACHE", envDir) + explicit := filepath.Join(t.TempDir(), "explicit-cache") + cfg := bundleConfig{cacheDir: explicit} + + got, err := cfg.cacheDirPath() + if err != nil { + t.Fatalf("cacheDirPath returned error: %v", err) + } + if got != explicit { + t.Fatalf("cacheDirPath=%s, want %s", got, explicit) + } +} + +func TestEnsureBundledBinaryUsesProvidedReleaseTag(t *testing.T) { + tmp := t.TempDir() + cfg := bundleConfig{cacheDir: tmp, releaseTag: "custom-release"} + t.Setenv("GODEX_CLI_RELEASE_TAG", "env-release") + + originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH + runtimeGOOS, runtimeGOARCH = "linux", "amd64" + t.Cleanup(func() { + runtimeGOOS, runtimeGOARCH = originalGOOS, originalGOARCH + }) + + var releaseUsed string + originalDownloader := downloadBinaryFunc + downloadBinaryFunc = func(info targetInfo, release, destPath string) error { + releaseUsed = release + return os.WriteFile(destPath, []byte("binary"), 0o700) + } + t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) + + if _, err := ensureBundledBinary(cfg); err != nil { + t.Fatalf("ensureBundledBinary returned error: %v", err) + } + if releaseUsed != "custom-release" { + t.Fatalf("expected release custom-release, got %s", releaseUsed) + } +} + +func TestEnsureBundledBinaryVerifiesChecksums(t *testing.T) { + tmp := t.TempDir() + cfg := bundleConfig{ + cacheDir: tmp, + checksumHex: sha256Hex([]byte("binary")), + } + + originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH + runtimeGOOS, runtimeGOARCH = "linux", "amd64" + t.Cleanup(func() { + runtimeGOOS, runtimeGOARCH = originalGOOS, originalGOARCH + }) + + originalDownloader := downloadBinaryFunc + downloadBinaryFunc = func(info targetInfo, release, destPath string) error { + return os.WriteFile(destPath, []byte("binary"), 0o700) + } + t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) + + if _, err := ensureBundledBinary(cfg); err != nil { + t.Fatalf("ensureBundledBinary returned error: %v", err) + } +} + +func TestEnsureBundledBinaryFailsOnChecksumMismatch(t *testing.T) { + tmp := t.TempDir() + cfg := bundleConfig{ + cacheDir: tmp, + checksumHex: strings.Repeat("00", 32), + } + + originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH + runtimeGOOS, runtimeGOARCH = "linux", "amd64" + t.Cleanup(func() { + runtimeGOOS, runtimeGOARCH = originalGOOS, originalGOARCH + }) + + originalDownloader := downloadBinaryFunc + downloadBinaryFunc = func(info targetInfo, release, destPath string) error { + return os.WriteFile(destPath, []byte("binary"), 0o700) + } + t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) + + if _, err := ensureBundledBinary(cfg); err == nil || !errors.Is(err, ErrChecksumMismatch) { + t.Fatalf("expected checksum mismatch error, got %v", err) + } +} + +func TestEnsureBundledBinaryRedownloadsWhenCachedChecksumMismatch(t *testing.T) { + tmp := t.TempDir() + cfg := bundleConfig{ + cacheDir: tmp, + checksumHex: sha256Hex([]byte("new")), + } + + originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH + runtimeGOOS, runtimeGOARCH = "linux", "amd64" + t.Cleanup(func() { + runtimeGOOS, runtimeGOARCH = originalGOOS, originalGOARCH + }) + + info, _ := detectTarget(runtimeGOOS, runtimeGOARCH) + release := cfg.releaseTagName() + targetDir := filepath.Join(tmp, release, info.triple) + if err := os.MkdirAll(targetDir, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + destPath := filepath.Join(targetDir, info.exeName) + if err := os.WriteFile(destPath, []byte("old"), 0o700); err != nil { + t.Fatalf("write cache: %v", err) + } + + var downloads int + originalDownloader := downloadBinaryFunc + downloadBinaryFunc = func(info targetInfo, release, destPath string) error { + downloads++ + return os.WriteFile(destPath, []byte("new"), 0o700) + } + t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) + + path, err := ensureBundledBinary(cfg) + if err != nil { + t.Fatalf("ensureBundledBinary returned error: %v", err) + } + if downloads != 1 { + t.Fatalf("expected 1 re-download, got %d", downloads) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read binary: %v", err) + } + if string(data) != "new" { + t.Fatalf("expected binary to be rewritten with new contents") + } +} + func TestFindCodexPathFallsBackToSystemBinary(t *testing.T) { tmpCache := t.TempDir() t.Setenv("GODEX_CLI_CACHE", tmpCache) @@ -137,7 +278,7 @@ func TestFindCodexPathFallsBackToSystemBinary(t *testing.T) { originalPath := os.Getenv("PATH") t.Setenv("PATH", tempBinDir+string(os.PathListSeparator)+originalPath) - path, err := findCodexPath() + path, err := findCodexPath(bundleConfig{}) if err != nil { t.Fatalf("findCodexPath returned error: %v", err) } @@ -145,3 +286,80 @@ func TestFindCodexPathFallsBackToSystemBinary(t *testing.T) { t.Fatalf("expected fallback path within %s, got %s", tempBinDir, path) } } + +func TestFindCodexPathReturnsErrorWhenChecksumConfigured(t *testing.T) { + tmpCache := t.TempDir() + cfg := bundleConfig{cacheDir: tmpCache, checksumHex: strings.Repeat("00", 32)} + + originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH + runtimeGOOS, runtimeGOARCH = runtime.GOOS, runtime.GOARCH + t.Cleanup(func() { + runtimeGOOS, runtimeGOARCH = originalGOOS, originalGOARCH + }) + + originalDownloader := downloadBinaryFunc + downloadBinaryFunc = func(info targetInfo, release, destPath string) error { + return os.WriteFile(destPath, []byte("binary"), 0o700) + } + t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) + + tempBinDir := t.TempDir() + dummyCodex := filepath.Join(tempBinDir, "codex") + if runtime.GOOS == "windows" { + dummyCodex += ".exe" + } + if err := os.WriteFile(dummyCodex, []byte("dummy"), 0o700); err != nil { + t.Fatalf("write dummy binary: %v", err) + } + originalPath := os.Getenv("PATH") + t.Setenv("PATH", tempBinDir+string(os.PathListSeparator)+originalPath) + + _, err := findCodexPath(cfg) + if err == nil { + t.Fatalf("expected checksum error") + } + if !errors.Is(err, ErrChecksumMismatch) { + t.Fatalf("expected ErrChecksumMismatch, got %v", err) + } +} + +func TestFindCodexPathReturnsErrorWhenReleasePinned(t *testing.T) { + tmpCache := t.TempDir() + cfg := bundleConfig{cacheDir: tmpCache, releaseTag: "custom-release"} + + originalGOOS, originalGOARCH := runtimeGOOS, runtimeGOARCH + runtimeGOOS, runtimeGOARCH = runtime.GOOS, runtime.GOARCH + t.Cleanup(func() { + runtimeGOOS, runtimeGOARCH = originalGOOS, originalGOARCH + }) + + originalDownloader := downloadBinaryFunc + downloadBinaryFunc = func(info targetInfo, release, destPath string) error { + return fmt.Errorf("simulated download failure") + } + t.Cleanup(func() { downloadBinaryFunc = originalDownloader }) + + tempBinDir := t.TempDir() + dummyCodex := filepath.Join(tempBinDir, "codex") + if runtime.GOOS == "windows" { + dummyCodex += ".exe" + } + if err := os.WriteFile(dummyCodex, []byte("dummy"), 0o700); err != nil { + t.Fatalf("write dummy binary: %v", err) + } + originalPath := os.Getenv("PATH") + t.Setenv("PATH", tempBinDir+string(os.PathListSeparator)+originalPath) + + _, err := findCodexPath(cfg) + if err == nil { + t.Fatalf("expected error due to pinned release") + } + if !strings.Contains(err.Error(), "simulated download failure") { + t.Fatalf("expected download failure error, got %v", err) + } +} + +func sha256Hex(data []byte) string { + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/codexexec/runner.go b/internal/codexexec/runner.go index 174c893..21bd826 100644 --- a/internal/codexexec/runner.go +++ b/internal/codexexec/runner.go @@ -18,6 +18,18 @@ const ( goSDKOriginator = "codex_sdk_go" ) +// RunnerOptions controls how the Codex CLI binary is discovered / bootstrapped before execution. +type RunnerOptions struct { + // PathOverride points directly at a Codex binary instead of discovering/downloading it. + PathOverride string + // CacheDir overrides the directory used to cache downloaded Codex binaries. + CacheDir string + // ReleaseTag pins the Codex CLI release to download. + ReleaseTag string + // ChecksumHex enforces an expected SHA-256 checksum (hex encoded) for the downloaded binary. + ChecksumHex string +} + // Args mirrors the CLI flags accepted by `codex exec`. type Args struct { Input string @@ -39,11 +51,16 @@ type Runner struct { } // New constructs a Runner, optionally overriding the codex binary path. -func New(override string) (*Runner, error) { - path := override +func New(options RunnerOptions) (*Runner, error) { + path := options.PathOverride + bootstrap := bundleConfig{ + cacheDir: options.CacheDir, + releaseTag: options.ReleaseTag, + checksumHex: options.ChecksumHex, + } if path == "" { var err error - path, err = findCodexPath() + path, err = findCodexPath(bootstrap) if err != nil { return nil, err } @@ -233,11 +250,14 @@ func indexByte(s string, b byte) int { return -1 } -func findCodexPath() (string, error) { - bundledPath, bundleErr := ensureBundledBinary() +func findCodexPath(cfg bundleConfig) (string, error) { + bundledPath, bundleErr := ensureBundledBinary(cfg) if bundleErr == nil { return bundledPath, nil } + if cfg.requireBundledBinary() { + return "", fmt.Errorf("ensure bundled codex binary: %w", bundleErr) + } path, err := exec.LookPath("codex") if err == nil { diff --git a/options.go b/options.go index 9c2a8a7..255a0b6 100644 --- a/options.go +++ b/options.go @@ -34,6 +34,16 @@ type CodexOptions struct { // ConfigOverrides forwards CLI configuration overrides as `-c key=value` pairs. When // the `profile` key is present it is emitted as `--profile ` instead. ConfigOverrides map[string]any + // CLICacheDir overrides the directory used to cache downloaded Codex binaries. When empty, + // the SDK falls back to $GODEX_CLI_CACHE, then the user cache directory. + CLICacheDir string + // CLIReleaseTag pins the Codex CLI release tag to download. When unset, the SDK checks + // $GODEX_CLI_RELEASE_TAG before falling back to its default bundled tag. + CLIReleaseTag string + // CLIChecksum optionally enforces integrity verification of the downloaded Codex binary. + // Provide the expected SHA-256 checksum (hex encoded). When empty, checksum verification + // is skipped. Use $GODEX_CLI_CHECKSUM to configure the same behavior via environment. + CLIChecksum string } // ThreadOptions configure how the CLI executes a particular thread. diff --git a/thread_cancel_test.go b/thread_cancel_test.go index ab92c22..5e5e90a 100644 --- a/thread_cancel_test.go +++ b/thread_cancel_test.go @@ -23,7 +23,7 @@ func TestThreadRunStreamedCancellationTerminatesProcess(t *testing.T) { fakeBinary := buildFakeCodexBinary(t) - runner, err := codexexec.New(fakeBinary) + runner, err := codexexec.New(codexexec.RunnerOptions{PathOverride: fakeBinary}) if err != nil { t.Fatalf("codexexec.New returned error: %v", err) }