From b87cebf465b7edfcc1985f605e1388187c907b7f Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 8 Mar 2026 03:05:15 -0700 Subject: [PATCH] feat(agent): inject userId into system prompt + fix agent-instance nullable columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SystemPromptBuilder: add userId/userEmail to context, expose internal API curl commands for OpenClaw creation - agent.controller.ts: extract userId from JWT, build system prompt via SystemPromptBuilder so iAgent knows current user - agent.module.ts: register SystemPromptBuilder as provider - agent-instance.entity.ts: make serverHost/sshUser nullable (pool mode doesn't set these upfront) - DB: ALTER TABLE agent_instances DROP NOT NULL on server_host/ssh_user Now iAgent can create 小龙虾 instances autonomously when user asks in natural language. Co-Authored-By: Claude Sonnet 4.6 --- .../agent-service/src/agent.module.ts | 2 ++ .../domain/entities/agent-instance.entity.ts | 8 ++--- .../claude-code-cli/system-prompt-builder.ts | 29 +++++++++++-------- .../rest/controllers/agent.controller.ts | 16 ++++++++-- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/services/agent-service/src/agent.module.ts b/packages/services/agent-service/src/agent.module.ts index 0b369f8..68eaac5 100644 --- a/packages/services/agent-service/src/agent.module.ts +++ b/packages/services/agent-service/src/agent.module.ts @@ -52,6 +52,7 @@ import { AgentInstance } from './domain/entities/agent-instance.entity'; import { AgentInstanceRepository } from './infrastructure/repositories/agent-instance.repository'; import { AgentInstanceDeployService } from './infrastructure/services/agent-instance-deploy.service'; import { AgentInstanceController } from './interfaces/rest/controllers/agent-instance.controller'; +import { SystemPromptBuilder } from './infrastructure/engines/claude-code-cli/system-prompt-builder'; @Module({ imports: [ @@ -99,6 +100,7 @@ import { AgentInstanceController } from './interfaces/rest/controllers/agent-ins OpenAISttService, AgentInstanceRepository, AgentInstanceDeployService, + SystemPromptBuilder, ], }) export class AgentModule {} diff --git a/packages/services/agent-service/src/domain/entities/agent-instance.entity.ts b/packages/services/agent-service/src/domain/entities/agent-instance.entity.ts index cedbb61..bbb39e0 100644 --- a/packages/services/agent-service/src/domain/entities/agent-instance.entity.ts +++ b/packages/services/agent-service/src/domain/entities/agent-instance.entity.ts @@ -17,14 +17,14 @@ export class AgentInstance { @Column({ type: 'uuid', name: 'pool_server_id', nullable: true }) poolServerId?: string; - @Column({ type: 'varchar', length: 255, name: 'server_host' }) - serverHost!: string; + @Column({ type: 'varchar', length: 255, name: 'server_host', nullable: true }) + serverHost?: string; @Column({ type: 'int', name: 'ssh_port', default: 22 }) sshPort!: number; - @Column({ type: 'varchar', length: 100, name: 'ssh_user' }) - sshUser!: string; + @Column({ type: 'varchar', length: 100, name: 'ssh_user', nullable: true }) + sshUser?: string; @Column({ type: 'varchar', length: 150, name: 'container_name', unique: true }) containerName!: string; diff --git a/packages/services/agent-service/src/infrastructure/engines/claude-code-cli/system-prompt-builder.ts b/packages/services/agent-service/src/infrastructure/engines/claude-code-cli/system-prompt-builder.ts index 1b8a78f..90f1f2b 100644 --- a/packages/services/agent-service/src/infrastructure/engines/claude-code-cli/system-prompt-builder.ts +++ b/packages/services/agent-service/src/infrastructure/engines/claude-code-cli/system-prompt-builder.ts @@ -5,6 +5,8 @@ import * as path from 'path'; export interface SystemPromptContext { tenantId: string; + userId?: string; + userEmail?: string; serverContext?: string; skills?: string[]; riskBoundary?: string; @@ -33,21 +35,24 @@ export class SystemPromptBuilder { 'You are iAgent, an AI-powered server cluster operations assistant built on IT0. ' + 'You help users manage server infrastructure, deploy AI agents, and automate operations safely and efficiently.\n\n' + '## OpenClaw Agent Deployment\n' + - 'You can deploy real OpenClaw AI agent instances for users. OpenClaw is an open-source autonomous AI agent.\n' + - 'To deploy an OpenClaw instance, call the IT0 API:\n' + - ' POST /api/v1/agent/instances\n' + - ' Body: { "name": "", "userId": "", "usePool": true }\n' + - 'The instance will be deployed on a pool server automatically. After creation, the user can connect\n' + - 'their OpenClaw to Telegram, WhatsApp, or other channels via the channel configuration.\n' + - 'To list existing instances: GET /api/v1/agent/instances\n' + - 'To stop an instance: PUT /api/v1/agent/instances/:id/stop\n' + - 'To remove an instance: DELETE /api/v1/agent/instances/:id\n\n' + - 'When a user says they want to create an OpenClaw, a personal AI agent, or an autonomous agent,\n' + - 'confirm their desired name, then call the deployment API and report the result.', + 'You can deploy real OpenClaw (小龙虾) AI agent instances for users. OpenClaw is an open-source autonomous AI agent.\n' + + 'Use the internal API (no auth required, direct service call):\n' + + ' Create: curl -s -X POST http://localhost:3002/api/v1/agent/instances -H "Content-Type: application/json" -d \'{"name":"","userId":"","usePool":true}\'\n' + + ' List: curl -s http://localhost:3002/api/v1/agent/instances\n' + + ' Stop: curl -s -X PUT http://localhost:3002/api/v1/agent/instances//stop\n' + + ' Delete: curl -s -X DELETE http://localhost:3002/api/v1/agent/instances/\n\n' + + 'When a user asks to create a OpenClaw / 小龙虾 / personal AI agent / autonomous agent:\n' + + '1. Ask for a name if not given\n' + + '2. Use the Current User ID from this prompt as userId\n' + + '3. Call the create API with Bash and report the result (id, status, containerName)', ); - // Tenant context + // Tenant + user context parts.push(`\nTenant: ${context.tenantId}`); + if (context.userId) { + parts.push(`Current User ID: ${context.userId}${context.userEmail ? ` (${context.userEmail})` : ''}`); + parts.push(`When creating OpenClaw instances for the current user, use userId: "${context.userId}"`); + } // Server context (available servers, environment, etc.) if (context.serverContext) { diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts index a3610de..f0cf284 100644 --- a/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts +++ b/packages/services/agent-service/src/interfaces/rest/controllers/agent.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Body, Param, Delete, Get, NotFoundException, BadRequestException, ForbiddenException, Logger, UseInterceptors, UploadedFile } from '@nestjs/common'; +import { Controller, Post, Body, Param, Delete, Get, NotFoundException, BadRequestException, ForbiddenException, Logger, UseInterceptors, UploadedFile, Req } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { memoryStorage } from 'multer'; import { TenantId, EventPatterns } from '@it0/common'; @@ -17,6 +17,7 @@ import { TaskStatus } from '../../../domain/value-objects/task-status.vo'; import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo'; import { AgentEnginePort, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port'; import { ClaudeAgentSdkEngine } from '../../../infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine'; +import { SystemPromptBuilder } from '../../../infrastructure/engines/claude-code-cli/system-prompt-builder'; import * as crypto from 'crypto'; @Controller('api/v1/agent') @@ -34,11 +35,13 @@ export class AgentController { private readonly contextService: ConversationContextService, private readonly eventPublisher: EventPublisherService, private readonly sttService: OpenAISttService, + private readonly systemPromptBuilder: SystemPromptBuilder, ) {} @Post('tasks') async executeTask( @TenantId() tenantId: string, + @Req() req: any, @Body() body: { prompt: string; sessionId?: string; @@ -136,10 +139,19 @@ export class AgentController { this.logger.log(`[Task ${task.id}] Resuming SDK session: ${resumeSessionId}`); } + // Build system prompt with user context so iAgent knows who it's serving + const userId: string | undefined = req.user?.sub ?? req.user?.userId; + const userEmail: string | undefined = req.user?.email; + const systemPrompt = body.systemPrompt || this.systemPromptBuilder.build({ + tenantId, + userId, + userEmail, + }); + // Fire-and-forget: run the task stream this.runTaskStream(engine, session, task, { prompt: body.prompt, - systemPrompt: body.systemPrompt || '', + systemPrompt, allowedTools: body.allowedTools || [], maxTurns: body.maxTurns || 10, conversationHistory: historyForEngine.length > 0 ? historyForEngine : undefined,