fix(agentic): surface persistence failures
Add warnings for silent filesystem write/delete failures in agentic persistence helpers and record two adjacent hardening gaps for follow-up.\n\nCo-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
db3ddc133e
commit
7bb5c31746
6 changed files with 89 additions and 41 deletions
|
|
@ -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.
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue