From 1572e50e0ef0e91ffe224dbec545735d7b51b60e Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 20:39:45 +0000 Subject: [PATCH] Give a build step's commands only the environment its caller names (issue 1497) --- go/forgekey/runner.go | 19 ++++++++++++++++--- go/forgekey/runner_test.go | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/go/forgekey/runner.go b/go/forgekey/runner.go index 8fe5040..14bba8a 100644 --- a/go/forgekey/runner.go +++ b/go/forgekey/runner.go @@ -37,18 +37,31 @@ func ExecRunner(ctx context.Context, env []string, name string, args ...string) // step's own context ends a command, and a fetch may outlast a tool call's thirty seconds. func ExecRunnerFor(bound time.Duration) Runner { return func(ctx context.Context, env []string, name string, args ...string) Ran { - return execRun(ctx, bound, env, name, args...) + return execRun(ctx, bound, false, env, name, args...) } } -func execRun(ctx context.Context, bound time.Duration, env []string, name string, args ...string) Ran { +// ExecRunnerClean is ExecRunnerFor whose commands get exactly the env argument as their environment, and nothing of +// this process's (issue 1497; novox/hq ADR 0466 ยง4: no registry password in a fetch's environment). The caller +// gives PATH and HOME itself. A directory is the command's own business (`git -C`, `go -C`). +func ExecRunnerClean(bound time.Duration) Runner { + return func(ctx context.Context, env []string, name string, args ...string) Ran { + return execRun(ctx, bound, true, env, name, args...) + } +} + +func execRun(ctx context.Context, bound time.Duration, clean bool, env []string, name string, args ...string) Ran { if bound > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, bound) defer cancel() } cmd := exec.CommandContext(ctx, name, args...) - cmd.Env = append(os.Environ(), env...) + if clean { + cmd.Env = append([]string{}, env...) // non-nil, so an empty list is an empty environment + } else { + cmd.Env = append(os.Environ(), env...) + } var out, errb bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &errb err := cmd.Run() diff --git a/go/forgekey/runner_test.go b/go/forgekey/runner_test.go index 79f9825..9c26b65 100644 --- a/go/forgekey/runner_test.go +++ b/go/forgekey/runner_test.go @@ -2,6 +2,8 @@ package forgekey import ( "context" + "os" + "strings" "testing" "time" ) @@ -43,3 +45,21 @@ func TestExecRunnerForZeroIsCutByItsContext(t *testing.T) { t.Errorf("ran = %+v after %v, want it cut", r, time.Since(began)) } } + +// 1497/AC4: ExecRunnerClean(0) runs env with only the given words: PATH and HOME, not the process's secret. +func TestExecRunnerCleanGivesTheCommandOnlyTheEnvArgument(t *testing.T) { + t.Setenv("MESH_TEST_SECRET", "x") + r := ExecRunnerClean(0)(context.Background(), []string{"PATH=" + os.Getenv("PATH"), "HOME=/h"}, "env") + if r.Status != 0 || !strings.Contains(r.Stdout, "HOME=/h") || !strings.Contains(r.Stdout, "PATH=") || strings.Contains(r.Stdout, "MESH_TEST_SECRET") { + t.Errorf("ran = %+v", r) + } +} + +// 1497/AC5: ExecRunnerFor(0) is unchanged: the env argument is added to the process's environment. +func TestExecRunnerForAddsToTheProcessEnvironment(t *testing.T) { + t.Setenv("MESH_TEST_SECRET", "x") + r := ExecRunnerFor(0)(context.Background(), []string{"HOME=/h"}, "env") + if !strings.Contains(r.Stdout, "MESH_TEST_SECRET=x") || !strings.Contains(r.Stdout, "HOME=/h") { + t.Errorf("ran = %+v", r) + } +}