fix(agentic): align workspace mount and repo sync
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
9d90e7532f
commit
130b2c84d1
8 changed files with 148 additions and 23 deletions
|
|
@ -146,8 +146,8 @@ func agentCommandResult(agent, prompt string) core.Result {
|
|||
|
||||
const defaultDockerImage = "core-dev"
|
||||
|
||||
// command, args := containerCommand("local", "codex", []string{"exec", "--oss"}, "/srv/.core/workspace/core/go-io/task-5/repo", "/srv/.core/workspace/core/go-io/task-5/.meta")
|
||||
func containerCommand(command string, args []string, repoDir, metaDir string) (string, []string) {
|
||||
// command, args := containerCommand("codex", []string{"exec", "--model", "gpt-5.4"}, "/srv/.core/workspace/core/go-io/task-5", "/srv/.core/workspace/core/go-io/task-5/.meta")
|
||||
func containerCommand(command string, args []string, workspaceDir, metaDir string) (string, []string) {
|
||||
image := core.Env("AGENT_DOCKER_IMAGE")
|
||||
if image == "" {
|
||||
image = defaultDockerImage
|
||||
|
|
@ -158,9 +158,9 @@ func containerCommand(command string, args []string, repoDir, metaDir string) (s
|
|||
dockerArgs := []string{
|
||||
"run", "--rm",
|
||||
"--add-host=host.docker.internal:host-gateway",
|
||||
"-v", core.Concat(repoDir, ":/workspace"),
|
||||
"-v", core.Concat(workspaceDir, ":/workspace"),
|
||||
"-v", core.Concat(metaDir, ":/workspace/.meta"),
|
||||
"-w", "/workspace",
|
||||
"-w", "/workspace/repo",
|
||||
"-v", core.Concat(core.JoinPath(home, ".codex"), ":/home/dev/.codex:ro"),
|
||||
"-e", "OPENAI_API_KEY",
|
||||
"-e", "ANTHROPIC_API_KEY",
|
||||
|
|
@ -188,11 +188,15 @@ func containerCommand(command string, args []string, repoDir, metaDir string) (s
|
|||
}
|
||||
|
||||
quoted := core.NewBuilder()
|
||||
quoted.WriteString(command)
|
||||
for _, a := range args {
|
||||
quoted.WriteString(" '")
|
||||
quoted.WriteString(core.Replace(a, "'", "'\\''"))
|
||||
quoted.WriteString("'")
|
||||
quoted.WriteString("if [ ! -d /workspace/repo ]; then echo 'missing /workspace/repo' >&2; exit 1; fi")
|
||||
if command != "" {
|
||||
quoted.WriteString("; ")
|
||||
quoted.WriteString(command)
|
||||
for _, a := range args {
|
||||
quoted.WriteString(" '")
|
||||
quoted.WriteString(core.Replace(a, "'", "'\\''"))
|
||||
quoted.WriteString("'")
|
||||
}
|
||||
}
|
||||
quoted.WriteString("; chmod -R a+w /workspace /workspace/.meta 2>/dev/null; true")
|
||||
|
||||
|
|
@ -341,13 +345,12 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, workspaceDir string) (int, str
|
|||
return 0, "", "", err
|
||||
}
|
||||
|
||||
repoDir := WorkspaceRepoDir(workspaceDir)
|
||||
metaDir := WorkspaceMetaDir(workspaceDir)
|
||||
outputFile := agentOutputFile(workspaceDir, agent)
|
||||
|
||||
fs.Delete(WorkspaceBlockedPath(workspaceDir))
|
||||
|
||||
command, args = containerCommand(command, args, repoDir, metaDir)
|
||||
command, args = containerCommand(command, args, workspaceDir, metaDir)
|
||||
|
||||
processResult := s.Core().Service("process")
|
||||
if !processResult.OK {
|
||||
|
|
@ -360,7 +363,7 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, workspaceDir string) (int, str
|
|||
proc, err := procSvc.StartWithOptions(context.Background(), process.RunOptions{
|
||||
Command: command,
|
||||
Args: args,
|
||||
Dir: repoDir,
|
||||
Dir: workspaceDir,
|
||||
Detach: true,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ package agentic
|
|||
import core "dappco.re/go/core"
|
||||
|
||||
func Example_containerCommand() {
|
||||
cmd, args := containerCommand("codex", []string{"--model", "gpt-5.4"}, "/workspace", "/meta")
|
||||
cmd, args := containerCommand("codex", []string{"--model", "gpt-5.4"}, "/workspace/task-5", "/workspace/task-5/.meta")
|
||||
core.Println(cmd)
|
||||
core.Println(len(args) > 0)
|
||||
// Output:
|
||||
|
|
|
|||
|
|
@ -9,13 +9,16 @@ import (
|
|||
)
|
||||
|
||||
func TestDispatchsync_ContainerCommand_Good(t *testing.T) {
|
||||
cmd, args := containerCommand("codex", []string{"--model", "gpt-5.4"}, "/workspace", "/meta")
|
||||
cmd, args := containerCommand("codex", []string{"--model", "gpt-5.4"}, "/workspace/task-5", "/workspace/task-5/.meta")
|
||||
assert.Equal(t, "docker", cmd)
|
||||
assert.Contains(t, args, "run")
|
||||
assert.Contains(t, args, "/workspace/task-5:/workspace")
|
||||
assert.Contains(t, args, "/workspace/task-5/.meta:/workspace/.meta")
|
||||
assert.Contains(t, args, "/workspace/repo")
|
||||
}
|
||||
|
||||
func TestDispatchsync_ContainerCommand_Bad_UnknownAgent(t *testing.T) {
|
||||
cmd, args := containerCommand("unknown", nil, "/workspace", "/meta")
|
||||
cmd, args := containerCommand("unknown", nil, "/workspace/task-5", "/workspace/task-5/.meta")
|
||||
assert.Equal(t, "docker", cmd)
|
||||
assert.NotEmpty(t, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -584,11 +584,13 @@ func TestDispatch_ContainerCommand_Bad(t *testing.T) {
|
|||
t.Setenv("DIR_HOME", "/home/dev")
|
||||
|
||||
// Empty command string — docker still runs, just with no command after image
|
||||
cmd, args := containerCommand("", []string{}, "/ws/repo", "/ws/.meta")
|
||||
cmd, args := containerCommand("", []string{}, "/ws", "/ws/.meta")
|
||||
assert.Equal(t, "docker", cmd)
|
||||
assert.Contains(t, args, "run")
|
||||
// The image should still be present in args
|
||||
assert.Contains(t, args, defaultDockerImage)
|
||||
assert.Contains(t, args, "/ws:/workspace")
|
||||
assert.Contains(t, args, "/workspace/repo")
|
||||
}
|
||||
|
||||
// --- canDispatchAgent ---
|
||||
|
|
|
|||
|
|
@ -121,14 +121,16 @@ func TestDispatch_ContainerCommand_Good_Codex(t *testing.T) {
|
|||
t.Setenv("AGENT_DOCKER_IMAGE", "")
|
||||
t.Setenv("DIR_HOME", "/home/dev")
|
||||
|
||||
cmd, args := containerCommand("codex", []string{"exec", "--dangerously-bypass-approvals-and-sandbox", "do it"}, "/ws/repo", "/ws/.meta")
|
||||
cmd, args := containerCommand("codex", []string{"exec", "--dangerously-bypass-approvals-and-sandbox", "do it"}, "/ws", "/ws/.meta")
|
||||
assert.Equal(t, "docker", cmd)
|
||||
assert.Contains(t, args, "run")
|
||||
assert.Contains(t, args, "--rm")
|
||||
assert.Contains(t, args, "/ws/repo:/workspace")
|
||||
assert.Contains(t, args, "/ws:/workspace")
|
||||
assert.Contains(t, args, "/ws/.meta:/workspace/.meta")
|
||||
assert.Contains(t, args, "/workspace/repo")
|
||||
// Command is wrapped in sh -c for chmod cleanup
|
||||
shCmd := args[len(args)-1]
|
||||
assert.Contains(t, shCmd, "missing /workspace/repo")
|
||||
assert.Contains(t, shCmd, "codex")
|
||||
// Should use default image
|
||||
assert.Contains(t, args, defaultDockerImage)
|
||||
|
|
@ -138,7 +140,7 @@ func TestDispatch_ContainerCommand_Good_CustomImage(t *testing.T) {
|
|||
t.Setenv("AGENT_DOCKER_IMAGE", "my-custom-image:latest")
|
||||
t.Setenv("DIR_HOME", "/home/dev")
|
||||
|
||||
cmd, args := containerCommand("codex", []string{"exec"}, "/ws/repo", "/ws/.meta")
|
||||
cmd, args := containerCommand("codex", []string{"exec"}, "/ws", "/ws/.meta")
|
||||
assert.Equal(t, "docker", cmd)
|
||||
assert.Contains(t, args, "my-custom-image:latest")
|
||||
}
|
||||
|
|
@ -147,7 +149,7 @@ func TestDispatch_ContainerCommand_Good_ClaudeMountsConfig(t *testing.T) {
|
|||
t.Setenv("AGENT_DOCKER_IMAGE", "")
|
||||
t.Setenv("DIR_HOME", "/home/dev")
|
||||
|
||||
_, args := containerCommand("claude", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta")
|
||||
_, args := containerCommand("claude", []string{"-p", "do it"}, "/ws", "/ws/.meta")
|
||||
joined := strings.Join(args, " ")
|
||||
assert.Contains(t, joined, ".claude:/home/dev/.claude:ro")
|
||||
}
|
||||
|
|
@ -156,7 +158,7 @@ func TestDispatch_ContainerCommand_Good_GeminiMountsConfig(t *testing.T) {
|
|||
t.Setenv("AGENT_DOCKER_IMAGE", "")
|
||||
t.Setenv("DIR_HOME", "/home/dev")
|
||||
|
||||
_, args := containerCommand("gemini", []string{"-p", "do it"}, "/ws/repo", "/ws/.meta")
|
||||
_, args := containerCommand("gemini", []string{"-p", "do it"}, "/ws", "/ws/.meta")
|
||||
joined := strings.Join(args, " ")
|
||||
assert.Contains(t, joined, ".gemini:/home/dev/.gemini:ro")
|
||||
}
|
||||
|
|
@ -165,7 +167,7 @@ func TestDispatch_ContainerCommand_Good_CodexNoClaudeMount(t *testing.T) {
|
|||
t.Setenv("AGENT_DOCKER_IMAGE", "")
|
||||
t.Setenv("DIR_HOME", "/home/dev")
|
||||
|
||||
_, args := containerCommand("codex", []string{"exec"}, "/ws/repo", "/ws/.meta")
|
||||
_, args := containerCommand("codex", []string{"exec"}, "/ws", "/ws/.meta")
|
||||
joined := strings.Join(args, " ")
|
||||
// codex agent must NOT mount .claude config
|
||||
assert.NotContains(t, joined, ".claude:/home/dev/.claude:ro")
|
||||
|
|
@ -175,7 +177,7 @@ func TestDispatch_ContainerCommand_Good_APIKeysPassedByRef(t *testing.T) {
|
|||
t.Setenv("AGENT_DOCKER_IMAGE", "")
|
||||
t.Setenv("DIR_HOME", "/home/dev")
|
||||
|
||||
_, args := containerCommand("codex", []string{"exec"}, "/ws/repo", "/ws/.meta")
|
||||
_, args := containerCommand("codex", []string{"exec"}, "/ws", "/ws/.meta")
|
||||
joined := strings.Join(args, " ")
|
||||
assert.Contains(t, joined, "OPENAI_API_KEY")
|
||||
assert.Contains(t, joined, "ANTHROPIC_API_KEY")
|
||||
|
|
|
|||
|
|
@ -93,6 +93,13 @@ func (m *Subsystem) handleAgentCompleted(ev messages.AgentCompleted) {
|
|||
go m.checkIdleAfterDelay()
|
||||
}
|
||||
|
||||
func (m *Subsystem) handleWorkspacePushed(ev messages.WorkspacePushed) {
|
||||
if m.ServiceRuntime == nil {
|
||||
return
|
||||
}
|
||||
m.syncWorkspacePush(ev.Repo, ev.Branch, ev.Org)
|
||||
}
|
||||
|
||||
// c.ACTION(messages.AgentStarted{Agent: "codex", Repo: "go-io", Workspace: "core/go-io/task-5"})
|
||||
// c.ACTION(messages.AgentCompleted{Agent: "codex", Repo: "go-io", Workspace: "core/go-io/task-5", Status: "completed"})
|
||||
func (m *Subsystem) HandleIPCEvents(_ *core.Core, msg core.Message) core.Result {
|
||||
|
|
@ -101,6 +108,8 @@ func (m *Subsystem) HandleIPCEvents(_ *core.Core, msg core.Message) core.Result
|
|||
m.handleAgentCompleted(ev)
|
||||
case messages.AgentStarted:
|
||||
m.handleAgentStarted(ev)
|
||||
case messages.WorkspacePushed:
|
||||
m.handleWorkspacePushed(ev)
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,71 @@ func (m *Subsystem) syncRepos() string {
|
|||
return core.Sprintf("Synced %d repo(s): %s", len(pulled), core.Join(", ", pulled...))
|
||||
}
|
||||
|
||||
func (m *Subsystem) syncWorkspacePush(repo, branch, org string) bool {
|
||||
if m.ServiceRuntime == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
repoDir := localRepoDir(org, repo)
|
||||
if repoDir == "" || !fs.Exists(repoDir) || fs.IsFile(repoDir) {
|
||||
return false
|
||||
}
|
||||
|
||||
targetBranch := core.Trim(branch)
|
||||
if targetBranch == "" {
|
||||
targetBranch = m.detectBranch(repoDir)
|
||||
}
|
||||
if targetBranch == "" {
|
||||
targetBranch = m.defaultBranch(repoDir)
|
||||
}
|
||||
if targetBranch == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !m.gitOK(repoDir, "fetch", "origin", targetBranch) {
|
||||
return false
|
||||
}
|
||||
|
||||
currentBranch := m.detectBranch(repoDir)
|
||||
if currentBranch != "" && currentBranch != targetBranch {
|
||||
return true
|
||||
}
|
||||
|
||||
return m.gitOK(repoDir, "reset", "--hard", core.Concat("origin/", targetBranch))
|
||||
}
|
||||
|
||||
func localRepoDir(org, repo string) string {
|
||||
basePath := core.Env("CODE_PATH")
|
||||
if basePath == "" {
|
||||
basePath = core.JoinPath(agentic.HomeDir(), "Code")
|
||||
}
|
||||
|
||||
normalisedRepo := core.Replace(repo, "\\", "/")
|
||||
repoName := core.PathBase(normalisedRepo)
|
||||
orgName := core.PathBase(core.Replace(org, "\\", "/"))
|
||||
repoParts := core.Split(normalisedRepo, "/")
|
||||
if orgName == "" && len(repoParts) > 1 {
|
||||
orgName = repoParts[0]
|
||||
}
|
||||
|
||||
candidates := []string{}
|
||||
if orgName != "" {
|
||||
candidates = append(candidates, core.JoinPath(basePath, orgName, repoName))
|
||||
}
|
||||
candidates = append(candidates, core.JoinPath(basePath, repoName))
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if fs.Exists(candidate) && !fs.IsFile(candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
return ""
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
func (m *Subsystem) initSyncTimestamp() {
|
||||
m.mu.Lock()
|
||||
if m.lastSyncTimestamp == 0 {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/agent/pkg/messages"
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -143,6 +144,46 @@ func TestSync_SyncRepos_Good_PullsChangedRepo(t *testing.T) {
|
|||
assert.Contains(t, msg, "test-repo")
|
||||
}
|
||||
|
||||
func TestSync_HandleWorkspacePushed_Good_ResetsTrackedRepo(t *testing.T) {
|
||||
remoteDir := core.JoinPath(t.TempDir(), "remote")
|
||||
fs.EnsureDir(remoteDir)
|
||||
run(t, remoteDir, "git", "init", "--bare")
|
||||
|
||||
codeDir := t.TempDir()
|
||||
orgDir := core.JoinPath(codeDir, "core")
|
||||
fs.EnsureDir(orgDir)
|
||||
repoDir := core.JoinPath(orgDir, "test-repo")
|
||||
run(t, orgDir, "git", "clone", remoteDir, "test-repo")
|
||||
run(t, repoDir, "git", "checkout", "-b", "main")
|
||||
fs.Write(core.JoinPath(repoDir, "README.md"), "# test")
|
||||
run(t, repoDir, "git", "add", ".")
|
||||
run(t, repoDir, "git", "commit", "-m", "init")
|
||||
run(t, repoDir, "git", "push", "-u", "origin", "main")
|
||||
|
||||
cloneParent := t.TempDir()
|
||||
tmpClone := core.JoinPath(cloneParent, "clone2")
|
||||
run(t, cloneParent, "git", "clone", remoteDir, "clone2")
|
||||
run(t, tmpClone, "git", "checkout", "main")
|
||||
fs.Write(core.JoinPath(tmpClone, "new.go"), "package main\n")
|
||||
run(t, tmpClone, "git", "add", ".")
|
||||
run(t, tmpClone, "git", "commit", "-m", "agent work")
|
||||
run(t, tmpClone, "git", "push", "origin", "main")
|
||||
|
||||
t.Setenv("CODE_PATH", codeDir)
|
||||
|
||||
mon := New()
|
||||
mon.ServiceRuntime = testMon.ServiceRuntime
|
||||
|
||||
result := mon.HandleIPCEvents(mon.Core(), messages.WorkspacePushed{
|
||||
Repo: "test-repo",
|
||||
Branch: "main",
|
||||
Org: "core",
|
||||
})
|
||||
assert.True(t, result.OK)
|
||||
assert.True(t, fs.Exists(core.JoinPath(repoDir, "new.go")))
|
||||
assert.Equal(t, mon.gitOutput(tmpClone, "rev-parse", "HEAD"), mon.gitOutput(repoDir, "rev-parse", "HEAD"))
|
||||
}
|
||||
|
||||
func TestSync_SyncRepos_Good_NormalisesWindowsRepoPath(t *testing.T) {
|
||||
remoteDir := core.JoinPath(t.TempDir(), "remote")
|
||||
fs.EnsureDir(remoteDir)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue