KeygraphHQ/shannon · error · PentestError
Failed to build login instructions: ${errMsg}
Error message
Failed to build login instructions: ${errMsg} What it means
Catch-all wrapper in buildLoginInstructions for any non-PentestError exception raised while assembling login instructions (e.g. fs.readFile EACCES/ENOENT after the pathExists check, a replaceLiteral regex failure, or a malformed credentials object). It re-throws PentestError instances unchanged and wraps everything else with category 'config', non-retryable, carrying the original error message and the full authentication config object in context.
Source
Thrown at apps/worker/src/services/prompt-manager.ts:243
`generated TOTP code using secret "${authentication.credentials.email_login.totp_secret}"`,
);
}
}
loginInstructions = replaceLiteral(loginInstructions, /{{user_instructions}}/g, userInstructions);
// 5. Replace TOTP secret placeholder if present in template
if (authentication.credentials?.totp_secret) {
loginInstructions = replaceLiteral(loginInstructions, /{{totp_secret}}/g, authentication.credentials.totp_secret);
}
return loginInstructions;
} catch (error) {
if (error instanceof PentestError) {
throw error;
}
const errMsg = error instanceof Error ? error.message : String(error);
throw new PentestError(`Failed to build login instructions: ${errMsg}`, 'config', false, {
authentication,
originalError: errMsg,
});
}
}
// Pure function: Process @include() directives
async function processIncludes(content: string, baseDir: string): Promise<string> {
const includeRegex = /@include\(([^)]+)\)/g;
const resolvedBase = path.resolve(baseDir);
const replacements: IncludeReplacement[] = await Promise.all(
Array.from(content.matchAll(includeRegex)).map(async (match) => {
const rawPath = match[1] ?? '';
const includePath = path.resolve(baseDir, rawPath);
if (!includePath.startsWith(resolvedBase + path.sep) && includePath !== resolvedBase) {
throw new PentestError(`Path traversal detected in @include(): ${rawPath}`, 'prompt', false, {
includePath,View on GitHub (pinned to 1ae0a142f8)
Solutions
- Read the context.originalError and context.authentication fields from the thrown PentestError to identify the underlying cause.
- Validate the authentication.credentials object shape against the config schema (apps/worker/configs/ config-schema.json) before starting the scan.
- Confirm the shared/login-instructions.txt file is readable (not just present) inside the worker container.
- Re-run after correcting the malformed config field named in originalError.
Example fix
// before: credentials.username is a number, replaceLiteral receives non-string // authentication: // credentials: // username: 1234 // after: supply strings as the schema requires // authentication: // credentials: // username: "1234"
Defensive patterns
Strategy: try-catch
Validate before calling
// Validate the credentials object shape before the scan starts
function validCredentials(c: any): boolean {
if (!c) return true; // credentials optional
return ['username', 'password', 'totp_secret'].every((k) => c[k] === undefined || typeof c[k] === 'string') &&
(c.email_login === undefined || (typeof c.email_login?.address === 'string' || c.email_login === undefined));
}
if (config.authentication && !validCredentials(config.authentication.credentials)) {
throw new Error('credentials fields must be strings');
} Type guard
function isStringRecord(o: unknown, keys: readonly string[]): boolean {
return typeof o === 'object' && o !== null && keys.every((k) => k in o ? typeof (o as any)[k] === 'string' : true);
} Try / catch
try {
await loadPrompt(name, vars, config, false, logger);
} catch (e) {
if (e instanceof PentestError && /Failed to build login instructions/.test(e.message)) {
const original = (e.context as any)?.originalError;
log.error('login build failed', { original, auth: (e.context as any)?.authentication });
}
throw e;
} Prevention
- Always supply credential fields as strings per the schema; never numbers or objects.
- Run the YAML config through the JSON-schema validator (config-parser) before the scan.
- Treat context.originalError as the source of truth when this wrapper fires.
- Keep the login template readable (not just present) throughout the run.
When it happens
Trigger: An authentication config passes the template-exists check at line 162 but a later operation throws a plain Error: the template file becomes unreadable between the check and read (race), a credentials field has an unexpected type that breaks replaceLiteral, or the login_flow array contains a non-string. The outer catch converts it into this PentestError.
Common situations: Permissions on the prompts dir change mid-run. A hand-edited YAML config supplies credentials with wrong shape (e.g. username as a number) that survives schema validation but breaks string substitution. Concurrent workspace migration moving the template file.
Related errors
- Variable interpolation failed: ${errMsg}
- Login instructions template not found
- CONFIG_VALIDATION_FAILED
- 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/9009f129d8950f8f.
Report an issue: GitHub.