/** * Doctor Tools - Diagnostic and repair operations * * Implements T-213: doctor.* * FR-018: Wired to DoctorService for real diagnostics. * * @module packages/runtime/src/tools/doctor */ import type { ToolDefinition, ToolCall, ToolResultEnvelope, ISOTimeString } from '@aircoding/contracts' import { DoctorService } from '../../doctor/DoctorService.js' export const doctor_check: ToolDefinition = { name: 'doctor.check', category: 'doctor', description: 'Run diagnostic checks', version: 1, output_schema: { type: 'object', properties: {}, required: [] }, input_schema: { type: 'object', properties: { scope: { type: 'string', enum: ['all', 'self_bootstrap', 'capability', 'project', 'runtime', 'toolchain', 'display', 'network', 'provider'], default: 'all' } } }, permissions: { read_paths: { allow: ["*"] } }, streaming: false } export const doctor_fix: ToolDefinition = { name: 'doctor.fix', category: 'doctor', description: 'Attempt to fix issues (requires permission policy)', version: 1, output_schema: { type: 'object', properties: {}, required: [] }, input_schema: { type: 'object', properties: { check_name: { type: 'string', description: 'Check name to fix (e.g., bun, display, toolchain.cmake)' }, fix: { type: 'boolean', default: true, description: 'Actually perform the fix vs dry-run' } }, required: ['check_name'] }, permissions: { write_paths: { allow: ["*"] } }, streaming: false } /** * FR-018: Real executor that wraps DoctorService */ export function createDoctorExecutor(project_root?: string, capability_registry?: any) { const doctor = new DoctorService(project_root || process.cwd(), capability_registry) return { 'doctor.check': async (call: ToolCall): Promise => { const scope = (call.arguments?.scope as any) || 'all' const valid_scopes = ['all', 'self_bootstrap', 'capability', 'project', 'runtime', 'toolchain', 'display', 'network', 'provider'] const actual_scope = valid_scopes.includes(scope) ? scope : 'all' try { const report = await doctor.run_diagnostics(actual_scope as any) return create_result(call.call_id, 'doctor.check', 'text', { scope: actual_scope, checks: report.checks.map(c => ({ name: c.name, category: c.category, passed: c.passed, message: c.message, fixable: c.fixable, fix: c.fix })), all_passed: report.all_passed, bootstrap_passed: report.bootstrap_passed, fixable_count: report.fixable_count, message: report.all_passed ? 'All checks passed' : `${report.fixable_count} fixable issues found` }) } catch (e: any) { return create_result(call.call_id, 'doctor.check', 'error', { message: e.message || String(e) }) } }, 'doctor.fix': async (call: ToolCall): Promise => { const { check_name, fix = true } = call.arguments as { check_name: string; fix?: boolean } try { // FR-018: Check permission before system modifications const requires_permission = check_name === 'display' || check_name.startsWith('toolchain.') || check_name.startsWith('capability.') const project_root_val = project_root || process.cwd() let has_permission = true if (requires_permission) { const { existsSync } = await import('fs') const permission_config_path = `${project_root_val}/.air/shared/permissions.yaml` if (existsSync(permission_config_path)) { // Check if auto-fix is allowed in permissions config has_permission = false // Require explicit consent for system changes } } if (requires_permission && !has_permission) { return create_result(call.call_id, 'doctor.fix', 'error', { message: `System modification "${check_name}" requires explicit permission. Use --ask-confirm flag or add to permissions.yaml.` }) } if (!fix) { // Dry-run: just describe what would be done const result = await doctor.fix(check_name) return create_result(call.call_id, 'doctor.fix', 'text', { check_name, dry_run: true, would_do: result.message, status: 'dry_run' }) } const result = await doctor.fix(check_name) return create_result(call.call_id, 'doctor.fix', 'text', { check_name, fixed: result.ok, message: result.message, status: result.ok ? 'fixed' : 'failed' }) } catch (e: any) { return create_result(call.call_id, 'doctor.fix', 'error', { message: e.message || String(e) }) } } } } function create_result(call_id: string, tool_name: string, type: 'text' | 'error', content: Record): ToolResultEnvelope { return { status: type === "error" ? "error" : "ok", output: type === "error" ? undefined : content, error: type === "error" ? { error_id: call_id, kind: "tool_error", severity: "error", message: typeof content?.message === "string" ? content.message : "tool error", retryability: "not_retryable", semantic_signature: tool_name } : undefined, metadata: { timestamp: new Date().toISOString() as ISOTimeString, call_id, tool_name, type: type === "error" ? "error" : "ok" } } }