hardening(prep): fail closed on specs copy

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-17 20:38:42 +01:00
parent 7bb5c31746
commit 2eda43d5ad
3 changed files with 112 additions and 26 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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()