Skip to content

fix: correct error-retry classification and resume checkpoint safety - #401

Open
prithvee07 wants to merge 8 commits into
KeygraphHQ:mainfrom
prithvee07:claude/codebase-review-wu6fdk
Open

fix: correct error-retry classification and resume checkpoint safety#401
prithvee07 wants to merge 8 commits into
KeygraphHQ:mainfrom
prithvee07:claude/codebase-review-wu6fdk

Conversation

@prithvee07

Copy link
Copy Markdown
  • classifyErrorForTemporal now honors a PentestError's explicit retryable flag when no ErrorCode is set, instead of letting message substring matching override author intent (was flipping intentionally retryable queue-validation failures to non-retryable, and vice versa for unrecoverable preflight config errors).
  • runPiPrompt's billing/session-limit text classification now only applies to turns the harness itself flags as errors, not every successful turn's assistant text — legitimate pentest report prose (e.g. "no usage limit enforced") could otherwise discard a successful, paid-for agent run.
  • runPiPrompt's catch block now preserves a caught PentestError's code and retryable fields instead of rebuilding them from generic string heuristics, restoring the billing/spending-cap retry path.
  • restoreGitCheckpoint now only treats a checkpoint-existence check failure as "checkpoint missing" for git's actual missing-object errors; any other failure (lock exhaustion, FS error) propagates instead of silently skipping the reset/clean/deliverable-cleanup steps during resume.

claude and others added 8 commits July 29, 2026 16:17
- classifyErrorForTemporal now honors a PentestError's explicit
  retryable flag when no ErrorCode is set, instead of letting message
  substring matching override author intent (was flipping intentionally
  retryable queue-validation failures to non-retryable, and vice versa
  for unrecoverable preflight config errors).
- runPiPrompt's billing/session-limit text classification now only
  applies to turns the harness itself flags as errors, not every
  successful turn's assistant text — legitimate pentest report prose
  (e.g. "no usage limit enforced") could otherwise discard a
  successful, paid-for agent run.
- runPiPrompt's catch block now preserves a caught PentestError's code
  and retryable fields instead of rebuilding them from generic string
  heuristics, restoring the billing/spending-cap retry path.
- restoreGitCheckpoint now only treats a checkpoint-existence check
  failure as "checkpoint missing" for git's actual missing-object
  errors; any other failure (lock exhaustion, FS error) propagates
  instead of silently skipping the reset/clean/deliverable-cleanup
  steps during resume.
- forwardEtcHostsFlags no longer brackets IPv6 literals for --add-host;
  Docker's flag parser splits on the first colon and validates the
  remainder as a bare IP, so a bracketed literal was rejected outright,
  breaking scan startup for anyone with a non-loopback IPv6 /etc/hosts
  entry.
- validateAuthentication now wraps its state-file cleanup and prompt
  loading in try/catch and returns Err instead of letting a thrown
  error escape as a raw promise rejection, honoring its
  Promise<Result<void, PentestError>> contract.
- findings-renderer skips and logs queue entries missing ID or
  vulnerability_type instead of rendering literal "undefined: undefined"
  into the customer-facing report.
- restoreGitCheckpoint's incompleteAgents is now derived from the run's
  actual expectedAgents scope instead of ALL_AGENTS, so a class-scoped
  resume doesn't target out-of-scope agents for git-clean/deletion.
- WorkflowLogger is now cached and reused per session (getOrCreateWorkflowLogger/
  closeWorkflowLogger) instead of every AuditSession opening its own
  WriteStream onto the same workflow.log; closed once at the true
  end-of-workflow point in logWorkflowComplete.
- Credential config writes now chmod 0o600 explicitly after write,
  since Node ignores the writeFileSync mode option when overwriting an
  existing file.
- migrateLegacyWorkspaceLayout now stages renames in a temp directory
  and renames it into place as one atomic step, so a failure partway
  through can no longer leave the internalPath existence check
  permanently (and silently) skipping migration on every future run.
Improve error classification and retry logic for transient failures
- task-tool.ts: replace Model<any> with Model<Api>, matching the
  upstream pi-ai Api union instead of suppressing the any-check.
- exploit-renderer.ts: use String.charAt(0) instead of value[0]! —
  charAt always returns a string, so it drops the non-null assertion
  without introducing the "undefined" + rest bug that optional
  chaining (value[0]?.) would have on an empty/out-of-range index.
chore: clean up remaining biome lint warnings
…iew pass

- resolveRepo now rejects bare repo names that resolve outside ./repos/
  (e.g. containing ../), which could otherwise bind-mount an arbitrary
  host directory into the ephemeral worker container.
- assembleReportActivity now always renders per-class findings from the
  exploitation queue before assembling the report, not only when
  exploit=false. Previously, a partial run with exploit=true whose
  exploit agent failed for one class would have neither an evidence
  nor a findings file for that class, so assembleFinalReport silently
  omitted it and the report claimed no vulnerabilities were found
  despite confirmed queue findings on disk. renderFindingsFromQueues
  now skips a class whenever its evidence file already exists, so this
  is a no-op for classes whose exploit agent succeeded.
- checkDeprecatedFields now rejects a top-level 'login' config block.
  The schema accepted it silently (deprecated:true is documentation
  only, not enforced) and distributeConfig never read it, so a legacy
  config using the old block resolved authentication to null and ran
  the scan unauthenticated with no indication why.
- rollbackGitWorkspace's git reset --hard is now scoped to the
  provided per-agent paths (via targeted reset + checkout, since
  reset --hard has no path-scoping form), matching the git clean call
  beside it. Previously only the untracked-file cleanup was scoped;
  the tracked-file reset always reverted the entire shared deliverables
  repo, so a failing agent's retry could silently discard a concurrent
  sibling agent's uncommitted work during the parallel vuln/exploit
  phase.
- MetricsTracker.recalculateAggregations now includes every agent's
  cost (and an effective duration when final_duration_ms was never
  set) regardless of status, instead of only 'success' agents. A
  permanently-failed agent's real, already-billed spend was being
  dropped from the cumulative cost surfaced to the user.
- login-instructions.txt now defines API and BASIC sections (the
  template previously only covered FORM/SSO). login_type: api/basic
  are two of the four documented login types but got no login
  instructions at all, since the fallback-to-full-template guard
  never fires while COMMON/VERIFICATION are present.
- config-schema.json's top-level anyOf now includes pipeline, so a
  config containing only pipeline settings (e.g. for subscription
  rate-limit retry behavior) no longer fails validation. Mirrored in
  config-parser.ts's hasAnySteering check so it doesn't warn about "no
  steering fields" for a pipeline-only config either.
- checkForDuplicates/checkForConflicts now compare rules using the
  same normalization sanitizeRule applies later (trimmed value,
  trimmed+lowercased type), so a duplicate/conflict hiding behind
  incidental whitespace no longer bypasses the check.
- performSecurityValidation now checks authentication.success_condition.value
  against DANGEROUS_PATTERNS, matching the other authentication fields
  rendered into the same login prompt templates.
- formatDuration now rounds before branching on the 60s boundary, so a
  duration like 59.95s no longer displays as "60.0s" — it correctly
  crosses into the "1m 0s" branch instead.
- AuditSession.initialize's session.json creation path is now
  protected by the same sessionMutex every other session.json mutator
  uses, closing a gap that was dormant only because of the current
  phase-ordering assumption (pre-recon is the sole non-parallel first
  agent).
- executeGitCommandWithRetry's retry-exhaustion PentestError was
  unreachable dead code — a lock error on the final attempt still hit
  the raw `throw error` branch. Restructured so exhausting retries on
  a genuine lock conflict now surfaces the classified, retryable
  error instead of the raw git/zx failure.
- DANGEROUS_PATTERNS in config-parser.ts was over-broad: /data:/ and
  /file:/ matched inside ordinary words ("metadata:", "profile:"),
  and /[<>]/ rejected any bare '<' or '>' (e.g. "count > 5"). Patterns
  now require a word boundary before URL schemes and require '<' to
  look like an actual tag/comment open, eliminating the false
  positives while still catching data:/javascript:/file: URLs and
  HTML/XML injection.
- processIncludes now resolves @include() directives recursively (with
  a depth guard against circular includes), so a shared prompt partial
  can itself use @include() without silently leaving the literal,
  unresolved directive text in the assembled prompt.
- save-deliverable.ts now writes via the shared atomicWrite (temp file
  + rename) instead of a direct writeFileSync, matching the
  crash-safety pattern used for every other deliverable/state write
  in the codebase.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants