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
- Inspect the prompt file named in the surrounding loadPrompt call and confirm it is non-empty and valid UTF-8.
- If the file is empty or truncated, restore it from the repo (apps/worker/prompts/<name>.txt) or git.
- Check the context.templateType and context.templateLength fields on the thrown PentestError to confirm the bad input.
- 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
- Treat an empty prompt file as a build/CI failure, not a runtime recoverable state.
- When calling interpolateVariables directly, assert the template is a non-empty string first.
- Version-control prompt files and restore truncated files from git.
- Add a startup self-test that every prompt in the registry is non-empty.
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
- Variables must include webUrl and repoPath
- Failed to build login instructions: ${errMsg}
- Path traversal detected in @include(): ${rawPath}
- Variable interpolation failed: ${errMsg}
- CONFIG_VALIDATION_FAILED
AI-assisted analysis of KeygraphHQ/shannon@1ae0a142f8 (2026-08-12).
Data as JSON: /api/errors/80da301ed7cb45be.
Report an issue: GitHub.