hardening(prep): fail closed on specs copy
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
7bb5c31746
commit
2eda43d5ad
3 changed files with 112 additions and 26 deletions
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue