From 52a83a61fc870424e57f16c8821c38f10fc7fc93 Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Wed, 8 Jul 2026 17:43:01 +0800 Subject: [PATCH] fix(openclaw): sync agent instructions into workspace --- internal/agent/service.go | 2 + internal/agent/service_profiles.go | 1 + internal/agent/service_test.go | 10 +- internal/app/runtimewiring/sandbox.go | 10 +- .../openclawsandbox/config_controller.go | 30 +++++ .../runtime/openclawsandbox/instructions.go | 121 ++++++++++++++++++ .../runtime/openclawsandbox/provision_test.go | 78 +++++++++++ internal/runtime/openclawsandbox/runtime.go | 3 + internal/runtime/provision.go | 1 + internal/runtime/sandboxgateway/runtime.go | 46 ++++++- 10 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 internal/runtime/openclawsandbox/config_controller.go create mode 100644 internal/runtime/openclawsandbox/instructions.go diff --git a/internal/agent/service.go b/internal/agent/service.go index 0470a947..41e09a84 100644 --- a/internal/agent/service.go +++ b/internal/agent/service.go @@ -1859,6 +1859,7 @@ func (s *Service) CreateWorker(ctx context.Context, spec CreateAgentSpec) (Agent AgentID: id, ParticipantID: participantIDForAgent(name, id), AgentName: name, + Instructions: instructions, Profile: runtimeProfile, WorkspaceOverlay: strings.TrimSpace(spec.FromTemplate), }); err != nil { @@ -2079,6 +2080,7 @@ func (s *Service) provisionRuntimeForAgent(ctx context.Context, rt agentruntime. AgentID: strings.TrimSpace(got.ID), ParticipantID: participantIDForAgent(got.Name, got.ID), AgentName: strings.TrimSpace(got.Name), + Instructions: strings.TrimSpace(got.Instructions), Profile: s.runtimeProfileForAgent(got), WorkspaceOverlay: strings.TrimSpace(workspaceOverlay), }) diff --git a/internal/agent/service_profiles.go b/internal/agent/service_profiles.go index 77be1356..f39bb92d 100644 --- a/internal/agent/service_profiles.go +++ b/internal/agent/service_profiles.go @@ -761,6 +761,7 @@ func (s *Service) recreate(ctx context.Context, id string, imageFor func(context AgentID: createSpec.AgentID, ParticipantID: participantIDForAgent(createSpec.AgentName, createSpec.AgentID), AgentName: createSpec.AgentName, + Instructions: strings.TrimSpace(got.Instructions), Profile: runtimeProfile, }); err != nil { return Agent{}, fmt.Errorf("provision agent runtime: %w", err) diff --git a/internal/agent/service_test.go b/internal/agent/service_test.go index 99675b21..21ea85d3 100644 --- a/internal/agent/service_test.go +++ b/internal/agent/service_test.go @@ -8206,6 +8206,7 @@ func withTestSandboxRuntimeHost(host PicoClawRuntimeHost, provider feishu.AgentC return func(s *Service) error { return WithRuntime(newRuntime(sandboxgateway.Dependencies{ FeishuProvider: provider, + AgentHome: host.AgentHome, EnsureRuntime: host.EnsureRuntime, RuntimeHome: host.RuntimeHome, CloseRuntime: host.CloseRuntime, @@ -8230,10 +8231,11 @@ func withTestSandboxRuntimeHost(host PicoClawRuntimeHost, provider feishu.AgentC return sandboxgateway.AgentRef{}, err } return sandboxgateway.AgentRef{ - ID: got.ID, - Name: got.Name, - RuntimeID: got.RuntimeID, - BoxID: got.BoxID, + ID: got.ID, + Name: got.Name, + RuntimeID: got.RuntimeID, + BoxID: got.BoxID, + Instructions: got.Instructions, }, nil }, SyncHandle: host.SyncHandle, diff --git a/internal/app/runtimewiring/sandbox.go b/internal/app/runtimewiring/sandbox.go index fb6df0a3..99f61d7b 100644 --- a/internal/app/runtimewiring/sandbox.go +++ b/internal/app/runtimewiring/sandbox.go @@ -24,6 +24,7 @@ func withSandboxRuntimeHost(host agent.PicoClawRuntimeHost, feishuProvider feish FeishuProvider: feishuProvider, SandboxProviderName: host.SandboxProviderName, SandboxToolsDir: sandboxToolsDir, + AgentHome: host.AgentHome, EnsureRuntime: host.EnsureRuntime, RuntimeHome: host.RuntimeHome, CloseRuntime: host.CloseRuntime, @@ -48,10 +49,11 @@ func withSandboxRuntimeHost(host agent.PicoClawRuntimeHost, feishuProvider feish return sandboxgateway.AgentRef{}, err } return sandboxgateway.AgentRef{ - ID: got.ID, - Name: got.Name, - RuntimeID: strings.TrimSpace(got.RuntimeID), - BoxID: got.BoxID, + ID: got.ID, + Name: got.Name, + RuntimeID: strings.TrimSpace(got.RuntimeID), + BoxID: got.BoxID, + Instructions: got.Instructions, }, nil }, SyncHandle: host.SyncHandle, diff --git a/internal/runtime/openclawsandbox/config_controller.go b/internal/runtime/openclawsandbox/config_controller.go new file mode 100644 index 00000000..e7fa734a --- /dev/null +++ b/internal/runtime/openclawsandbox/config_controller.go @@ -0,0 +1,30 @@ +package openclawsandbox + +import ( + "context" + "path/filepath" + + agentruntime "csgclaw/internal/runtime" +) + +var _ agentruntime.RuntimeConfigController = (*Runtime)(nil) + +func (r *Runtime) ValidateConfig(_ context.Context, _ agentruntime.RuntimeConfigSnapshot) error { + return nil +} + +func (r *Runtime) RestartRequired(agentruntime.RuntimeConfigChange) (bool, error) { + return false, nil +} + +func (r *Runtime) ReconcileConfig(_ context.Context, h agentruntime.Handle, _ agentruntime.RuntimeConfigChange) error { + agentRef, err := r.ResolveAgentForHandle(h) + if err != nil { + return err + } + agentHome, err := r.AgentHomeForAgentID(agentRef.ID) + if err != nil { + return err + } + return refreshWorkspaceAgentsFile(filepath.Join(r.Layout(agentHome).WorkspaceRoot, "AGENTS.md"), agentRef.Instructions) +} diff --git a/internal/runtime/openclawsandbox/instructions.go b/internal/runtime/openclawsandbox/instructions.go new file mode 100644 index 00000000..15177d4f --- /dev/null +++ b/internal/runtime/openclawsandbox/instructions.go @@ -0,0 +1,121 @@ +package openclawsandbox + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +const ( + workspaceInstructionsBlockStart = "" + workspaceInstructionsBlockEnd = "" +) + +func refreshWorkspaceAgentsFile(path, instructions string) error { + path = strings.TrimSpace(path) + if path == "" { + return fmt.Errorf("workspace AGENTS.md path is required") + } + instructions = strings.TrimSpace(instructions) + current, err := os.ReadFile(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("read openclaw workspace AGENTS.md %s: %w", path, err) + } + + var merged string + if instructions == "" { + if errors.Is(err, os.ErrNotExist) { + return nil + } + var changed bool + merged, changed = removeWorkspaceInstructionsBlock(string(current)) + if !changed { + return nil + } + } else { + block := renderWorkspaceInstructionsBlock(instructions) + merged = mergeWorkspaceInstructionsBlock(string(current), block) + } + if err == nil && string(current) == merged { + return nil + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return fmt.Errorf("create openclaw workspace AGENTS.md dir %s: %w", filepath.Dir(path), err) + } + if err := os.WriteFile(path, []byte(merged), 0o644); err != nil { + return fmt.Errorf("write openclaw workspace AGENTS.md %s: %w", path, err) + } + return nil +} + +func renderWorkspaceInstructionsBlock(instructions string) string { + instructions = strings.TrimSpace(instructions) + if instructions == "" { + return "" + } + return strings.Join([]string{ + workspaceInstructionsBlockStart, + "# Agent Instructions", + instructions, + workspaceInstructionsBlockEnd, + }, "\n\n") + "\n" +} + +func mergeWorkspaceInstructionsBlock(current, block string) string { + current = strings.ReplaceAll(current, "\r\n", "\n") + block = strings.TrimRight(strings.ReplaceAll(block, "\r\n", "\n"), "\n") + if block == "" { + return current + } + if replaced, ok := replaceWorkspaceInstructionsBlock(current, block); ok { + return replaced + } + if strings.TrimSpace(current) == "" { + return block + "\n" + } + return joinWorkspaceInstructionsSections(current, block, "") +} + +func replaceWorkspaceInstructionsBlock(current, block string) (string, bool) { + startIdx := strings.Index(current, workspaceInstructionsBlockStart) + if startIdx < 0 { + return "", false + } + endIdx := strings.Index(current[startIdx:], workspaceInstructionsBlockEnd) + if endIdx < 0 { + return joinWorkspaceInstructionsSections(current[:startIdx], block, ""), true + } + endPos := startIdx + endIdx + len(workspaceInstructionsBlockEnd) + return joinWorkspaceInstructionsSections(current[:startIdx], block, current[endPos:]), true +} + +func removeWorkspaceInstructionsBlock(current string) (string, bool) { + current = strings.ReplaceAll(current, "\r\n", "\n") + startIdx := strings.Index(current, workspaceInstructionsBlockStart) + if startIdx < 0 { + return "", false + } + endIdx := strings.Index(current[startIdx:], workspaceInstructionsBlockEnd) + if endIdx < 0 { + return joinWorkspaceInstructionsSections(current[:startIdx]), true + } + endPos := startIdx + endIdx + len(workspaceInstructionsBlockEnd) + return joinWorkspaceInstructionsSections(current[:startIdx], current[endPos:]), true +} + +func joinWorkspaceInstructionsSections(parts ...string) string { + sections := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(strings.ReplaceAll(part, "\r\n", "\n")) + if part == "" { + continue + } + sections = append(sections, part) + } + if len(sections) == 0 { + return "" + } + return strings.Join(sections, "\n\n") + "\n" +} diff --git a/internal/runtime/openclawsandbox/provision_test.go b/internal/runtime/openclawsandbox/provision_test.go index 5c519761..a9413ef0 100644 --- a/internal/runtime/openclawsandbox/provision_test.go +++ b/internal/runtime/openclawsandbox/provision_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" "csgclaw/internal/config" @@ -91,6 +92,83 @@ func TestProvisionPreparesGatewayAssets(t *testing.T) { } } +func TestProvisionWritesInstructionsToWorkspaceAgentsFile(t *testing.T) { + agentHome := t.TempDir() + projectsRoot := t.TempDir() + rt := New(Dependencies{}) + + if err := rt.Provision(context.Background(), agentruntime.ProvisionRequest{ + RuntimeID: "rt-1", + AgentID: "u-alice", + AgentName: "alice", + Instructions: "Only answer contract template questions.", + Gateway: &agentruntime.GatewayProvision{ + ModelFallback: "fallback-model", + Server: config.ServerConfig{AdvertiseBaseURL: "http://127.0.0.1:18080", AccessToken: "shared-token"}, + ManagerBaseURL: "http://127.0.0.1:18080", + AgentHome: agentHome, + ProjectsRoot: projectsRoot, + WorkspaceTemplate: templateembed.OpenClawWorkerRoot, + }, + }); err != nil { + t.Fatalf("Provision() error = %v", err) + } + + raw, err := os.ReadFile(filepath.Join(workspaceRoot(agentHome), "AGENTS.md")) + if err != nil { + t.Fatalf("ReadFile(AGENTS.md) error = %v", err) + } + text := string(raw) + if !strings.Contains(text, "Only answer contract template questions.") { + t.Fatalf("AGENTS.md = %q, want provisioned instructions", text) + } + if !strings.Contains(text, workspaceInstructionsBlockStart) || !strings.Contains(text, workspaceInstructionsBlockEnd) { + t.Fatalf("AGENTS.md = %q, want managed instructions markers", text) + } +} + +func TestReconcileConfigRefreshesWorkspaceInstructions(t *testing.T) { + agentHome := t.TempDir() + workspace := workspaceRoot(agentHome) + if err := os.MkdirAll(workspace, 0o755); err != nil { + t.Fatalf("MkdirAll(workspace) error = %v", err) + } + agentsPath := filepath.Join(workspace, "AGENTS.md") + if err := os.WriteFile(agentsPath, []byte("# Existing Rules\n\nKeep this.\n\n"+renderWorkspaceInstructionsBlock("Old instructions.")), 0o644); err != nil { + t.Fatalf("WriteFile(AGENTS.md) error = %v", err) + } + rt := New(Dependencies{ + AgentHome: func(string) (string, error) { + return agentHome, nil + }, + ResolveAgent: func(agentruntime.Handle) (AgentRef, error) { + return AgentRef{ID: "u-alice", Name: "alice", Instructions: "New instructions."}, nil + }, + }) + + if err := rt.ReconcileConfig(context.Background(), agentruntime.Handle{RuntimeID: "rt-1"}, agentruntime.RuntimeConfigChange{}); err != nil { + t.Fatalf("ReconcileConfig() error = %v", err) + } + + raw, err := os.ReadFile(agentsPath) + if err != nil { + t.Fatalf("ReadFile(AGENTS.md) error = %v", err) + } + text := string(raw) + if !strings.Contains(text, "Keep this.") { + t.Fatalf("AGENTS.md = %q, want existing content preserved", text) + } + if strings.Contains(text, "Old instructions.") { + t.Fatalf("AGENTS.md = %q, want stale instructions removed", text) + } + if !strings.Contains(text, "New instructions.") { + t.Fatalf("AGENTS.md = %q, want new instructions", text) + } + if strings.Count(text, workspaceInstructionsBlockStart) != 1 { + t.Fatalf("AGENTS.md marker count = %d, want 1", strings.Count(text, workspaceInstructionsBlockStart)) + } +} + func TestWorkspaceLayoutForWindowsAvoidsMountingOpenClawHome(t *testing.T) { agentHome := filepath.Join("tmp", "agent-home") diff --git a/internal/runtime/openclawsandbox/runtime.go b/internal/runtime/openclawsandbox/runtime.go index 5155ae70..fa12461c 100644 --- a/internal/runtime/openclawsandbox/runtime.go +++ b/internal/runtime/openclawsandbox/runtime.go @@ -97,6 +97,9 @@ func (r *Runtime) Provision(_ context.Context, req agentruntime.ProvisionRequest if err != nil { return err } + if err := refreshWorkspaceAgentsFile(filepath.Join(prepared.WorkspaceLayout.WorkspaceHostPath, "AGENTS.md"), req.Instructions); err != nil { + return err + } r.RememberPreparedGatewayProvision(req.AgentID, prepared) return nil } diff --git a/internal/runtime/provision.go b/internal/runtime/provision.go index a14f60d2..1ab4a3c8 100644 --- a/internal/runtime/provision.go +++ b/internal/runtime/provision.go @@ -28,6 +28,7 @@ type ProvisionRequest struct { AgentID string ParticipantID string AgentName string + Instructions string Profile Profile WorkspaceOverlay string Gateway *GatewayProvision diff --git a/internal/runtime/sandboxgateway/runtime.go b/internal/runtime/sandboxgateway/runtime.go index bca05ae2..026022a5 100644 --- a/internal/runtime/sandboxgateway/runtime.go +++ b/internal/runtime/sandboxgateway/runtime.go @@ -20,10 +20,11 @@ import ( ) type AgentRef struct { - ID string - Name string - RuntimeID string - BoxID string + ID string + Name string + RuntimeID string + BoxID string + Instructions string } type WorkspaceLayout struct { @@ -40,6 +41,7 @@ type Dependencies struct { SandboxProviderName func() string SandboxToolsDir func() (string, error) + AgentHome func(agentID string) (string, error) EnsureRuntime func(agentID string) (sandbox.Runtime, error) RuntimeHome func(agentID string) (string, error) CloseRuntime func(homeDir string, rt sandbox.Runtime) error @@ -571,6 +573,34 @@ func (r *Runtime) preparedGatewayProvision(agentID string) (PreparedGatewayProvi return prepared, nil } +func (r *Runtime) PreparedGatewayProvisionForHandle(h agentruntime.Handle) (PreparedGatewayProvision, error) { + got, err := r.ResolveAgentForHandle(h) + if err != nil { + return PreparedGatewayProvision{}, err + } + return r.preparedGatewayProvision(got.ID) +} + +func (r *Runtime) ResolveAgentForHandle(h agentruntime.Handle) (AgentRef, error) { + if r == nil { + return AgentRef{}, fmt.Errorf("runtime is required") + } + if r.deps.ResolveAgent == nil { + return AgentRef{}, fmt.Errorf("runtime agent resolver is required") + } + return r.deps.ResolveAgent(h) +} + +func (r *Runtime) AgentHomeForAgentID(agentID string) (string, error) { + if r == nil { + return "", fmt.Errorf("runtime is required") + } + if r.deps.AgentHome == nil { + return "", fmt.Errorf("runtime agent home resolver is required") + } + return r.deps.AgentHome(strings.TrimSpace(agentID)) +} + func (r *Runtime) GatewayLogPath() string { return r.gatewayLogPath() } @@ -645,6 +675,14 @@ func (r *Runtime) openSandboxRuntime(agentID string) (sandbox.Runtime, string, e return rt, runtimeHome, nil } +func (r *Runtime) AgentHomeForHandle(h agentruntime.Handle) (string, error) { + got, err := r.ResolveAgentForHandle(h) + if err != nil { + return "", err + } + return r.AgentHomeForAgentID(got.ID) +} + func (r *Runtime) openBoxForHandle(ctx context.Context, h agentruntime.Handle) (sandbox.Instance, func(), error) { got, err := r.deps.ResolveAgent(h) if err != nil {