diff --git a/packages/runtime/src/app/RuntimeApp.ts b/packages/runtime/src/app/RuntimeApp.ts index d1e350a..7d3b0f5 100755 --- a/packages/runtime/src/app/RuntimeApp.ts +++ b/packages/runtime/src/app/RuntimeApp.ts @@ -273,7 +273,7 @@ export class RuntimeApp { // Register tools through the CppToolRegistrar // This follows INV-4: registered via capability boundary - registrar.register(this.tool_registry, this.config.project_root) + registrar.register(this.tool_registry, this.config.project_root, this.event_ingestor) this.logger.info('cpp toolchain registered', { capability_id: 'aircoding-cpp-toolchain' }) } catch (e: any) { this.logger.warn('cpp toolchain registration failed', { error: e.message }) diff --git a/packages/toolchain-cpp/src/CppToolRegistrar.ts b/packages/toolchain-cpp/src/CppToolRegistrar.ts index bbce474..c59ceb8 100755 --- a/packages/toolchain-cpp/src/CppToolRegistrar.ts +++ b/packages/toolchain-cpp/src/CppToolRegistrar.ts @@ -5,6 +5,9 @@ * @module packages/toolchain-cpp/src/CppToolRegistrar */ +import { writeFileSync, mkdirSync, existsSync } from 'fs' +import { join, dirname } from 'path' +import { randomUUID } from 'crypto' import { CPP_TOOLCHAIN_CAPABILITY } from './capability.js' import { CppProjectDetector } from './detect/CppProjectDetector.js' import { CMakeConfigurator } from './build/CMakeConfigurator.js' @@ -13,14 +16,37 @@ import { CppTestRunner } from './test/CppTestRunner.js' import { CppcheckRunner } from './analysis/CppcheckRunner.js' import { ClangdClient } from './analysis/ClangdClient.js' +// Duck-typed EventSink — avoids direct runtime import (INV-4) +export interface EventSink { + ingest(event: any): Promise +} + +// Context passed through from RuntimeApp +export interface CppContext { + session_id: string + project_id: string + project_root: string + task_id?: string + agent_id?: string + agent_type?: string + tool_run_id?: string +} + +interface ArtifactInfo { + artifact_id: string + path: string + size_bytes: number + sha256_calc: string +} + export class CppToolRegistrar { manifest = CPP_TOOLCHAIN_CAPABILITY - /** - * Register all cpp.* tools with the provided registry. - * INV-4: This is called through CapabilityRegistry boundary, never via direct runtime import. - */ - register(registry: { register(name: string, definition: any, executor: (call: any) => Promise): void }, project_root: string): void { + register( + registry: { register(name: string, definition: any, executor: (call: any, ctx?: any) => Promise): void }, + project_root: string, + event_sink?: EventSink, + ): void { const detector = new CppProjectDetector(project_root) const configurator = new CMakeConfigurator() const builder = new CppBuilder() @@ -28,90 +54,269 @@ export class CppToolRegistrar { const cppcheck = new CppcheckRunner() const clangd = new ClangdClient() + // cpp.detect — no external command, no evidence needed // cpp.detect registry.register('cpp.detect', { name: 'cpp.detect', category: 'toolchain', description: 'Detect C++ project structure and toolchain', input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[0].input_schema, permissions: { read: true, write: false, network: false }, streaming: false - }, async (call) => { + }, async (call, tool_ctx) => { const result = detector.detect() return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.detect', output: result, metadata: { timestamp: new Date().toISOString() } } }) - // cpp.configure + // cpp.configure — cmake configure, evidence on failure registry.register('cpp.configure', { name: 'cpp.configure', category: 'toolchain', description: 'Configure C++ build with CMake', input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[1].input_schema, permissions: { read: true, write: true, network: false }, streaming: false - }, async (call) => { + }, async (call, tool_ctx) => { + const ctx: CppContext = tool_ctx || {} + const cmd_id = this.cmd_id() + await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'cmake configure')) + const result = configurator.configure({ project_root, generator: call.arguments?.generator as any, build_type: call.arguments?.build_type as any }) + const duration_ms = Date.now() - Date.parse(new Date().toISOString()) + 1 + if (result.ok) { - return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.configure', output: result, metadata: { timestamp: new Date().toISOString() } } + await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms)) + return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.configure', output: result, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } } } else { - return { status: 'error', call_id: call.call_id, tool_name: 'cpp.configure', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'configure failed', retryability: 'not_retryable', semantic_signature: 'cpp.configure' }, metadata: { timestamp: new Date().toISOString() } } + const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, '', result.error || 'configure failed') + const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, [{ file: 'CMakeLists.txt', line: 0, column: 0, severity: 'error', message: result.error || 'configure failed', semantic_signature: 'cmake.configure' }]) + await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, diag_ids)) + await this.emit_evidence(event_sink, ctx, cmd_id, 'other', { error: result.error }) + return { status: 'error', call_id: call.call_id, tool_name: 'cpp.configure', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'configure failed', retryability: 'not_retryable', semantic_signature: 'cpp.configure' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id, diagnostic_ids: diag_ids } } } }) - // cpp.build + // cpp.build — cmake build, diagnostics + evidence registry.register('cpp.build', { name: 'cpp.build', category: 'toolchain', description: 'Build C++ project', input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[2].input_schema, permissions: { read: true, write: true, network: false }, streaming: false - }, async (call) => { + }, async (call, tool_ctx) => { + const ctx: CppContext = tool_ctx || {} + const cmd_id = this.cmd_id() + await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'cmake --build')) + + const build_start = Date.now() const result = builder.build(project_root + '/build', call.arguments?.target as string) + const duration_ms = Date.now() - build_start + if (result.ok) { - return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.build', output: { built: true, output: result.output, diagnostics: result.diagnostics, elapsed_ms: result.elapsed_ms }, metadata: { timestamp: new Date().toISOString() } } + await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms)) + return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.build', output: { built: true, output: result.output, diagnostics: result.diagnostics, elapsed_ms: result.elapsed_ms }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } } } else { - return { status: 'error', call_id: call.call_id, tool_name: 'cpp.build', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'build failed', retryability: 'not_retryable', semantic_signature: 'cpp.build' }, metadata: { timestamp: new Date().toISOString() } } + const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, result.output, result.output) + const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, result.diagnostics.map(d => ({ file: d.file || '', line: d.line || 0, column: d.column || 0, severity: d.severity || 'error', message: d.message, semantic_signature: d.semantic_signature || `build.${d.file}.${d.line}` }))) + await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, diag_ids)) + await this.emit_evidence(event_sink, ctx, cmd_id, 'build_output', { diagnostics: result.diagnostics.length }) + return { status: 'error', call_id: call.call_id, tool_name: 'cpp.build', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'build failed', retryability: 'not_retryable', semantic_signature: 'cpp.build' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id, diagnostic_ids: diag_ids } } } }) - // cpp.test + // cpp.test — ctest, evidence on failure registry.register('cpp.test', { name: 'cpp.test', category: 'toolchain', description: 'Run C++ tests', input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[3].input_schema, permissions: { read: true, write: false, network: false }, streaming: false - }, async (call) => { + }, async (call, tool_ctx) => { + const ctx: CppContext = tool_ctx || {} + const cmd_id = this.cmd_id() + await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'ctest')) + + const test_start = Date.now() const result = tester.run_tests(project_root + '/build') + const duration_ms = Date.now() - test_start + if (result.ok) { - return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.test', output: result, metadata: { timestamp: new Date().toISOString() } } + await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms)) + return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.test', output: result, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } } } else { - return { status: 'error', call_id: call.call_id, tool_name: 'cpp.test', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'test failed', retryability: 'not_retryable', semantic_signature: 'cpp.test' }, metadata: { timestamp: new Date().toISOString() } } + const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, result.output, result.output) + await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, [])) + await this.emit_evidence(event_sink, ctx, cmd_id, 'test_output', { passed: result.passed, failed: result.failed }) + return { status: 'error', call_id: call.call_id, tool_name: 'cpp.test', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'test failed', retryability: 'not_retryable', semantic_signature: 'cpp.test' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } } } }) - // cpp.cppcheck + // cpp.cppcheck — static analysis, diagnostics + evidence registry.register('cpp.cppcheck', { name: 'cpp.cppcheck', category: 'toolchain', description: 'Run cppcheck static analysis', input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[4].input_schema, permissions: { read: true, write: false, network: false }, streaming: false - }, async (call) => { + }, async (call, tool_ctx) => { + const ctx: CppContext = tool_ctx || {} + const cmd_id = this.cmd_id() + await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'cppcheck')) + + const check_start = Date.now() const result = cppcheck.run(project_root, { enable_all: call.arguments?.enable_all as boolean, check_config: call.arguments?.check_config as boolean }) + const duration_ms = Date.now() - check_start + + const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, result.diagnostics.map(d => ({ file: d.file || '', line: d.line || 0, column: d.column || 0, severity: d.severity || 'warning', message: d.message, semantic_signature: d.semantic_signature || `cppcheck.${d.file}.${d.line}` }))) + if (result.ok) { - return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.cppcheck', output: result, metadata: { timestamp: new Date().toISOString() } } + await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms)) + return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.cppcheck', output: { ...result, diagnostic_ids: diag_ids }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } } } else { - return { status: 'error', call_id: call.call_id, tool_name: 'cpp.cppcheck', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'cppcheck failed', retryability: 'not_retryable', semantic_signature: 'cpp.cppcheck' }, metadata: { timestamp: new Date().toISOString() } } + const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, result.output, result.output) + await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, diag_ids)) + await this.emit_evidence(event_sink, ctx, cmd_id, 'other', { diagnostics: result.diagnostics.length }) + return { status: 'error', call_id: call.call_id, tool_name: 'cpp.cppcheck', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'cppcheck failed', retryability: 'not_retryable', semantic_signature: 'cpp.cppcheck' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id, diagnostic_ids: diag_ids } } } }) - // cpp.clangd + // cpp.clangd — LSP query, diagnostics registry.register('cpp.clangd', { name: 'cpp.clangd', category: 'toolchain', description: 'Query clangd for symbol info', input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[5].input_schema, permissions: { read: true, write: false, network: false }, streaming: false - }, async (call) => { + }, async (call, tool_ctx) => { + const ctx: CppContext = tool_ctx || {} + const cmd_id = this.cmd_id() + await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'clangd --check')) + + const clangd_start = Date.now() const result = await clangd.query_symbol(call.arguments?.file as string, call.arguments?.line as number, call.arguments?.column as number) + const duration_ms = Date.now() - clangd_start + if (result.ok) { - return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.clangd', output: result, metadata: { timestamp: new Date().toISOString() } } + const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, (result.diagnostics || []).map(d => ({ file: d.file, line: d.line, column: 0, severity: d.severity, message: d.message, semantic_signature: `clangd.${d.file}.${d.line}` }))) + await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms)) + return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.clangd', output: { ...result, diagnostic_ids: diag_ids }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } } } else { - return { status: 'error', call_id: call.call_id, tool_name: 'cpp.clangd', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'clangd query failed', retryability: 'not_retryable', semantic_signature: 'cpp.clangd' }, metadata: { timestamp: new Date().toISOString() } } + await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, '', '', [])) + await this.emit_evidence(event_sink, ctx, cmd_id, 'other', { error: result.error }) + return { status: 'error', call_id: call.call_id, tool_name: 'cpp.clangd', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'clangd query failed', retryability: 'not_retryable', semantic_signature: 'cpp.clangd' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } } } }) } + + // ===== Private: event emission (no runtime import — uses duck-typed EventSink) ===== + + private cmd_id(): string { + return `cmd_${randomUUID().slice(0, 12)}` + } + + private async emit(sink: EventSink | undefined, ctx: CppContext, event: any): Promise { + if (!sink) return + try { + await sink.ingest(event) + } catch { /* evidence emission is best-effort, never breaks tool execution */ } + } + + private async write_artifacts( + sink: EventSink | undefined, ctx: CppContext, project_root: string, + cmd_id: string, stdout: string, stderr: string, + ): Promise<{ stdout_id: string; stderr_id: string }> { + const now = new Date().toISOString() + const artifacts_dir = join(project_root, '.air', 'local', 'artifacts') + if (!existsSync(artifacts_dir)) mkdirSync(artifacts_dir, { recursive: true }) + + const stdout_path = join(artifacts_dir, `${cmd_id}.stdout.log`) + const stderr_path = join(artifacts_dir, `${cmd_id}.stderr.log`) + + writeFileSync(stdout_path, stdout.slice(0, 65536), 'utf-8') + writeFileSync(stderr_path, stderr.slice(0, 65536), 'utf-8') + + const stdout_id = `art_${cmd_id}_stdout` + const stderr_id = `art_${cmd_id}_stderr` + + if (sink) { + await this.emit(sink, ctx, { + id: `evt_${stdout_id}`, type: 'artifact.created', version: 1, timestamp: now, + session_id: ctx.session_id, project_id: ctx.project_id, + source: { kind: 'tool' }, route: ['cpp', 'tool'], + payload: { artifact_id: stdout_id, type: 'log', uri: `file://${stdout_path}`, path: stdout_path, original_name: `${cmd_id}.stdout.log`, size_bytes: stdout.length, sha256: '', task_id: ctx.task_id || '', agent_id: ctx.agent_id || '', tool_run_id: ctx.tool_run_id || '', command_run_id: cmd_id, associated_entity_type: 'command_run', associated_entity_id: cmd_id, metadata: {} } + }) + await this.emit(sink, ctx, { + id: `evt_${stderr_id}`, type: 'artifact.created', version: 1, timestamp: now, + session_id: ctx.session_id, project_id: ctx.project_id, + source: { kind: 'tool' }, route: ['cpp', 'tool'], + payload: { artifact_id: stderr_id, type: 'log', uri: `file://${stderr_path}`, path: stderr_path, original_name: `${cmd_id}.stderr.log`, size_bytes: stderr.length, sha256: '', task_id: ctx.task_id || '', agent_id: ctx.agent_id || '', tool_run_id: ctx.tool_run_id || '', command_run_id: cmd_id, associated_entity_type: 'command_run', associated_entity_id: cmd_id, metadata: {} } + }) + } + + return { stdout_id, stderr_id } + } + + private async emit_diagnostics( + sink: EventSink | undefined, ctx: CppContext, cmd_id: string, + diags: Array<{ file: string; line: number; column: number; severity: string; message: string; semantic_signature: string }>, + ): Promise { + if (!sink || diags.length === 0) return [] + const now = new Date().toISOString() + const ids: string[] = [] + for (let i = 0; i < diags.length; i++) { + const d = diags[i] + const did = `diag_${cmd_id}_${i}` + ids.push(did) + try { + await sink.ingest({ + id: `evt_${did}`, type: 'diagnostic.created', version: 1, timestamp: now, + session_id: ctx.session_id, project_id: ctx.project_id, + source: { kind: 'tool' }, route: ['cpp', 'tool'], + payload: { + diagnostic_id: did, task_id: ctx.task_id || '', agent_id: ctx.agent_id || '', + command_run_id: cmd_id, artifact_id: '', language: 'cpp', toolchain: 'gcc', + severity: d.severity, file: d.file, line: d.line, column: d.column, + code: '', message: d.message, semantic_signature: d.semantic_signature, metadata: {}, + } + }) + } catch { /* diagnostic ingestion is best-effort */ } + } + return ids + } + + private async emit_evidence(sink: EventSink | undefined, ctx: CppContext, cmd_id: string, kind: string, extra: any): Promise { + if (!sink || !ctx.task_id) return + try { + await sink.ingest({ + id: `evt_evr_${cmd_id}`, type: 'evidence.created', version: 1, + timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id, + source: { kind: 'tool' }, route: ['cpp', 'tool'], + payload: { + evidence_ref_id: `evr_${cmd_id}`, kind, ref: `command_run:${cmd_id}`, + location_json: {}, claim: JSON.stringify(extra), + task_id: ctx.task_id, agent_id: ctx.agent_id || '', tool_run_id: ctx.tool_run_id || '', + command_run_id: cmd_id, artifact_id: '', diagnostic_id: '', message_id: '', + } + }) + } catch { /* best-effort */ } + } + + private command_started(cmd_id: string, ctx: CppContext, command: string) { + return { + id: `evt_${cmd_id}_started`, type: 'command.started', version: 1, + timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id, + source: { kind: 'tool' }, route: ['cpp', 'tool'], + payload: { command_run_id: cmd_id, task_id: ctx.task_id || '', agent_id: ctx.agent_id || '', origin_message_id: '', tool_run_id: ctx.tool_run_id || '', command, cwd: ctx.project_root, metadata: {} } + } + } + + private command_completed(cmd_id: string, ctx: CppContext, exit_code: number, duration_ms: number) { + return { + id: `evt_${cmd_id}_completed`, type: 'command.completed', version: 1, + timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id, + source: { kind: 'tool' }, route: ['cpp', 'tool'], + payload: { command_run_id: cmd_id, exit_code, duration_ms, stdout_artifact_id: '', stderr_artifact_id: '', combined_artifact_id: '', diagnostic_ids: [], parsed_diagnostics_json: {}, metadata: {} } + } + } + + private command_failed(cmd_id: string, ctx: CppContext, exit_code: number, duration_ms: number, stdout_artifact_id: string, stderr_artifact_id: string, diagnostic_ids: string[]) { + return { + id: `evt_${cmd_id}_failed`, type: 'command.failed', version: 1, + timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id, + source: { kind: 'tool' }, route: ['cpp', 'tool'], + payload: { command_run_id: cmd_id, exit_code, duration_ms, stdout_artifact_id, stderr_artifact_id, combined_artifact_id: '', error: {}, evidence_refs: [], metadata: {} } + } + } }