KeygraphHQ/shannon · error · PentestError

Template must be a non-empty string

Error message

Template must be a non-empty string

What it means

First validation guard in interpolateVariables: the template argument must be a non-empty string. If template is falsy (null, undefined, '') or not a string (number, object), this throws before any substitution. Category 'validation', non-retryable. It is an internal-contract guard; loadPrompt reads the file into a string before calling, so a hit indicates the read returned empty/non-string or an internal caller misused the function.

Source

Thrown at apps/worker/src/services/prompt-manager.ts:318

  if (auth.credentials?.totp_secret) {
    lines.push('- MFA: TOTP enabled');
  }

  return lines.join('\n');
}

// Pure function: Variable interpolation
async function interpolateVariables(
  template: string,
  variables: PromptVariables,
  config: DistributedConfig | null = null,
  logger: ActivityLogger,
  promptsBaseDir: string = PROMPTS_DIR,
): Promise<string> {
  try {
    if (!template || typeof template !== 'string') {
      throw new PentestError('Template must be a non-empty string', 'validation', false, {
        templateType: typeof template,
        templateLength: template?.length,
      });
    }

    if (!variables || !variables.webUrl || !variables.repoPath) {
      throw new PentestError('Variables must include webUrl and repoPath', 'validation', false, {
        variables: Object.keys(variables || {}),
      });
    }

    // replaceLiteral is used for all value insertions so config values that
    // contain `$&`/`$$`/`$1`/etc. aren't mangled as replacement patterns.
    let result = template;
    result = replaceLiteral(result, /{{WEB_URL}}/g, variables.webUrl);
    result = replaceLiteral(result, /{{REPO_PATH}}/g, variables.repoPath);
    result = replaceLiteral(result, /{{PLAYWRIGHT_SESSION}}/g, variables.PLAYWRIGHT_SESSION || 'agent1');
    result = replaceLiteral(result, /{{AUTH_CONTEXT}}/g, buildAuthContext(config));

View on GitHub (pinned to 1ae0a142f8)

Solutions

  1. Inspect the prompt file named in the surrounding loadPrompt call and confirm it is non-empty and valid UTF-8.
  2. If the file is empty or truncated, restore it from the repo (apps/worker/prompts/<name>.txt) or git.
  3. Check the context.templateType and context.templateLength fields on the thrown PentestError to confirm the bad input.
  4. If calling interpolateVariables directly, ensure the template argument is a string read from a file, not an optional/undefined value.

Example fix

// before: empty prompt file read into template
//   const template = await fs.readFile(missingPromptPath, 'utf8'); // ''
//   await interpolateVariables(template, vars, ...);
// after: guard the read, or restore the file
//   const template = await fs.readFile(promptPath, 'utf8');
//   if (!template) throw new Error(`Empty prompt: ${promptPath}`);
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the template is a non-empty string before interpolating
function isValidTemplate(t: unknown): t is string {
  return typeof t === 'string' && t.length > 0;
}
if (!isValidTemplate(template)) {
  throw new Error(`Invalid prompt template: type=${typeof template}, len=${(template as any)?.length}`);
}

Type guard

function isNonEmptyString(v: unknown): v is string {
  return typeof v === 'string' && v.length > 0;
}

Try / catch

try {
  await interpolateVariables(template, vars, config, logger);
} catch (e) {
  if (e instanceof PentestError && /Template must be a non-empty string/.test(e.message)) {
    // the prompt file is empty/corrupt — restore from git before retrying
    await restorePromptFromGit(promptName);
  }
  throw e;
}

Prevention

When it happens

Trigger: interpolateVariables is called with a template that is undefined/null/'' — e.g. an empty prompt file was read (0 bytes), a programmatic caller passed a non-string, or fs.readFile returned an unexpected type. The guard fires before webUrl/repoPath are even checked.

Common situations: A prompt file under apps/worker/prompts/ is empty (truncated by a failed save). A custom promptDir points at a zero-byte file. An internal refactor calls interpolateVariables with a computed value that evaluated to undefined.

Related errors


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