mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-07-25 22:10:54 +02:00
feat: share preflight authenticated session across agents (#345)
* feat(auth): reuse preflight's authenticated session across agents * fix(preflight): verify saved auth state parses and has cookies or origins * fix(prompts): strip shared-session block when no auth is configured * fix(shannon): store shared auth state in the per-session audit dir * fix(prompts): write stub auth-state in pipeline-testing preflight * fix(preflight): clear stale auth-state.json before validate-authentication * fix(preflight): drop auth-state.json on workflow completion * docs(claude): refresh auth-state.json description for new layout and cleanup * refactor(prompts): drop unused PLAYWRIGHT_SESSION resolve in login instructions * style(prompts): collapse verifySavedAuthState signature per biome * refactor(prompts): require AUTH_STATE_FILE on authenticated runs * style(prompts): trim numbered-step comments back to step headers
This commit is contained in:
@@ -28,7 +28,7 @@ const sessionMutex = new SessionMutex();
|
||||
* AuditSession - Main audit system facade
|
||||
*/
|
||||
export class AuditSession {
|
||||
private sessionMetadata: SessionMetadata;
|
||||
readonly sessionMetadata: SessionMetadata;
|
||||
private sessionId: string;
|
||||
private metricsTracker: MetricsTracker;
|
||||
private workflowLogger: WorkflowLogger;
|
||||
|
||||
@@ -74,6 +74,14 @@ export function generateSessionJsonPath(sessionMetadata: SessionMetadata): strin
|
||||
return path.join(auditPath, 'session.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to the shared authenticated browser session saved by the preflight
|
||||
* validator and consumed by downstream agents via `_shared-session.txt`.
|
||||
*/
|
||||
export function authStateFile(sessionMetadata: SessionMetadata): string {
|
||||
return path.join(generateAuditPath(sessionMetadata), 'auth-state.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate path to workflow.log file
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ import { fs, path } from 'zx';
|
||||
import { type ClaudePromptResult, runClaudePrompt, validateAgentOutput } from '../ai/claude-executor.js';
|
||||
import { getOutputFormat, getQueueFilename } from '../ai/queue-schemas.js';
|
||||
import type { AuditSession } from '../audit/index.js';
|
||||
import { authStateFile } from '../audit/utils.js';
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||
import type { AgentName } from '../types/agents.js';
|
||||
@@ -122,7 +123,7 @@ export class AgentExecutionService {
|
||||
try {
|
||||
prompt = await loadPrompt(
|
||||
promptTemplate,
|
||||
{ webUrl, repoPath },
|
||||
{ webUrl, repoPath, AUTH_STATE_FILE: authStateFile(auditSession.sessionMetadata) },
|
||||
distributedConfig,
|
||||
pipelineTestingMode,
|
||||
logger,
|
||||
|
||||
@@ -118,6 +118,7 @@ function renderReportFilterRules(report: ReportConfig | undefined): string {
|
||||
interface PromptVariables {
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
AUTH_STATE_FILE: string;
|
||||
PLAYWRIGHT_SESSION?: string;
|
||||
}
|
||||
|
||||
@@ -321,6 +322,12 @@ async function interpolateVariables(
|
||||
result = result.replace(/<rules_of_engagement>[\s\S]*?<\/rules_of_engagement>\s*/g, '');
|
||||
}
|
||||
|
||||
if (!config?.authentication) {
|
||||
result = result.replace(/<shared_authenticated_session>[\s\S]*?<\/shared_authenticated_session>\s*/g, '');
|
||||
} else {
|
||||
result = result.replace(/{{AUTH_STATE_FILE}}/g, variables.AUTH_STATE_FILE);
|
||||
}
|
||||
|
||||
if (config?.authentication?.login_flow) {
|
||||
const loginInstructions = await buildLoginInstructions(config.authentication, logger, promptsBaseDir);
|
||||
result = result.replace(/{{LOGIN_INSTRUCTIONS}}/g, loginInstructions);
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
* pipeline burns hours on broken auth.
|
||||
*/
|
||||
|
||||
import { readFile, rm } from 'node:fs/promises';
|
||||
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 { authStateFile } from '../audit/utils.js';
|
||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||
import type { AgentEndResult } from '../types/audit.js';
|
||||
import type { DistributedConfig, ProviderConfig } from '../types/config.js';
|
||||
@@ -93,9 +95,12 @@ export async function validateAuthentication(input: ValidateAuthInput): Promise<
|
||||
loginType: authentication.login_type,
|
||||
});
|
||||
|
||||
const stateFile = authStateFile(auditSession.sessionMetadata);
|
||||
await rm(stateFile, { force: true });
|
||||
|
||||
const prompt = await loadPrompt(
|
||||
AGENT_NAME,
|
||||
{ webUrl, repoPath },
|
||||
{ webUrl, repoPath, AUTH_STATE_FILE: stateFile },
|
||||
distributedConfig,
|
||||
pipelineTestingMode ?? false,
|
||||
logger,
|
||||
@@ -120,7 +125,14 @@ export async function validateAuthentication(input: ValidateAuthInput): Promise<
|
||||
providerConfig,
|
||||
);
|
||||
|
||||
const classification = classifyResult(result, authentication);
|
||||
let classification = classifyResult(result, authentication);
|
||||
|
||||
if (classification.ok) {
|
||||
const sessionCheck = await verifySavedAuthState(stateFile, logger);
|
||||
if (!sessionCheck.ok) {
|
||||
classification = sessionCheck;
|
||||
}
|
||||
}
|
||||
|
||||
const endResult: AgentEndResult = {
|
||||
attemptNumber,
|
||||
@@ -135,6 +147,62 @@ export async function validateAuthentication(input: ValidateAuthInput): Promise<
|
||||
return classification;
|
||||
}
|
||||
|
||||
async function verifySavedAuthState(stateFile: string, logger: ActivityLogger): Promise<Result<void, PentestError>> {
|
||||
let contents: string;
|
||||
try {
|
||||
contents = await readFile(stateFile, 'utf8');
|
||||
} catch {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Preflight reported login success but did not save the authenticated session to ${stateFile}.`,
|
||||
'validation',
|
||||
true,
|
||||
{ stateFile },
|
||||
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
} catch (parseErr) {
|
||||
const detail = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
||||
return err(
|
||||
new PentestError(
|
||||
`Preflight saved an authenticated session to ${stateFile}, but the file is not valid JSON: ${detail}`,
|
||||
'validation',
|
||||
true,
|
||||
{ stateFile, parseError: detail },
|
||||
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const cookieCount = countStorageEntries(parsed, 'cookies');
|
||||
const originCount = countStorageEntries(parsed, 'origins');
|
||||
if (cookieCount === 0 && originCount === 0) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Preflight saved an authenticated session to ${stateFile}, but it contains no cookies or origins — the browser was not actually logged in.`,
|
||||
'validation',
|
||||
true,
|
||||
{ stateFile, cookieCount, originCount },
|
||||
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Preflight authenticated session saved', { stateFile, cookieCount, originCount });
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
function countStorageEntries(parsed: unknown, key: 'cookies' | 'origins'): number {
|
||||
if (typeof parsed !== 'object' || parsed === null) return 0;
|
||||
const value = (parsed as Record<string, unknown>)[key];
|
||||
return Array.isArray(value) ? value.length : 0;
|
||||
}
|
||||
|
||||
function classifyResult(
|
||||
result: import('../ai/claude-executor.js').ClaudePromptResult,
|
||||
authentication: NonNullable<DistributedConfig['authentication']>,
|
||||
|
||||
@@ -22,7 +22,7 @@ import { writePlaywrightStealthConfig } from '../ai/playwright-config-writer.js'
|
||||
import { writeUserSettingsForCodePathAvoids } from '../ai/settings-writer.js';
|
||||
import { AuditSession } from '../audit/index.js';
|
||||
import type { ResumeAttempt } from '../audit/metrics-tracker.js';
|
||||
import { generateSessionJsonPath, type SessionMetadata } from '../audit/utils.js';
|
||||
import { authStateFile, generateSessionJsonPath, type SessionMetadata } from '../audit/utils.js';
|
||||
import type { WorkflowSummary } from '../audit/workflow-logger.js';
|
||||
import type { CheckpointContext } from '../interfaces/checkpoint-provider.js';
|
||||
import { DEFAULT_DELIVERABLES_SUBDIR, deliverablesDir } from '../paths.js';
|
||||
@@ -926,7 +926,15 @@ export async function logWorkflowComplete(input: ActivityInput, summary: Workflo
|
||||
// 5. Write completion entry to workflow.log
|
||||
await auditSession.logWorkflowComplete(cumulativeSummary);
|
||||
|
||||
// 6. Clean up container
|
||||
// 6. Drop the authenticated browser session
|
||||
try {
|
||||
await fs.rm(authStateFile(sessionMetadata), { force: true });
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Failed to clean up auth-state.json: ${detail}`);
|
||||
}
|
||||
|
||||
// 7. Clean up container
|
||||
removeContainer(workflowId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user