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

  1. Read the context.originalError and context.authentication fields from the thrown PentestError to identify the underlying cause.
  2. Validate the authentication.credentials object shape against the config schema (apps/worker/configs/ config-schema.json) before starting the scan.
  3. Confirm the shared/login-instructions.txt file is readable (not just present) inside the worker container.
  4. 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

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


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