11package sandbox
22
33import (
4+ "context"
45 "encoding/json"
56 "fmt"
67 "os"
@@ -148,7 +149,7 @@ func runHerdrSetup(c *cli.Context, doctorOnly bool) error {
148149 if err != nil {
149150 return fmt .Errorf ("herdr is not on PATH — install it from https://herdr.dev" )
150151 }
151- version , err := herdrVersion (herdrBin )
152+ version , err := herdrVersion (c . Context , herdrBin )
152153 if err != nil {
153154 return err
154155 }
@@ -157,12 +158,12 @@ func runHerdrSetup(c *cli.Context, doctorOnly bool) error {
157158 }
158159 fmt .Printf ("herdr %s found at %s\n " , version , herdrBin )
159160
160- if _ , err := exec .LookPath ("bun" ); err != nil {
161+ if _ , lookErr := exec .LookPath ("bun" ); lookErr != nil {
161162 return fmt .Errorf ("bun is not on PATH — the plugin runs on it; install it from https://bun.sh" )
162163 }
163164 fmt .Println ("bun found on PATH" )
164165
165- if _ , err := exec .LookPath ("git" ); err != nil {
166+ if _ , lookErr := exec .LookPath ("git" ); lookErr != nil {
166167 return fmt .Errorf ("git is not on PATH; the plugin uploads what git tracks" )
167168 }
168169 fmt .Println ("git found on PATH" )
@@ -175,21 +176,21 @@ func runHerdrSetup(c *cli.Context, doctorOnly bool) error {
175176 // ---- install -----------------------------------------------------------
176177
177178 if local := strings .TrimSpace (c .String ("local" )); local != "" {
178- if err := herdrLink (herdrBin , local ); err != nil {
179- return err
179+ if linkErr := herdrLink (c . Context , herdrBin , local ); linkErr != nil {
180+ return linkErr
180181 }
181182 } else {
182183 source := herdrPluginRepo + "/" + herdrPluginPkgPath
183184 fmt .Printf ("installing %s from %s\n " , herdrPluginID , source )
184- if out , err := herdrRun (herdrBin , "plugin" , "install" , source , "--yes" ); err != nil {
185- return fmt .Errorf ("herdr plugin install failed: %w\n %s" , err , out )
185+ if out , runErr := herdrRun (c . Context , herdrBin , "plugin" , "install" , source , "--yes" ); runErr != nil {
186+ return fmt .Errorf ("herdr plugin install failed: %w\n %s" , runErr , out )
186187 }
187188 fmt .Println ("plugin installed" )
188189 }
189190
190191 // ---- plugin config -----------------------------------------------------
191192
192- configDir , err := herdrRun (herdrBin , "plugin" , "config-dir" , herdrPluginID )
193+ configDir , err := herdrRun (c . Context , herdrBin , "plugin" , "config-dir" , herdrPluginID )
193194 if err != nil {
194195 return fmt .Errorf ("could not find the plugin config directory: %w" , err )
195196 }
@@ -216,17 +217,16 @@ func runHerdrSetup(c *cli.Context, doctorOnly bool) error {
216217 if err != nil {
217218 return err
218219 }
219- switch {
220- case added == 0 :
220+ if added == 0 {
221221 fmt .Printf ("keybindings already present in %s\n " , path )
222- default :
222+ } else {
223223 fmt .Printf ("added %d keybindings to %s (undo with 'herdr config reset-keys')\n " , added , path )
224224 }
225- if out , err := herdrRun (herdrBin , "config" , "check" ); err != nil {
225+ if out , err := herdrRun (c . Context , herdrBin , "config" , "check" ); err != nil {
226226 return fmt .Errorf ("herdr rejected the updated config: %w\n %s" , err , out )
227227 }
228228 // Only a running server can reload; a failure here is not a setup failure.
229- if _ , err := herdrRun (herdrBin , "server" , "reload-config" ); err != nil {
229+ if _ , err := herdrRun (c . Context , herdrBin , "server" , "reload-config" ); err != nil {
230230 fmt .Println ("no running Herdr server to reload — the keys apply next time you start one" )
231231 } else {
232232 fmt .Println ("reloaded the running Herdr server" )
@@ -242,7 +242,7 @@ func runHerdrSetup(c *cli.Context, doctorOnly bool) error {
242242 return nil
243243}
244244
245- func herdrLink (herdrBin , local string ) error {
245+ func herdrLink (ctx context. Context , herdrBin , local string ) error {
246246 dir , err := filepath .Abs (local )
247247 if err != nil {
248248 return fmt .Errorf ("could not resolve %q: %w" , local , err )
@@ -251,7 +251,7 @@ func herdrLink(herdrBin, local string) error {
251251 return fmt .Errorf ("%s does not look like the plugin: no herdr-plugin.toml" , dir )
252252 }
253253 fmt .Printf ("linking %s\n " , dir )
254- if out , err := herdrRun (herdrBin , "plugin" , "link" , dir ); err != nil {
254+ if out , err := herdrRun (ctx , herdrBin , "plugin" , "link" , dir ); err != nil {
255255 return fmt .Errorf ("herdr plugin link failed: %w\n %s" , err , out )
256256 }
257257 // `plugin link` deliberately does not run build commands, so the generated
@@ -261,7 +261,9 @@ func herdrLink(herdrBin, local string) error {
261261 return fmt .Errorf ("%s is missing; the plugin cannot generate its launcher" , build )
262262 }
263263 fmt .Println ("running the plugin build step" )
264- cmd := exec .Command ("sh" , build )
264+ // #nosec G204 -- build is filepath.Join of an --local path the user chose
265+ // and the literal "build.sh"; it is one argv element, never a shell string.
266+ cmd := exec .CommandContext (ctx , "sh" , build )
265267 cmd .Dir = dir
266268 cmd .Stdout = os .Stderr
267269 cmd .Stderr = os .Stderr
@@ -292,7 +294,7 @@ func herdrWritePluginConfig(c *cli.Context, configDir, agent string) (bool, erro
292294 if err != nil {
293295 return false , fmt .Errorf ("could not build the plugin config: %w" , err )
294296 }
295- if err := os .MkdirAll (configDir , 0o755 ); err != nil {
297+ if err := os .MkdirAll (configDir , 0o750 ); err != nil {
296298 return false , fmt .Errorf ("could not create %s: %w" , configDir , err )
297299 }
298300 if err := os .WriteFile (path , append (body , '\n' ), 0o600 ); err != nil {
@@ -332,32 +334,37 @@ func herdrWriteKeys(pluginConfigDir string) (int, string, error) {
332334
333335 if len (existing ) > 0 {
334336 backup := path + ".before-createos"
337+ // #nosec G703 -- path is derived from `herdr plugin config-dir`, not user input.
335338 if err := os .WriteFile (backup , existing , 0o600 ); err != nil {
336339 return 0 , path , fmt .Errorf ("could not back up %s: %w" , path , err )
337340 }
338341 fmt .Printf ("backed up your config to %s\n " , backup )
339342 }
340- if err := os .MkdirAll (root , 0o755 ); err != nil {
343+ if err := os .MkdirAll (root , 0o750 ); err != nil {
341344 return 0 , path , fmt .Errorf ("could not create %s: %w" , root , err )
342345 }
343346 updated := current
344347 if updated != "" && ! strings .HasSuffix (updated , "\n " ) {
345348 updated += "\n "
346349 }
347350 updated += "\n # Added by 'createos sandbox setup herdr'.\n " + strings .TrimPrefix (block .String (), "\n " )
351+ // #nosec G703 -- path is derived from `herdr plugin config-dir`, not user input.
348352 if err := os .WriteFile (path , []byte (updated ), 0o600 ); err != nil {
349353 return 0 , path , fmt .Errorf ("could not write %s: %w" , path , err )
350354 }
351355 return added , path , nil
352356}
353357
354- func herdrRun (bin string , args ... string ) (string , error ) {
355- out , err := exec .Command (bin , args ... ).CombinedOutput ()
358+ func herdrRun (ctx context.Context , bin string , args ... string ) (string , error ) {
359+ // #nosec G204 -- bin is the exec.LookPath("herdr") result and every arg is
360+ // a literal from this file; nothing here comes from a sandbox or the network.
361+ out , err := exec .CommandContext (ctx , bin , args ... ).CombinedOutput ()
356362 return string (out ), err
357363}
358364
359- func herdrVersion (bin string ) (string , error ) {
360- out , err := exec .Command (bin , "--version" ).Output ()
365+ func herdrVersion (ctx context.Context , bin string ) (string , error ) {
366+ // #nosec G204 -- bin is the exec.LookPath("herdr") result; the arg is a literal.
367+ out , err := exec .CommandContext (ctx , bin , "--version" ).Output ()
361368 if err != nil {
362369 return "" , fmt .Errorf ("could not run 'herdr --version': %w" , err )
363370 }
@@ -377,12 +384,22 @@ func herdrVersionLess(have, want string) bool {
377384 for i := 0 ; i < len (wantParts ); i ++ {
378385 var h int
379386 if i < len (haveParts ) {
380- h , _ = strconv . Atoi (haveParts [i ])
387+ h = herdrVersionPart (haveParts [i ])
381388 }
382- w , _ := strconv . Atoi (wantParts [i ])
389+ w := herdrVersionPart (wantParts [i ])
383390 if h != w {
384391 return h < w
385392 }
386393 }
387394 return false
388395}
396+
397+ // herdrVersionPart reads one dotted version part. Anything that is not a
398+ // number counts as 0, so a suffix never reads as newer than a release.
399+ func herdrVersionPart (s string ) int {
400+ n , err := strconv .Atoi (s )
401+ if err != nil {
402+ return 0
403+ }
404+ return n
405+ }
0 commit comments