diff --git a/cmd/executor.go b/cmd/executor.go index 5c8d265..3e95dfe 100644 --- a/cmd/executor.go +++ b/cmd/executor.go @@ -100,10 +100,21 @@ func upLocal(opts upLocalOpts) error { return fmt.Errorf("rendering payload: %w", err) } - // Resolve payload entries into upload pairs + // Resolve payload entries into upload pairs. Inline content payloads are + // written to temp files that are uploaded individually by their own + // sandbox_path, so their source paths must survive until SandboxCreate. + // They MUST NOT live inside payloadDir: createSandbox renames payloadDir + // into a staging directory, which would invalidate any path pointing inside + // it and fail every upload with "local path does not exist" (issue #84). var extraUploads []gateway.Upload if opts.harness != nil && len(opts.harness.Payloads) > 0 { - resolved, err := agent.ResolvePayloads(opts.harness.Payloads, opts.harnessDir, payloadDir) + contentDir, err := os.MkdirTemp("", "harness-payload-content-") + if err != nil { + return fmt.Errorf("creating payload content dir: %w", err) + } + defer os.RemoveAll(contentDir) + + resolved, err := agent.ResolvePayloads(opts.harness.Payloads, opts.harnessDir, contentDir) if err != nil { return fmt.Errorf("resolving payloads: %w", err) } diff --git a/cmd/executor_test.go b/cmd/executor_test.go index 0bcb15b..ee95520 100644 --- a/cmd/executor_test.go +++ b/cmd/executor_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/stackrox/harness-openshell/internal/agent" "github.com/stackrox/harness-openshell/internal/gateway" ) @@ -264,3 +265,50 @@ providers: t.Fatal("expected error for --agent research when file doesn't exist, should not fall back to embedded") } } + +// TestUpLocal_InlineContentPayloadPathSurvivesRename is a regression test for +// issue #84: inline content payloads were written into payloadDir, which +// createSandbox renames into a staging directory. The rename invalidated the +// stored upload paths, so every SandboxCreate failed with "local path does not +// exist". The fix stages inline content in a separate temp dir that survives +// the rename — so every upload Src must still resolve at create time. +func TestUpLocal_InlineContentPayloadPathSurvivesRename(t *testing.T) { + dir := setupTestAgent(t) + + var uploadsChecked bool + gw := &mockGW{ + providers: map[string]bool{"github": true, "google-vertex-ai": true, "atlassian": true}, + onSandboxCreate: func(opts gateway.SandboxCreateOpts) error { + for _, u := range opts.Uploads { + if _, err := os.Stat(u.Src); err != nil { + return fmt.Errorf("upload Src %q not accessible at create time: %w", u.Src, err) + } + } + uploadsChecked = true + return nil + }, + } + + harness := &agent.Harness{ + Payloads: []agent.PayloadEntry{ + {SandboxPath: "/sandbox/opencode.json", Content: `{"model": "test"}`}, + }, + } + + err := upLocal(upLocalOpts{ + harnessDir: dir, + gw: gw, + agentPath: filepath.Join(dir, "agents", "default.yaml"), + noTTY: true, + harness: harness, + }) + if err != nil { + t.Fatalf("upLocal: %v", err) + } + if !uploadsChecked { + t.Fatal("SandboxCreate was not called; upload paths were never validated") + } + if gw.createCalls != 1 { + t.Fatalf("expected exactly 1 SandboxCreate call, got %d", gw.createCalls) + } +} diff --git a/cmd/helpers_test.go b/cmd/helpers_test.go index 386a7bc..ba35721 100644 --- a/cmd/helpers_test.go +++ b/cmd/helpers_test.go @@ -28,6 +28,7 @@ type mockGW struct { deletedNames []string gatewayListResult []gateway.GatewayInfo onGatewayRemove func(string) + onSandboxCreate func(gateway.SandboxCreateOpts) error } func (m *mockGW) InferenceGet() error { return m.inferenceErr } @@ -41,6 +42,11 @@ func (m *mockGW) ProviderList() ([]string, error) { return m.providerList, m.pro func (m *mockGW) SandboxCreate(opts gateway.SandboxCreateOpts) error { m.createCalls++ m.createOpts = append(m.createOpts, opts) + if m.onSandboxCreate != nil { + if err := m.onSandboxCreate(opts); err != nil { + return err + } + } if m.createErr != nil && m.createCalls == 1 { return m.createErr }