135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
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<T>(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<Record<string, any>>(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<Record<string, any>>(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<Record<string, any>>(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');
|
|
}
|
|
}
|
|
}
|