import { ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { execFileSync } from 'node:child_process'; import { join } from 'node:path'; import { APP_ROOT } from '../common/app-root.js'; import type { SettingsActor } from '../settings/settings.service.js'; export interface ChatAskInput { message: string; chatId?: string; agentId?: string; skillId?: string; delegationLevel?: number; } interface CommandResult { status: number; stdout: string; stderr: string; } const HARNESS_BIN = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash'); const CHAT_CLI = join(HARNESS_BIN, 'chat-turn.py'); const OPERATOR_CLI = join(HARNESS_BIN, 'chat-operator.py'); const AGENT_CLI = join(HARNESS_BIN, 'chat-agent-resolver.py'); const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py'); function runPython(script: string, args: string[]): CommandResult { try { const stdout = execFileSync('python3', [script, ...args], { cwd: APP_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: process.env, }); return { status: 0, stdout: stdout.trim(), stderr: '' }; } catch (err: any) { return { status: Number(err?.status ?? 1), stdout: String(err?.stdout ?? '').trim(), stderr: String(err?.stderr ?? '').trim(), }; } } function parseJson(raw: string): T | null { if (!raw) return null; try { return JSON.parse(raw) as T; } catch { return null; } } @Injectable() export class ChatService { ask(input: ChatAskInput, actor: SettingsActor) { if (!input.message || !input.message.trim()) { throw new ForbiddenException('CHAT_DENY message required'); } this.requireRead(actor); const args = [ 'ask', '--message', input.message, '--actor', actor.actor, '--role', actor.role, '--project', actor.project, '--chat-id', input.chatId || 'chat-default', '--tenant', actor.tenant, ]; if (input.agentId) args.push('--agent', input.agentId); if (input.skillId) args.push('--skill', input.skillId); if (input.delegationLevel !== undefined) args.push('--delegation-level', String(input.delegationLevel)); const res = runPython(CHAT_CLI, args); const parsed = parseJson>(res.stdout); if (parsed) { return { ...parsed, actor, audit_verify: this.verifyAudit() }; } if (res.status !== 0) { throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_CLI_FAILED'); } throw new InternalServerErrorException('CHAT_CLI_EMPTY_RESPONSE'); } verifyAudit() { const res = runPython(CHAT_CLI, ['verify-audit']); return { ok: res.status === 0, output: res.stdout || res.stderr }; } listActions(actor: SettingsActor) { this.requireRead(actor); const res = runPython(OPERATOR_CLI, ['list-actions']); const parsed = parseJson>(res.stdout); if (parsed) return parsed; throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_OPERATOR_ACTIONS_FAILED'); } listAgents(actor: SettingsActor) { this.requireRead(actor); const res = runPython(AGENT_CLI, ['list-agents', '--role', actor.role]); const parsed = parseJson>(res.stdout); if (parsed) return parsed; throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_AGENTS_FAILED'); } private requireRead(actor: SettingsActor) { const res = runPython(RBAC_CLI, [ 'check', '--role', actor.role, '--resource', 'monitoring', '--action', 'read', '--role-project', actor.project, '--target-project', actor.project, '--role-tenant', actor.tenant, '--target-tenant', actor.tenant, ]); if (res.status !== 0) { throw new ForbiddenException(res.stderr || res.stdout || 'RBAC_DENY'); } } }