From e6bae7ad886ea63d50efcf9cd48136f4a4cac41e Mon Sep 17 00:00:00 2001 From: nyk <93952610+0xNyk@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:36:14 +0700 Subject: [PATCH] fix: sync agent SOUL content with workspace files (#91) (#95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace file is now the primary source for soul.md with DB as fallback. Reads prefer workspace → DB. Writes go to both. Config sync imports soul.md from each agent's workspace using double resolveWithin guard to prevent path traversal. --- src/app/api/agents/[id]/soul/route.ts | 70 +++++++++++++++++++++------ src/lib/agent-sync.ts | 48 ++++++++++++++---- 2 files changed, 94 insertions(+), 24 deletions(-) diff --git a/src/app/api/agents/[id]/soul/route.ts b/src/app/api/agents/[id]/soul/route.ts index 7ee94b8..f576750 100644 --- a/src/app/api/agents/[id]/soul/route.ts +++ b/src/app/api/agents/[id]/soul/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase, db_helpers } from '@/lib/db'; -import { readFileSync, existsSync, readdirSync } from 'fs'; -import { join } from 'path'; +import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; import { config } from '@/lib/config'; import { resolveWithin } from '@/lib/paths'; import { getUserFromRequest, requireRole } from '@/lib/auth'; @@ -34,9 +34,33 @@ export async function GET( return NextResponse.json({ error: 'Agent not found' }, { status: 404 }); } + // Try reading soul.md from workspace first, fall back to DB + let soulContent = '' + let source: 'workspace' | 'database' | 'none' = 'none' + + try { + const agentConfig = agent.config ? JSON.parse(agent.config) : {} + if (agentConfig.workspace && config.openclawHome) { + const safeWorkspace = resolveWithin(config.openclawHome, agentConfig.workspace) + const safeSoulPath = resolveWithin(safeWorkspace, 'soul.md') + if (existsSync(safeSoulPath)) { + soulContent = readFileSync(safeSoulPath, 'utf-8') + source = 'workspace' + } + } + } catch (err) { + logger.warn({ err, agent: agent.name }, 'Failed to read soul.md from workspace') + } + + // Fall back to database value + if (!soulContent && agent.soul_content) { + soulContent = agent.soul_content + source = 'database' + } + const templatesPath = config.soulTemplatesDir; let availableTemplates: string[] = []; - + try { if (templatesPath && existsSync(templatesPath)) { const files = readdirSync(templatesPath); @@ -47,14 +71,15 @@ export async function GET( } catch (error) { logger.warn({ err: error }, 'Could not read soul templates directory'); } - + return NextResponse.json({ agent: { id: agent.id, name: agent.name, role: agent.role }, - soul_content: agent.soul_content || '', + soul_content: soulContent, + source, available_templates: availableTemplates, updated_at: agent.updated_at }); @@ -125,34 +150,51 @@ export async function PUT( } const now = Math.floor(Date.now() / 1000); - - // Update SOUL content + + // Write to workspace file if available + let savedToWorkspace = false + try { + const agentConfig = agent.config ? JSON.parse(agent.config) : {} + if (agentConfig.workspace && config.openclawHome) { + const safeWorkspace = resolveWithin(config.openclawHome, agentConfig.workspace) + const safeSoulPath = resolveWithin(safeWorkspace, 'soul.md') + mkdirSync(dirname(safeSoulPath), { recursive: true }) + writeFileSync(safeSoulPath, newSoulContent || '', 'utf-8') + savedToWorkspace = true + } + } catch (err) { + logger.warn({ err, agent: agent.name }, 'Failed to write soul.md to workspace, saving to DB only') + } + + // Update SOUL content in DB const updateStmt = db.prepare(` - UPDATE agents + UPDATE agents SET soul_content = ?, updated_at = ? WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? `); - + updateStmt.run(newSoulContent, now, agentId); - + // Log activity db_helpers.logActivity( 'agent_soul_updated', 'agent', agent.id, getUserFromRequest(request)?.username || 'system', - `SOUL content updated for agent ${agent.name}${template_name ? ` using template: ${template_name}` : ''}`, - { + `SOUL content updated for agent ${agent.name}${template_name ? ` using template: ${template_name}` : ''}${savedToWorkspace ? ' (synced to workspace)' : ''}`, + { template_used: template_name || null, content_length: newSoulContent ? newSoulContent.length : 0, - previous_content_length: agent.soul_content ? agent.soul_content.length : 0 + previous_content_length: agent.soul_content ? agent.soul_content.length : 0, + saved_to_workspace: savedToWorkspace } ); - + return NextResponse.json({ success: true, message: `SOUL content updated for ${agent.name}`, soul_content: newSoulContent, + saved_to_workspace: savedToWorkspace, updated_at: now }); } catch (error) { diff --git a/src/lib/agent-sync.ts b/src/lib/agent-sync.ts index 12004e6..d149628 100644 --- a/src/lib/agent-sync.ts +++ b/src/lib/agent-sync.ts @@ -9,6 +9,9 @@ import { config } from './config' import { getDatabase, db_helpers, logAuditEvent } from './db' import { eventBus } from './event-bus' import { join } from 'path' +import { existsSync, readFileSync } from 'fs' +import { resolveWithin } from './paths' +import { logger } from './logger' interface OpenClawAgent { id: string @@ -64,6 +67,21 @@ function getConfigPath(): string | null { return join(config.openclawHome, 'openclaw.json') } +/** Safely read a file from an agent's workspace directory */ +function readWorkspaceFile(workspace: string | undefined, filename: string): string | null { + if (!workspace || !config.openclawHome) return null + try { + const safeWorkspace = resolveWithin(config.openclawHome, workspace) + const safePath = resolveWithin(safeWorkspace, filename) + if (existsSync(safePath)) { + return readFileSync(safePath, 'utf-8') + } + } catch (err) { + logger.warn({ err, workspace, filename }, 'Failed to read workspace file') + } + return null +} + /** Read and parse openclaw.json agents list */ async function readOpenClawAgents(): Promise { const configPath = getConfigPath() @@ -80,6 +98,7 @@ function mapAgentToMC(agent: OpenClawAgent): { name: string role: string config: any + soul_content: string | null } { const name = agent.identity?.name || agent.name || agent.id const role = agent.identity?.theme || 'agent' @@ -98,7 +117,10 @@ function mapAgentToMC(agent: OpenClawAgent): { isDefault: agent.default || false, } - return { name, role, config: configData } + // Read soul.md from the agent's workspace if available + const soul_content = readWorkspaceFile(agent.workspace, 'soul.md') + + return { name, role, config: configData, soul_content } } /** Sync agents from openclaw.json into the MC database */ @@ -120,13 +142,13 @@ export async function syncAgentsFromConfig(actor: string = 'system'): Promise { @@ -136,17 +158,23 @@ export async function syncAgentsFromConfig(actor: string = 'system'): Promise