KeygraphHQ/shannon · error · PentestError

DELIVERABLE_NOT_FOUND

DELIVERABLE_NOT_FOUND

Error message

Required deliverable file not found: ${file.paths.join(' or ')}

What it means

Thrown by assembleFinalReport when a deliverable file marked required=true is absent from the deliverables directory (none of its candidate paths in file.paths exist or are readable). Code DELIVERABLE_NOT_FOUND, category 'filesystem', non-retryable. In the current hardcoded deliverableFiles list every entry is required:false, so this is dormant unless a required entry is added or the list is extended.

Source

Thrown at apps/worker/src/services/reporting.ts:65

    let added = false;
    for (const candidate of file.paths) {
      const filePath = path.join(dir, candidate);
      try {
        if (await fs.pathExists(filePath)) {
          const content = await fs.readFile(filePath, 'utf8');
          sections.push(content);
          logger.info(`Added ${file.name} section from ${candidate}`);
          added = true;
          break;
        }
      } catch (error) {
        const err = error as Error;
        logger.warn(`Could not read ${candidate}: ${err.message}`);
      }
    }
    if (!added) {
      if (file.required) {
        throw new PentestError(
          `Required deliverable file not found: ${file.paths.join(' or ')}`,
          'filesystem',
          false,
          { deliverableFile: file.paths, sourceDir },
          ErrorCode.DELIVERABLE_NOT_FOUND,
        );
      }
      logger.info(`No ${file.name} deliverable found`);
    }
  }

  const finalContent = sections.join('\n\n');
  const finalReportPath = path.join(dir, ASSEMBLED_REPORT_FILENAME);

  try {
    await fs.ensureDir(dir);
    await fs.writeFile(finalReportPath, finalContent);
    logger.info(`Final report assembled at ${finalReportPath}`);

View on GitHub (pinned to 1ae0a142f8)

Solutions

  1. Confirm whether the entry should be required; in the shipped list all are optional, so verify the list was not modified.
  2. Check context.deliverableFile and context.sourceDir on the thrown PentestError to see which file/dir was expected.
  3. Ensure the agent that produces that deliverable ran successfully (session.metrics.agents) and that vuln_classes scoping did not exclude it.
  4. If the file exists under .shannon/deliverables but sourceDir points elsewhere, fix the path resolution (deliverablesDir) or run the workspace migration.
  5. Start a fresh workspace if the deliverable was never produced and cannot be regenerated.

Example fix

// before: a required deliverable was added but agent disabled by scope
//   { name: 'Auth', paths: ['auth_findings.md'], required: true }
//   vuln_classes excludes 'auth' -> no file produced
// after: either include the class or keep it optional
//   { name: 'Auth', paths: ['auth_findings.md'], required: false }
Defensive patterns

Strategy: validation

Validate before calling

// Before assembling the report, confirm any required deliverable is present
import { pathExists } from 'fs-extra';
for (const f of deliverableFiles) {
  if (!f.required) continue;
  const present = await Promise.any(f.paths.map((p) => pathExists(path.join(dir, p)).then((e) => { if (!e) throw 0; })));
  if (!present) throw new Error(`Required deliverable missing: ${f.paths.join(' or ')}`);
}

Type guard

function isDeliverableFile(o: unknown): o is DeliverableFile {
  return typeof o === 'object' && o !== null &&
    typeof (o as any).name === 'string' &&
    Array.isArray((o as any).paths) &&
    typeof (o as any).required === 'boolean';
}

Try / catch

try {
  await assembleFinalReport(sourceDir, deliverablesSubdir, logger);
} catch (e) {
  if (e instanceof PentestError && e.code === ErrorCode.DELIVERABLE_NOT_FOUND) {
    // either the agent was scoped out (drop the entry) or never produced the file (re-run that phase)
    const paths = (e.context as any)?.deliverableFile;
    log.error('deliverable missing', { paths, sourceDir: (e.context as any)?.sourceDir });
  }
  throw e;
}

Prevention

When it happens

Trigger: assembleFinalReport iterates its deliverableFiles list; for a required entry, if every candidate filename in file.paths is missing from deliverablesDir(sourceDir, deliverablesSubdir) or unreadable (the read error path at line 58 only logs a warning and continues, so it takes a true absence to fail), the throw fires. Today this requires editing the list to add a required:true file that an agent never produced.

Common situations: A maintainer adds a required deliverable to the reporting list but the producing agent was disabled (e.g. via vuln_classes scoping) or failed silently. The deliverables subdir path is wrong so all candidates resolve to a non-existent directory. A migration moved deliverables under .shannon/ but sourceDir was computed from the legacy run root.

Related errors


AI-assisted analysis of KeygraphHQ/shannon@1ae0a142f8 (2026-08-12). Data as JSON: /api/errors/6686c9df2b878aa6. Report an issue: GitHub.