feat: add backend controllers for agent config, skills, and hooks
Implement missing REST API endpoints that the web-admin frontend pages were calling but had no backend support: - GET/POST/PUT /api/v1/agent-config (engine, prompt, turns, budget, tools) - GET/POST/PUT/DELETE /api/v1/agent/skills (CRUD for agent skills) - GET/POST/PUT/DELETE /api/v1/agent/hooks (CRUD for hook scripts) Each endpoint includes entity, repository, service, and controller layers following the existing DDD + tenant-aware patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8b92abcce9
commit
5ee1227800
|
|
@ -6,6 +6,9 @@ import { AgentController } from './interfaces/rest/controllers/agent.controller'
|
||||||
import { SessionController } from './interfaces/rest/controllers/session.controller';
|
import { SessionController } from './interfaces/rest/controllers/session.controller';
|
||||||
import { RiskRulesController } from './interfaces/rest/controllers/risk-rules.controller';
|
import { RiskRulesController } from './interfaces/rest/controllers/risk-rules.controller';
|
||||||
import { TenantAgentConfigController } from './interfaces/rest/controllers/tenant-agent-config.controller';
|
import { TenantAgentConfigController } from './interfaces/rest/controllers/tenant-agent-config.controller';
|
||||||
|
import { AgentConfigController } from './interfaces/rest/controllers/agent-config.controller';
|
||||||
|
import { SkillsController } from './interfaces/rest/controllers/skills.controller';
|
||||||
|
import { HooksController } from './interfaces/rest/controllers/hooks.controller';
|
||||||
import { AgentStreamGateway } from './interfaces/ws/agent-stream.gateway';
|
import { AgentStreamGateway } from './interfaces/ws/agent-stream.gateway';
|
||||||
import { EngineRegistry } from './infrastructure/engines/engine-registry';
|
import { EngineRegistry } from './infrastructure/engines/engine-registry';
|
||||||
import { ClaudeCodeCliEngine } from './infrastructure/engines/claude-code-cli/claude-code-engine';
|
import { ClaudeCodeCliEngine } from './infrastructure/engines/claude-code-cli/claude-code-engine';
|
||||||
|
|
@ -19,20 +22,35 @@ import { AllowedToolsResolverService } from './domain/services/allowed-tools-res
|
||||||
import { SessionRepository } from './infrastructure/repositories/session.repository';
|
import { SessionRepository } from './infrastructure/repositories/session.repository';
|
||||||
import { TaskRepository } from './infrastructure/repositories/task.repository';
|
import { TaskRepository } from './infrastructure/repositories/task.repository';
|
||||||
import { TenantAgentConfigRepository } from './infrastructure/repositories/tenant-agent-config.repository';
|
import { TenantAgentConfigRepository } from './infrastructure/repositories/tenant-agent-config.repository';
|
||||||
|
import { AgentConfigRepository } from './infrastructure/repositories/agent-config.repository';
|
||||||
|
import { AgentSkillRepository } from './infrastructure/repositories/agent-skill.repository';
|
||||||
|
import { HookScriptRepository } from './infrastructure/repositories/hook-script.repository';
|
||||||
import { TenantAgentConfigService } from './infrastructure/services/tenant-agent-config.service';
|
import { TenantAgentConfigService } from './infrastructure/services/tenant-agent-config.service';
|
||||||
|
import { AgentConfigService } from './infrastructure/services/agent-config.service';
|
||||||
|
import { AgentSkillService } from './infrastructure/services/agent-skill.service';
|
||||||
|
import { HookScriptService } from './infrastructure/services/hook-script.service';
|
||||||
import { AgentSession } from './domain/entities/agent-session.entity';
|
import { AgentSession } from './domain/entities/agent-session.entity';
|
||||||
import { AgentTask } from './domain/entities/agent-task.entity';
|
import { AgentTask } from './domain/entities/agent-task.entity';
|
||||||
import { CommandRecord } from './domain/entities/command-record.entity';
|
import { CommandRecord } from './domain/entities/command-record.entity';
|
||||||
import { StandingOrderRef } from './domain/entities/standing-order.entity';
|
import { StandingOrderRef } from './domain/entities/standing-order.entity';
|
||||||
import { TenantAgentConfig } from './domain/entities/tenant-agent-config.entity';
|
import { TenantAgentConfig } from './domain/entities/tenant-agent-config.entity';
|
||||||
|
import { AgentConfig } from './domain/entities/agent-config.entity';
|
||||||
|
import { AgentSkill } from './domain/entities/agent-skill.entity';
|
||||||
|
import { HookScript } from './domain/entities/hook-script.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
DatabaseModule.forRoot(),
|
DatabaseModule.forRoot(),
|
||||||
TypeOrmModule.forFeature([AgentSession, AgentTask, CommandRecord, StandingOrderRef, TenantAgentConfig]),
|
TypeOrmModule.forFeature([
|
||||||
|
AgentSession, AgentTask, CommandRecord, StandingOrderRef,
|
||||||
|
TenantAgentConfig, AgentConfig, AgentSkill, HookScript,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [
|
||||||
|
AgentController, SessionController, RiskRulesController,
|
||||||
|
TenantAgentConfigController, AgentConfigController, SkillsController, HooksController,
|
||||||
],
|
],
|
||||||
controllers: [AgentController, SessionController, RiskRulesController, TenantAgentConfigController],
|
|
||||||
providers: [
|
providers: [
|
||||||
AgentStreamGateway,
|
AgentStreamGateway,
|
||||||
EngineRegistry,
|
EngineRegistry,
|
||||||
|
|
@ -47,7 +65,13 @@ import { TenantAgentConfig } from './domain/entities/tenant-agent-config.entity'
|
||||||
SessionRepository,
|
SessionRepository,
|
||||||
TaskRepository,
|
TaskRepository,
|
||||||
TenantAgentConfigRepository,
|
TenantAgentConfigRepository,
|
||||||
|
AgentConfigRepository,
|
||||||
|
AgentSkillRepository,
|
||||||
|
HookScriptRepository,
|
||||||
TenantAgentConfigService,
|
TenantAgentConfigService,
|
||||||
|
AgentConfigService,
|
||||||
|
AgentSkillService,
|
||||||
|
HookScriptService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AgentModule {}
|
export class AgentModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Per-tenant agent configuration entity.
|
||||||
|
*
|
||||||
|
* Stores the global agent settings for a tenant:
|
||||||
|
* - engine: which Claude engine to use (claude-cli or claude-api)
|
||||||
|
* - systemPrompt: base system prompt prepended to every agent interaction
|
||||||
|
* - maxTurns: maximum agentic turns per task
|
||||||
|
* - maxBudget: maximum USD spend per task
|
||||||
|
* - allowedTools: which SDK tools the agent may invoke
|
||||||
|
*/
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('agent_configs')
|
||||||
|
export class AgentConfig {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 20 })
|
||||||
|
tenantId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 30, default: 'claude-cli' })
|
||||||
|
engine!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '' })
|
||||||
|
systemPrompt!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 10 })
|
||||||
|
maxTurns!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'float', default: 5.0 })
|
||||||
|
maxBudget!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', array: true, default: "'{Bash,Read,Write,Glob,Grep}'" })
|
||||||
|
allowedTools!: string[];
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* Per-tenant agent skill entity.
|
||||||
|
*
|
||||||
|
* Skills are reusable script templates that the AI agent can execute.
|
||||||
|
* Each skill has a category, script content, and can be enabled/disabled.
|
||||||
|
*/
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('agent_skills')
|
||||||
|
export class AgentSkill {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 20 })
|
||||||
|
tenantId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 100 })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '' })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 30, default: 'custom' })
|
||||||
|
category!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '' })
|
||||||
|
script!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', array: true, default: '{}' })
|
||||||
|
tags!: string[];
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
enabled!: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Per-tenant hook script entity.
|
||||||
|
*
|
||||||
|
* Hook scripts run at specific lifecycle points during agent tool execution:
|
||||||
|
* - PreToolUse: runs before a tool is invoked (can block execution)
|
||||||
|
* - PostToolUse: runs after a tool completes (for auditing/logging)
|
||||||
|
* - PreNotification: runs before sending a notification
|
||||||
|
* - PostNotification: runs after sending a notification
|
||||||
|
*/
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('hook_scripts')
|
||||||
|
export class HookScript {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 20 })
|
||||||
|
tenantId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 100 })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 30 })
|
||||||
|
event!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 200, default: '*' })
|
||||||
|
toolPattern!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '' })
|
||||||
|
script!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 30 })
|
||||||
|
timeout!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
enabled!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '' })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Repository for AgentConfig.
|
||||||
|
* Extends TenantAwareRepository for schema-per-tenant isolation.
|
||||||
|
*/
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { TenantAwareRepository } from '@it0/database';
|
||||||
|
import { AgentConfig } from '../../domain/entities/agent-config.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentConfigRepository extends TenantAwareRepository<AgentConfig> {
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
super(dataSource, AgentConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTenantId(tenantId: string): Promise<AgentConfig | null> {
|
||||||
|
const repo = await this.getRepository();
|
||||||
|
return repo.findOneBy({ tenantId } as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Repository for AgentSkill.
|
||||||
|
* Extends TenantAwareRepository for schema-per-tenant isolation.
|
||||||
|
*/
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { TenantAwareRepository } from '@it0/database';
|
||||||
|
import { AgentSkill } from '../../domain/entities/agent-skill.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentSkillRepository extends TenantAwareRepository<AgentSkill> {
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
super(dataSource, AgentSkill);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTenantId(tenantId: string): Promise<AgentSkill[]> {
|
||||||
|
const repo = await this.getRepository();
|
||||||
|
return repo.find({ where: { tenantId } as any, order: { createdAt: 'DESC' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByIdAndTenant(id: string, tenantId: string): Promise<AgentSkill | null> {
|
||||||
|
const repo = await this.getRepository();
|
||||||
|
return repo.findOneBy({ id, tenantId } as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteById(id: string): Promise<void> {
|
||||||
|
const repo = await this.getRepository();
|
||||||
|
await repo.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Repository for HookScript.
|
||||||
|
* Extends TenantAwareRepository for schema-per-tenant isolation.
|
||||||
|
*/
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { TenantAwareRepository } from '@it0/database';
|
||||||
|
import { HookScript } from '../../domain/entities/hook-script.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HookScriptRepository extends TenantAwareRepository<HookScript> {
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
super(dataSource, HookScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTenantId(tenantId: string): Promise<HookScript[]> {
|
||||||
|
const repo = await this.getRepository();
|
||||||
|
return repo.find({ where: { tenantId } as any, order: { createdAt: 'DESC' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByIdAndTenant(id: string, tenantId: string): Promise<HookScript | null> {
|
||||||
|
const repo = await this.getRepository();
|
||||||
|
return repo.findOneBy({ id, tenantId } as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteById(id: string): Promise<void> {
|
||||||
|
const repo = await this.getRepository();
|
||||||
|
await repo.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Service for managing per-tenant agent configuration (engine, system prompt, max turns, budget, tools).
|
||||||
|
*/
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AgentConfigRepository } from '../repositories/agent-config.repository';
|
||||||
|
import { AgentConfig } from '../../domain/entities/agent-config.entity';
|
||||||
|
|
||||||
|
export interface UpdateAgentConfigDto {
|
||||||
|
engine?: string;
|
||||||
|
system_prompt?: string;
|
||||||
|
max_turns?: number;
|
||||||
|
max_budget?: number;
|
||||||
|
allowed_tools?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentConfigService {
|
||||||
|
constructor(private readonly repo: AgentConfigRepository) {}
|
||||||
|
|
||||||
|
async findByTenantId(tenantId: string): Promise<AgentConfig | null> {
|
||||||
|
return this.repo.findByTenantId(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(tenantId: string, dto: UpdateAgentConfigDto): Promise<AgentConfig> {
|
||||||
|
const config = new AgentConfig();
|
||||||
|
config.tenantId = tenantId;
|
||||||
|
this.applyDto(config, dto);
|
||||||
|
return this.repo.save(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, tenantId: string, dto: UpdateAgentConfigDto): Promise<AgentConfig | null> {
|
||||||
|
const config = await this.repo.findByTenantId(tenantId);
|
||||||
|
if (!config || config.id !== id) return null;
|
||||||
|
this.applyDto(config, dto);
|
||||||
|
return this.repo.save(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyDto(config: AgentConfig, dto: UpdateAgentConfigDto): void {
|
||||||
|
if (dto.engine !== undefined) config.engine = dto.engine;
|
||||||
|
if (dto.system_prompt !== undefined) config.systemPrompt = dto.system_prompt;
|
||||||
|
if (dto.max_turns !== undefined) config.maxTurns = dto.max_turns;
|
||||||
|
if (dto.max_budget !== undefined) config.maxBudget = dto.max_budget;
|
||||||
|
if (dto.allowed_tools !== undefined) config.allowedTools = dto.allowed_tools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* Service for managing per-tenant agent skills (CRUD).
|
||||||
|
*/
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AgentSkillRepository } from '../repositories/agent-skill.repository';
|
||||||
|
import { AgentSkill } from '../../domain/entities/agent-skill.entity';
|
||||||
|
|
||||||
|
export interface CreateSkillDto {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
|
script?: string;
|
||||||
|
tags?: string[];
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSkillDto {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
|
script?: string;
|
||||||
|
tags?: string[];
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentSkillService {
|
||||||
|
constructor(private readonly repo: AgentSkillRepository) {}
|
||||||
|
|
||||||
|
async findAllByTenant(tenantId: string): Promise<{ data: AgentSkill[]; total: number }> {
|
||||||
|
const skills = await this.repo.findByTenantId(tenantId);
|
||||||
|
return { data: skills, total: skills.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(tenantId: string, dto: CreateSkillDto): Promise<AgentSkill> {
|
||||||
|
const skill = new AgentSkill();
|
||||||
|
skill.tenantId = tenantId;
|
||||||
|
skill.name = dto.name;
|
||||||
|
skill.description = dto.description ?? '';
|
||||||
|
skill.category = dto.category ?? 'custom';
|
||||||
|
skill.script = dto.script ?? '';
|
||||||
|
skill.tags = dto.tags ?? [];
|
||||||
|
skill.enabled = dto.enabled ?? true;
|
||||||
|
return this.repo.save(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, tenantId: string, dto: UpdateSkillDto): Promise<AgentSkill | null> {
|
||||||
|
const skill = await this.repo.findOneByIdAndTenant(id, tenantId);
|
||||||
|
if (!skill) return null;
|
||||||
|
if (dto.name !== undefined) skill.name = dto.name;
|
||||||
|
if (dto.description !== undefined) skill.description = dto.description;
|
||||||
|
if (dto.category !== undefined) skill.category = dto.category;
|
||||||
|
if (dto.script !== undefined) skill.script = dto.script;
|
||||||
|
if (dto.tags !== undefined) skill.tags = dto.tags;
|
||||||
|
if (dto.enabled !== undefined) skill.enabled = dto.enabled;
|
||||||
|
return this.repo.save(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string, tenantId: string): Promise<boolean> {
|
||||||
|
const skill = await this.repo.findOneByIdAndTenant(id, tenantId);
|
||||||
|
if (!skill) return false;
|
||||||
|
await this.repo.deleteById(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Service for managing per-tenant hook scripts (CRUD).
|
||||||
|
*/
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { HookScriptRepository } from '../repositories/hook-script.repository';
|
||||||
|
import { HookScript } from '../../domain/entities/hook-script.entity';
|
||||||
|
|
||||||
|
export interface CreateHookDto {
|
||||||
|
name: string;
|
||||||
|
event: string;
|
||||||
|
toolPattern?: string;
|
||||||
|
script: string;
|
||||||
|
timeout?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateHookDto {
|
||||||
|
name?: string;
|
||||||
|
event?: string;
|
||||||
|
toolPattern?: string;
|
||||||
|
script?: string;
|
||||||
|
timeout?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HookScriptService {
|
||||||
|
constructor(private readonly repo: HookScriptRepository) {}
|
||||||
|
|
||||||
|
async findAllByTenant(tenantId: string): Promise<{ data: HookScript[]; total: number }> {
|
||||||
|
const hooks = await this.repo.findByTenantId(tenantId);
|
||||||
|
return { data: hooks, total: hooks.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(tenantId: string, dto: CreateHookDto): Promise<HookScript> {
|
||||||
|
const hook = new HookScript();
|
||||||
|
hook.tenantId = tenantId;
|
||||||
|
hook.name = dto.name;
|
||||||
|
hook.event = dto.event;
|
||||||
|
hook.toolPattern = dto.toolPattern ?? '*';
|
||||||
|
hook.script = dto.script;
|
||||||
|
hook.timeout = dto.timeout ?? 30;
|
||||||
|
hook.enabled = dto.enabled ?? true;
|
||||||
|
hook.description = dto.description ?? '';
|
||||||
|
return this.repo.save(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, tenantId: string, dto: UpdateHookDto): Promise<HookScript | null> {
|
||||||
|
const hook = await this.repo.findOneByIdAndTenant(id, tenantId);
|
||||||
|
if (!hook) return null;
|
||||||
|
if (dto.name !== undefined) hook.name = dto.name;
|
||||||
|
if (dto.event !== undefined) hook.event = dto.event;
|
||||||
|
if (dto.toolPattern !== undefined) hook.toolPattern = dto.toolPattern;
|
||||||
|
if (dto.script !== undefined) hook.script = dto.script;
|
||||||
|
if (dto.timeout !== undefined) hook.timeout = dto.timeout;
|
||||||
|
if (dto.enabled !== undefined) hook.enabled = dto.enabled;
|
||||||
|
if (dto.description !== undefined) hook.description = dto.description;
|
||||||
|
return this.repo.save(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string, tenantId: string): Promise<boolean> {
|
||||||
|
const hook = await this.repo.findOneByIdAndTenant(id, tenantId);
|
||||||
|
if (!hook) return false;
|
||||||
|
await this.repo.deleteById(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* REST controller for per-tenant agent configuration (engine, prompt, turns, budget, tools).
|
||||||
|
*
|
||||||
|
* Endpoints (all JWT-protected):
|
||||||
|
* GET /api/v1/agent-config → Get current tenant's config (returns defaults if none set)
|
||||||
|
* POST /api/v1/agent-config → Create new config
|
||||||
|
* PUT /api/v1/agent-config/:id → Update existing config
|
||||||
|
*/
|
||||||
|
import { Controller, Get, Post, Put, Body, Param, UseGuards } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { TenantId } from '@it0/common';
|
||||||
|
import { AgentConfigService, UpdateAgentConfigDto } from '../../../infrastructure/services/agent-config.service';
|
||||||
|
|
||||||
|
@Controller('api/v1/agent-config')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
export class AgentConfigController {
|
||||||
|
constructor(private readonly configService: AgentConfigService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getConfig(@TenantId() tenantId: string) {
|
||||||
|
const config = await this.configService.findByTenantId(tenantId);
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
engine: 'claude-cli',
|
||||||
|
system_prompt: '',
|
||||||
|
max_turns: 10,
|
||||||
|
max_budget: 5.0,
|
||||||
|
allowed_tools: ['Bash', 'Read', 'Write', 'Glob', 'Grep'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: config.id,
|
||||||
|
engine: config.engine,
|
||||||
|
system_prompt: config.systemPrompt,
|
||||||
|
max_turns: config.maxTurns,
|
||||||
|
max_budget: config.maxBudget,
|
||||||
|
allowed_tools: config.allowedTools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async createConfig(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Body() dto: UpdateAgentConfigDto,
|
||||||
|
) {
|
||||||
|
const config = await this.configService.create(tenantId, dto);
|
||||||
|
return {
|
||||||
|
id: config.id,
|
||||||
|
engine: config.engine,
|
||||||
|
system_prompt: config.systemPrompt,
|
||||||
|
max_turns: config.maxTurns,
|
||||||
|
max_budget: config.maxBudget,
|
||||||
|
allowed_tools: config.allowedTools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
async updateConfig(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateAgentConfigDto,
|
||||||
|
) {
|
||||||
|
const config = await this.configService.update(id, tenantId, dto);
|
||||||
|
if (!config) {
|
||||||
|
return { error: 'Config not found' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: config.id,
|
||||||
|
engine: config.engine,
|
||||||
|
system_prompt: config.systemPrompt,
|
||||||
|
max_turns: config.maxTurns,
|
||||||
|
max_budget: config.maxBudget,
|
||||||
|
allowed_tools: config.allowedTools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* REST controller for per-tenant hook scripts (CRUD).
|
||||||
|
*
|
||||||
|
* Endpoints (all JWT-protected):
|
||||||
|
* GET /api/v1/agent/hooks → List all hooks for current tenant
|
||||||
|
* POST /api/v1/agent/hooks → Create a new hook
|
||||||
|
* PUT /api/v1/agent/hooks/:id → Update an existing hook
|
||||||
|
* DELETE /api/v1/agent/hooks/:id → Delete a hook
|
||||||
|
*/
|
||||||
|
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, NotFoundException } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { TenantId } from '@it0/common';
|
||||||
|
import { HookScriptService, CreateHookDto, UpdateHookDto } from '../../../infrastructure/services/hook-script.service';
|
||||||
|
|
||||||
|
@Controller('api/v1/agent/hooks')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
export class HooksController {
|
||||||
|
constructor(private readonly hookService: HookScriptService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async list(@TenantId() tenantId: string) {
|
||||||
|
return this.hookService.findAllByTenant(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async create(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Body() dto: CreateHookDto,
|
||||||
|
) {
|
||||||
|
return this.hookService.create(tenantId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
async update(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateHookDto,
|
||||||
|
) {
|
||||||
|
const hook = await this.hookService.update(id, tenantId, dto);
|
||||||
|
if (!hook) {
|
||||||
|
throw new NotFoundException('Hook not found');
|
||||||
|
}
|
||||||
|
return hook;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async remove(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
) {
|
||||||
|
const deleted = await this.hookService.delete(id, tenantId);
|
||||||
|
if (!deleted) {
|
||||||
|
throw new NotFoundException('Hook not found');
|
||||||
|
}
|
||||||
|
return { message: 'Hook deleted' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* REST controller for per-tenant agent skills (CRUD).
|
||||||
|
*
|
||||||
|
* Endpoints (all JWT-protected):
|
||||||
|
* GET /api/v1/agent/skills → List all skills for current tenant
|
||||||
|
* POST /api/v1/agent/skills → Create a new skill
|
||||||
|
* PUT /api/v1/agent/skills/:id → Update an existing skill
|
||||||
|
* DELETE /api/v1/agent/skills/:id → Delete a skill
|
||||||
|
*/
|
||||||
|
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, NotFoundException } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { TenantId } from '@it0/common';
|
||||||
|
import { AgentSkillService, CreateSkillDto, UpdateSkillDto } from '../../../infrastructure/services/agent-skill.service';
|
||||||
|
|
||||||
|
@Controller('api/v1/agent/skills')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
export class SkillsController {
|
||||||
|
constructor(private readonly skillService: AgentSkillService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async list(@TenantId() tenantId: string) {
|
||||||
|
return this.skillService.findAllByTenant(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async create(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Body() dto: CreateSkillDto,
|
||||||
|
) {
|
||||||
|
return this.skillService.create(tenantId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
async update(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateSkillDto,
|
||||||
|
) {
|
||||||
|
const skill = await this.skillService.update(id, tenantId, dto);
|
||||||
|
if (!skill) {
|
||||||
|
throw new NotFoundException('Skill not found');
|
||||||
|
}
|
||||||
|
return skill;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async remove(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
) {
|
||||||
|
const deleted = await this.skillService.delete(id, tenantId);
|
||||||
|
if (!deleted) {
|
||||||
|
throw new NotFoundException('Skill not found');
|
||||||
|
}
|
||||||
|
return { message: 'Skill deleted' };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue