From a76db05a094ddfc9091687979cc4187a0eec0270 Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Sun, 22 Feb 2026 15:31:40 +0000 Subject: [PATCH] refactor(pkg): adapt agent, runtime, migrate, fantasy for SDK/CLI structure Supporting changes for the extracted CLI and dragonscale SDK: - pkg/agent: agent_run, loop, toolloop adjustments for service wiring - pkg/runtime: bootstrap, config updates for new invocation paths - pkg/migrate: migrate logic updates - pkg/fantasy: adapter changes for tool wiring - pkg/channels: manager tweak - pkg/heartbeat: service adjustment - pkg/messages: types update - pkg/sync: identity_test update Remove t.Parallel from TestLoadResolvedConfig_FallsBackWhenExplicitBaseConfigInvalid (t.Setenv incompatible with t.Parallel). --- pkg/agent/agent_run.go | 14 ++-- pkg/agent/loop.go | 38 +++++------ pkg/agent/toolloop.go | 8 +-- pkg/channels/manager.go | 2 +- pkg/fantasy/adapter.go | 26 ++++---- pkg/fantasy/adapter_test.go | 20 +++--- pkg/fantasy/convert_test.go | 2 +- pkg/heartbeat/service.go | 2 +- pkg/messages/types.go | 2 +- pkg/migrate/migrate.go | 36 +++++------ pkg/migrate/migrate_test.go | 78 +++++++++++----------- pkg/runtime/bootstrap.go | 6 +- pkg/runtime/config.go | 52 ++++++++++++--- pkg/runtime/runtime_test.go | 125 +++++++++++++++++++++++++++++++++++- pkg/sync/identity_test.go | 2 +- 15 files changed, 283 insertions(+), 130 deletions(-) diff --git a/pkg/agent/agent_run.go b/pkg/agent/agent_run.go index 40b6431a3..570c2ac2b 100644 --- a/pkg/agent/agent_run.go +++ b/pkg/agent/agent_run.go @@ -13,7 +13,7 @@ import ( "github.com/ZanzyTHEbar/dragonscale/pkg" "github.com/ZanzyTHEbar/dragonscale/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/constants" - picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" + dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" "github.com/ZanzyTHEbar/dragonscale/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/memory" @@ -93,7 +93,7 @@ func (al *AgentLoop) assembleContext(ctx context.Context, opts processOptions) ( "history": formatMessagesForLog(historyMsgs), }) - fantasyHistory := picofantasy.MessagesToFantasy(historyMsgs) + fantasyHistory := dragonfantasy.MessagesToFantasy(historyMsgs) adaptedTools, prepareStep := al.prepareToolset(ctx, opts) agent, err := al.createFantasyAgent(ctx, opts, systemPrompt, adaptedTools, prepareStep) if err != nil { @@ -177,13 +177,13 @@ func (al *AgentLoop) splitMessages(opts processOptions, builtMsgs []messages.Mes } func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([]fantasy.AgentTool, func(context.Context, fantasy.PrepareStepFunctionOptions) (context.Context, fantasy.PrepareStepResult, error)) { - adaptCfg := picofantasy.AdaptedToolsConfig{ + adaptCfg := dragonfantasy.AdaptedToolsConfig{ MemStore: al.memoryStore, AgentID: pkg.NAME, SessionKey: opts.SessionKey, } - adaptedTools := picofantasy.BuildAdaptedTools(al.tools, al.bus, opts.Channel, opts.ChatID, adaptCfg) + adaptedTools := dragonfantasy.BuildAdaptedTools(al.tools, al.bus, opts.Channel, opts.ChatID, adaptCfg) if al.toolResultSearch != nil { adaptedTools = append(adaptedTools, al.toolResultSearch) } @@ -217,7 +217,7 @@ func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([ return ctx, fantasy.PrepareStepResult{}, nil } - newAdapted := picofantasy.AdaptTools(newTools, msgBus, channel, chatID, adaptCfg) + newAdapted := dragonfantasy.AdaptTools(newTools, msgBus, channel, chatID, adaptCfg) expanded := append(adaptedTools, newAdapted...) adaptedTools = expanded @@ -419,7 +419,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, opts processOptions) (str } for _, step := range result.Steps { - stepMsgs := picofantasy.StepToMessages(step) + stepMsgs := dragonfantasy.StepToMessages(step) for _, m := range stepMsgs { al.sessions.AddFullMessage(opts.SessionKey, m) } @@ -458,7 +458,7 @@ func (al *AgentLoop) runStreaming(ctx context.Context, opts processOptions, ac a }, OnStepFinish: func(step fantasy.StepResult) error { - stepMsgs := picofantasy.StepToMessages(step) + stepMsgs := dragonfantasy.StepToMessages(step) for _, m := range stepMsgs { al.sessions.AddFullMessage(opts.SessionKey, m) } diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index f3d82cff4..ec89e1343 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1,5 +1,5 @@ // DragonScale - Ultra-lightweight personal AI agent -// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot +// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw // License: MIT // // Copyright (c) 2026 DragonScale contributors @@ -32,7 +32,7 @@ import ( "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus" "github.com/ZanzyTHEbar/dragonscale/pkg/session" "github.com/ZanzyTHEbar/dragonscale/pkg/state" - picosync "github.com/ZanzyTHEbar/dragonscale/pkg/sync" + dragonsync "github.com/ZanzyTHEbar/dragonscale/pkg/sync" "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) @@ -47,21 +47,21 @@ type AgentLoop struct { state *state.Manager contextBuilder *ContextBuilder tools *tools.ToolRegistry - memoryStore *memstore.MemoryStore // 3-tier MemGPT memory (always initialized) - memDelegate memory.MemoryDelegate // DB delegate (always initialized) - obsManager *observation.Manager // Observational memory (always initialized) - secureBus *securebus.Bus // ITR SecureBus (always initialized) - queries *memsqlc.Queries // SQL query surface for runtime persistence - kvDelegate KVDelegate // KV adapter for offloaded tool results - stateStore *StateStore // Agent run state persistence - conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path - conversationMu sync.Mutex // serializes conversation creation path - identitySync *picosync.IdentitySync // File→DB sync for identity docs (nil if memory disabled) - activeSessionKey atomic.Value // Owner: agent_run.go — written in runAgentLoop, read by router/toolloop for context routing - running atomic.Bool // Owner: loop.go — lifecycle gate controlled by Run/Stop only - summarizing sync.Map // Owner: summarizer.go — intended for async summarization lockout, currently gated by TODO path - summarizeFailures sync.Map // Owner: summarizer.go — write/read in forceCompression + summarizeSession error paths - cfg *config.Config // Stored for subagent factory access + memoryStore *memstore.MemoryStore // 3-tier MemGPT memory (always initialized) + memDelegate memory.MemoryDelegate // DB delegate (always initialized) + obsManager *observation.Manager // Observational memory (always initialized) + secureBus *securebus.Bus // ITR SecureBus (always initialized) + queries *memsqlc.Queries // SQL query surface for runtime persistence + kvDelegate KVDelegate // KV adapter for offloaded tool results + stateStore *StateStore // Agent run state persistence + conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path + conversationMu sync.Mutex // serializes conversation creation path + identitySync *dragonsync.IdentitySync // File→DB sync for identity docs (nil if memory disabled) + activeSessionKey atomic.Value // Owner: agent_run.go — written in runAgentLoop, read by router/toolloop for context routing + running atomic.Bool // Owner: loop.go — lifecycle gate controlled by Run/Stop only + summarizing sync.Map // Owner: summarizer.go — intended for async summarization lockout, currently gated by TODO path + summarizeFailures sync.Map // Owner: summarizer.go — write/read in forceCompression + summarizeSession error paths + cfg *config.Config // Stored for subagent factory access channelManager *channels.Manager commandRegistry []SlashCommand outputOverride atomic.Value // Owner: command_handler.go — CLI output redirection target for internal messages @@ -217,13 +217,13 @@ func NewAgentLoop(ctx context.Context, cfg *config.Config, msgBus *bus.MessageBu } // Identity file sync (disk → DB) - var idSync *picosync.IdentitySync + var idSync *dragonsync.IdentitySync identityDir, idErr := config.IdentityDir() if idErr != nil { logger.WarnCF("agent", "Could not resolve identity dir, identity sync disabled", map[string]interface{}{"error": idErr.Error()}) } else { - idSync = picosync.New(identityDir, pkg.NAME, memDelegate) + idSync = dragonsync.New(identityDir, pkg.NAME, memDelegate) if syncErr := idSync.SyncAll(ctx); syncErr != nil { logger.WarnCF("agent", "Initial identity sync failed (non-fatal)", map[string]interface{}{"error": syncErr.Error()}) diff --git a/pkg/agent/toolloop.go b/pkg/agent/toolloop.go index 48bd88f53..258736d2f 100644 --- a/pkg/agent/toolloop.go +++ b/pkg/agent/toolloop.go @@ -12,7 +12,7 @@ import ( fantasy "charm.land/fantasy" "github.com/ZanzyTHEbar/dragonscale/pkg" - picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" + dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" "github.com/ZanzyTHEbar/dragonscale/pkg/logger" memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store" "github.com/ZanzyTHEbar/dragonscale/pkg/tools" @@ -90,12 +90,12 @@ func runToolLoopWithRuntime( return nil, fmt.Errorf("tool runtime is required") } - adaptCfg := picofantasy.AdaptedToolsConfig{ + adaptCfg := dragonfantasy.AdaptedToolsConfig{ MemStore: ms, AgentID: pkg.NAME, SessionKey: sessionKey, } - adaptedTools := picofantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg) + adaptedTools := dragonfantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg) if len(extraTools) > 0 { adaptedTools = append(adaptedTools, extraTools...) } @@ -126,7 +126,7 @@ func runToolLoopWithRuntime( if len(newTools) == 0 { return ctx, fantasy.PrepareStepResult{}, nil } - newAdapted := picofantasy.AdaptTools(newTools, config.Bus, channel, chatID, adaptCfg) + newAdapted := dragonfantasy.AdaptTools(newTools, config.Bus, channel, chatID, adaptCfg) adaptedTools = append(adaptedTools, newAdapted...) return ctx, fantasy.PrepareStepResult{Tools: adaptedTools}, nil } diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 678912250..3789525fe 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -1,5 +1,5 @@ // DragonScale - Ultra-lightweight personal AI agent -// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot +// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw // License: MIT // // Copyright (c) 2026 DragonScale contributors diff --git a/pkg/fantasy/adapter.go b/pkg/fantasy/adapter.go index 9b531b1e6..0731e5f25 100644 --- a/pkg/fantasy/adapter.go +++ b/pkg/fantasy/adapter.go @@ -18,11 +18,11 @@ import ( "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) -// PicoToolAdapter wraps a DragonScale tool as a Fantasy AgentTool. +// DragonToolAdapter wraps a DragonScale tool as a Fantasy AgentTool. // It bridges DragonScale's dual-channel ToolResult semantics with Fantasy's // simple ToolResponse by publishing ForUser content to the bus as a side effect // and returning only ForLLM content to Fantasy. -type PicoToolAdapter struct { +type DragonToolAdapter struct { inner tools.Tool bus *bus.MessageBus channel string @@ -32,8 +32,8 @@ type PicoToolAdapter struct { sessionKey string } -// Compile-time check that PicoToolAdapter implements fantasy.AgentTool. -var _ fantasy.AgentTool = (*PicoToolAdapter)(nil) +// Compile-time check that DragonToolAdapter implements fantasy.AgentTool. +var _ fantasy.AgentTool = (*DragonToolAdapter)(nil) // Info returns Fantasy-compatible tool metadata from the DragonScale tool. // Fantasy's ToolInfo expects Parameters to be just the properties map and @@ -41,7 +41,7 @@ var _ fantasy.AgentTool = (*PicoToolAdapter)(nil) // Schema object from Parameters() (with "type", "properties", "required" // keys), so we must unwrap it here to avoid double-wrapping in // agent.prepareTools() and agent.validateToolCall(). -func (a *PicoToolAdapter) Info() fantasy.ToolInfo { +func (a *DragonToolAdapter) Info() fantasy.ToolInfo { params := a.inner.Parameters() properties, required := unwrapSchema(params) return fantasy.ToolInfo{ @@ -83,7 +83,7 @@ func unwrapSchema(params map[string]interface{}) (map[string]interface{}, []stri // - If the tool result has ForUser content and is not Silent, publishes to the bus. // - If the tool is a ContextualTool, sets channel/chatID context before execution. // - If the tool is an AsyncTool, wires a callback that publishes results to the bus. -func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) { +func (a *DragonToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) { // 1. Deserialize Fantasy's JSON string input into DragonScale's map format. args, err := parseToolArgs(call.Input) if err != nil { @@ -147,12 +147,12 @@ func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fanta } // ProviderOptions returns nil — DragonScale tools have no provider-specific options. -func (a *PicoToolAdapter) ProviderOptions() fantasy.ProviderOptions { +func (a *DragonToolAdapter) ProviderOptions() fantasy.ProviderOptions { return fantasy.ProviderOptions{} } // SetProviderOptions is a no-op for DragonScale tools. -func (a *PicoToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {} +func (a *DragonToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {} // AdaptedToolsConfig configures how tools are adapted for the Fantasy agent. type AdaptedToolsConfig struct { @@ -182,7 +182,7 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha if !ok { continue } - adapted = append(adapted, &PicoToolAdapter{ + adapted = append(adapted, &DragonToolAdapter{ inner: tool, bus: msgBus, channel: channel, @@ -198,10 +198,10 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha // AdaptTools wraps specific Tool instances as Fantasy AgentTools. // Used by PrepareStep to promote discovered tools to native callables. -func AdaptTools(picoTools []tools.Tool, msgBus *bus.MessageBus, channel, chatID string, cfg AdaptedToolsConfig) []fantasy.AgentTool { - adapted := make([]fantasy.AgentTool, 0, len(picoTools)) - for _, tool := range picoTools { - adapted = append(adapted, &PicoToolAdapter{ +func AdaptTools(dragonTools []tools.Tool, msgBus *bus.MessageBus, channel, chatID string, cfg AdaptedToolsConfig) []fantasy.AgentTool { + adapted := make([]fantasy.AgentTool, 0, len(dragonTools)) + for _, tool := range dragonTools { + adapted = append(adapted, &DragonToolAdapter{ inner: tool, bus: msgBus, channel: channel, diff --git a/pkg/fantasy/adapter_test.go b/pkg/fantasy/adapter_test.go index 20aad5925..7ed297c5c 100644 --- a/pkg/fantasy/adapter_test.go +++ b/pkg/fantasy/adapter_test.go @@ -100,11 +100,11 @@ func (t *mockNilResultTool) Execute(_ context.Context, _ map[string]interface{}) return nil } -// --- PicoToolAdapter.Info() Tests --- +// --- DragonToolAdapter.Info() Tests --- func TestAdapter_Info(t *testing.T) { t.Parallel() - adapter := &PicoToolAdapter{inner: &mockDualChannelTool{}} + adapter := &DragonToolAdapter{inner: &mockDualChannelTool{}} info := adapter.Info() if info.Name != "dual_tool" { @@ -130,7 +130,7 @@ func TestAdapter_Info(t *testing.T) { func TestAdapter_Info_UnwrapsSchemaWithRequired(t *testing.T) { t.Parallel() mock := &mockToolWithRequired{} - adapter := &PicoToolAdapter{inner: mock} + adapter := &DragonToolAdapter{inner: mock} info := adapter.Info() if _, ok := info.Parameters["path"]; !ok { @@ -158,12 +158,12 @@ func (t *mockToolWithRequired) Execute(_ context.Context, _ map[string]interface return &tools.ToolResult{ForLLM: "ok"} } -// --- PicoToolAdapter.Run() Tests --- +// --- DragonToolAdapter.Run() Tests --- func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) { t.Parallel() msgBus := bus.NewMessageBus() - adapter := &PicoToolAdapter{ + adapter := &DragonToolAdapter{ inner: &mockSilentTool{}, bus: msgBus, channel: "test", @@ -192,7 +192,7 @@ func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) { func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) { t.Parallel() msgBus := bus.NewMessageBus() - adapter := &PicoToolAdapter{ + adapter := &DragonToolAdapter{ inner: &mockDualChannelTool{}, bus: msgBus, channel: "telegram", @@ -221,7 +221,7 @@ func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) { func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) { t.Parallel() - adapter := &PicoToolAdapter{ + adapter := &DragonToolAdapter{ inner: &mockErrorTool{}, } @@ -246,7 +246,7 @@ func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) { func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) { t.Parallel() - adapter := &PicoToolAdapter{ + adapter := &DragonToolAdapter{ inner: &mockNilResultTool{}, } @@ -272,7 +272,7 @@ func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) { func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) { t.Parallel() ctxTool := &mockContextualTool{} - adapter := &PicoToolAdapter{ + adapter := &DragonToolAdapter{ inner: ctxTool, channel: "discord", chatID: "guild-1", @@ -297,7 +297,7 @@ func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) { func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) { t.Parallel() - adapter := &PicoToolAdapter{ + adapter := &DragonToolAdapter{ inner: &mockSilentTool{}, } diff --git a/pkg/fantasy/convert_test.go b/pkg/fantasy/convert_test.go index 1989754f4..423602aba 100644 --- a/pkg/fantasy/convert_test.go +++ b/pkg/fantasy/convert_test.go @@ -428,7 +428,7 @@ func TestAgentResultToMessages_MultipleSteps(t *testing.T) { // --- Round-trip fidelity test --- -func TestRoundTrip_PicoClawToFantasyAndBack(t *testing.T) { +func TestRoundTrip_DragonScaleToFantasyAndBack(t *testing.T) { t.Parallel( // Start with DragonScale messages representing a typical conversation ) diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go index cce1448aa..fdc2f64f9 100644 --- a/pkg/heartbeat/service.go +++ b/pkg/heartbeat/service.go @@ -1,5 +1,5 @@ // DragonScale - Ultra-lightweight personal AI agent -// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot +// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw // License: MIT // // Copyright (c) 2026 DragonScale contributors diff --git a/pkg/messages/types.go b/pkg/messages/types.go index 0b8025460..a583e5e86 100644 --- a/pkg/messages/types.go +++ b/pkg/messages/types.go @@ -1,5 +1,5 @@ // DragonScale - Ultra-lightweight personal AI agent -// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot +// Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw // License: MIT // // Copyright (c) 2026 DragonScale contributors diff --git a/pkg/migrate/migrate.go b/pkg/migrate/migrate.go index 96cc85ecd..0eaa84ac5 100644 --- a/pkg/migrate/migrate.go +++ b/pkg/migrate/migrate.go @@ -22,13 +22,13 @@ const ( ) type Options struct { - DryRun bool - ConfigOnly bool - WorkspaceOnly bool - Force bool - Refresh bool - OpenClawHome string - PicoClawHome string + DryRun bool + ConfigOnly bool + WorkspaceOnly bool + Force bool + Refresh bool + OpenClawHome string + DragonScaleHome string } type Action struct { @@ -62,7 +62,7 @@ func Run(opts Options) (*Result, error) { return nil, err } - picoClawHome, err := resolvePicoClawHome(opts.PicoClawHome) + dragonscaleHome, err := resolveDragonScaleHome(opts.DragonScaleHome) if err != nil { return nil, err } @@ -71,14 +71,14 @@ func Run(opts Options) (*Result, error) { return nil, fmt.Errorf("OpenClaw installation not found at %s", openclawHome) } - actions, warnings, err := Plan(opts, openclawHome, picoClawHome) + actions, warnings, err := Plan(opts, openclawHome, dragonscaleHome) if err != nil { return nil, err } fmt.Println("Migrating from OpenClaw to DragonScale") fmt.Printf(" Source: %s\n", openclawHome) - fmt.Printf(" Destination: %s\n", picoClawHome) + fmt.Printf(" Destination: %s\n", dragonscaleHome) fmt.Println() if opts.DryRun { @@ -95,12 +95,12 @@ func Run(opts Options) (*Result, error) { fmt.Println() } - result := Execute(actions, openclawHome, picoClawHome) + result := Execute(actions, openclawHome, dragonscaleHome) result.Warnings = warnings return result, nil } -func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string, error) { +func Plan(opts Options, openclawHome, dragonscaleHome string) ([]Action, []string, error) { var actions []Action var warnings []string @@ -117,7 +117,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string, actions = append(actions, Action{ Type: ActionConvertConfig, Source: configPath, - Destination: filepath.Join(picoClawHome, "config.json"), + Destination: filepath.Join(dragonscaleHome, "config.json"), Description: "convert OpenClaw config to DragonScale format", }) @@ -131,7 +131,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string, if !opts.ConfigOnly { srcWorkspace := resolveWorkspace(openclawHome) - dstWorkspace := resolveWorkspace(picoClawHome) + dstWorkspace := resolveWorkspace(dragonscaleHome) if _, err := os.Stat(srcWorkspace); err == nil { wsActions, err := PlanWorkspaceMigration(srcWorkspace, dstWorkspace, force) @@ -147,13 +147,13 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string, return actions, warnings, nil } -func Execute(actions []Action, openclawHome, picoClawHome string) *Result { +func Execute(actions []Action, openclawHome, dragonscaleHome string) *Result { result := &Result{} for _, action := range actions { switch action.Type { case ActionConvertConfig: - if err := executeConfigMigration(action.Source, action.Destination, picoClawHome); err != nil { + if err := executeConfigMigration(action.Source, action.Destination, dragonscaleHome); err != nil { result.Errors = append(result.Errors, fmt.Errorf("config migration: %w", err)) fmt.Printf(" ✗ Config migration failed: %v\n", err) } else { @@ -207,7 +207,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result { return result } -func executeConfigMigration(srcConfigPath, dstConfigPath, picoClawHome string) error { +func executeConfigMigration(srcConfigPath, dstConfigPath, dragonscaleHome string) error { data, err := LoadOpenClawConfig(srcConfigPath) if err != nil { return err @@ -326,7 +326,7 @@ func resolveOpenClawHome(override string) (string, error) { return filepath.Join(home, ".openclaw"), nil } -func resolvePicoClawHome(override string) (string, error) { +func resolveDragonScaleHome(override string) (string, error) { if override != "" { return expandHome(override), nil } diff --git a/pkg/migrate/migrate_test.go b/pkg/migrate/migrate_test.go index 6731ec6ef..97b2bd125 100644 --- a/pkg/migrate/migrate_test.go +++ b/pkg/migrate/migrate_test.go @@ -579,7 +579,7 @@ func TestRewriteWorkspacePath(t *testing.T) { func TestRunDryRun(t *testing.T) { t.Parallel() openclawHome := t.TempDir() - picoClawHome := t.TempDir() + dragonscaleHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") os.MkdirAll(wsDir, 0755) @@ -597,9 +597,9 @@ func TestRunDryRun(t *testing.T) { os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) opts := Options{ - DryRun: true, - OpenClawHome: openclawHome, - PicoClawHome: picoClawHome, + DryRun: true, + OpenClawHome: openclawHome, + DragonScaleHome: dragonscaleHome, } result, err := Run(opts) @@ -607,11 +607,11 @@ func TestRunDryRun(t *testing.T) { t.Fatalf("Run: %v", err) } - picoWs := filepath.Join(picoClawHome, "workspace") - if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) { + dragonWs := filepath.Join(dragonscaleHome, "workspace") + if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) { t.Error("dry run should not create files") } - if _, err := os.Stat(filepath.Join(picoClawHome, "config.json")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(dragonscaleHome, "config.json")); !os.IsNotExist(err) { t.Error("dry run should not create config") } @@ -621,7 +621,7 @@ func TestRunDryRun(t *testing.T) { func TestRunFullMigration(t *testing.T) { t.Parallel() openclawHome := t.TempDir() - picoClawHome := t.TempDir() + dragonscaleHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") os.MkdirAll(wsDir, 0755) @@ -653,9 +653,9 @@ func TestRunFullMigration(t *testing.T) { os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) opts := Options{ - Force: true, - OpenClawHome: openclawHome, - PicoClawHome: picoClawHome, + Force: true, + OpenClawHome: openclawHome, + DragonScaleHome: dragonscaleHome, } result, err := Run(opts) @@ -663,9 +663,9 @@ func TestRunFullMigration(t *testing.T) { t.Fatalf("Run: %v", err) } - picoWs := filepath.Join(picoClawHome, "workspace") + dragonWs := filepath.Join(dragonscaleHome, "workspace") - soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md")) + soulData, err := os.ReadFile(filepath.Join(dragonWs, "SOUL.md")) if err != nil { t.Fatalf("reading SOUL.md: %v", err) } @@ -673,7 +673,7 @@ func TestRunFullMigration(t *testing.T) { t.Errorf("SOUL.md content = %q, want %q", string(soulData), "# Soul from OpenClaw") } - agentsData, err := os.ReadFile(filepath.Join(picoWs, "AGENTS.md")) + agentsData, err := os.ReadFile(filepath.Join(dragonWs, "AGENTS.md")) if err != nil { t.Fatalf("reading AGENTS.md: %v", err) } @@ -681,7 +681,7 @@ func TestRunFullMigration(t *testing.T) { t.Errorf("AGENTS.md content = %q", string(agentsData)) } - memData, err := os.ReadFile(filepath.Join(picoWs, "memory", "MEMORY.md")) + memData, err := os.ReadFile(filepath.Join(dragonWs, "memory", "MEMORY.md")) if err != nil { t.Fatalf("reading memory/MEMORY.md: %v", err) } @@ -689,21 +689,21 @@ func TestRunFullMigration(t *testing.T) { t.Errorf("MEMORY.md content = %q", string(memData)) } - picoConfig, err := config.LoadConfig(filepath.Join(picoClawHome, "config.json")) + dragonConfig, err := config.LoadConfig(filepath.Join(dragonscaleHome, "config.json")) if err != nil { t.Fatalf("loading DragonScale config: %v", err) } - if picoConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" { - t.Errorf("Anthropic.APIKey = %q, want %q", picoConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test") + if dragonConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" { + t.Errorf("Anthropic.APIKey = %q, want %q", dragonConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test") } - if picoConfig.Providers.OpenRouter.APIKey != "sk-or-migrate-test" { - t.Errorf("OpenRouter.APIKey = %q, want %q", picoConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test") + if dragonConfig.Providers.OpenRouter.APIKey != "sk-or-migrate-test" { + t.Errorf("OpenRouter.APIKey = %q, want %q", dragonConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test") } - if !picoConfig.Channels.Telegram.Enabled { + if !dragonConfig.Channels.Telegram.Enabled { t.Error("Telegram should be enabled") } - if picoConfig.Channels.Telegram.Token != "tg-migrate-test" { - t.Errorf("Telegram.Token = %q, want %q", picoConfig.Channels.Telegram.Token, "tg-migrate-test") + if dragonConfig.Channels.Telegram.Token != "tg-migrate-test" { + t.Errorf("Telegram.Token = %q, want %q", dragonConfig.Channels.Telegram.Token, "tg-migrate-test") } if result.FilesCopied < 3 { @@ -720,8 +720,8 @@ func TestRunFullMigration(t *testing.T) { func TestRunOpenClawNotFound(t *testing.T) { t.Parallel() opts := Options{ - OpenClawHome: "/nonexistent/path/to/openclaw", - PicoClawHome: t.TempDir(), + OpenClawHome: "/nonexistent/path/to/openclaw", + DragonScaleHome: t.TempDir(), } _, err := Run(opts) @@ -787,7 +787,7 @@ func TestCopyFile(t *testing.T) { func TestRunConfigOnly(t *testing.T) { t.Parallel() openclawHome := t.TempDir() - picoClawHome := t.TempDir() + dragonscaleHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") os.MkdirAll(wsDir, 0755) @@ -804,10 +804,10 @@ func TestRunConfigOnly(t *testing.T) { os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) opts := Options{ - Force: true, - ConfigOnly: true, - OpenClawHome: openclawHome, - PicoClawHome: picoClawHome, + Force: true, + ConfigOnly: true, + OpenClawHome: openclawHome, + DragonScaleHome: dragonscaleHome, } result, err := Run(opts) @@ -819,8 +819,8 @@ func TestRunConfigOnly(t *testing.T) { t.Error("config should have been migrated") } - picoWs := filepath.Join(picoClawHome, "workspace") - if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) { + dragonWs := filepath.Join(dragonscaleHome, "workspace") + if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) { t.Error("config-only should not copy workspace files") } } @@ -828,7 +828,7 @@ func TestRunConfigOnly(t *testing.T) { func TestRunWorkspaceOnly(t *testing.T) { t.Parallel() openclawHome := t.TempDir() - picoClawHome := t.TempDir() + dragonscaleHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") os.MkdirAll(wsDir, 0755) @@ -845,10 +845,10 @@ func TestRunWorkspaceOnly(t *testing.T) { os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) opts := Options{ - Force: true, - WorkspaceOnly: true, - OpenClawHome: openclawHome, - PicoClawHome: picoClawHome, + Force: true, + WorkspaceOnly: true, + OpenClawHome: openclawHome, + DragonScaleHome: dragonscaleHome, } result, err := Run(opts) @@ -860,8 +860,8 @@ func TestRunWorkspaceOnly(t *testing.T) { t.Error("workspace-only should not migrate config") } - picoWs := filepath.Join(picoClawHome, "workspace") - soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md")) + dragonWs := filepath.Join(dragonscaleHome, "workspace") + soulData, err := os.ReadFile(filepath.Join(dragonWs, "SOUL.md")) if err != nil { t.Fatalf("reading SOUL.md: %v", err) } diff --git a/pkg/runtime/bootstrap.go b/pkg/runtime/bootstrap.go index 732e42fb8..3e6532314 100644 --- a/pkg/runtime/bootstrap.go +++ b/pkg/runtime/bootstrap.go @@ -9,7 +9,7 @@ import ( "github.com/ZanzyTHEbar/dragonscale/pkg/agent" "github.com/ZanzyTHEbar/dragonscale/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/config" - picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" + dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" ) type OutboundMode string @@ -72,13 +72,13 @@ func Bootstrap(parent context.Context, cfg *config.Config, opts BootstrapOptions ctx, cancel := withExecutionContext(parent, opts.Timeout) - provider, err := picofantasy.CreateProvider(cfg) + provider, err := dragonfantasy.CreateProvider(cfg) if err != nil { cancel() return nil, fmt.Errorf("provider error: %w", err) } - model, err := provider.LanguageModel(ctx, picofantasy.ModelID(cfg)) + model, err := provider.LanguageModel(ctx, dragonfantasy.ModelID(cfg)) if err != nil { cancel() return nil, fmt.Errorf("model error: %w", err) diff --git a/pkg/runtime/config.go b/pkg/runtime/config.go index 52c78886b..5091aa290 100644 --- a/pkg/runtime/config.go +++ b/pkg/runtime/config.go @@ -6,10 +6,15 @@ import ( "path/filepath" "time" + "github.com/ZanzyTHEbar/dragonscale/pkg" "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) -const EvalConfigEnvVar = "DRAGONSCALE_EVAL_CONFIG" +const ( + EvalConfigEnvVar = "DRAGONSCALE_EVAL_CONFIG" + EvalBaseConfigEnvVar = "DRAGONSCALE_EVAL_BASE_CONFIG" + EvalHostHomeEnvVar = "DRAGONSCALE_EVAL_HOST_HOME" +) type LoadConfigOptions struct { BaseConfigPath string @@ -18,24 +23,51 @@ type LoadConfigOptions struct { } func ResolveBaseConfigPath() string { + if explicit := os.Getenv(EvalBaseConfigEnvVar); explicit != "" { + if _, err := os.Stat(explicit); err == nil { + return explicit + } + } + + checkHostConfig := func(hostHome string) string { + if hostHome == "" { + return "" + } + // XDG standard path. + hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json") + if _, err := os.Stat(hostXDG); err == nil { + return hostXDG + } + + return "" + } + + // Prefer host-mounted config when running in containerized eval. + if hostHome := os.Getenv(EvalHostHomeEnvVar); hostHome != "" { + if path := checkHostConfig(hostHome); path != "" { + return path + } + } + + // Fall back to the default devcontainer host mount location. + if path := checkHostConfig("/host_home"); path != "" { + return path + } + // Prefer XDG standard path (~/.config/dragonscale/config.json) when present. - if xdgPath, err := config.DefaultConfigPath(); err == nil { + xdgPath, err := config.DefaultConfigPath() + if err == nil { if _, statErr := os.Stat(xdgPath); statErr == nil { return xdgPath } } - home, _ := os.UserHomeDir() - legacy := filepath.Join(home, ".dragonscale", "config.json") - if _, err := os.Stat(legacy); err == nil { - return legacy - } - // Neither exists; return XDG path if resolvable so defaults still load. - if xdgPath, err := config.DefaultConfigPath(); err == nil { + if err == nil { return xdgPath } - return legacy + + return "" } func LoadResolvedConfig(opts LoadConfigOptions) (*config.Config, error) { diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 26f75c115..a44e48603 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -44,18 +44,139 @@ func TestResolveBaseConfigPath_PrefersXDGOverLegacy(t *testing.T) { assert.Empty(t, cmp.Diff(xdgPath, got)) } -func TestResolveBaseConfigPath_FallsBackToLegacyWhenXDGMissing(t *testing.T) { +func TestResolveBaseConfigPath_FallsBackToXDGWhenLegacyOnlyExists(t *testing.T) { home := t.TempDir() xdg := t.TempDir() t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", xdg) + xdgPath := filepath.Join(xdg, pkg.NAME, "config.json") legacyPath := filepath.Join(home, ".dragonscale", "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(xdgPath), 0o755)) require.NoError(t, os.MkdirAll(filepath.Dir(legacyPath), 0o755)) + require.NoError(t, os.WriteFile(xdgPath, []byte(`{}`), 0o644)) require.NoError(t, os.WriteFile(legacyPath, []byte(`{}`), 0o644)) got := ResolveBaseConfigPath() - assert.Empty(t, cmp.Diff(legacyPath, got)) + assert.Empty(t, cmp.Diff(xdgPath, got)) + assert.NotEmpty(t, legacyPath) +} + +func TestResolveBaseConfigPath_UsesEvalHostHomeWhenContainerConfigMissing(t *testing.T) { + home := t.TempDir() + xdg := t.TempDir() + hostHome := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", xdg) + t.Setenv(EvalHostHomeEnvVar, hostHome) + + hostXDGConfig := filepath.Join(hostHome, ".config", pkg.NAME, "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(hostXDGConfig), 0o755)) + require.NoError(t, os.WriteFile(hostXDGConfig, []byte(`{}`), 0o644)) + + got := ResolveBaseConfigPath() + assert.Empty(t, cmp.Diff(hostXDGConfig, got)) +} + +func TestResolveBaseConfigPath_PrefersEvalHostHomeOverContainerConfig(t *testing.T) { + home := t.TempDir() + xdg := t.TempDir() + hostHome := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", xdg) + t.Setenv(EvalHostHomeEnvVar, hostHome) + + containerXDG := filepath.Join(xdg, pkg.NAME, "config.json") + hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(containerXDG), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755)) + require.NoError(t, os.WriteFile(containerXDG, []byte(`{}`), 0o644)) + require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644)) + + got := ResolveBaseConfigPath() + assert.Empty(t, cmp.Diff(hostXDG, got)) +} + +func TestResolveBaseConfigPath_DoesNotUseLegacyHostPathForHostHome(t *testing.T) { + home := t.TempDir() + xdg := t.TempDir() + hostHome := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", xdg) + t.Setenv(EvalHostHomeEnvVar, hostHome) + + containerXDG := filepath.Join(xdg, pkg.NAME, "config.json") + hostLegacy := filepath.Join(hostHome, ".dragonscale", "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(containerXDG), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Dir(hostLegacy), 0o755)) + require.NoError(t, os.WriteFile(containerXDG, []byte(`{}`), 0o644)) + require.NoError(t, os.WriteFile(hostLegacy, []byte(`{}`), 0o644)) + + got := ResolveBaseConfigPath() + assert.Empty(t, cmp.Diff(containerXDG, got)) +} + +func TestResolveBaseConfigPath_UsesEvalBaseConfigOverride(t *testing.T) { + override := filepath.Join(t.TempDir(), "explicit-config.json") + require.NoError(t, os.WriteFile(override, []byte(`{}`), 0o644)) + t.Setenv(EvalBaseConfigEnvVar, override) + + got := ResolveBaseConfigPath() + assert.Empty(t, cmp.Diff(override, got)) +} + +func TestResolveBaseConfigPath_UsesEvalBaseConfigUnsetAsFallback(t *testing.T) { + home := t.TempDir() + xdg := t.TempDir() + hostHome := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", xdg) + t.Setenv(EvalHostHomeEnvVar, hostHome) + + hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755)) + require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644)) + + t.Setenv(EvalBaseConfigEnvVar, "") + got := ResolveBaseConfigPath() + assert.Empty(t, cmp.Diff(hostXDG, got)) +} + +func TestResolveBaseConfigPath_InvalidEvalBaseConfigFallsBackToHostConfig(t *testing.T) { + home := t.TempDir() + xdg := t.TempDir() + hostHome := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", xdg) + t.Setenv(EvalHostHomeEnvVar, hostHome) + + hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755)) + require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644)) + + t.Setenv(EvalBaseConfigEnvVar, filepath.Join(t.TempDir(), "no-such-file.json")) + got := ResolveBaseConfigPath() + assert.Empty(t, cmp.Diff(hostXDG, got)) +} + +func TestLoadResolvedConfig_FallsBackWhenExplicitBaseConfigInvalid(t *testing.T) { + workDir := t.TempDir() + hostDir := t.TempDir() + home := t.TempDir() + xdg := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", xdg) + t.Setenv(EvalHostHomeEnvVar, hostDir) + + basePath := filepath.Join(hostDir, ".config", pkg.NAME, "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(basePath), 0o755)) + require.NoError(t, os.WriteFile(basePath, []byte(`{"agents":{"defaults":{"restrict_to_sandbox":true,"max_tool_iterations":20}}}`), 0o644)) + + t.Setenv(EvalBaseConfigEnvVar, filepath.Join(workDir, "does-not-exist.json")) + + cfg, err := LoadResolvedConfig(LoadConfigOptions{MinProviderTimeout: 2 * time.Second}) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(true, cfg.Agents.Defaults.RestrictToSandbox)) } func TestLoadResolvedConfig_AppliesOverlayAndKeepsBaseValues(t *testing.T) { diff --git a/pkg/sync/identity_test.go b/pkg/sync/identity_test.go index 8d5caa3ca..da2511dcc 100644 --- a/pkg/sync/identity_test.go +++ b/pkg/sync/identity_test.go @@ -91,7 +91,7 @@ func TestSyncAll_InsertsNewFiles(t *testing.T) { "AGENT.md": "# Agent\nYou are helpful.", "SOUL.md": "# Soul\nCurious and kind.", "USER.md": "# User\nName: Alice", - "IDENTITY.md": "# Identity\nPicoClaw v1", + "IDENTITY.md": "# Identity\nDragonScale v1", }) store := newMockStore()