KeygraphHQ/shannon · error · PentestError
Variable interpolation failed: ${errMsg}
Error message
Variable interpolation failed: ${errMsg} What it means
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.
Source
Thrown at apps/worker/src/services/prompt-manager.ts:442
renderReportFilterRules(config?.report, exploitEnabled),
);
// Collapse runs of 3+ newlines (left behind by tag-strip and empty-fragment substitutions).
result = result.replace(/\n{3,}/g, '\n\n');
// Validate that all placeholders have been replaced (excluding instructional text)
const remainingPlaceholders = result.match(/\{\{[^}]+\}\}/g);
if (remainingPlaceholders) {
logger.warn(`Found unresolved placeholders in prompt: ${remainingPlaceholders.join(', ')}`);
}
return result;
} catch (error) {
if (error instanceof PentestError) {
throw error;
}
const errMsg = error instanceof Error ? error.message : String(error);
throw new PentestError(`Variable interpolation failed: ${errMsg}`, 'prompt', false, { originalError: errMsg });
}
}
// Resolve promptDir override against SHANNON_WORKER_ROOT so relative paths
// from callers stay cwd-independent.
function resolvePromptDir(promptDir: string | undefined): string {
if (!promptDir) return PROMPTS_DIR;
if (path.isAbsolute(promptDir)) return promptDir;
return path.resolve(process.env.SHANNON_WORKER_ROOT ?? process.cwd(), promptDir);
}
// Pure function: Load and interpolate prompt template
export async function loadPrompt(
promptName: string,
variables: PromptVariables,
config: DistributedConfig | null = null,
pipelineTestingMode: boolean = false,
logger: ActivityLogger,View on GitHub (pinned to 1ae0a142f8)
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.
Example fix
// before: config.description is a number, breaks string concat during interpolation
// config = { description: 42 }
// after: keep it a string per schema
// config = { description: 'Auth assessment' } Defensive patterns
Strategy: try-catch
Validate before calling
// Validate config field types that interpolation depends on, before starting
function interpolationConfigIsSafe(c: DistributedConfig | null): boolean {
if (!c) return true;
if (c.description !== undefined && typeof c.description !== 'string') return false;
if (c.rules_of_engagement !== undefined && typeof c.rules_of_engagement !== 'string') return false;
if (c.vuln_classes !== undefined && !Array.isArray(c.vuln_classes)) return false;
return true;
} Type guard
function isInterpolatableConfig(c: unknown): boolean {
return c === null || (typeof c === 'object' &&
(c as any).description === undefined || typeof (c as any).description === 'string');
} Try / catch
try {
await interpolateVariables(template, vars, config, logger);
} catch (e) {
if (e instanceof PentestError && /Variable interpolation failed/.test(e.message)) {
// read context.originalError, then either fix the config field or report
log.error('interpolation failed', { original: (e.context as any)?.originalError });
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to build login instructions: ${errMsg}
- CONFIG_VALIDATION_FAILED
- Login instructions template not found
- Path traversal detected in @include(): ${rawPath}
- Template must be a non-empty string
AI-assisted analysis of KeygraphHQ/shannon@1ae0a142f8 (2026-08-12).
Data as JSON: /api/errors/7afa16fe8b1f157e.
Report an issue: GitHub.