Files
goclaw/internal/gateway/methods/agents_create.go
T
viettranx 9115169c03 feat: expand audit logging via pub/sub event pattern
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.
2026-03-12 18:34:56 +07:00

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, &params)
}
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)
}