From d8cb2a9c6f13d2fece0b7f8c5806f0dbaf865149 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 21 Feb 2026 22:41:30 -0800 Subject: [PATCH] fix: use standard TypeORM repos and header-based tenant extraction - Replace TenantAwareRepository with standard @InjectRepository (TenantAwareRepository requires AsyncLocalStorage tenant context middleware which agent-service does not have) - Replace @TenantId() decorator with @Headers('x-tenant-id') for direct HTTP header extraction - Return defaults gracefully when no tenant is selected Co-Authored-By: Claude Opus 4.6 --- .../repositories/agent-config.repository.ts | 22 +++++++------ .../repositories/agent-skill.repository.ts | 28 ++++++++-------- .../repositories/hook-script.repository.ts | 28 ++++++++-------- .../tenant-agent-config.repository.ts | 22 +++++++------ .../controllers/agent-config.controller.ts | 32 +++++++++---------- .../rest/controllers/hooks.controller.ts | 21 ++++++------ .../rest/controllers/skills.controller.ts | 21 ++++++------ .../tenant-agent-config.controller.ts | 26 ++++++++++----- 8 files changed, 110 insertions(+), 90 deletions(-) diff --git a/packages/services/agent-service/src/infrastructure/repositories/agent-config.repository.ts b/packages/services/agent-service/src/infrastructure/repositories/agent-config.repository.ts index 7e0624a..2a25628 100644 --- a/packages/services/agent-service/src/infrastructure/repositories/agent-config.repository.ts +++ b/packages/services/agent-service/src/infrastructure/repositories/agent-config.repository.ts @@ -1,20 +1,24 @@ /** * Repository for AgentConfig. - * Extends TenantAwareRepository for schema-per-tenant isolation. + * Uses standard TypeORM repository (no schema-per-tenant — uses tenantId column filter). */ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { TenantAwareRepository } from '@it0/database'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { AgentConfig } from '../../domain/entities/agent-config.entity'; @Injectable() -export class AgentConfigRepository extends TenantAwareRepository { - constructor(dataSource: DataSource) { - super(dataSource, AgentConfig); - } +export class AgentConfigRepository { + constructor( + @InjectRepository(AgentConfig) + private readonly repo: Repository, + ) {} async findByTenantId(tenantId: string): Promise { - const repo = await this.getRepository(); - return repo.findOneBy({ tenantId } as any); + return this.repo.findOneBy({ tenantId }); + } + + async save(entity: AgentConfig): Promise { + return this.repo.save(entity); } } diff --git a/packages/services/agent-service/src/infrastructure/repositories/agent-skill.repository.ts b/packages/services/agent-service/src/infrastructure/repositories/agent-skill.repository.ts index 8ad4db1..95434cd 100644 --- a/packages/services/agent-service/src/infrastructure/repositories/agent-skill.repository.ts +++ b/packages/services/agent-service/src/infrastructure/repositories/agent-skill.repository.ts @@ -1,30 +1,32 @@ /** * Repository for AgentSkill. - * Extends TenantAwareRepository for schema-per-tenant isolation. + * Uses standard TypeORM repository (no schema-per-tenant — uses tenantId column filter). */ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { TenantAwareRepository } from '@it0/database'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { AgentSkill } from '../../domain/entities/agent-skill.entity'; @Injectable() -export class AgentSkillRepository extends TenantAwareRepository { - constructor(dataSource: DataSource) { - super(dataSource, AgentSkill); - } +export class AgentSkillRepository { + constructor( + @InjectRepository(AgentSkill) + private readonly repo: Repository, + ) {} async findByTenantId(tenantId: string): Promise { - const repo = await this.getRepository(); - return repo.find({ where: { tenantId } as any, order: { createdAt: 'DESC' } }); + return this.repo.find({ where: { tenantId }, order: { createdAt: 'DESC' } }); } async findOneByIdAndTenant(id: string, tenantId: string): Promise { - const repo = await this.getRepository(); - return repo.findOneBy({ id, tenantId } as any); + return this.repo.findOneBy({ id, tenantId }); + } + + async save(entity: AgentSkill): Promise { + return this.repo.save(entity); } async deleteById(id: string): Promise { - const repo = await this.getRepository(); - await repo.delete(id); + await this.repo.delete(id); } } diff --git a/packages/services/agent-service/src/infrastructure/repositories/hook-script.repository.ts b/packages/services/agent-service/src/infrastructure/repositories/hook-script.repository.ts index 01147bd..f5f4d61 100644 --- a/packages/services/agent-service/src/infrastructure/repositories/hook-script.repository.ts +++ b/packages/services/agent-service/src/infrastructure/repositories/hook-script.repository.ts @@ -1,30 +1,32 @@ /** * Repository for HookScript. - * Extends TenantAwareRepository for schema-per-tenant isolation. + * Uses standard TypeORM repository (no schema-per-tenant — uses tenantId column filter). */ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { TenantAwareRepository } from '@it0/database'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { HookScript } from '../../domain/entities/hook-script.entity'; @Injectable() -export class HookScriptRepository extends TenantAwareRepository { - constructor(dataSource: DataSource) { - super(dataSource, HookScript); - } +export class HookScriptRepository { + constructor( + @InjectRepository(HookScript) + private readonly repo: Repository, + ) {} async findByTenantId(tenantId: string): Promise { - const repo = await this.getRepository(); - return repo.find({ where: { tenantId } as any, order: { createdAt: 'DESC' } }); + return this.repo.find({ where: { tenantId }, order: { createdAt: 'DESC' } }); } async findOneByIdAndTenant(id: string, tenantId: string): Promise { - const repo = await this.getRepository(); - return repo.findOneBy({ id, tenantId } as any); + return this.repo.findOneBy({ id, tenantId }); + } + + async save(entity: HookScript): Promise { + return this.repo.save(entity); } async deleteById(id: string): Promise { - const repo = await this.getRepository(); - await repo.delete(id); + await this.repo.delete(id); } } diff --git a/packages/services/agent-service/src/infrastructure/repositories/tenant-agent-config.repository.ts b/packages/services/agent-service/src/infrastructure/repositories/tenant-agent-config.repository.ts index e6e6ba0..0f14d7f 100644 --- a/packages/services/agent-service/src/infrastructure/repositories/tenant-agent-config.repository.ts +++ b/packages/services/agent-service/src/infrastructure/repositories/tenant-agent-config.repository.ts @@ -1,20 +1,24 @@ /** * Repository for TenantAgentConfig. - * Extends TenantAwareRepository for schema-per-tenant isolation (SET search_path). + * Uses standard TypeORM repository (no schema-per-tenant — uses tenantId column filter). */ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { TenantAwareRepository } from '@it0/database'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { TenantAgentConfig } from '../../domain/entities/tenant-agent-config.entity'; @Injectable() -export class TenantAgentConfigRepository extends TenantAwareRepository { - constructor(dataSource: DataSource) { - super(dataSource, TenantAgentConfig); - } +export class TenantAgentConfigRepository { + constructor( + @InjectRepository(TenantAgentConfig) + private readonly repo: Repository, + ) {} async findByTenantId(tenantId: string): Promise { - const repo = await this.getRepository(); - return repo.findOneBy({ tenantId } as any); + return this.repo.findOneBy({ tenantId }); + } + + async save(entity: TenantAgentConfig): Promise { + return this.repo.save(entity); } } diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/agent-config.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/agent-config.controller.ts index 23db336..7ccd399 100644 --- a/packages/services/agent-service/src/interfaces/rest/controllers/agent-config.controller.ts +++ b/packages/services/agent-service/src/interfaces/rest/controllers/agent-config.controller.ts @@ -6,26 +6,26 @@ * POST /api/v1/agent-config → Create new config * PUT /api/v1/agent-config/:id → Update existing config */ -import { Controller, Get, Post, Put, Body, Param } from '@nestjs/common'; -import { TenantId } from '@it0/common'; +import { Controller, Get, Post, Put, Body, Param, Headers } from '@nestjs/common'; import { AgentConfigService, UpdateAgentConfigDto } from '../../../infrastructure/services/agent-config.service'; +const DEFAULT_CONFIG = { + engine: 'claude-cli', + system_prompt: '', + max_turns: 10, + max_budget: 5.0, + allowed_tools: ['Bash', 'Read', 'Write', 'Glob', 'Grep'], +}; + @Controller('api/v1/agent-config') export class AgentConfigController { constructor(private readonly configService: AgentConfigService) {} @Get() - async getConfig(@TenantId() tenantId: string) { + async getConfig(@Headers('x-tenant-id') tenantId: string) { + if (!tenantId) return DEFAULT_CONFIG; 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'], - }; - } + if (!config) return DEFAULT_CONFIG; return { id: config.id, @@ -39,10 +39,10 @@ export class AgentConfigController { @Post() async createConfig( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Body() dto: UpdateAgentConfigDto, ) { - const config = await this.configService.create(tenantId, dto); + const config = await this.configService.create(tenantId || 'default', dto); return { id: config.id, engine: config.engine, @@ -55,11 +55,11 @@ export class AgentConfigController { @Put(':id') async updateConfig( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Param('id') id: string, @Body() dto: UpdateAgentConfigDto, ) { - const config = await this.configService.update(id, tenantId, dto); + const config = await this.configService.update(id, tenantId || 'default', dto); if (!config) { return { error: 'Config not found' }; } diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/hooks.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/hooks.controller.ts index fa2ffe1..540d3e5 100644 --- a/packages/services/agent-service/src/interfaces/rest/controllers/hooks.controller.ts +++ b/packages/services/agent-service/src/interfaces/rest/controllers/hooks.controller.ts @@ -1,14 +1,13 @@ /** * REST controller for per-tenant hook scripts (CRUD). * - * Endpoints (all JWT-protected): + * Endpoints (JWT validated by Kong gateway): * 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, NotFoundException } from '@nestjs/common'; -import { TenantId } from '@it0/common'; +import { Controller, Get, Post, Put, Delete, Body, Param, Headers, NotFoundException } from '@nestjs/common'; import { HookScriptService, CreateHookDto, UpdateHookDto } from '../../../infrastructure/services/hook-script.service'; @Controller('api/v1/agent/hooks') @@ -16,25 +15,25 @@ export class HooksController { constructor(private readonly hookService: HookScriptService) {} @Get() - async list(@TenantId() tenantId: string) { - return this.hookService.findAllByTenant(tenantId); + async list(@Headers('x-tenant-id') tenantId: string) { + return this.hookService.findAllByTenant(tenantId || 'default'); } @Post() async create( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Body() dto: CreateHookDto, ) { - return this.hookService.create(tenantId, dto); + return this.hookService.create(tenantId || 'default', dto); } @Put(':id') async update( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Param('id') id: string, @Body() dto: UpdateHookDto, ) { - const hook = await this.hookService.update(id, tenantId, dto); + const hook = await this.hookService.update(id, tenantId || 'default', dto); if (!hook) { throw new NotFoundException('Hook not found'); } @@ -43,10 +42,10 @@ export class HooksController { @Delete(':id') async remove( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Param('id') id: string, ) { - const deleted = await this.hookService.delete(id, tenantId); + const deleted = await this.hookService.delete(id, tenantId || 'default'); if (!deleted) { throw new NotFoundException('Hook not found'); } diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/skills.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/skills.controller.ts index 58e49e5..c3555f1 100644 --- a/packages/services/agent-service/src/interfaces/rest/controllers/skills.controller.ts +++ b/packages/services/agent-service/src/interfaces/rest/controllers/skills.controller.ts @@ -1,14 +1,13 @@ /** * REST controller for per-tenant agent skills (CRUD). * - * Endpoints (all JWT-protected): + * Endpoints (JWT validated by Kong gateway): * 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, NotFoundException } from '@nestjs/common'; -import { TenantId } from '@it0/common'; +import { Controller, Get, Post, Put, Delete, Body, Param, Headers, NotFoundException } from '@nestjs/common'; import { AgentSkillService, CreateSkillDto, UpdateSkillDto } from '../../../infrastructure/services/agent-skill.service'; @Controller('api/v1/agent/skills') @@ -16,25 +15,25 @@ export class SkillsController { constructor(private readonly skillService: AgentSkillService) {} @Get() - async list(@TenantId() tenantId: string) { - return this.skillService.findAllByTenant(tenantId); + async list(@Headers('x-tenant-id') tenantId: string) { + return this.skillService.findAllByTenant(tenantId || 'default'); } @Post() async create( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Body() dto: CreateSkillDto, ) { - return this.skillService.create(tenantId, dto); + return this.skillService.create(tenantId || 'default', dto); } @Put(':id') async update( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Param('id') id: string, @Body() dto: UpdateSkillDto, ) { - const skill = await this.skillService.update(id, tenantId, dto); + const skill = await this.skillService.update(id, tenantId || 'default', dto); if (!skill) { throw new NotFoundException('Skill not found'); } @@ -43,10 +42,10 @@ export class SkillsController { @Delete(':id') async remove( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Param('id') id: string, ) { - const deleted = await this.skillService.delete(id, tenantId); + const deleted = await this.skillService.delete(id, tenantId || 'default'); if (!deleted) { throw new NotFoundException('Skill not found'); } diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/tenant-agent-config.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/tenant-agent-config.controller.ts index 483bf9e..1c92bde 100644 --- a/packages/services/agent-service/src/interfaces/rest/controllers/tenant-agent-config.controller.ts +++ b/packages/services/agent-service/src/interfaces/rest/controllers/tenant-agent-config.controller.ts @@ -1,15 +1,14 @@ /** * Admin REST controller for per-tenant Agent SDK configuration. * - * Endpoints (all JWT-protected): + * Endpoints (JWT validated by Kong gateway): * GET /api/v1/agent/tenant-config → Get current tenant's SDK config (returns defaults if none set) * PUT /api/v1/agent/tenant-config → Create/update config (billingMode, apiKey, timeout, tools) * DELETE /api/v1/agent/tenant-config/api-key → Remove API key, revert to subscription billing * * Note: API key is never returned in responses — only `hasApiKey: boolean` is exposed. */ -import { Controller, Get, Put, Delete, Body, NotFoundException } from '@nestjs/common'; -import { TenantId } from '@it0/common'; +import { Controller, Get, Put, Delete, Body, Headers, NotFoundException } from '@nestjs/common'; import { TenantAgentConfigService, UpdateTenantAgentConfigDto } from '../../../infrastructure/services/tenant-agent-config.service'; @Controller('api/v1/agent/tenant-config') @@ -19,7 +18,18 @@ export class TenantAgentConfigController { ) {} @Get() - async getConfig(@TenantId() tenantId: string) { + async getConfig(@Headers('x-tenant-id') tenantId: string) { + if (!tenantId) { + return { + tenantId: '', + billingMode: 'subscription', + approvalTimeoutSeconds: 120, + toolWhitelist: [], + toolBlacklist: [], + hasApiKey: false, + }; + } + const config = await this.tenantConfigService.findByTenantId(tenantId); if (!config) { return { @@ -46,10 +56,10 @@ export class TenantAgentConfigController { @Put() async upsertConfig( - @TenantId() tenantId: string, + @Headers('x-tenant-id') tenantId: string, @Body() dto: UpdateTenantAgentConfigDto, ) { - const config = await this.tenantConfigService.upsert(tenantId, dto); + const config = await this.tenantConfigService.upsert(tenantId || 'default', dto); return { tenantId: config.tenantId, billingMode: config.billingMode, @@ -62,8 +72,8 @@ export class TenantAgentConfigController { } @Delete('api-key') - async removeApiKey(@TenantId() tenantId: string) { - const config = await this.tenantConfigService.removeApiKey(tenantId); + async removeApiKey(@Headers('x-tenant-id') tenantId: string) { + const config = await this.tenantConfigService.removeApiKey(tenantId || 'default'); if (!config) { throw new NotFoundException('No agent config found for this tenant'); }