From ce4e7840ec79b0284e36d9274bba196e42f3b57f Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 27 Feb 2026 21:21:36 -0800 Subject: [PATCH] fix: route AgentSkillService to per-tenant schema to match SDK engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously AgentSkillService wrote skills to public.agent_skills (TypeORM entity with tenantId column filter), while ClaudeAgentSdkEngine read from it0_t_{tenantId}.skills (per-tenant schema). The two tables were never connected, so any skill added via the CRUD API was invisible to the agent. This fix: - Rewrites AgentSkillService to use DataSource + raw SQL against the per-tenant schema it0_t_{tenantId}.skills - Maps API fields: script→content, enabled→is_active - Removes AgentSkillRepository and AgentSkill entity from module (no longer needed) - CRUD API response shape is unchanged (fields mapped back to script/enabled) Co-Authored-By: Claude Sonnet 4.6 --- .../agent-service/src/agent.module.ts | 5 +- .../services/agent-skill.service.ts | 138 ++++++++++++++---- 2 files changed, 108 insertions(+), 35 deletions(-) diff --git a/packages/services/agent-service/src/agent.module.ts b/packages/services/agent-service/src/agent.module.ts index a626775..b80d016 100644 --- a/packages/services/agent-service/src/agent.module.ts +++ b/packages/services/agent-service/src/agent.module.ts @@ -23,7 +23,6 @@ import { SessionRepository } from './infrastructure/repositories/session.reposit import { TaskRepository } from './infrastructure/repositories/task.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 { AgentConfigService } from './infrastructure/services/agent-config.service'; @@ -35,7 +34,6 @@ import { CommandRecord } from './domain/entities/command-record.entity'; import { StandingOrderRef } from './domain/entities/standing-order.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'; import { ConversationMessage } from './domain/entities/conversation-message.entity'; import { MessageRepository } from './infrastructure/repositories/message.repository'; @@ -47,7 +45,7 @@ import { ConversationContextService } from './domain/services/conversation-conte DatabaseModule.forRoot(), TypeOrmModule.forFeature([ AgentSession, AgentTask, CommandRecord, StandingOrderRef, - TenantAgentConfig, AgentConfig, AgentSkill, HookScript, + TenantAgentConfig, AgentConfig, HookScript, ConversationMessage, ]), ], @@ -72,7 +70,6 @@ import { ConversationContextService } from './domain/services/conversation-conte MessageRepository, TenantAgentConfigRepository, AgentConfigRepository, - AgentSkillRepository, HookScriptRepository, TenantAgentConfigService, AgentConfigService, diff --git a/packages/services/agent-service/src/infrastructure/services/agent-skill.service.ts b/packages/services/agent-service/src/infrastructure/services/agent-skill.service.ts index 294ea93..bf4eb87 100644 --- a/packages/services/agent-service/src/infrastructure/services/agent-skill.service.ts +++ b/packages/services/agent-service/src/infrastructure/services/agent-skill.service.ts @@ -1,9 +1,16 @@ /** * Service for managing per-tenant agent skills (CRUD). + * + * Skills are stored in the per-tenant schema table `it0_t_{tenantId}.skills` + * so the Claude Agent SDK engine can read them directly during task execution. + * + * Field mapping between API and DB: + * API `script` → DB `content` + * API `enabled` → DB `is_active` */ -import { Injectable } from '@nestjs/common'; -import { AgentSkillRepository } from '../repositories/agent-skill.repository'; -import { AgentSkill } from '../../domain/entities/agent-skill.entity'; +import { Injectable, Logger } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; export interface CreateSkillDto { name: string; @@ -23,43 +30,112 @@ export interface UpdateSkillDto { enabled?: boolean; } +export interface SkillRecord { + id: string; + tenant_id: string; + name: string; + description: string; + category: string; + script: string; // mapped from DB `content` + tags: string[]; + enabled: boolean; // mapped from DB `is_active` + createdAt: Date; + updatedAt: Date; +} + @Injectable() export class AgentSkillService { - constructor(private readonly repo: AgentSkillRepository) {} + private readonly logger = new Logger(AgentSkillService.name); - async findAllByTenant(tenantId: string): Promise<{ data: AgentSkill[]; total: number }> { - const skills = await this.repo.findByTenantId(tenantId); - return { data: skills, total: skills.length }; + constructor(@InjectDataSource() private readonly dataSource: DataSource) {} + + private schema(tenantId: string): string { + return `it0_t_${tenantId.replace(/[^a-zA-Z0-9_]/g, '')}`; } - async create(tenantId: string, dto: CreateSkillDto): Promise { - 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); + private toRecord(row: any): SkillRecord { + return { + id: row.id, + tenant_id: row.tenant_id, + name: row.name, + description: row.description ?? '', + category: row.category ?? 'custom', + script: row.content ?? '', + tags: row.tags ?? [], + enabled: row.is_active ?? true, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; } - async update(id: string, tenantId: string, dto: UpdateSkillDto): Promise { - 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 findAllByTenant(tenantId: string): Promise<{ data: SkillRecord[]; total: number }> { + const schema = this.schema(tenantId); + const rows = await this.dataSource.query( + `SELECT * FROM "${schema}".skills ORDER BY created_at DESC`, + ); + const data = rows.map((r: any) => this.toRecord(r)); + return { data, total: data.length }; + } + + async create(tenantId: string, dto: CreateSkillDto): Promise { + const schema = this.schema(tenantId); + const rows = await this.dataSource.query( + `INSERT INTO "${schema}".skills + (tenant_id, name, description, category, content, tags, is_active) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *`, + [ + tenantId, + dto.name, + dto.description ?? '', + dto.category ?? 'custom', + dto.script ?? '', + dto.tags ?? [], + dto.enabled ?? true, + ], + ); + return this.toRecord(rows[0]); + } + + async update(id: string, tenantId: string, dto: UpdateSkillDto): Promise { + const schema = this.schema(tenantId); + const existing = await this.dataSource.query( + `SELECT * FROM "${schema}".skills WHERE id = $1`, + [id], + ); + if (!existing.length) return null; + + const row = existing[0]; + const rows = await this.dataSource.query( + `UPDATE "${schema}".skills SET + name = $1, + description = $2, + category = $3, + content = $4, + tags = $5, + is_active = $6, + updated_at = now() + WHERE id = $7 + RETURNING *`, + [ + dto.name ?? row.name, + dto.description ?? row.description, + dto.category ?? row.category, + dto.script ?? row.content, + dto.tags ?? row.tags, + dto.enabled ?? row.is_active, + id, + ], + ); + return this.toRecord(rows[0]); } async delete(id: string, tenantId: string): Promise { - const skill = await this.repo.findOneByIdAndTenant(id, tenantId); - if (!skill) return false; - await this.repo.deleteById(id); - return true; + const schema = this.schema(tenantId); + const result = await this.dataSource.query( + `DELETE FROM "${schema}".skills WHERE id = $1 RETURNING id`, + [id], + ); + return result.length > 0; } }