outcome-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. package/src/utils/output-parsers.ts +216 -0
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Shadow Session Orchestrator
3
+ *
4
+ * Manages virtualized browser environments with persistent authentication.
5
+ * Each Shadow Session is isolated, fingerprinted, and maintains its own state.
6
+ *
7
+ * Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.5
8
+ */
9
+
10
+ import { randomBytes } from 'crypto';
11
+ import type {
12
+ ShadowSession,
13
+ DeviceFingerprint,
14
+ SessionConfig,
15
+ } from '../core/types.js';
16
+ import type { SessionVault, RawSessionData } from '../auth/session-vault.js';
17
+
18
+ // =============================================================================
19
+ // Types
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Browser context state for isolation.
24
+ */
25
+ export interface BrowserContextState {
26
+ /** Unique context ID */
27
+ contextId: string;
28
+ /** Cookies stored in this context */
29
+ cookies: CookieData[];
30
+ /** localStorage data */
31
+ localStorage: Record<string, string>;
32
+ /** sessionStorage data */
33
+ sessionStorage: Record<string, string>;
34
+ /** Headers to use for requests */
35
+ headers: Record<string, string>;
36
+ /** Last activity timestamp */
37
+ lastActivity: number;
38
+ }
39
+
40
+ /**
41
+ * Cookie data structure.
42
+ */
43
+ export interface CookieData {
44
+ name: string;
45
+ value: string;
46
+ domain: string;
47
+ path: string;
48
+ expires?: number;
49
+ httpOnly?: boolean;
50
+ secure?: boolean;
51
+ sameSite?: 'Strict' | 'Lax' | 'None';
52
+ }
53
+
54
+ /**
55
+ * Shadow Session with internal state.
56
+ */
57
+ export interface ShadowSessionInternal extends ShadowSession {
58
+ /** Browser context state */
59
+ contextState: BrowserContextState;
60
+ /** Agent ID this session belongs to */
61
+ agentId?: string;
62
+ /** Whether session is currently locked for use */
63
+ locked: boolean;
64
+ }
65
+
66
+ /**
67
+ * Session creation result.
68
+ */
69
+ export interface SessionCreationResult {
70
+ success: boolean;
71
+ session?: ShadowSession;
72
+ error?: string;
73
+ }
74
+
75
+ /**
76
+ * Session resumption result.
77
+ */
78
+ export interface SessionResumptionResult {
79
+ success: boolean;
80
+ session?: ShadowSession;
81
+ error?: string;
82
+ reAuthRequired?: boolean;
83
+ }
84
+
85
+ /**
86
+ * Heartbeat result.
87
+ */
88
+ export interface HeartbeatResult {
89
+ success: boolean;
90
+ sessionActive: boolean;
91
+ nextHeartbeatMs: number;
92
+ error?: string;
93
+ }
94
+
95
+ // =============================================================================
96
+ // Fingerprint Generation
97
+ // =============================================================================
98
+
99
+ /**
100
+ * Common user agents for realistic fingerprinting.
101
+ */
102
+ const USER_AGENTS = [
103
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
104
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
105
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
106
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
107
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
108
+ ];
109
+
110
+ /**
111
+ * Common screen resolutions.
112
+ */
113
+ const RESOLUTIONS = [
114
+ { width: 1920, height: 1080 },
115
+ { width: 1366, height: 768 },
116
+ { width: 1536, height: 864 },
117
+ { width: 1440, height: 900 },
118
+ { width: 2560, height: 1440 },
119
+ { width: 1280, height: 720 },
120
+ ];
121
+
122
+ /**
123
+ * Common fonts for fingerprinting.
124
+ */
125
+ const FONT_SETS = [
126
+ ['Arial', 'Helvetica', 'Times New Roman', 'Georgia', 'Verdana', 'Courier New'],
127
+ ['Arial', 'Helvetica Neue', 'Segoe UI', 'Roboto', 'Open Sans', 'Lato'],
128
+ ['San Francisco', 'Helvetica', 'Arial', 'Lucida Grande', 'Geneva'],
129
+ ];
130
+
131
+ /**
132
+ * Common timezones.
133
+ */
134
+ const TIMEZONES = [
135
+ 'America/New_York',
136
+ 'America/Los_Angeles',
137
+ 'America/Chicago',
138
+ 'Europe/London',
139
+ 'Europe/Paris',
140
+ 'Asia/Tokyo',
141
+ 'Asia/Shanghai',
142
+ ];
143
+
144
+ /**
145
+ * Common languages.
146
+ */
147
+ const LANGUAGES = ['en-US', 'en-GB', 'en', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP', 'zh-CN'];
148
+
149
+ /**
150
+ * Generate a unique device fingerprint.
151
+ * Creates realistic browser fingerprints to avoid bot detection.
152
+ *
153
+ * Requirements: 4.1, 4.2
154
+ */
155
+ export function generateFingerprint(seed?: string): DeviceFingerprint {
156
+ // Use seed for deterministic generation if provided
157
+ const random = seed
158
+ ? createSeededRandom(seed)
159
+ : () => Math.random();
160
+
161
+ const resolution = RESOLUTIONS[Math.floor(random() * RESOLUTIONS.length)];
162
+ const userAgent = USER_AGENTS[Math.floor(random() * USER_AGENTS.length)];
163
+ const fonts = FONT_SETS[Math.floor(random() * FONT_SETS.length)];
164
+ const timezone = TIMEZONES[Math.floor(random() * TIMEZONES.length)];
165
+ const language = LANGUAGES[Math.floor(random() * LANGUAGES.length)];
166
+
167
+ // Generate a unique GPU signature
168
+ const gpuVendors = ['NVIDIA', 'AMD', 'Intel'];
169
+ const gpuModels = ['GeForce RTX 3080', 'Radeon RX 6800', 'UHD Graphics 630', 'GeForce GTX 1660'];
170
+ const gpuVendor = gpuVendors[Math.floor(random() * gpuVendors.length)];
171
+ const gpuModel = gpuModels[Math.floor(random() * gpuModels.length)];
172
+ const gpuSignature = `${gpuVendor} ${gpuModel}`;
173
+
174
+ return {
175
+ resolution,
176
+ userAgent,
177
+ fonts,
178
+ gpuSignature,
179
+ timezone,
180
+ language,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Create a seeded random number generator for deterministic fingerprints.
186
+ */
187
+ function createSeededRandom(seed: string): () => number {
188
+ let hash = 0;
189
+ for (let i = 0; i < seed.length; i++) {
190
+ const char = seed.charCodeAt(i);
191
+ hash = ((hash << 5) - hash) + char;
192
+ hash = hash & hash;
193
+ }
194
+
195
+ return () => {
196
+ hash = Math.sin(hash) * 10000;
197
+ return hash - Math.floor(hash);
198
+ };
199
+ }
200
+
201
+ // =============================================================================
202
+ // Shadow Session Orchestrator
203
+ // =============================================================================
204
+
205
+ /**
206
+ * Configuration for the Shadow Session Orchestrator.
207
+ */
208
+ export interface ShadowSessionOrchestratorConfig {
209
+ /** Session Vault for credential storage */
210
+ vault?: SessionVault;
211
+ /** Heartbeat interval in milliseconds (default: 30000) */
212
+ heartbeatIntervalMs?: number;
213
+ /** Session timeout in milliseconds (default: 3600000 = 1 hour) */
214
+ sessionTimeoutMs?: number;
215
+ /** Maximum concurrent sessions (default: 100) */
216
+ maxConcurrentSessions?: number;
217
+ }
218
+
219
+ /**
220
+ * Shadow Session Orchestrator
221
+ *
222
+ * Manages virtualized browser environments with:
223
+ * - Unique device fingerprints per session
224
+ * - Per-agent isolation (no cross-contamination)
225
+ * - Persistent authentication via Session Vault
226
+ * - Automatic heartbeat and session maintenance
227
+ *
228
+ * Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.5
229
+ */
230
+ export class ShadowSessionOrchestrator {
231
+ private readonly vault?: SessionVault;
232
+ private readonly heartbeatIntervalMs: number;
233
+ private readonly sessionTimeoutMs: number;
234
+ private readonly maxConcurrentSessions: number;
235
+
236
+ /** Active sessions by ID */
237
+ private sessions: Map<string, ShadowSessionInternal> = new Map();
238
+
239
+ /** Sessions by agent ID for isolation tracking */
240
+ private sessionsByAgent: Map<string, Set<string>> = new Map();
241
+
242
+ /** Heartbeat timers by session ID */
243
+ private heartbeatTimers: Map<string, NodeJS.Timeout> = new Map();
244
+
245
+ constructor(config: ShadowSessionOrchestratorConfig = {}) {
246
+ this.vault = config.vault;
247
+ this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? 30000;
248
+ this.sessionTimeoutMs = config.sessionTimeoutMs ?? 3600000;
249
+ this.maxConcurrentSessions = config.maxConcurrentSessions ?? 100;
250
+ }
251
+
252
+ // ===========================================================================
253
+ // Session Creation
254
+ // ===========================================================================
255
+
256
+ /**
257
+ * Generate a unique session ID.
258
+ */
259
+ private generateSessionId(): string {
260
+ return `shadow_${randomBytes(16).toString('hex')}`;
261
+ }
262
+
263
+ /**
264
+ * Generate a unique context ID for browser isolation.
265
+ */
266
+ private generateContextId(): string {
267
+ return `ctx_${randomBytes(12).toString('hex')}`;
268
+ }
269
+
270
+ /**
271
+ * Create a new Shadow Session with unique fingerprint.
272
+ *
273
+ * Requirements: 4.1, 4.2
274
+ */
275
+ async create(config: SessionConfig, agentId?: string): Promise<SessionCreationResult> {
276
+ // Check concurrent session limit
277
+ if (this.sessions.size >= this.maxConcurrentSessions) {
278
+ return {
279
+ success: false,
280
+ error: `Maximum concurrent sessions (${this.maxConcurrentSessions}) reached`,
281
+ };
282
+ }
283
+
284
+ const sessionId = this.generateSessionId();
285
+ const contextId = this.generateContextId();
286
+ const now = Date.now();
287
+
288
+ // Use provided fingerprint or generate a new one
289
+ const fingerprint = config.fingerprint ?? generateFingerprint(sessionId);
290
+
291
+ // Create isolated browser context state
292
+ const contextState: BrowserContextState = {
293
+ contextId,
294
+ cookies: [],
295
+ localStorage: {},
296
+ sessionStorage: {},
297
+ headers: this.generateHeaders(fingerprint),
298
+ lastActivity: now,
299
+ };
300
+
301
+ // Create the session
302
+ const session: ShadowSessionInternal = {
303
+ id: sessionId,
304
+ domain: config.targetDomain,
305
+ status: 'active',
306
+ createdAt: now,
307
+ lastHeartbeat: now,
308
+ fingerprint,
309
+ contextState,
310
+ agentId,
311
+ locked: false,
312
+ };
313
+
314
+ // Store the session
315
+ this.sessions.set(sessionId, session);
316
+
317
+ // Track by agent ID for isolation
318
+ if (agentId) {
319
+ if (!this.sessionsByAgent.has(agentId)) {
320
+ this.sessionsByAgent.set(agentId, new Set());
321
+ }
322
+ this.sessionsByAgent.get(agentId)!.add(sessionId);
323
+ }
324
+
325
+ // Start heartbeat timer
326
+ this.startHeartbeat(sessionId);
327
+
328
+ // Return public session (without internal state)
329
+ return {
330
+ success: true,
331
+ session: this.toPublicSession(session),
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Generate realistic HTTP headers based on fingerprint.
337
+ */
338
+ private generateHeaders(fingerprint: DeviceFingerprint): Record<string, string> {
339
+ return {
340
+ 'User-Agent': fingerprint.userAgent,
341
+ 'Accept-Language': `${fingerprint.language},en;q=0.9`,
342
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
343
+ 'Accept-Encoding': 'gzip, deflate, br',
344
+ 'Connection': 'keep-alive',
345
+ 'Upgrade-Insecure-Requests': '1',
346
+ 'Sec-Fetch-Dest': 'document',
347
+ 'Sec-Fetch-Mode': 'navigate',
348
+ 'Sec-Fetch-Site': 'none',
349
+ 'Sec-Fetch-User': '?1',
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Convert internal session to public session (hide internal state).
355
+ */
356
+ private toPublicSession(internal: ShadowSessionInternal): ShadowSession {
357
+ return {
358
+ id: internal.id,
359
+ domain: internal.domain,
360
+ status: internal.status,
361
+ createdAt: internal.createdAt,
362
+ lastHeartbeat: internal.lastHeartbeat,
363
+ fingerprint: internal.fingerprint,
364
+ };
365
+ }
366
+
367
+ // ===========================================================================
368
+ // Session Heartbeat and Persistence
369
+ // ===========================================================================
370
+
371
+ /**
372
+ * Start heartbeat timer for a session.
373
+ *
374
+ * Requirements: 4.3
375
+ */
376
+ private startHeartbeat(sessionId: string): void {
377
+ // Clear existing timer if any
378
+ this.stopHeartbeat(sessionId);
379
+
380
+ const timer = setInterval(() => {
381
+ this.performHeartbeat(sessionId);
382
+ }, this.heartbeatIntervalMs);
383
+
384
+ this.heartbeatTimers.set(sessionId, timer);
385
+ }
386
+
387
+ /**
388
+ * Stop heartbeat timer for a session.
389
+ */
390
+ private stopHeartbeat(sessionId: string): void {
391
+ const timer = this.heartbeatTimers.get(sessionId);
392
+ if (timer) {
393
+ clearInterval(timer);
394
+ this.heartbeatTimers.delete(sessionId);
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Perform heartbeat for a session.
400
+ * Refreshes cookies and rotates headers to maintain session.
401
+ *
402
+ * Requirements: 4.3
403
+ */
404
+ private async performHeartbeat(sessionId: string): Promise<void> {
405
+ const session = this.sessions.get(sessionId);
406
+ if (!session) {
407
+ this.stopHeartbeat(sessionId);
408
+ return;
409
+ }
410
+
411
+ const now = Date.now();
412
+
413
+ // Check if session has timed out
414
+ if (now - session.lastHeartbeat > this.sessionTimeoutMs) {
415
+ session.status = 'expired';
416
+ this.stopHeartbeat(sessionId);
417
+ return;
418
+ }
419
+
420
+ // Update heartbeat timestamp
421
+ session.lastHeartbeat = now;
422
+ session.contextState.lastActivity = now;
423
+
424
+ // Rotate some headers to appear more human-like
425
+ this.rotateHeaders(session);
426
+ }
427
+
428
+ /**
429
+ * Rotate headers to maintain realistic behavior.
430
+ */
431
+ private rotateHeaders(session: ShadowSessionInternal): void {
432
+ // Slightly vary Accept-Language quality values
433
+ const lang = session.fingerprint.language;
434
+ const quality = (0.8 + Math.random() * 0.2).toFixed(1);
435
+ session.contextState.headers['Accept-Language'] = `${lang},en;q=${quality}`;
436
+ }
437
+
438
+ /**
439
+ * Manually trigger heartbeat for a session.
440
+ *
441
+ * Requirements: 4.3
442
+ */
443
+ async heartbeat(sessionId: string): Promise<HeartbeatResult> {
444
+ const session = this.sessions.get(sessionId);
445
+
446
+ if (!session) {
447
+ return {
448
+ success: false,
449
+ sessionActive: false,
450
+ nextHeartbeatMs: 0,
451
+ error: 'Session not found',
452
+ };
453
+ }
454
+
455
+ if (session.status === 'expired') {
456
+ return {
457
+ success: false,
458
+ sessionActive: false,
459
+ nextHeartbeatMs: 0,
460
+ error: 'Session expired',
461
+ };
462
+ }
463
+
464
+ await this.performHeartbeat(sessionId);
465
+
466
+ return {
467
+ success: true,
468
+ sessionActive: session.status === 'active',
469
+ nextHeartbeatMs: this.heartbeatIntervalMs,
470
+ };
471
+ }
472
+
473
+ // ===========================================================================
474
+ // Session Isolation
475
+ // ===========================================================================
476
+
477
+ /**
478
+ * Get a session by ID.
479
+ */
480
+ getSession(sessionId: string): ShadowSession | null {
481
+ const session = this.sessions.get(sessionId);
482
+ return session ? this.toPublicSession(session) : null;
483
+ }
484
+
485
+ /**
486
+ * Get internal session (for testing).
487
+ */
488
+ getInternalSession(sessionId: string): ShadowSessionInternal | null {
489
+ return this.sessions.get(sessionId) ?? null;
490
+ }
491
+
492
+ /**
493
+ * Get all sessions for an agent.
494
+ *
495
+ * Requirements: 4.4
496
+ */
497
+ getSessionsForAgent(agentId: string): ShadowSession[] {
498
+ const sessionIds = this.sessionsByAgent.get(agentId);
499
+ if (!sessionIds) {
500
+ return [];
501
+ }
502
+
503
+ return Array.from(sessionIds)
504
+ .map((id) => this.sessions.get(id))
505
+ .filter((s): s is ShadowSessionInternal => s !== undefined)
506
+ .map((s) => this.toPublicSession(s));
507
+ }
508
+
509
+ /**
510
+ * Check if two sessions are isolated (no shared state).
511
+ *
512
+ * Requirements: 4.4
513
+ */
514
+ areSessionsIsolated(sessionId1: string, sessionId2: string): boolean {
515
+ const session1 = this.sessions.get(sessionId1);
516
+ const session2 = this.sessions.get(sessionId2);
517
+
518
+ if (!session1 || !session2) {
519
+ return true; // Non-existent sessions are trivially isolated
520
+ }
521
+
522
+ // Sessions are isolated if they have different context IDs
523
+ return session1.contextState.contextId !== session2.contextState.contextId;
524
+ }
525
+
526
+ /**
527
+ * Modify session state (for testing isolation).
528
+ */
529
+ modifySessionState(
530
+ sessionId: string,
531
+ key: string,
532
+ value: string,
533
+ storage: 'localStorage' | 'sessionStorage' = 'localStorage'
534
+ ): boolean {
535
+ const session = this.sessions.get(sessionId);
536
+ if (!session) {
537
+ return false;
538
+ }
539
+
540
+ if (storage === 'localStorage') {
541
+ session.contextState.localStorage[key] = value;
542
+ } else {
543
+ session.contextState.sessionStorage[key] = value;
544
+ }
545
+
546
+ return true;
547
+ }
548
+
549
+ /**
550
+ * Get session state value (for testing isolation).
551
+ */
552
+ getSessionStateValue(
553
+ sessionId: string,
554
+ key: string,
555
+ storage: 'localStorage' | 'sessionStorage' = 'localStorage'
556
+ ): string | undefined {
557
+ const session = this.sessions.get(sessionId);
558
+ if (!session) {
559
+ return undefined;
560
+ }
561
+
562
+ if (storage === 'localStorage') {
563
+ return session.contextState.localStorage[key];
564
+ } else {
565
+ return session.contextState.sessionStorage[key];
566
+ }
567
+ }
568
+
569
+ // ===========================================================================
570
+ // Session Resumption from Vault
571
+ // ===========================================================================
572
+
573
+ /**
574
+ * Resume a session from the vault without re-authentication.
575
+ *
576
+ * Requirements: 5.5
577
+ */
578
+ async resume(domain: string, agentId?: string): Promise<SessionResumptionResult> {
579
+ if (!this.vault) {
580
+ return {
581
+ success: false,
582
+ error: 'No vault configured for session resumption',
583
+ reAuthRequired: true,
584
+ };
585
+ }
586
+
587
+ // Try to retrieve session from vault
588
+ const vaultedSession = await this.vault.retrieve(domain);
589
+
590
+ if (!vaultedSession) {
591
+ return {
592
+ success: false,
593
+ error: 'No vaulted session found for domain',
594
+ reAuthRequired: true,
595
+ };
596
+ }
597
+
598
+ // Check if session is expired
599
+ if (vaultedSession.expiresAt < Date.now()) {
600
+ return {
601
+ success: false,
602
+ error: 'Vaulted session has expired',
603
+ reAuthRequired: true,
604
+ };
605
+ }
606
+
607
+ // Decrypt session data
608
+ const rawData = await this.vault.retrieveDecrypted(domain);
609
+ if (!rawData) {
610
+ return {
611
+ success: false,
612
+ error: 'Failed to decrypt vaulted session',
613
+ reAuthRequired: true,
614
+ };
615
+ }
616
+
617
+ // Create a new session with the vaulted state
618
+ const fingerprint = generateFingerprint();
619
+ const config: SessionConfig = {
620
+ targetDomain: domain,
621
+ fingerprint,
622
+ isolationLevel: 'strict',
623
+ };
624
+
625
+ const createResult = await this.create(config, agentId);
626
+ if (!createResult.success || !createResult.session) {
627
+ return {
628
+ success: false,
629
+ error: createResult.error ?? 'Failed to create session',
630
+ reAuthRequired: true,
631
+ };
632
+ }
633
+
634
+ // Restore the vaulted state to the new session
635
+ const session = this.sessions.get(createResult.session.id);
636
+ if (session) {
637
+ // Parse and restore cookies
638
+ try {
639
+ session.contextState.cookies = JSON.parse(rawData.cookies);
640
+ } catch {
641
+ session.contextState.cookies = [];
642
+ }
643
+
644
+ // Parse and restore localStorage
645
+ try {
646
+ session.contextState.localStorage = JSON.parse(rawData.localStorage);
647
+ } catch {
648
+ session.contextState.localStorage = {};
649
+ }
650
+
651
+ // Parse and restore sessionStorage
652
+ try {
653
+ session.contextState.sessionStorage = JSON.parse(rawData.sessionStorage);
654
+ } catch {
655
+ session.contextState.sessionStorage = {};
656
+ }
657
+ }
658
+
659
+ return {
660
+ success: true,
661
+ session: createResult.session,
662
+ reAuthRequired: false,
663
+ };
664
+ }
665
+
666
+ /**
667
+ * Save current session state to vault.
668
+ */
669
+ async saveToVault(sessionId: string): Promise<boolean> {
670
+ if (!this.vault) {
671
+ return false;
672
+ }
673
+
674
+ const session = this.sessions.get(sessionId);
675
+ if (!session) {
676
+ return false;
677
+ }
678
+
679
+ const rawData: RawSessionData = {
680
+ cookies: JSON.stringify(session.contextState.cookies),
681
+ localStorage: JSON.stringify(session.contextState.localStorage),
682
+ sessionStorage: JSON.stringify(session.contextState.sessionStorage),
683
+ };
684
+
685
+ // Store with 24-hour expiration
686
+ const expiresAt = Date.now() + 86400000;
687
+ await this.vault.store(session.domain, rawData, expiresAt);
688
+
689
+ return true;
690
+ }
691
+
692
+ /**
693
+ * Check if a vaulted session exists and is valid.
694
+ *
695
+ * Requirements: 5.5
696
+ */
697
+ async hasValidVaultedSession(domain: string): Promise<boolean> {
698
+ if (!this.vault) {
699
+ return false;
700
+ }
701
+
702
+ const session = await this.vault.retrieve(domain);
703
+ return session !== null && session.expiresAt > Date.now();
704
+ }
705
+
706
+ // ===========================================================================
707
+ // Session Destruction
708
+ // ===========================================================================
709
+
710
+ /**
711
+ * Destroy a session and clean up resources.
712
+ */
713
+ async destroy(sessionId: string): Promise<void> {
714
+ const session = this.sessions.get(sessionId);
715
+ if (!session) {
716
+ return;
717
+ }
718
+
719
+ // Stop heartbeat
720
+ this.stopHeartbeat(sessionId);
721
+
722
+ // Remove from agent tracking
723
+ if (session.agentId) {
724
+ const agentSessions = this.sessionsByAgent.get(session.agentId);
725
+ if (agentSessions) {
726
+ agentSessions.delete(sessionId);
727
+ if (agentSessions.size === 0) {
728
+ this.sessionsByAgent.delete(session.agentId);
729
+ }
730
+ }
731
+ }
732
+
733
+ // Remove session
734
+ this.sessions.delete(sessionId);
735
+ }
736
+
737
+ /**
738
+ * Destroy all sessions for an agent.
739
+ */
740
+ async destroyAllForAgent(agentId: string): Promise<number> {
741
+ const sessionIds = this.sessionsByAgent.get(agentId);
742
+ if (!sessionIds) {
743
+ return 0;
744
+ }
745
+
746
+ const count = sessionIds.size;
747
+ for (const sessionId of sessionIds) {
748
+ await this.destroy(sessionId);
749
+ }
750
+
751
+ return count;
752
+ }
753
+
754
+ /**
755
+ * Get count of active sessions.
756
+ */
757
+ getActiveSessionCount(): number {
758
+ return Array.from(this.sessions.values()).filter((s) => s.status === 'active').length;
759
+ }
760
+
761
+ /**
762
+ * Get total session count.
763
+ */
764
+ getTotalSessionCount(): number {
765
+ return this.sessions.size;
766
+ }
767
+
768
+ /**
769
+ * Clear all sessions (for testing).
770
+ */
771
+ clear(): void {
772
+ // Stop all heartbeats
773
+ for (const sessionId of this.heartbeatTimers.keys()) {
774
+ this.stopHeartbeat(sessionId);
775
+ }
776
+
777
+ this.sessions.clear();
778
+ this.sessionsByAgent.clear();
779
+ this.heartbeatTimers.clear();
780
+ }
781
+ }
782
+
783
+ // =============================================================================
784
+ // Factory Function
785
+ // =============================================================================
786
+
787
+ /**
788
+ * Create a new Shadow Session Orchestrator.
789
+ */
790
+ export function createShadowSessionOrchestrator(
791
+ config?: ShadowSessionOrchestratorConfig
792
+ ): ShadowSessionOrchestrator {
793
+ return new ShadowSessionOrchestrator(config);
794
+ }