feat: add chat replay verification
This commit is contained in:
@@ -68,6 +68,9 @@ Governed Chat (Plan-18 MVP-0/1 + MVP-2 Track 4 and Operator Track 5/6):
|
||||
with role visibility; selected agent/skill/delegation are bound by harness
|
||||
`chat-agent-resolver.py`.
|
||||
- `GET /api/v1/chat/audit/verify` — verifies the chat audit hash chain.
|
||||
- `GET /api/v1/chat/replay?chatId=<id>` — verifies chat-chain integrity,
|
||||
evidence artifact hashes, and OPERATOR loop trace replay through harness
|
||||
`chat-replay.py`.
|
||||
- `/chat` UI shows actor/role scope, `mode/risk/decision` badges, certified answer,
|
||||
evidence sources, registered operator actions, agent binding, loop certification,
|
||||
action-gate status, router details, and audit hash. Side-effect requests outside
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Headers, Inject, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Headers, Inject, Post, Query } from '@nestjs/common';
|
||||
import { ok } from '../common/api-response.js';
|
||||
import { actorFromHeaders } from '../common/auth-context.js';
|
||||
import { ChatAskInput, ChatService } from './chat.service.js';
|
||||
@@ -17,6 +17,11 @@ export class ChatController {
|
||||
return ok(this.svc.verifyAudit());
|
||||
}
|
||||
|
||||
@Get('replay')
|
||||
replay(@Query('chatId') chatId?: string, @Query('turnId') turnId?: string) {
|
||||
return ok(this.svc.replay(chatId || '', turnId || ''));
|
||||
}
|
||||
|
||||
@Get('actions')
|
||||
actions(@Headers() headers: Record<string, string | string[] | undefined>) {
|
||||
return ok(this.svc.listActions(actorFromHeaders(headers)));
|
||||
|
||||
@@ -22,6 +22,7 @@ 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 REPLAY_CLI = join(HARNESS_BIN, 'chat-replay.py');
|
||||
const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py');
|
||||
|
||||
function runPython(script: string, args: string[]): CommandResult {
|
||||
@@ -93,6 +94,16 @@ export class ChatService {
|
||||
return { ok: res.status === 0, output: res.stdout || res.stderr };
|
||||
}
|
||||
|
||||
replay(chatId = '', turnId = '') {
|
||||
const args = ['replay'];
|
||||
if (chatId) args.push('--chat-id', chatId);
|
||||
if (turnId) args.push('--turn-id', turnId);
|
||||
const res = runPython(REPLAY_CLI, args);
|
||||
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');
|
||||
}
|
||||
|
||||
listActions(actor: SettingsActor) {
|
||||
this.requireRead(actor);
|
||||
const res = runPython(OPERATOR_CLI, ['list-actions']);
|
||||
|
||||
@@ -89,6 +89,10 @@ test('chat ask executes registered operator action through action-gate', () => {
|
||||
assert.equal(res.loop_run.side_effect_released, true);
|
||||
assert.equal(res.loop_run.trace_verify.ok, true);
|
||||
assert.equal(res.loop_run.replay.ok, true);
|
||||
const replay = svc.replay('operator-chat') as any;
|
||||
assert.equal(replay.ok, true);
|
||||
assert.equal(replay.decision, 'MATCH');
|
||||
assert.equal(replay.loop_replayed, 1);
|
||||
assert.equal(res.audit_verify.ok, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,6 +157,15 @@ export interface ChatAgent {
|
||||
allowed_for_role?: boolean;
|
||||
}
|
||||
|
||||
export interface ChatReplay {
|
||||
ok: boolean;
|
||||
decision: 'MATCH' | 'DRIFT' | 'BREAK' | string;
|
||||
records: number;
|
||||
loop_replayed?: number;
|
||||
diffs?: any[];
|
||||
audit_path?: string;
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
actor: SettingsActor;
|
||||
capabilities: {
|
||||
@@ -206,6 +215,7 @@ export const api = {
|
||||
askChat: (actor: SettingsActor, body: { message: string; chatId?: string; agentId?: string; skillId?: string; delegationLevel?: number }) =>
|
||||
post<ChatAnswer>('chat/ask', body, actorHeaders(actor)),
|
||||
verifyChatAudit: () => get<{ ok: boolean; output: string }>('chat/audit/verify'),
|
||||
replayChat: (chatId = '', turnId = '') => get<ChatReplay>(`chat/replay?chatId=${encodeURIComponent(chatId)}&turnId=${encodeURIComponent(turnId)}`),
|
||||
chatActions: (actor: SettingsActor) => getWithHeaders<{ success: boolean; actions: ChatAction[] }>('chat/actions', actorHeaders(actor)),
|
||||
chatAgents: (actor: SettingsActor) => getWithHeaders<{ success: boolean; agents: ChatAgent[] }>('chat/agents', actorHeaders(actor)),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user