feat: clone workspace dependencies + Docker cleanup
- cloneWorkspaceDeps: reads go.mod, clones Core ecosystem modules from Forge into workspace alongside ./repo, rebuilds go.work with all use directives - Deduplicates deps (dappco.re + forge.lthn.ai map to same repos) - Container chmod: workspace files made writable before exit so host can clean up - GONOSUMCHECK for local workspace modules (bypass checksum for dev branches) - Removed stale OLLAMA_HOST env from container Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
0ffaacca87
commit
ba281a2a2d
4 changed files with 162 additions and 5 deletions
139
pkg/agentic/deps.go
Normal file
139
pkg/agentic/deps.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// Workspace dependency cloning.
|
||||
// Reads the repo's go.mod, finds Core ecosystem modules, clones them into
|
||||
// the workspace alongside ./repo, and builds a go.work that includes them all.
|
||||
// This gives the agent a complete, buildable workspace without needing go.work=off.
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
// cloneWorkspaceDeps clones Core ecosystem dependencies into the workspace.
|
||||
// After this, the workspace go.work includes ./repo and all ./dep-* dirs,
|
||||
// giving the agent everything needed to build and test.
|
||||
//
|
||||
// s.cloneWorkspaceDeps(ctx, wsDir, repoDir, "core")
|
||||
func (s *PrepSubsystem) cloneWorkspaceDeps(ctx context.Context, wsDir, repoDir, org string) {
|
||||
goModPath := core.JoinPath(repoDir, "go.mod")
|
||||
r := fs.Read(goModPath)
|
||||
if !r.OK {
|
||||
return // no go.mod — not a Go project
|
||||
}
|
||||
|
||||
// Parse requires from go.mod
|
||||
deps := parseCoreDeps(r.Value.(string))
|
||||
if len(deps) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Deduplicate (dappco.re and forge.lthn.ai may map to same repo)
|
||||
dedupSeen := make(map[string]bool)
|
||||
var unique []coreDep
|
||||
for _, dep := range deps {
|
||||
if !dedupSeen[dep.dir] {
|
||||
dedupSeen[dep.dir] = true
|
||||
unique = append(unique, dep)
|
||||
}
|
||||
}
|
||||
deps = unique
|
||||
|
||||
// Clone each dependency
|
||||
var cloned []string
|
||||
for _, dep := range deps {
|
||||
depDir := core.JoinPath(wsDir, dep.dir)
|
||||
if fs.IsDir(core.JoinPath(depDir, ".git")) {
|
||||
cloned = append(cloned, dep.dir) // already cloned (resume)
|
||||
continue
|
||||
}
|
||||
|
||||
repoURL := forgeSSHURL(org, dep.repo)
|
||||
if result := s.gitCmd(ctx, wsDir, "clone", "--depth=1", repoURL, dep.dir); result.OK {
|
||||
cloned = append(cloned, dep.dir)
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild go.work with repo + all cloned deps
|
||||
if len(cloned) > 0 {
|
||||
b := core.NewBuilder()
|
||||
b.WriteString("go 1.26.0\n\nuse (\n")
|
||||
b.WriteString("\t./repo\n")
|
||||
for _, dir := range cloned {
|
||||
b.WriteString(core.Concat("\t./", dir, "\n"))
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
fs.Write(core.JoinPath(wsDir, "go.work"), b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// coreDep maps a Go module path to a Forge repo clone target.
|
||||
type coreDep struct {
|
||||
module string // e.g. "dappco.re/go/core"
|
||||
repo string // e.g. "go" (Forge repo name)
|
||||
dir string // e.g. "core-go" (workspace subdir)
|
||||
}
|
||||
|
||||
// parseCoreDeps extracts Core ecosystem dependencies from go.mod content.
|
||||
// Finds all require lines matching dappco.re/go/* or forge.lthn.ai/core/*.
|
||||
func parseCoreDeps(gomod string) []coreDep {
|
||||
var deps []coreDep
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, line := range core.Split(gomod, "\n") {
|
||||
line = core.Trim(line)
|
||||
|
||||
// Match dappco.re/go/* requires
|
||||
if core.HasPrefix(line, "dappco.re/go/") {
|
||||
parts := core.Split(line, " ")
|
||||
mod := parts[0]
|
||||
if seen[mod] {
|
||||
continue
|
||||
}
|
||||
seen[mod] = true
|
||||
|
||||
// dappco.re/go/core → repo "go", dir "core-go"
|
||||
// dappco.re/go/core/process → repo "go-process", dir "core-go-process"
|
||||
// dappco.re/go/core/ws → repo "go-ws", dir "core-go-ws"
|
||||
// dappco.re/go/mcp → repo "mcp", dir "core-mcp"
|
||||
suffix := core.TrimPrefix(mod, "dappco.re/go/")
|
||||
repo := suffix
|
||||
if core.HasPrefix(suffix, "core/") {
|
||||
// core/process → go-process
|
||||
sub := core.TrimPrefix(suffix, "core/")
|
||||
repo = core.Concat("go-", sub)
|
||||
} else if suffix == "core" {
|
||||
repo = "go"
|
||||
}
|
||||
dir := core.Concat("core-", core.Replace(repo, "/", "-"))
|
||||
deps = append(deps, coreDep{module: mod, repo: repo, dir: dir})
|
||||
}
|
||||
|
||||
// Match forge.lthn.ai/core/* requires (legacy paths)
|
||||
if core.HasPrefix(line, "forge.lthn.ai/core/") {
|
||||
parts := core.Split(line, " ")
|
||||
mod := parts[0]
|
||||
if seen[mod] {
|
||||
continue
|
||||
}
|
||||
seen[mod] = true
|
||||
|
||||
suffix := core.TrimPrefix(mod, "forge.lthn.ai/core/")
|
||||
repo := suffix
|
||||
dir := core.Concat("core-", core.Replace(repo, "/", "-"))
|
||||
deps = append(deps, coreDep{module: mod, repo: repo, dir: dir})
|
||||
}
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
// forgeSSHURL builds the Forge SSH clone URL for a repo.
|
||||
//
|
||||
// forgeSSHURL("core", "go-io") → "ssh://git@forge.lthn.ai:2223/core/go-io.git"
|
||||
func forgeSSHURL(org, repo string) string {
|
||||
return core.Concat("ssh://git@forge.lthn.ai:2223/", org, "/", repo, ".git")
|
||||
}
|
||||
|
|
@ -166,8 +166,9 @@ func containerCommand(agentType, command string, args []string, repoDir, metaDir
|
|||
"-e", "CI=true",
|
||||
"-e", "GIT_USER_NAME=Virgil",
|
||||
"-e", "GIT_USER_EMAIL=virgil@lethean.io",
|
||||
// Local model access — Ollama on host
|
||||
"-e", "OLLAMA_HOST=http://host.docker.internal:11434",
|
||||
// Go workspace — local modules bypass checksum verification
|
||||
"-e", "GONOSUMCHECK=dappco.re/*,forge.lthn.ai/*",
|
||||
"-e", "GOFLAGS=-mod=mod",
|
||||
}
|
||||
|
||||
// Mount Claude config if dispatching claude agent
|
||||
|
|
@ -184,8 +185,18 @@ func containerCommand(agentType, command string, args []string, repoDir, metaDir
|
|||
)
|
||||
}
|
||||
|
||||
dockerArgs = append(dockerArgs, image, command)
|
||||
dockerArgs = append(dockerArgs, args...)
|
||||
// Wrap agent command in sh -c to chmod workspace after exit.
|
||||
// Docker runs as a different user — without this, host can't delete workspace files.
|
||||
quoted := core.NewBuilder()
|
||||
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")
|
||||
|
||||
dockerArgs = append(dockerArgs, image, "sh", "-c", quoted.String())
|
||||
|
||||
return "docker", dockerArgs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,9 @@ func TestDispatch_ContainerCommand_Good_Codex(t *testing.T) {
|
|||
assert.Contains(t, args, "--rm")
|
||||
assert.Contains(t, args, "/ws/repo:/workspace")
|
||||
assert.Contains(t, args, "/ws/.meta:/workspace/.meta")
|
||||
assert.Contains(t, args, "codex")
|
||||
// Command is wrapped in sh -c for chmod cleanup
|
||||
shCmd := args[len(args)-1]
|
||||
assert.Contains(t, shCmd, "codex")
|
||||
// Should use default image
|
||||
assert.Contains(t, args, defaultDockerImage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -465,6 +465,11 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
|||
out.Branch = s.gitOutput(ctx, repoDir, "rev-parse", "--abbrev-ref", "HEAD")
|
||||
}
|
||||
|
||||
// Clone workspace dependencies — Core modules needed to build the repo.
|
||||
// Reads go.mod, finds dappco.re/go/core/* imports, clones from Forge,
|
||||
// and updates go.work so the agent can build inside the workspace.
|
||||
s.cloneWorkspaceDeps(ctx, wsDir, repoDir, input.Org)
|
||||
|
||||
// Build the rich prompt with all context
|
||||
out.Prompt, out.Memories, out.Consumers = s.buildPrompt(ctx, input, out.Branch, repoPath)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue