diff --git a/.core/TODO.md b/.core/TODO.md index 15b85a5..e69de29 100644 --- a/.core/TODO.md +++ b/.core/TODO.md @@ -1,2 +0,0 @@ -- @hardening pkg/agentic/prep.go:892 — `fs.EnsureDir(specsDir)` ignores failures while seeding workspace specs, so template scaffolding can silently half-complete. -- @hardening pkg/agentic/prep.go:904 — `fs.EnsureDir(core.PathDir(dst))` ignores failures during template copy, which can leave copied files missing without a warning. diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index d002cf3..2426f9a 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -24,22 +24,22 @@ type AgentOptions struct{} // core.New(core.WithService(agentic.Register)) type PrepSubsystem struct { *core.ServiceRuntime[AgentOptions] - forge *forge.Forge - forgeURL string - forgeToken string - brainURL string - brainKey string - codePath string - startupContext context.Context - drainCh chan struct{} - pokeCh chan struct{} - frozen bool - backoff map[string]time.Time - failCount map[string]int - providers *ProviderManager - workspaces *core.Registry[*WorkspaceStatus] - stateOnce sync.Once - state *stateStoreRef + forge *forge.Forge + forgeURL string + forgeToken string + brainURL string + brainKey string + codePath string + startupContext context.Context + drainCh chan struct{} + pokeCh chan struct{} + frozen bool + backoff map[string]time.Time + failCount map[string]int + providers *ProviderManager + workspaces *core.Registry[*WorkspaceStatus] + stateOnce sync.Once + state *stateStoreRef workspaceStatsOnce sync.Once workspaceStats *workspaceStatsRef } @@ -843,7 +843,9 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques } } - s.copyRepoSpecs(workspaceDir, input.Repo) + if err := s.copyRepoSpecs(workspaceDir, input.Repo); err != nil { + return nil, PrepOutput{}, err + } out.Prompt, out.Memories, out.Consumers = s.buildPrompt(ctx, input, out.Branch, repoPath) if versionResult := writePromptSnapshot(workspaceDir, out.Prompt); !versionResult.OK { @@ -862,12 +864,12 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques // s.copyRepoSpecs("/tmp/workspace", "go-io") // copies plans/core/go/io/**/RFC*.md → /tmp/workspace/specs/ // s.copyRepoSpecs("/tmp/workspace", "core-bio") // copies plans/core/php/bio/**/RFC*.md → /tmp/workspace/specs/ -func (s *PrepSubsystem) copyRepoSpecs(workspaceDir, repo string) { +func (s *PrepSubsystem) copyRepoSpecs(workspaceDir, repo string) error { fs := (&core.Fs{}).NewUnrestricted() plansBase := core.JoinPath(s.codePath, "host-uk", "core", "plans") if !fs.IsDir(plansBase) { - return + return nil } var specDir string @@ -885,11 +887,13 @@ func (s *PrepSubsystem) copyRepoSpecs(workspaceDir, repo string) { } if !fs.IsDir(specDir) { - return + return nil } specsDir := core.JoinPath(workspaceDir, "specs") - fs.EnsureDir(specsDir) + if ensureResult := fs.EnsureDir(specsDir); !ensureResult.OK { + return core.E("copyRepoSpecs", core.Concat("failed to create specs dir ", specsDir), nil) + } patterns := []string{ core.JoinPath(specDir, "RFC*.md"), @@ -901,13 +905,22 @@ func (s *PrepSubsystem) copyRepoSpecs(workspaceDir, repo string) { for _, entry := range core.PathGlob(pattern) { rel := entry[len(specDir)+1:] dst := core.JoinPath(specsDir, rel) - fs.EnsureDir(core.PathDir(dst)) + if ensureResult := fs.EnsureDir(core.PathDir(dst)); !ensureResult.OK { + return core.E("copyRepoSpecs", core.Concat("failed to create specs parent dir ", core.PathDir(dst)), nil) + } r := fs.Read(entry) - if r.OK { - fs.Write(dst, r.Value.(string)) + if !r.OK { + err, _ := r.Value.(error) + return core.E("copyRepoSpecs", core.Concat("failed to read specs file ", entry), err) + } + if writeResult := fs.Write(dst, r.Value.(string)); !writeResult.OK { + err, _ := writeResult.Value.(error) + return core.E("copyRepoSpecs", core.Concat("failed to write specs file ", dst), err) } } } + + return nil } // _, out, err := prep.PrepareWorkspace(ctx, input) diff --git a/pkg/agentic/prep_extra_test.go b/pkg/agentic/prep_extra_test.go index ab8e0b0..b835222 100644 --- a/pkg/agentic/prep_extra_test.go +++ b/pkg/agentic/prep_extra_test.go @@ -117,6 +117,81 @@ func TestPrep_FindConsumersList_Bad_NoGoWork(t *testing.T) { assert.Empty(t, list) } +// --- copyRepoSpecs --- + +func TestPrep_CopyRepoSpecs_Good(t *testing.T) { + root := t.TempDir() + codePath := core.JoinPath(root, "src") + plansBase := core.JoinPath(codePath, "host-uk", "core", "plans") + specDir := core.JoinPath(plansBase, "core", "go", "test-repo") + + require.True(t, fs.EnsureDir(core.JoinPath(specDir, "sub")).OK) + require.True(t, fs.Write(core.JoinPath(specDir, "RFC.md"), "root-spec").OK) + require.True(t, fs.Write(core.JoinPath(specDir, "sub", "RFC-2.md"), "nested-spec").OK) + + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + codePath: codePath, + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + require.NoError(t, s.copyRepoSpecs(core.JoinPath(root, "workspace"), "go-test-repo")) + + rootCopy := fs.Read(core.JoinPath(root, "workspace", "specs", "RFC.md")) + require.True(t, rootCopy.OK) + assert.Equal(t, "root-spec", rootCopy.Value.(string)) + + nestedCopy := fs.Read(core.JoinPath(root, "workspace", "specs", "sub", "RFC-2.md")) + require.True(t, nestedCopy.OK) + assert.Equal(t, "nested-spec", nestedCopy.Value.(string)) +} + +func TestPrep_CopyRepoSpecs_Bad_SpecsDirBlocked(t *testing.T) { + root := t.TempDir() + codePath := core.JoinPath(root, "src") + plansBase := core.JoinPath(codePath, "host-uk", "core", "plans") + specDir := core.JoinPath(plansBase, "core", "go", "test-repo") + + require.True(t, fs.EnsureDir(specDir).OK) + require.True(t, fs.Write(core.JoinPath(specDir, "RFC.md"), "root-spec").OK) + require.True(t, fs.Write(core.JoinPath(root, "workspace", "specs"), "blocked").OK) + + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + codePath: codePath, + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + err := s.copyRepoSpecs(core.JoinPath(root, "workspace"), "go-test-repo") + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create specs dir") +} + +func TestPrep_CopyRepoSpecs_Ugly_ParentDirBlocked(t *testing.T) { + root := t.TempDir() + codePath := core.JoinPath(root, "src") + plansBase := core.JoinPath(codePath, "host-uk", "core", "plans") + specDir := core.JoinPath(plansBase, "core", "go", "test-repo") + + require.True(t, fs.EnsureDir(core.JoinPath(specDir, "sub")).OK) + require.True(t, fs.Write(core.JoinPath(specDir, "sub", "RFC-2.md"), "nested-spec").OK) + require.True(t, fs.EnsureDir(core.JoinPath(root, "workspace", "specs")).OK) + require.True(t, fs.Write(core.JoinPath(root, "workspace", "specs", "sub"), "blocked").OK) + + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + codePath: codePath, + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + err := s.copyRepoSpecs(core.JoinPath(root, "workspace"), "go-test-repo") + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create specs parent dir") +} + func writeFakeLanguageCommand(t *testing.T, dir, name, logPath string, exitCode int) { t.Helper()