All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
- SOUL-Dateien: System-Prompts aus Chat-Routen extrahiert nach agent-core/soul/*.soul.md - soul-reader.ts: Lese-/Schreib-API mit 30s TTL-Cache und Backup-Versionierung - agent-registry.ts: Statische Konfiguration der 2 Compliance-Agenten - 5 API-Routen: /api/sdk/agents (Liste, Detail, SOUL GET/PUT, Sessions, Statistiken) - 5 Frontend-Seiten: Dashboard, Detail mit SOUL-Editor, Architektur, Sessions, Statistiken - Sidebar: "Agenten" Link nach Architektur eingefügt - Wire-Up: compliance-advisor + drafting-engine lesen SOUL-Datei mit Fallback - Dockerfile: agent-core wird in Production-Image kopiert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
3.5 KiB
TypeScript
132 lines
3.5 KiB
TypeScript
/**
|
|
* SOUL File Reader — reads/writes .soul.md files from agent-core/soul/
|
|
* with 30s TTL cache and backup support.
|
|
*/
|
|
|
|
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
|
|
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || path.join(process.cwd(), 'agent-core')
|
|
const SOUL_DIR = path.join(AGENT_CORE_PATH, 'soul')
|
|
const BACKUPS_DIR = path.join(SOUL_DIR, '.backups')
|
|
|
|
// 30s TTL cache
|
|
const cache = new Map<string, { content: string; timestamp: number }>()
|
|
const CACHE_TTL = 30_000
|
|
|
|
function getSoulFilePath(agentId: string): string {
|
|
// Prevent path traversal
|
|
const safe = agentId.replace(/[^a-z0-9-]/g, '')
|
|
return path.join(SOUL_DIR, `${safe}.soul.md`)
|
|
}
|
|
|
|
/**
|
|
* Read a SOUL file with 30s cache
|
|
*/
|
|
export async function readSoulFile(agentId: string): Promise<string | null> {
|
|
const cached = cache.get(agentId)
|
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
return cached.content
|
|
}
|
|
|
|
try {
|
|
const filePath = getSoulFilePath(agentId)
|
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
cache.set(agentId, { content, timestamp: Date.now() })
|
|
return content
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write a SOUL file — creates backup first, then writes new content
|
|
*/
|
|
export async function writeSoulFile(agentId: string, content: string): Promise<void> {
|
|
const filePath = getSoulFilePath(agentId)
|
|
|
|
// Ensure backups dir exists
|
|
await fs.mkdir(BACKUPS_DIR, { recursive: true })
|
|
|
|
// Create backup of current file
|
|
try {
|
|
const current = await fs.readFile(filePath, 'utf-8')
|
|
const backupName = `${agentId}-${Date.now()}.soul.md`
|
|
await fs.writeFile(path.join(BACKUPS_DIR, backupName), current, 'utf-8')
|
|
} catch {
|
|
// No existing file to backup
|
|
}
|
|
|
|
// Write new content
|
|
await fs.writeFile(filePath, content, 'utf-8')
|
|
|
|
// Invalidate cache
|
|
cache.delete(agentId)
|
|
}
|
|
|
|
/**
|
|
* List backup versions for an agent
|
|
*/
|
|
export async function listSoulBackups(agentId: string): Promise<Array<{ filename: string; timestamp: number; size: number }>> {
|
|
try {
|
|
const files = await fs.readdir(BACKUPS_DIR)
|
|
const prefix = `${agentId}-`
|
|
const backups: Array<{ filename: string; timestamp: number; size: number }> = []
|
|
|
|
for (const file of files) {
|
|
if (!file.startsWith(prefix) || !file.endsWith('.soul.md')) continue
|
|
const tsStr = file.slice(prefix.length, -'.soul.md'.length)
|
|
const ts = parseInt(tsStr, 10)
|
|
if (isNaN(ts)) continue
|
|
|
|
const stat = await fs.stat(path.join(BACKUPS_DIR, file))
|
|
backups.push({ filename: file, timestamp: ts, size: stat.size })
|
|
}
|
|
|
|
return backups.sort((a, b) => b.timestamp - a.timestamp)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a specific backup file
|
|
*/
|
|
export async function readSoulBackup(filename: string): Promise<string | null> {
|
|
try {
|
|
// Prevent path traversal
|
|
const safe = path.basename(filename)
|
|
return await fs.readFile(path.join(BACKUPS_DIR, safe), 'utf-8')
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if SOUL file exists
|
|
*/
|
|
export async function soulFileExists(agentId: string): Promise<boolean> {
|
|
try {
|
|
await fs.access(getSoulFilePath(agentId))
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get SOUL file stats
|
|
*/
|
|
export async function getSoulFileStats(agentId: string): Promise<{ size: number; createdAt: string; updatedAt: string } | null> {
|
|
try {
|
|
const stat = await fs.stat(getSoulFilePath(agentId))
|
|
return {
|
|
size: stat.size,
|
|
createdAt: stat.birthtime.toISOString(),
|
|
updatedAt: stat.mtime.toISOString(),
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|