mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-12 18:11:12 +00:00
9115169c03
Replace direct ActivityStore injection with event-driven audit system. Handlers emit audit events via msgBus.Broadcast(), a single subscriber with buffered channel persists to activity_logs table. Coverage expanded from 3 agent CRUD actions to ~65 audit points across all HTTP handlers and WebSocket RPC methods including agents, providers, skills, MCP servers, cron, sessions, teams, pairing, and more.
168 lines
5.8 KiB
Go
168 lines
5.8 KiB
Go
package methods
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
// --- agents.create ---
|
|
// Matching TS src/gateway/server-methods/agents.ts:216-287
|
|
|
|
func (m *AgentsMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
var params struct {
|
|
Name string `json:"name"`
|
|
Workspace string `json:"workspace"`
|
|
Emoji string `json:"emoji"`
|
|
Avatar string `json:"avatar"`
|
|
AgentType string `json:"agent_type"` // "open" (default) or "predefined"
|
|
OwnerIDs []string `json:"owner_ids,omitempty"` // first entry used as DB owner_id; falls back to "system"
|
|
// Per-agent config overrides
|
|
ToolsConfig json.RawMessage `json:"tools_config,omitempty"`
|
|
SubagentsConfig json.RawMessage `json:"subagents_config,omitempty"`
|
|
SandboxConfig json.RawMessage `json:"sandbox_config,omitempty"`
|
|
MemoryConfig json.RawMessage `json:"memory_config,omitempty"`
|
|
CompactionConfig json.RawMessage `json:"compaction_config,omitempty"`
|
|
ContextPruning json.RawMessage `json:"context_pruning,omitempty"`
|
|
OtherConfig json.RawMessage `json:"other_config,omitempty"`
|
|
}
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
|
|
if params.Name == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
|
|
return
|
|
}
|
|
|
|
agentType := params.AgentType
|
|
if agentType == "" {
|
|
agentType = store.AgentTypeOpen
|
|
}
|
|
|
|
agentID := config.NormalizeAgentID(params.Name)
|
|
if agentID == "default" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "cannot create agent with reserved id 'default'")))
|
|
return
|
|
}
|
|
|
|
// Resolve workspace
|
|
ws := params.Workspace
|
|
if ws == "" {
|
|
ws = filepath.Join(m.workspace, "agents", agentID)
|
|
} else {
|
|
ws = config.ExpandHome(ws)
|
|
}
|
|
|
|
if m.agentStore != nil {
|
|
// --- DB-backed: create agent in store ---
|
|
ctx := context.Background()
|
|
|
|
// Check if agent already exists in DB
|
|
if existing, _ := m.agentStore.GetByKey(ctx, agentID); existing != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAlreadyExists, "agent", agentID)))
|
|
return
|
|
}
|
|
|
|
// Resolve owner: use first provided ID so external provisioning tools (e.g. goclaw-wizards)
|
|
// can set a real user as owner at creation time. Falls back to "system" for backward compat.
|
|
ownerID := "system"
|
|
if len(params.OwnerIDs) > 0 && params.OwnerIDs[0] != "" {
|
|
ownerID = params.OwnerIDs[0]
|
|
}
|
|
|
|
agentData := &store.AgentData{
|
|
AgentKey: agentID,
|
|
DisplayName: params.Name,
|
|
OwnerID: ownerID,
|
|
AgentType: agentType,
|
|
Provider: m.cfg.Agents.Defaults.Provider,
|
|
Model: m.cfg.Agents.Defaults.Model,
|
|
Workspace: ws,
|
|
Status: store.AgentStatusActive,
|
|
ToolsConfig: params.ToolsConfig,
|
|
SubagentsConfig: params.SubagentsConfig,
|
|
SandboxConfig: params.SandboxConfig,
|
|
MemoryConfig: params.MemoryConfig,
|
|
CompactionConfig: params.CompactionConfig,
|
|
ContextPruning: params.ContextPruning,
|
|
OtherConfig: params.OtherConfig,
|
|
}
|
|
if err := m.agentStore.Create(ctx, agentData); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "agent", fmt.Sprintf("%v", err))))
|
|
return
|
|
}
|
|
|
|
// Seed context files to DB (skipped for open agents)
|
|
if _, err := bootstrap.SeedToStore(ctx, m.agentStore, agentData.ID, agentData.AgentType); err != nil {
|
|
slog.Warn("failed to seed bootstrap for agent", "agent", agentID, "error", err)
|
|
}
|
|
|
|
// Set identity in DB bootstrap
|
|
if params.Name != "" || params.Emoji != "" || params.Avatar != "" {
|
|
content := buildIdentityContent(params.Name, params.Emoji, params.Avatar)
|
|
if err := m.agentStore.SetAgentContextFile(ctx, agentData.ID, "IDENTITY.md", content); err != nil {
|
|
slog.Warn("failed to set IDENTITY.md", "agent", agentID, "error", err)
|
|
}
|
|
}
|
|
|
|
// Invalidate router cache so resolver re-loads from DB
|
|
m.agents.InvalidateAgent(agentID)
|
|
} else {
|
|
// --- Fallback: config.json + filesystem ---
|
|
if _, ok := m.cfg.Agents.List[agentID]; ok {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAlreadyExists, "agent", agentID)))
|
|
return
|
|
}
|
|
|
|
spec := config.AgentSpec{
|
|
DisplayName: params.Name,
|
|
Workspace: ws,
|
|
}
|
|
if params.Emoji != "" || params.Avatar != "" {
|
|
spec.Identity = &config.IdentityConfig{
|
|
Emoji: params.Emoji,
|
|
}
|
|
}
|
|
|
|
if m.cfg.Agents.List == nil {
|
|
m.cfg.Agents.List = make(map[string]config.AgentSpec)
|
|
}
|
|
m.cfg.Agents.List[agentID] = spec
|
|
|
|
if err := config.Save(m.cfgPath, m.cfg); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error())))
|
|
return
|
|
}
|
|
|
|
// Append identity metadata to IDENTITY.md
|
|
if params.Name != "" || params.Emoji != "" || params.Avatar != "" {
|
|
identityPath := filepath.Join(ws, "IDENTITY.md")
|
|
appendIdentityFields(identityPath, params.Name, params.Emoji, params.Avatar)
|
|
}
|
|
}
|
|
|
|
// Both modes: create workspace dir + seed filesystem backup
|
|
os.MkdirAll(ws, 0755)
|
|
bootstrap.EnsureWorkspaceFiles(ws)
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
|
"ok": true,
|
|
"agentId": agentID,
|
|
"name": params.Name,
|
|
"workspace": ws,
|
|
}))
|
|
emitAudit(m.eventBus, client, "agent.created", "agent", agentID)
|
|
}
|