feat: backport auth-validation preflight + email_login credentials
Backport upstream Shannon PR #335: - Add credential validation activity that drives a real browser login before the full pipeline, catching bad credentials early - New email_login credentials type for magic-link and email-OTP flows - Make credentials.password optional for passwordless flows - Playwright stealth config (chrome.runtime, plugin simulation, UA) - Centralize prompt directory resolution into resolvePromptDir helper - New AUTH_LOGIN_FAILED error code with non-retryable classification - Remove dangerous-pattern validation on credentials.password - Pipeline-testing stub for auth validation (returns success) - Auth validation timeout of 10 minutes for browser-based login - .playwright directory workspace overlay for CLI/Docker Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -138,6 +138,9 @@ function classifyByErrorCode(code: ErrorCode, retryableFromError: boolean): { ty
|
||||
case ErrorCode.AUTH_FAILED:
|
||||
return { type: 'AuthenticationError', retryable: false };
|
||||
|
||||
case ErrorCode.AUTH_LOGIN_FAILED:
|
||||
return { type: 'AuthLoginFailedError', retryable: false };
|
||||
|
||||
case ErrorCode.BILLING_ERROR:
|
||||
return { type: 'BillingError', retryable: true };
|
||||
|
||||
|
||||
@@ -76,6 +76,17 @@ async function buildLoginInstructions(
|
||||
`generated TOTP code using secret "${authentication.credentials.totp_secret}"`,
|
||||
);
|
||||
}
|
||||
if (authentication.credentials.email_login) {
|
||||
const emailLogin = authentication.credentials.email_login;
|
||||
userInstructions = userInstructions.replace(/\$email_address/g, emailLogin.address);
|
||||
userInstructions = userInstructions.replace(/\$email_password/g, emailLogin.password);
|
||||
if (emailLogin.totp_secret) {
|
||||
userInstructions = userInstructions.replace(
|
||||
/\$email_totp/g,
|
||||
`generated TOTP code using secret "${emailLogin.totp_secret}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loginInstructions = loginInstructions.replace(/{{user_instructions}}/g, userInstructions);
|
||||
@@ -221,6 +232,16 @@ async function interpolateVariables(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a prompt directory override to an absolute path.
|
||||
* Falls back to the compiled-in PROMPTS_DIR when no override is given.
|
||||
*/
|
||||
export 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,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Auth-validation preflight service.
|
||||
*
|
||||
* Drives a real browser login before the full pipeline runs,
|
||||
* catching bad credentials early and saving API budget.
|
||||
*/
|
||||
|
||||
import type { JsonSchemaOutputFormat } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { z } from 'zod';
|
||||
import { runClaudePrompt } from '../ai/claude-executor.js';
|
||||
import type { AuditSession } from '../audit/index.js';
|
||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||
import type { DistributedConfig, ProviderConfig } from '../types/config.js';
|
||||
import { ErrorCode } from '../types/errors.js';
|
||||
import type { Result } from '../types/result.js';
|
||||
import { err, ok } from '../types/result.js';
|
||||
import { PentestError } from './error-handling.js';
|
||||
import { loadPrompt } from './prompt-manager.js';
|
||||
|
||||
type FailurePoint = 'username_or_password' | 'totp_secret' | 'out_of_band';
|
||||
|
||||
const AuthValidationSchema = z.object({
|
||||
login_success: z.boolean(),
|
||||
failure_point: z.enum(['username_or_password', 'totp_secret', 'out_of_band']).optional(),
|
||||
failure_detail: z.string().max(250).optional(),
|
||||
});
|
||||
|
||||
const AUTH_VALIDATION_OUTPUT_FORMAT: JsonSchemaOutputFormat = {
|
||||
type: 'json_schema',
|
||||
schema: z.toJSONSchema(AuthValidationSchema, { target: 'draft-07' }) as Record<string, unknown>,
|
||||
};
|
||||
|
||||
export interface AuthValidationInput {
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
config: DistributedConfig;
|
||||
pipelineTestingMode: boolean;
|
||||
auditSession: AuditSession;
|
||||
logger: ActivityLogger;
|
||||
promptDir?: string;
|
||||
apiKey?: string;
|
||||
providerConfig?: ProviderConfig;
|
||||
}
|
||||
|
||||
function classifyResult(parsed: z.infer<typeof AuthValidationSchema>): Result<void, PentestError> {
|
||||
if (parsed.login_success) {
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
const failurePoint: FailurePoint = parsed.failure_point ?? 'username_or_password';
|
||||
const detail = parsed.failure_detail ?? 'Login failed';
|
||||
|
||||
return err(
|
||||
new PentestError(
|
||||
`Authentication validation failed at "${failurePoint}": ${detail}`,
|
||||
'config',
|
||||
false,
|
||||
{ failurePoint, failureDetail: detail },
|
||||
ErrorCode.AUTH_LOGIN_FAILED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateAuthentication(input: AuthValidationInput): Promise<Result<void, PentestError>> {
|
||||
const { webUrl, repoPath, config, pipelineTestingMode, auditSession, logger, promptDir, apiKey, providerConfig } =
|
||||
input;
|
||||
|
||||
// 1. Load the validation prompt
|
||||
const prompt = await loadPrompt(
|
||||
'validate-authentication',
|
||||
{ webUrl, repoPath },
|
||||
config,
|
||||
pipelineTestingMode,
|
||||
logger,
|
||||
promptDir,
|
||||
);
|
||||
|
||||
// 2. Run the agent with structured output
|
||||
const result = await runClaudePrompt(
|
||||
prompt,
|
||||
repoPath,
|
||||
'',
|
||||
'Auth validation',
|
||||
'validate-authentication',
|
||||
auditSession,
|
||||
logger,
|
||||
'medium',
|
||||
AUTH_VALIDATION_OUTPUT_FORMAT,
|
||||
apiKey,
|
||||
undefined,
|
||||
providerConfig,
|
||||
);
|
||||
|
||||
// 3. Parse structured output
|
||||
if (!result.success || !result.structuredOutput) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Auth validation agent did not return a structured verdict: ${result.error ?? 'unknown error'}`,
|
||||
'validation',
|
||||
true,
|
||||
{ agentError: result.error },
|
||||
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const parseResult = AuthValidationSchema.safeParse(result.structuredOutput);
|
||||
if (!parseResult.success) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Auth validation output failed schema validation: ${parseResult.error.message}`,
|
||||
'validation',
|
||||
true,
|
||||
{ zodErrors: parseResult.error.issues },
|
||||
ErrorCode.OUTPUT_VALIDATION_FAILED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Classify the verdict
|
||||
return classifyResult(parseResult.data);
|
||||
}
|
||||
Reference in New Issue
Block a user