diff --git a/.core/TODO.md b/.core/TODO.md index e69de29..15b85a5 100644 --- a/.core/TODO.md +++ b/.core/TODO.md @@ -0,0 +1,2 @@ +- @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/dispatch.go b/pkg/agentic/dispatch.go index 4269f77..acbc5ef 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -593,7 +593,9 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, workspaceDir string) (int, str metaDir := WorkspaceMetaDir(workspaceDir) outputFile := agentOutputFile(workspaceDir, agent) - fs.Delete(WorkspaceBlockedPath(workspaceDir)) + if deleteResult := fs.Delete(WorkspaceBlockedPath(workspaceDir)); !deleteResult.OK { + core.Warn("agentic: failed to remove blocked marker", "path", WorkspaceBlockedPath(workspaceDir), "reason", deleteResult.Value) + } if !isNativeAgent(agent) { runtimeName := resolveContainerRuntime(s.dispatchRuntime()) diff --git a/pkg/agentic/qa.go b/pkg/agentic/qa.go index 543270b..f9365f5 100644 --- a/pkg/agentic/qa.go +++ b/pkg/agentic/qa.go @@ -80,20 +80,20 @@ type QAReport struct { // // Usage example: `report := DispatchReport{Summary: map[string]any{"finding": 12, "tool_run": 3}, Findings: findings, Tools: tools, BuildPassed: true, TestPassed: true}` type DispatchReport struct { - Workspace string `json:"workspace"` - Commit string `json:"commit,omitempty"` - Summary map[string]any `json:"summary"` - Findings []QAFinding `json:"findings,omitempty"` - Tools []QAToolRun `json:"tools,omitempty"` - BuildPassed bool `json:"build_passed"` - TestPassed bool `json:"test_passed"` - LintPassed bool `json:"lint_passed"` - Passed bool `json:"passed"` - GeneratedAt time.Time `json:"generated_at"` - New []map[string]any `json:"new,omitempty"` - Resolved []map[string]any `json:"resolved,omitempty"` - Persistent []map[string]any `json:"persistent,omitempty"` - Clusters []DispatchCluster `json:"clusters,omitempty"` + Workspace string `json:"workspace"` + Commit string `json:"commit,omitempty"` + Summary map[string]any `json:"summary"` + Findings []QAFinding `json:"findings,omitempty"` + Tools []QAToolRun `json:"tools,omitempty"` + BuildPassed bool `json:"build_passed"` + TestPassed bool `json:"test_passed"` + LintPassed bool `json:"lint_passed"` + Passed bool `json:"passed"` + GeneratedAt time.Time `json:"generated_at"` + New []map[string]any `json:"new,omitempty"` + Resolved []map[string]any `json:"resolved,omitempty"` + Persistent []map[string]any `json:"persistent,omitempty"` + Clusters []DispatchCluster `json:"clusters,omitempty"` } // DispatchCluster groups similar findings together so human reviewers can see @@ -103,11 +103,11 @@ type DispatchReport struct { // // Usage example: `cluster := DispatchCluster{Tool: "gosec", Severity: "error", Category: "security", Count: 3, RuleID: "G101"}` type DispatchCluster struct { - Tool string `json:"tool,omitempty"` - Severity string `json:"severity,omitempty"` - Category string `json:"category,omitempty"` - RuleID string `json:"rule_id,omitempty"` - Count int `json:"count"` + Tool string `json:"tool,omitempty"` + Severity string `json:"severity,omitempty"` + Category string `json:"category,omitempty"` + RuleID string `json:"rule_id,omitempty"` + Count int `json:"count"` Samples []DispatchClusterSample `json:"samples,omitempty"` } @@ -461,14 +461,18 @@ func writeDispatchReport(workspaceDir string, report DispatchReport) { return } metaDir := WorkspaceMetaDir(workspaceDir) - if !fs.EnsureDir(metaDir).OK { + if ensureResult := fs.EnsureDir(metaDir); !ensureResult.OK { + core.Warn("agentic: failed to prepare dispatch report directory", "path", metaDir, "reason", ensureResult.Value) return } payload := core.JSONMarshalString(report) if payload == "" { return } - fs.WriteAtomic(core.JoinPath(metaDir, "report.json"), payload) + reportPath := core.JoinPath(metaDir, "report.json") + if writeResult := fs.WriteAtomic(reportPath, payload); !writeResult.OK { + core.Warn("agentic: failed to write dispatch report", "path", reportPath, "reason", writeResult.Value) + } } // stringOutput extracts the process output from a core.Result, returning an @@ -760,4 +764,3 @@ func clusterLess(left, right DispatchCluster) bool { } return left.RuleID < right.RuleID } - diff --git a/pkg/agentic/review_queue.go b/pkg/agentic/review_queue.go index ce8fdda..173aec8 100644 --- a/pkg/agentic/review_queue.go +++ b/pkg/agentic/review_queue.go @@ -389,12 +389,17 @@ func (s *PrepSubsystem) buildReviewCommand(repoDir, reviewer string) (string, [] // s.storeReviewOutput(repoDir, "go-io", "coderabbit", output) func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string) { dataDir := core.JoinPath(HomeDir(), ".core", "training", "reviews") - fs.EnsureDir(dataDir) + if ensureResult := fs.EnsureDir(dataDir); !ensureResult.OK { + core.Warn("reviewQueue: failed to prepare review output directory", "path", dataDir, "reason", ensureResult.Value) + return + } timestamp := time.Now().Format("2006-01-02T15-04-05") filename := core.Sprintf("%s_%s_%s.txt", repo, reviewer, timestamp) - - fs.Write(core.JoinPath(dataDir, filename), output) + outputPath := core.JoinPath(dataDir, filename) + if writeResult := fs.Write(outputPath, output); !writeResult.OK { + core.Warn("reviewQueue: failed to write review output", "path", outputPath, "reason", writeResult.Value) + } entry := map[string]string{ "repo": repo, @@ -411,6 +416,7 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string jsonlPath := core.JoinPath(dataDir, "reviews.jsonl") r := fs.Append(jsonlPath) if !r.OK { + core.Warn("reviewQueue: failed to open review journal", "path", jsonlPath, "reason", r.Value) return } core.WriteAll(r.Value, core.Concat(jsonLine, "\n")) diff --git a/pkg/agentic/runtime_state.go b/pkg/agentic/runtime_state.go index 9d971d9..b7fe900 100644 --- a/pkg/agentic/runtime_state.go +++ b/pkg/agentic/runtime_state.go @@ -101,14 +101,20 @@ func (s *PrepSubsystem) persistRuntimeState() { } if len(state.Backoff) == 0 && len(state.FailCount) == 0 { - fs.Delete(runtimeStatePath()) + if deleteResult := fs.Delete(runtimeStatePath()); !deleteResult.OK { + core.Warn("agentic: failed to delete runtime state file", "path", runtimeStatePath(), "reason", deleteResult.Value) + } s.stateStoreDelete(stateRuntimeGroup, "backoff") s.stateStoreDelete(stateRuntimeGroup, "fail_count") return } - fs.EnsureDir(runtimeStateDir()) - fs.WriteAtomic(runtimeStatePath(), core.JSONMarshalString(state)) + if ensureResult := fs.EnsureDir(runtimeStateDir()); !ensureResult.OK { + core.Warn("agentic: failed to prepare runtime state directory", "path", runtimeStateDir(), "reason", ensureResult.Value) + } + if writeResult := fs.WriteAtomic(runtimeStatePath(), core.JSONMarshalString(state)); !writeResult.OK { + core.Warn("agentic: failed to write runtime state", "path", runtimeStatePath(), "reason", writeResult.Value) + } // Mirror the authoritative JSON to the go-store cache so restarts see // the same state even when the JSON file is archived or rotated. diff --git a/pkg/agentic/sync.go b/pkg/agentic/sync.go index 2bd2486..af3e971 100644 --- a/pkg/agentic/sync.go +++ b/pkg/agentic/sync.go @@ -335,11 +335,18 @@ func readSyncLedger() map[string]string { // can skip workspaces that have already been pushed. func writeSyncLedger(ledger map[string]string) { if len(ledger) == 0 { - fs.Delete(syncLedgerPath()) + if deleteResult := fs.Delete(syncLedgerPath()); !deleteResult.OK { + core.Warn("agentic: failed to delete sync ledger", "path", syncLedgerPath(), "reason", deleteResult.Value) + } return } - fs.EnsureDir(syncStateDir()) - fs.WriteAtomic(syncLedgerPath(), core.JSONMarshalString(ledger)) + if ensureResult := fs.EnsureDir(syncStateDir()); !ensureResult.OK { + core.Warn("agentic: failed to prepare sync ledger directory", "path", syncStateDir(), "reason", ensureResult.Value) + return + } + if writeResult := fs.WriteAtomic(syncLedgerPath(), core.JSONMarshalString(ledger)); !writeResult.OK { + core.Warn("agentic: failed to write sync ledger", "path", syncLedgerPath(), "reason", writeResult.Value) + } } // markDispatchesSynced records which dispatches were successfully pushed so @@ -438,11 +445,18 @@ func readSyncQueue() []syncQueuedPush { func writeSyncQueue(queued []syncQueuedPush) { if len(queued) == 0 { - fs.Delete(syncQueuePath()) + if deleteResult := fs.Delete(syncQueuePath()); !deleteResult.OK { + core.Warn("agentic: failed to delete sync queue", "path", syncQueuePath(), "reason", deleteResult.Value) + } return } - fs.EnsureDir(syncStateDir()) - fs.WriteAtomic(syncQueuePath(), core.JSONMarshalString(queued)) + if ensureResult := fs.EnsureDir(syncStateDir()); !ensureResult.OK { + core.Warn("agentic: failed to prepare sync queue directory", "path", syncStateDir(), "reason", ensureResult.Value) + return + } + if writeResult := fs.WriteAtomic(syncQueuePath(), core.JSONMarshalString(queued)); !writeResult.OK { + core.Warn("agentic: failed to write sync queue", "path", syncQueuePath(), "reason", writeResult.Value) + } } // syncQueueStoreKey is the canonical key for the sync queue inside go-store — @@ -499,8 +513,13 @@ func readSyncContext() []map[string]any { } func writeSyncContext(contextData []map[string]any) { - fs.EnsureDir(syncStateDir()) - fs.WriteAtomic(syncContextPath(), core.JSONMarshalString(contextData)) + if ensureResult := fs.EnsureDir(syncStateDir()); !ensureResult.OK { + core.Warn("agentic: failed to prepare sync context directory", "path", syncStateDir(), "reason", ensureResult.Value) + return + } + if writeResult := fs.WriteAtomic(syncContextPath(), core.JSONMarshalString(contextData)); !writeResult.OK { + core.Warn("agentic: failed to write sync context", "path", syncContextPath(), "reason", writeResult.Value) + } } func syncContextPayload(payload map[string]any) []map[string]any { @@ -573,8 +592,13 @@ func readSyncStatusState() syncStatusState { } func writeSyncStatusState(state syncStatusState) { - fs.EnsureDir(syncStateDir()) - fs.WriteAtomic(syncStatusPath(), core.JSONMarshalString(state)) + if ensureResult := fs.EnsureDir(syncStateDir()); !ensureResult.OK { + core.Warn("agentic: failed to prepare sync status directory", "path", syncStateDir(), "reason", ensureResult.Value) + return + } + if writeResult := fs.WriteAtomic(syncStatusPath(), core.JSONMarshalString(state)); !writeResult.OK { + core.Warn("agentic: failed to write sync status", "path", syncStatusPath(), "reason", writeResult.Value) + } } func syncRecordsPath() string { @@ -597,8 +621,13 @@ func readSyncRecords() []SyncRecord { } func writeSyncRecords(records []SyncRecord) { - fs.EnsureDir(syncStateDir()) - fs.WriteAtomic(syncRecordsPath(), core.JSONMarshalString(records)) + if ensureResult := fs.EnsureDir(syncStateDir()); !ensureResult.OK { + core.Warn("agentic: failed to prepare sync records directory", "path", syncStateDir(), "reason", ensureResult.Value) + return + } + if writeResult := fs.WriteAtomic(syncRecordsPath(), core.JSONMarshalString(records)); !writeResult.OK { + core.Warn("agentic: failed to write sync records", "path", syncRecordsPath(), "reason", writeResult.Value) + } } func recordSyncHistory(direction, agentID string, fleetNodeID, payloadSize, itemsCount int, at time.Time) {