Skip to content

Commit ce2c94d

Browse files
fix(cli): address PR review findings for binary vendoring
Validate --fullsend-binary requires --vendor-fullsend-binary, update flag help text, improve download size-limit errors, restore run arch hint and 120s OIDC timeout, and add ResolveForRun fallback tests. Signed-off-by: Barak Korren <bkorren@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2f32bdf commit ce2c94d

8 files changed

Lines changed: 197 additions & 7 deletions

File tree

docs/guides/dev/cli-internals.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ Linux binary resolution for `fullsend run` and vendoring lives in `internal/bina
243243

244244
| Function | Policy |
245245
|----------|--------|
246-
| `ResolveForRun` | Release download (if released CLI) → cross-compile → latest release |
246+
| `ResolveForRun` | Release download (released CLI only) → cross-compile → latest release |
247247
| `ResolveForVendor` | Cross-compile → matching release (released CLI only) → fail (no latest) |
248248
| `ResolveExplicit` | Validate linux/{arch} ELF for `--fullsend-binary` |
249249

internal/binary/download.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@ import (
1818
)
1919

2020
// ReleaseBaseURL is the GitHub releases download base URL. Tests may override.
21+
// Not safe for concurrent test mutation.
2122
var ReleaseBaseURL = "https://github.com/fullsend-ai/fullsend/releases/download"
2223

2324
// HTTPClient is used for release downloads. Tests may override.
25+
// Not safe for concurrent test mutation.
2426
var HTTPClient = &http.Client{Timeout: 120 * time.Second}
2527

28+
const defaultMaxDownloadSize = 200 * 1024 * 1024 // 200 MB compressed
29+
30+
// maxDownloadSize caps release asset downloads. Tests may lower temporarily.
31+
var maxDownloadSize = defaultMaxDownloadSize
32+
2633
const maxBinarySize = 500 * 1024 * 1024 // 500 MB — reasonable upper bound for a Go binary
2734

2835
// DownloadRelease downloads the fullsend binary for linux/{arch} from the
@@ -48,11 +55,14 @@ func DownloadRelease(ver, arch, destPath string) error {
4855
return fmt.Errorf("GET %s returned %d", url, resp.StatusCode)
4956
}
5057

51-
const maxDownloadSize = 200 * 1024 * 1024 // 200 MB compressed
58+
maxSize := int64(maxDownloadSize)
5259
var buf bytes.Buffer
53-
if _, err := io.Copy(&buf, io.LimitReader(resp.Body, maxDownloadSize)); err != nil {
60+
if _, err := io.Copy(&buf, io.LimitReader(resp.Body, maxSize+1)); err != nil {
5461
return fmt.Errorf("reading %s: %w", assetName, err)
5562
}
63+
if int64(buf.Len()) > maxSize {
64+
return fmt.Errorf("download of %s exceeds maximum size (%d bytes)", assetName, maxSize)
65+
}
5666

5767
h := sha256.Sum256(buf.Bytes())
5868
actualHash := hex.EncodeToString(h[:])

internal/binary/download_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,42 @@ import (
1616
"strings"
1717
"sync/atomic"
1818
"testing"
19+
"time"
1920

2021
"github.com/stretchr/testify/assert"
2122
"github.com/stretchr/testify/require"
2223
)
2324

25+
type redirectTransport struct {
26+
srvURL string
27+
base http.RoundTripper
28+
}
29+
30+
func (t redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) {
31+
clone := req.Clone(req.Context())
32+
clone.URL.Scheme = "http"
33+
clone.URL.Host = strings.TrimPrefix(strings.TrimPrefix(t.srvURL, "https://"), "http://")
34+
if t.base == nil {
35+
t.base = http.DefaultTransport
36+
}
37+
return t.base.RoundTrip(clone)
38+
}
39+
40+
func withTestReleaseServer(t *testing.T, srv *httptest.Server) {
41+
t.Helper()
42+
origClient := HTTPClient
43+
origBaseURL := ReleaseBaseURL
44+
HTTPClient = &http.Client{
45+
Transport: redirectTransport{srvURL: srv.URL},
46+
Timeout: 120 * time.Second,
47+
}
48+
ReleaseBaseURL = srv.URL
49+
t.Cleanup(func() {
50+
HTTPClient = origClient
51+
ReleaseBaseURL = origBaseURL
52+
})
53+
}
54+
2455
func TestExtractFullsendFromTarGz_PathTraversal(t *testing.T) {
2556
var buf bytes.Buffer
2657
gw := gzip.NewWriter(&buf)
@@ -412,6 +443,132 @@ func TestResolveForRun_PrefersReleaseBeforeCrossCompile(t *testing.T) {
412443
assert.Equal(t, SourceReleaseDownload, result.Source)
413444
}
414445

446+
func TestDownloadRelease_ExceedsMaxSize(t *testing.T) {
447+
origLimit := maxDownloadSize
448+
maxDownloadSize = 512
449+
t.Cleanup(func() { maxDownloadSize = origLimit })
450+
451+
content := bytes.Repeat([]byte("x"), 2000)
452+
453+
var tarBuf bytes.Buffer
454+
gw, err := gzip.NewWriterLevel(&tarBuf, gzip.NoCompression)
455+
require.NoError(t, err)
456+
tw := tar.NewWriter(gw)
457+
require.NoError(t, tw.WriteHeader(&tar.Header{
458+
Name: "fullsend",
459+
Size: int64(len(content)),
460+
Mode: 0o755,
461+
Typeflag: tar.TypeReg,
462+
}))
463+
_, err = tw.Write(content)
464+
require.NoError(t, err)
465+
require.NoError(t, tw.Close())
466+
require.NoError(t, gw.Close())
467+
468+
tarBytes := tarBuf.Bytes()
469+
h := sha256.Sum256(tarBytes)
470+
checksumBody := fmt.Sprintf("%s fullsend_1.0.0_linux_amd64.tar.gz\n", hex.EncodeToString(h[:]))
471+
472+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
473+
if r.URL.Path == "/v1.0.0/checksums.txt" {
474+
fmt.Fprint(w, checksumBody)
475+
} else if r.URL.Path == "/v1.0.0/fullsend_1.0.0_linux_amd64.tar.gz" {
476+
w.Write(tarBytes)
477+
} else {
478+
http.NotFound(w, r)
479+
}
480+
}))
481+
defer srv.Close()
482+
withTestReleaseServer(t, srv)
483+
484+
destPath := filepath.Join(t.TempDir(), "fullsend")
485+
err = DownloadRelease("1.0.0", "amd64", destPath)
486+
require.Error(t, err)
487+
assert.Contains(t, err.Error(), "exceeds maximum size")
488+
}
489+
490+
func TestResolveForRun_CrossCompileFallback(t *testing.T) {
491+
if testing.Short() {
492+
t.Skip("skipping cross-compilation in short mode")
493+
}
494+
495+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
496+
http.NotFound(w, r)
497+
}))
498+
defer srv.Close()
499+
withTestReleaseServer(t, srv)
500+
501+
result, err := ResolveForRun("0.4.0", "amd64")
502+
require.NoError(t, err)
503+
t.Cleanup(func() { os.RemoveAll(result.TmpDir) })
504+
assert.Equal(t, SourceCheckoutBuild, result.Source)
505+
}
506+
507+
func TestResolveForRun_LatestReleaseFallback(t *testing.T) {
508+
var tarBuf bytes.Buffer
509+
gw := gzip.NewWriter(&tarBuf)
510+
tw := tar.NewWriter(gw)
511+
content := []byte("latest release binary")
512+
require.NoError(t, tw.WriteHeader(&tar.Header{
513+
Name: "fullsend",
514+
Size: int64(len(content)),
515+
Mode: 0o755,
516+
Typeflag: tar.TypeReg,
517+
}))
518+
_, err := tw.Write(content)
519+
require.NoError(t, err)
520+
require.NoError(t, tw.Close())
521+
require.NoError(t, gw.Close())
522+
523+
tarBytes := tarBuf.Bytes()
524+
h := sha256.Sum256(tarBytes)
525+
correctHash := hex.EncodeToString(h[:])
526+
checksumBody := fmt.Sprintf("%s fullsend_9.9.9_linux_amd64.tar.gz\n", correctHash)
527+
528+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
529+
if r.URL.Path == "/repos/fullsend-ai/fullsend/releases/latest" {
530+
fmt.Fprint(w, `{"tag_name":"v9.9.9"}`)
531+
} else if r.URL.Path == "/v9.9.9/checksums.txt" {
532+
fmt.Fprint(w, checksumBody)
533+
} else if r.URL.Path == "/v9.9.9/fullsend_9.9.9_linux_amd64.tar.gz" {
534+
w.Write(tarBytes)
535+
} else {
536+
http.NotFound(w, r)
537+
}
538+
}))
539+
defer srv.Close()
540+
withTestReleaseServer(t, srv)
541+
542+
origDir, err := os.Getwd()
543+
require.NoError(t, err)
544+
tmpDir := t.TempDir()
545+
require.NoError(t, os.Chdir(tmpDir))
546+
t.Cleanup(func() { _ = os.Chdir(origDir) })
547+
548+
result, err := ResolveForRun("dev", "amd64")
549+
require.NoError(t, err)
550+
t.Cleanup(func() { os.RemoveAll(result.TmpDir) })
551+
assert.Equal(t, SourceReleaseDownload, result.Source)
552+
}
553+
554+
func TestResolveForRun_AllStrategiesFail(t *testing.T) {
555+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
556+
http.NotFound(w, r)
557+
}))
558+
defer srv.Close()
559+
withTestReleaseServer(t, srv)
560+
561+
origDir, err := os.Getwd()
562+
require.NoError(t, err)
563+
tmpDir := t.TempDir()
564+
require.NoError(t, os.Chdir(tmpDir))
565+
t.Cleanup(func() { _ = os.Chdir(origDir) })
566+
567+
_, err = ResolveForRun("dev", "amd64")
568+
require.Error(t, err)
569+
assert.Contains(t, err.Error(), "all strategies failed")
570+
}
571+
415572
func TestResolveExplicit_ValidatesELF(t *testing.T) {
416573
tmp := filepath.Join(t.TempDir(), "not-elf")
417574
require.NoError(t, os.WriteFile(tmp, []byte("not binary"), 0o644))

internal/cli/admin.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ Inference authentication:
272272
if err := appsetup.ValidateAppSet(appSet); err != nil {
273273
return fmt.Errorf("invalid --app-set: %w", err)
274274
}
275+
if err := validateVendorBinaryFlags(vendorBinary, fullsendBinary); err != nil {
276+
return err
277+
}
275278

276279
arg := args[0]
277280
if strings.Contains(arg, "/") {
@@ -543,7 +546,7 @@ Inference authentication:
543546
cmd.Flags().StringVar(&agents, "agents", strings.Join(config.DefaultAgentRoles(), ","), "comma-separated agent roles")
544547
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview changes without making them")
545548
cmd.Flags().BoolVar(&skipAppSetup, "skip-app-setup", false, "skip GitHub App creation/setup")
546-
cmd.Flags().BoolVar(&vendorBinary, "vendor-fullsend-binary", false, "cross-compile and vendor the fullsend binary for development iteration")
549+
cmd.Flags().BoolVar(&vendorBinary, "vendor-fullsend-binary", false, "resolve and upload a linux/amd64 fullsend binary for CI")
547550
cmd.Flags().StringVar(&fullsendBinary, "fullsend-binary", "", "path to a Linux fullsend binary to upload when vendoring (default: auto-resolve)")
548551
cmd.Flags().BoolVar(&enrollAllFlag, "enroll-all", false, "enroll all repositories without prompting")
549552
cmd.Flags().BoolVar(&enrollNoneFlag, "enroll-none", false, "skip repository enrollment without prompting")

internal/cli/github.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ values (mint URL, WIF provider, project ID) are provided as flags.`,
9090
if err := appsetup.ValidateAppSet(cfg.appSet); err != nil {
9191
return fmt.Errorf("invalid --app-set: %w", err)
9292
}
93+
if err := validateVendorBinaryFlags(cfg.vendorBinary, cfg.fullsendBinary); err != nil {
94+
return err
95+
}
9396

