{"record":{"id":"7afa16fe8b1f157e","repo":"KeygraphHQ/shannon","slug":"variable-interpolation-failed-errmsg","errorCode":null,"errorMessage":"Variable interpolation failed: ${errMsg}","messagePattern":"Variable interpolation failed: (.+?)","errorType":"exception","errorClass":"PentestError","httpStatus":null,"severity":"error","filePath":"apps/worker/src/services/prompt-manager.ts","lineNumber":442,"sourceCode":"      renderReportFilterRules(config?.report, exploitEnabled),\n    );\n\n    // Collapse runs of 3+ newlines (left behind by tag-strip and empty-fragment substitutions).\n    result = result.replace(/\\n{3,}/g, '\\n\\n');\n\n    // Validate that all placeholders have been replaced (excluding instructional text)\n    const remainingPlaceholders = result.match(/\\{\\{[^}]+\\}\\}/g);\n    if (remainingPlaceholders) {\n      logger.warn(`Found unresolved placeholders in prompt: ${remainingPlaceholders.join(', ')}`);\n    }\n\n    return result;\n  } catch (error) {\n    if (error instanceof PentestError) {\n      throw error;\n    }\n    const errMsg = error instanceof Error ? error.message : String(error);\n    throw new PentestError(`Variable interpolation failed: ${errMsg}`, 'prompt', false, { originalError: errMsg });\n  }\n}\n\n// Resolve promptDir override against SHANNON_WORKER_ROOT so relative paths\n// from callers stay cwd-independent.\nfunction resolvePromptDir(promptDir: string | undefined): string {\n  if (!promptDir) return PROMPTS_DIR;\n  if (path.isAbsolute(promptDir)) return promptDir;\n  return path.resolve(process.env.SHANNON_WORKER_ROOT ?? process.cwd(), promptDir);\n}\n\n// Pure function: Load and interpolate prompt template\nexport async function loadPrompt(\n  promptName: string,\n  variables: PromptVariables,\n  config: DistributedConfig | null = null,\n  pipelineTestingMode: boolean = false,\n  logger: ActivityLogger,","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/KeygraphHQ/shannon/blob/1ae0a142f8525410a688f0309fd003cc5b1d92de/apps/worker/src/services/prompt-manager.ts#L424-L460","documentation":"Catch-all wrapper in interpolateVariables for any non-PentestError exception during substitution (e.g. buildLoginInstructions throws a plain Error, replaceLiteral receives a malformed value, or a regex/tag-strip operation fails). PentestError instances are re-thrown unchanged; everything else is wrapped with category 'prompt', non-retryable, carrying context.originalError. This is the outer safety net for the whole interpolation pipeline.","triggerScenarios":"Any unexpected plain Error thrown between the two validation guards and the final return of interpolateVariables: a config field of the wrong type breaking a replaceLiteral call, a malformed rules_of_engagement, or buildLoginInstructions failing with a non-PentestError. The context.originalError string identifies the real failure.","commonSituations":"A config field (e.g. config.description, rules_of_engagement, vuln_classes) has an unexpected type that passed schema validation but breaks string interpolation. A shared partial is missing mid-render. An internal helper throws an unguarded Error.","solutions":["Read context.originalError on the PentestError to find the underlying message and the failing operation.","Validate the supplied config object against the JSON schema before the scan.","If originalError references a file (e.g. login template), follow the fix for the corresponding error (41/42).","Simplify the config to isolate which field triggers interpolation, then correct its type/value."],"exampleFix":"// before: config.description is a number, breaks string concat during interpolation\n//   config = { description: 42 }\n// after: keep it a string per schema\n//   config = { description: 'Auth assessment' }","handlingStrategy":"try-catch","validationCode":"// Validate config field types that interpolation depends on, before starting\nfunction interpolationConfigIsSafe(c: DistributedConfig | null): boolean {\n  if (!c) return true;\n  if (c.description !== undefined && typeof c.description !== 'string') return false;\n  if (c.rules_of_engagement !== undefined && typeof c.rules_of_engagement !== 'string') return false;\n  if (c.vuln_classes !== undefined && !Array.isArray(c.vuln_classes)) return false;\n  return true;\n}","typeGuard":"function isInterpolatableConfig(c: unknown): boolean {\n  return c === null || (typeof c === 'object' &&\n    (c as any).description === undefined || typeof (c as any).description === 'string');\n}","tryCatchPattern":"try {\n  await interpolateVariables(template, vars, config, logger);\n} catch (e) {\n  if (e instanceof PentestError && /Variable interpolation failed/.test(e.message)) {\n    // read context.originalError, then either fix the config field or report\n    log.error('interpolation failed', { original: (e.context as any)?.originalError });\n  }\n  throw e;\n}","preventionTips":["Keep config.description, rules_of_engagement, and vuln_classes as their schema types (string, string, array).","Use context.originalError to locate the failing substitution.","Validate the config through the JSON-schema parser before the workflow starts.","Isolate a flaky interpolation by reducing the config to minimal fields."],"tags":["prompt","error-wrapping","validation","config"],"backgroundTag":null,"analyzedSha":"1ae0a142f8525410a688f0309fd003cc5b1d92de","analyzedAt":"2026-08-12T17:40:03.583Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}