feat: extract pipeline core for library consumption (#282)

* feat: extract pipeline core for library consumption

* fix: chmod workspace directory for container write access

* fix: resolve playwright output dir relative to deliverables parent

* feat: add multi-provider LLM support via ProviderConfig

* fix: resolve model overrides via options.model, remove unused model env passthrough

* fix: use ANTHROPIC_AUTH_TOKEN for custom base URL and router auth

* fix: skip env-based credential validation when providerConfig is present

* fix: support large UID/GID values for AD/LDAP users in container
This commit is contained in:
ezl-keygraph
2026-04-10 04:53:36 +05:30
committed by GitHub
parent f6fd1edad6
commit 1f6dfd7e17
32 changed files with 616 additions and 106 deletions
+40 -21
View File
@@ -39,7 +39,7 @@ function isLoopbackAddress(address: string): boolean {
// === Repository Validation ===
async function validateRepo(repoPath: string, logger: ActivityLogger): Promise<Result<void, PentestError>> {
async function validateRepo(repoPath: string, logger: ActivityLogger, skipGitCheck?: boolean): Promise<Result<void, PentestError>> {
logger.info('Checking repository path...', { repoPath });
// 1. Check repo directory exists
@@ -68,10 +68,22 @@ async function validateRepo(repoPath: string, logger: ActivityLogger): Promise<R
);
}
// 2. Check .git directory exists
try {
const gitStats = await fs.stat(`${repoPath}/.git`);
if (!gitStats.isDirectory()) {
// 2. Check .git directory exists (skipped when consumer removes .git after clone)
if (!skipGitCheck) {
try {
const gitStats = await fs.stat(`${repoPath}/.git`);
if (!gitStats.isDirectory()) {
return err(
new PentestError(
`Not a git repository (no .git directory): ${repoPath}`,
'config',
false,
{ repoPath },
ErrorCode.REPO_NOT_FOUND,
),
);
}
} catch {
return err(
new PentestError(
`Not a git repository (no .git directory): ${repoPath}`,
@@ -82,16 +94,8 @@ async function validateRepo(repoPath: string, logger: ActivityLogger): Promise<R
),
);
}
} catch {
return err(
new PentestError(
`Not a git repository (no .git directory): ${repoPath}`,
'config',
false,
{ repoPath },
ErrorCode.REPO_NOT_FOUND,
),
);
} else {
logger.info('Skipping .git check (skipGitCheck enabled)');
}
logger.info('Repository path OK');
@@ -180,9 +184,21 @@ function classifySdkError(sdkError: SDKAssistantMessageError, authType: string):
}
/** Validate credentials via a minimal Claude Agent SDK query. */
async function validateCredentials(logger: ActivityLogger): Promise<Result<void, PentestError>> {
async function validateCredentials(logger: ActivityLogger, apiKey?: string, providerConfig?: import('../types/config.js').ProviderConfig): Promise<Result<void, PentestError>> {
// 0. If providerConfig is present, credentials are managed by the caller.
// The executor will map providerConfig directly to sdkEnv — no process.env needed.
if (providerConfig) {
logger.info(`Provider config present (type: ${providerConfig.providerType || 'anthropic_api'}) — skipping env-based credential validation`);
return ok(undefined);
}
// 0b. If apiKey provided via config, set it in env for SDK validation
// This avoids requiring process.env.ANTHROPIC_API_KEY when key is threaded via input
if (apiKey) {
process.env.ANTHROPIC_API_KEY = apiKey;
}
// 1. Custom base URL — validate endpoint is reachable via SDK query
if (process.env.ANTHROPIC_BASE_URL) {
if (process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN) {
const baseUrl = process.env.ANTHROPIC_BASE_URL;
logger.info(`Validating custom base URL: ${baseUrl}`);
@@ -289,7 +305,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
}
// 4. Check that at least one credential is present
if (!process.env.ANTHROPIC_API_KEY && !process.env.CLAUDE_CODE_OAUTH_TOKEN) {
if (!process.env.ANTHROPIC_API_KEY && !process.env.CLAUDE_CODE_OAUTH_TOKEN && !process.env.ANTHROPIC_AUTH_TOKEN) {
return err(
new PentestError(
'No API credentials found. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env (or use CLAUDE_CODE_USE_BEDROCK=1 for AWS Bedrock, or CLAUDE_CODE_USE_VERTEX=1 for Google Vertex AI)',
@@ -457,9 +473,12 @@ export async function runPreflightChecks(
repoPath: string,
configPath: string | undefined,
logger: ActivityLogger,
skipGitCheck?: boolean,
apiKey?: string,
providerConfig?: import('../types/config.js').ProviderConfig,
): Promise<Result<void, PentestError>> {
// 1. Repository check (free — filesystem only)
const repoResult = await validateRepo(repoPath, logger);
const repoResult = await validateRepo(repoPath, logger, skipGitCheck);
if (!repoResult.ok) {
return repoResult;
}
@@ -472,8 +491,8 @@ export async function runPreflightChecks(
}
}
// 3. Credential check (cheap — 1 SDK round-trip)
const credResult = await validateCredentials(logger);
// 3. Credential check (cheap — 1 SDK round-trip, skipped when providerConfig present)
const credResult = await validateCredentials(logger, apiKey, providerConfig);
if (!credResult.ok) {
return credResult;
}