Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/agent/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
})
Expand Down
1 change: 1 addition & 0 deletions internal/agent/service_profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions internal/agent/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions internal/app/runtimewiring/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions internal/runtime/openclawsandbox/config_controller.go
Original file line number Diff line number Diff line change
@@ -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)
}
121 changes: 121 additions & 0 deletions internal/runtime/openclawsandbox/instructions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package openclawsandbox

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)

const (
workspaceInstructionsBlockStart = "<!-- BEGIN CSGCLAW-INSTRUCTIONS (auto-generated; do not edit) -->"
workspaceInstructionsBlockEnd = "<!-- END CSGCLAW-INSTRUCTIONS -->"
)

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"
}
78 changes: 78 additions & 0 deletions internal/runtime/openclawsandbox/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"csgclaw/internal/config"
Expand Down Expand Up @@ -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")

Expand Down
3 changes: 3 additions & 0 deletions internal/runtime/openclawsandbox/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions internal/runtime/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type ProvisionRequest struct {
AgentID string
ParticipantID string
AgentName string
Instructions string
Profile Profile
WorkspaceOverlay string
Gateway *GatewayProvision
Expand Down
Loading
Loading