feat: prepare CASAN paid PoC release package

This commit is contained in:
thanhnv
2026-07-18 00:07:04 +07:00
parent c818eaf8b0
commit 881ee01691
55 changed files with 5992 additions and 44 deletions
@@ -7,9 +7,10 @@ import { ApprovalsModule } from './approvals/approvals.module.js';
import { ChatModule } from './chat/chat.module.js';
import { ProviderAuthModule } from './provider-auth/provider-auth.module.js';
import { GoalsModule } from './goals/goals.module.js';
import { EvidenceModule } from './evidence/evidence.module.js';
@Module({
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule, GoalsModule],
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule, GoalsModule, EvidenceModule],
controllers: [HealthController],
})
export class AppModule {}
@@ -23,8 +23,8 @@ export class ChatController {
}
@Get('audit/verify')
verifyAudit() {
return ok(this.svc.verifyAudit());
verifyAudit(@Headers() headers: Record<string, string | string[] | undefined>) {
return ok(this.svc.verifyAudit(actorFromHeaders(headers)));
}
@Get('history')
@@ -37,8 +37,12 @@ export class ChatController {
}
@Get('replay')
replay(@Query('chatId') chatId?: string, @Query('turnId') turnId?: string, @Query('tenant') tenant?: string) {
return ok(this.svc.replay(chatId || '', turnId || '', tenant || ''));
replay(
@Headers() headers: Record<string, string | string[] | undefined>,
@Query('chatId') chatId?: string,
@Query('turnId') turnId?: string,
) {
return ok(this.svc.replay(actorFromHeaders(headers), chatId || '', turnId || ''));
}
@Get('actions')
@@ -107,7 +107,7 @@ export class ChatService {
const res = runPython(CHAT_CLI, args, runtime.env);
const parsed = parseJson<Record<string, any>>(res.stdout);
if (parsed) {
return { ...parsed, actor, audit_verify: this.verifyAudit() };
return { ...parsed, actor, audit_verify: this.verifyAudit(actor) };
}
if (res.status !== 0) {
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_CLI_FAILED');
@@ -115,8 +115,9 @@ export class ChatService {
throw new InternalServerErrorException('CHAT_CLI_EMPTY_RESPONSE');
}
verifyAudit() {
const res = runPython(CHAT_CLI, ['verify-audit']);
verifyAudit(actor: SettingsActor) {
this.requireRead(actor);
const res = runPython(CHAT_CLI, ['verify-audit'], this.tenantEnv(actor));
return { ok: res.status === 0, output: res.stdout || res.stderr };
}
@@ -125,7 +126,7 @@ export class ChatService {
const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? Math.trunc(limit) : 50, 100));
const args = ['history', '--actor', actor.actor, '--tenant', actor.tenant, '--limit', String(safeLimit)];
if (chatId) args.push('--chat-id', chatId);
const res = runPython(CHAT_CLI, args);
const res = runPython(CHAT_CLI, args, this.tenantEnv(actor));
const parsed = parseJson<Record<string, unknown>>(res.stdout);
if (res.status === 0 && parsed?.ok === true) return parsed;
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_HISTORY_FAILED');
@@ -176,12 +177,12 @@ export class ChatService {
child.on('close', () => res.end());
}
replay(chatId = '', turnId = '', tenant = '') {
replay(actor: SettingsActor, chatId = '', turnId = '') {
this.requireRead(actor);
const args = ['replay'];
if (chatId) args.push('--chat-id', chatId);
if (turnId) args.push('--turn-id', turnId);
const env = tenant && tenant !== 'default' ? { CASAN_TENANT_ID: tenant } : {};
const res = runPython(REPLAY_CLI, args, env);
const res = runPython(REPLAY_CLI, args, this.tenantEnv(actor));
const parsed = parseJson<Record<string, any>>(res.stdout);
if (parsed) return { ok: res.status === 0, ...parsed };
throw new InternalServerErrorException(res.stderr || res.stdout || 'CHAT_REPLAY_FAILED');
@@ -272,6 +273,10 @@ export class ChatService {
return { policyProvider, env: { CASAN_TENANT_ID: actor.tenant || 'default', ...parsed.env } };
}
private tenantEnv(actor: SettingsActor): NodeJS.ProcessEnv {
return { CASAN_TENANT_ID: actor.tenant || 'default' };
}
private requireConnectionAdmin(actor: SettingsActor) {
if (!['project-admin', 'org-admin'].includes(actor.role)) {
throw new ForbiddenException('MODEL_CONNECTION_ADMIN_REQUIRED');
@@ -52,6 +52,7 @@ export const PATHS = {
chatAudit: env('CASAN_CP_CHAT_AUDIT', '.specify/logs/chat/chat-turns.jsonl'),
delegationPolicy: env('CASAN_CP_DELEGATION_POLICY', 'packages/casan-harness/config/delegation-policy.yaml'),
selfImprove: env('CASAN_CP_SELF_IMPROVE', 'packages/casan-harness/scripts/bash/self-improve.py'),
evidencePacks: env('CASAN_CP_EVIDENCE_PACKS', 'docs/output/casan/evidence-packs'),
};
export const STALE_AFTER_S = Number(process.env.CASAN_DASHBOARD_STALE_S ?? 3600);
@@ -0,0 +1,14 @@
import { Controller, Get, Inject, Param } from '@nestjs/common';
import { ok } from '../common/api-response.js';
import { EvidenceService } from './evidence.service.js';
@Controller('api/v1/evidence-packs')
export class EvidenceController {
constructor(@Inject(EvidenceService) private readonly service: EvidenceService) {}
@Get()
list() { return ok(this.service.list()); }
@Get(':id')
get(@Param('id') id: string) { return ok(this.service.get(id)); }
}
@@ -0,0 +1,6 @@
import { Module } from '@nestjs/common';
import { EvidenceController } from './evidence.controller.js';
import { EvidenceService } from './evidence.service.js';
@Module({ controllers: [EvidenceController], providers: [EvidenceService] })
export class EvidenceModule {}
@@ -0,0 +1,107 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, join, relative, resolve } from 'node:path';
import { execFileSync } from 'node:child_process';
import { APP_ROOT, PATHS } from '../common/app-root.js';
const PACK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const JSON_LIMIT = 1024 * 1024;
const TEXT_LIMIT = 256 * 1024;
const REPORTS = [
'h1-context-report.json', 'h2-tool-audit.json', 'h3-eval-scorecard.json',
'h4-security-report.json', 'h5-audit-chain-proof.json', 'h6-cost-telemetry.json',
'h7-orchestration-report.json', 'redteam-result.json', 'benign-fp-report.json',
];
function isInside(root: string, path: string) {
const rel = relative(root, path);
return rel === '' || (!rel.startsWith('..') && !rel.includes(`..${String.fromCharCode(47)}`));
}
function readBounded(path: string, max: number): string | null {
try {
if (statSync(path).size > max) return null;
return readFileSync(path, 'utf8');
} catch { return null; }
}
function readObject(path: string): Record<string, unknown> | null {
const source = readBounded(path, JSON_LIMIT);
if (!source) return null;
try {
const value: unknown = JSON.parse(source);
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
} catch { return null; }
}
@Injectable()
export class EvidenceService {
private readonly root = resolve(PATHS.evidencePacks);
private pack(id: string): string {
if (!PACK_ID.test(id)) throw new NotFoundException('Evidence pack not found');
const path = resolve(this.root, id);
if (!isInside(this.root, path) || basename(path) !== id || !existsSync(path)) {
throw new NotFoundException('Evidence pack not found');
}
return path;
}
private integrity(dir: string) {
const script = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash', 'evidence-pack-verify.py');
if (!existsSync(script)) return { ok: false, status: 'verifier_unavailable', output: 'Evidence verifier is unavailable.' };
try {
const output = execFileSync('python3', [script, dir], { cwd: APP_ROOT, encoding: 'utf8', timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
return { ok: true, status: 'verified', output };
} catch (error: unknown) {
const detail = error && typeof error === 'object' && 'stderr' in error ? String((error as { stderr?: unknown }).stderr ?? '') : '';
return { ok: false, status: 'failed', output: detail.trim().slice(0, 500) || 'Evidence manifest verification failed.' };
}
}
private summary(id: string, dir: string) {
const run = readObject(join(dir, 'run-summary.json')) ?? {};
const manifest = readObject(join(dir, 'artifact-manifest.json')) ?? {};
const signature = existsSync(join(dir, 'evidence-pack.sig')) ? 'present' : 'unsigned';
return {
run_id: String(run.run_id ?? id),
certified: run.certified === true,
certification_reasons: Array.isArray(run.certification_reasons) ? run.certification_reasons.map(String) : [],
pack_version: String(run.pack_version ?? 'unknown'),
created_at: statSync(dir).mtime.toISOString(),
file_count: manifest.files && typeof manifest.files === 'object' && !Array.isArray(manifest.files) ? Object.keys(manifest.files as Record<string, unknown>).length : 0,
signature,
integrity: this.integrity(dir),
};
}
list() {
if (!existsSync(this.root)) return { root: this.root, packs: [] };
const packs = readdirSync(this.root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && PACK_ID.test(entry.name))
.map((entry) => this.summary(entry.name, join(this.root, entry.name)))
.sort((a, b) => b.created_at.localeCompare(a.created_at));
return { root: this.root, packs };
}
get(id: string) {
const dir = this.pack(id);
const manifest = readObject(join(dir, 'artifact-manifest.json')) ?? {};
const hashes = manifest.files && typeof manifest.files === 'object' && !Array.isArray(manifest.files)
? manifest.files as Record<string, unknown> : {};
const files = Object.entries(hashes).map(([path, sha256]) => ({
path,
sha256: String(sha256),
bytes: (() => { try { return statSync(join(dir, path)).size; } catch { return 0; } })(),
}));
const reports = Object.fromEntries(REPORTS.map((name) => [name, readObject(join(dir, name))]).filter(([, value]) => value));
return {
...this.summary(id, dir),
manifest_head: readBounded(join(dir, 'manifest-head.txt'), 512)?.trim() || String(manifest.manifest_head ?? '') || null,
run_summary: readObject(join(dir, 'run-summary.json')),
decision_log: readBounded(join(dir, 'decision-log.md'), TEXT_LIMIT),
reports,
files,
};
}
}