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

  1. Read context.originalError on the PentestError to find the underlying message and the failing operation.
  2. Validate the supplied config object against the JSON schema before the scan.
  3. If originalError references a file (e.g. login template), follow the fix for the corresponding error (41/42).
  4. 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

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


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