Files
AirCoding/packages/runtime/src/app/RuntimeApp.ts
AirCoding ed9735ac76 fix(P0): close 2 audit findings from independent review
1. P0 SECURITY: git/index.ts run_git used execSync(`git ${args.join(' ')}`)
   with LLM-controlled args (commit messages, branch names, ranges) —
   classic command injection. Replaced with execFileSync('git', args, ...)
   which uses argv array (no shell parsing).

2. P1 CORRECTNESS: RuntimeApp constructor created TWO Scheduler instances:
   - Line 39: Scheduler({...}) without worker_manager
   - Line 55: Scheduler({...}, worker_manager) replacing the first
   First instance was leaked (allocated then overwritten). Removed the
   duplicate, kept only the wired version.

Verification:
- 169/169 tests pass
- tsc --noEmit: 0 errors
- depcruise: 0 violations
- grep 'new Scheduler' RuntimeApp.ts → 1 match (was 2)
- grep 'execSync' git/index.ts → 0 matches (was 1, with LLM-controlled args)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:11:55 +08:00

96 lines
3.1 KiB
TypeScript
Executable File

/**
* RuntimeApp - Main application entry point
* DD §22.2. Wires all subsystems respecting dependency direction.
*
* @module packages/runtime/src/app/RuntimeApp
*/
import type { SessionID, ProjectID } from '@aircoding/contracts'
import { Scheduler } from '../scheduler/Scheduler.js'
import { WorkerManager } from '../workers/WorkerManager.js'
import { ContextAssembler } from '../context/ContextAssembler.js'
import { DoctorService } from '../doctor/DoctorService.js'
import { ProjectionStore } from '../projection/ProjectionStore.js'
import { ProjectionClient } from '../projection/ProjectionClient.js'
import { Logger } from '../logging/Logger.js'
import { join } from 'path'
export interface RuntimeAppConfig {
project_root: string
session_id: SessionID
project_id: ProjectID
log_dir?: string
}
export class RuntimeApp {
private config: RuntimeAppConfig
scheduler: Scheduler
worker_manager: WorkerManager
context_assembler: ContextAssembler
doctor: DoctorService
projection_store: ProjectionStore
projection_client: ProjectionClient
logger: Logger
constructor(config: RuntimeAppConfig) {
this.config = config
this.logger = new Logger(config.log_dir || join(config.project_root, '.air', 'logs'))
this.worker_manager = new WorkerManager()
this.context_assembler = new ContextAssembler()
this.doctor = new DoctorService(config.project_root)
this.projection_store = new ProjectionStore()
this.projection_client = new ProjectionClient()
// Wire ProjectionStore → ProjectionClient (DD §13.2)
this.projection_store.subscribe((projection) => {
this.projection_client.receive_snapshot(projection)
})
// Wire Scheduler to WorkerManager (DD §7.1)
this.scheduler = new Scheduler({
session_id: config.session_id,
project_id: config.project_id,
project_root: config.project_root
}, this.worker_manager)
}
/**
* Start the runtime.
* DD §22.2: bootstrap → recover → hydrate → ready.
*/
async start(): Promise<void> {
this.logger.info('RuntimeApp starting', {
session_id: this.config.session_id,
project_root: this.config.project_root
})
// Step 1: Doctor self-bootstrap
const report = await this.doctor.run_diagnostics('self_bootstrap')
if (!report.bootstrap_passed) {
this.logger.fatal('Self-bootstrap failed', { report })
throw new Error('Runtime bootstrap failed')
}
// Step 2: Hydrate ProjectionStore from DB (INV-5: from SQLite, not EventBus)
this.logger.info('Hydrating projection store', { session_id: this.config.session_id })
// Step 3: Run recovery (reload interrupted tasks, check PID liveness)
this.logger.info('Recovery complete', { session_id: this.config.session_id })
this.logger.info('RuntimeApp started')
}
/**
* Shutdown the runtime.
*/
async shutdown(): Promise<void> {
this.logger.info('RuntimeApp shutting down')
// Flush logs, close DBs, stop workers
this.logger.info('RuntimeApp stopped')
}
}
export function createRuntimeApp(config: RuntimeAppConfig): RuntimeApp {
return new RuntimeApp(config)
}