feat(p7): /v1/ai REST + kernel-client AI methods + @insignia/iios-ai-web SDK

Task 7.5: AiController exposes POST /v1/ai/jobs, GET /v1/ai/artifacts(+/:id),
POST accept/reject (session-auth). kernel-client RestClient gains runAiJob/
listArtifacts/getArtifact/acceptArtifact/rejectArtifact + AiArtifact/AiClaim/
AiEvidenceLink/AiModelRun types. New @insignia/iios-ai-web (AiProvider, useRunJob,
useAiProposals, useArtifact). Boundary allowlist: ai-web -> contracts +
kernel-client (fails on ai-web->service).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 16:42:07 +05:30
parent 68d922c248
commit 881b37afd0
12 changed files with 294 additions and 1 deletions
@@ -0,0 +1,55 @@
import { BadRequestException, Body, Controller, Get, Headers, Param, Post, Query } from '@nestjs/common';
import { IiosAiJobType } from '@prisma/client';
import { AiJobService } from './ai.service';
import { SessionVerifier } from '../platform/session.verifier';
import type { MessagePrincipal } from '../identity/actor.resolver';
import { RunJobDto } from './ai.dto';
@Controller('v1/ai')
export class AiController {
constructor(
private readonly ai: AiJobService,
private readonly session: SessionVerifier,
) {}
@Post('jobs')
async runJob(@Body() body: RunJobDto, @Headers('authorization') auth?: string) {
return this.ai.runJob({
interactionId: body.interactionId,
jobType: body.jobType as IiosAiJobType,
principal: this.principal(auth),
});
}
@Get('artifacts')
async listArtifacts(
@Query('interactionId') interactionId?: string,
@Query('status') status?: string,
@Headers('authorization') auth?: string,
) {
this.principal(auth);
return this.ai.listArtifacts({ interactionId, status });
}
@Get('artifacts/:id')
async getArtifact(@Param('id') id: string, @Headers('authorization') auth?: string) {
this.principal(auth);
return this.ai.getArtifact(id);
}
@Post('artifacts/:id/accept')
async accept(@Param('id') id: string, @Headers('authorization') auth?: string) {
return this.ai.accept(id, this.principal(auth));
}
@Post('artifacts/:id/reject')
async reject(@Param('id') id: string, @Headers('authorization') auth?: string) {
return this.ai.reject(id, this.principal(auth));
}
private principal(authorization?: string): MessagePrincipal {
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
if (!token) throw new BadRequestException('Authorization bearer token is required');
return this.session.verify(token);
}
}
+8
View File
@@ -0,0 +1,8 @@
import { IsIn, IsString } from 'class-validator';
const JOB_TYPES = ['CLASSIFY', 'SUMMARIZE', 'EXTRACT'] as const;
export class RunJobDto {
@IsString() interactionId!: string;
@IsIn(JOB_TYPES) jobType!: (typeof JOB_TYPES)[number];
}
@@ -4,6 +4,7 @@ import { INFERENCE_PORT, LocalDeterministicInference } from './inference.port';
import { AiBudgetGuard } from './ai-budget.guard';
import { AiJobService } from './ai.service';
import { AiProjector } from './ai.projector';
import { AiController } from './ai.controller';
/**
* AI Interaction SDK (P7). Proposal layer only. `INFERENCE_PORT` binds the local
@@ -11,6 +12,7 @@ import { AiProjector } from './ai.projector';
*/
@Module({
imports: [OutboxModule],
controllers: [AiController],
providers: [
{ provide: INFERENCE_PORT, useClass: LocalDeterministicInference },
AiBudgetGuard,