9497
if err := validateMintURLHTTPS(cfg.mintURL); err != nil {
9598
return err
@@ -133,7 +136,7 @@ values (mint URL, WIF provider, project ID) are provided as flags.`,
133136
cmd.Flags().StringVar(&cfg.appSet, "app-set", appsetup.DefaultAppSet, "app set name prefix for GitHub Apps")
134137
cmd.Flags().BoolVar(&cfg.enrollAll, "enroll-all", false, "enroll all repositories without prompting")
135138
cmd.Flags().BoolVar(&cfg.enrollNone, "enroll-none", false, "skip repository enrollment without prompting")
136-
cmd.Flags().BoolVar(&cfg.vendorBinary, "vendor-fullsend-binary", false, "cross-compile and upload the fullsend binary")
139+
cmd.Flags().BoolVar(&cfg.vendorBinary, "vendor-fullsend-binary", false, "resolve and upload a linux/amd64 fullsend binary for CI")
137140
cmd.Flags().StringVar(&cfg.fullsendBinary, "fullsend-binary", "", "path to a Linux fullsend binary to upload when vendoring (default: auto-resolve)")
138141
cmd.Flags().BoolVar(&cfg.dryRun, "dry-run", false, "preview changes without making them")
139142

internal/cli/run.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,7 @@ func bootstrapCommon(sandboxName, fullsendBinary string, h *harness.Harness) err
912912
}
913913
if localBinary != "" {
914914
if err := binary.ValidateLinuxBinary(localBinary, sandboxArch()); err != nil {
915-
return fmt.Errorf("fullsend binary %q is not valid for the sandbox: %w", localBinary, err)
915+
return fmt.Errorf("fullsend binary %q is not valid for the sandbox: %w\nSet FULLSEND_SANDBOX_ARCH to override the target architecture", localBinary, err)
916916
}
917917
// Use UploadDir (tarball-based) instead of Upload for the binary.
918918
// Upload silently fails for large files (~16MB); the tarball
@@ -1233,7 +1233,7 @@ func runOIDCRefresh(ctx context.Context, sandboxName, oidcURL, oidcAuth string,
12331233
}
12341234
}
12351235

1236-
var oidcHTTPClient = &http.Client{Timeout: 30 * time.Second}
1236+
var oidcHTTPClient = &http.Client{Timeout: 120 * time.Second} // matches pre-refactor shared httpClient timeout
12371237

12381238
func refreshOIDCToken(ctx context.Context, sandboxName, oidcURL, oidcAuth string) error {
12391239
req, err := http.NewRequestWithContext(ctx, "GET", oidcURL, nil)

internal/cli/vendor.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ import (
1313

1414
const vendorArch = binary.DefaultArch
1515

16+
func validateVendorBinaryFlags(vendorBinary bool, fullsendBinary string) error {
17+
if fullsendBinary != "" && !vendorBinary {
18+
return fmt.Errorf("--fullsend-binary requires --vendor-fullsend-binary")
19+
}
20+
return nil
21+
}
22+
1623
// makeVendorFunc returns a VendorFunc closure that uploads a fullsend binary
1724
// using the vendoring acquisition policy.
1825
func makeVendorFunc(fullsendBinary string) layers.VendorFunc {

internal/cli/vendor_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ import (
1515
"github.com/fullsend-ai/fullsend/internal/ui"
1616
)
1717

18+
func TestValidateVendorBinaryFlags(t *testing.T) {
19+
require.NoError(t, validateVendorBinaryFlags(false, ""))
20+
require.NoError(t, validateVendorBinaryFlags(true, ""))
21+
require.NoError(t, validateVendorBinaryFlags(true, "/tmp/fullsend"))
22+
23+
err := validateVendorBinaryFlags(false, "/tmp/fullsend")
24+
require.Error(t, err)
25+
assert.Contains(t, err.Error(), "--fullsend-binary requires --vendor-fullsend-binary")
26+
}
27+
1828
func TestInstallCmd_HasFullsendBinaryFlag(t *testing.T) {
1929
cmd := newInstallCmd()
2030
flag := cmd.Flags().Lookup("fullsend-binary")

0 commit comments

Comments
 (0)