fix(pi-agent): copy settle-policy into the runner image
runner.mjs imports ./settle-policy.mjs but the Dockerfile only copied runner.mjs, which crashed shuimu-pi-agent-dev on import. COPY the runtime module and assert every relative import is in the image set. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,9 +22,10 @@ RUN pi --version
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 runner、扩展工具以及入口脚本
|
||||
# 复制 runner、其相对 import 运行时模块、扩展工具以及入口脚本
|
||||
COPY deploy/pi-agent/extensions/ /app/extensions/
|
||||
COPY deploy/pi-agent/runner.mjs /app/runner.mjs
|
||||
COPY deploy/pi-agent/settle-policy.mjs /app/settle-policy.mjs
|
||||
COPY deploy/pi-agent/entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh /app/runner.mjs
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func piAgentRepoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller")
|
||||
}
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
|
||||
}
|
||||
|
||||
func TestPiAgentDockerfileCopiesRunnerRelativeImports(t *testing.T) {
|
||||
root := piAgentRepoRoot(t)
|
||||
runnerRel := filepath.ToSlash("deploy/pi-agent/runner.mjs")
|
||||
df, err := os.ReadFile(filepath.Join(root, "deploy", "pi-agent", "Dockerfile"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copied := dockerCopyDestinations(string(df))
|
||||
if len(copied) == 0 {
|
||||
t.Fatal("deploy/pi-agent/Dockerfile has no COPY lines")
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
var missing []string
|
||||
var walk func(repoRel string)
|
||||
walk = func(repoRel string) {
|
||||
repoRel = filepath.ToSlash(repoRel)
|
||||
if seen[repoRel] {
|
||||
return
|
||||
}
|
||||
seen[repoRel] = true
|
||||
abs := filepath.Join(root, filepath.FromSlash(repoRel))
|
||||
body, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", repoRel, err)
|
||||
}
|
||||
dir := filepath.ToSlash(filepath.Dir(repoRel))
|
||||
for _, spec := range relativeModuleSpecs(string(body)) {
|
||||
child := filepath.ToSlash(filepath.Clean(dir + "/" + strings.TrimPrefix(spec, "./")))
|
||||
imageDest := runnerImportImagePath(child)
|
||||
if !dockerCopyCovers(copied, imageDest) {
|
||||
missing = append(missing, spec+" -> "+imageDest+" (from "+repoRel+")")
|
||||
}
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
walk(runnerRel)
|
||||
if len(missing) > 0 {
|
||||
t.Fatalf("pi-agent image COPY set is missing runner runtime modules:\n %s", strings.Join(missing, "\n "))
|
||||
}
|
||||
if !dockerCopyCovers(copied, "/app/settle-policy.mjs") {
|
||||
t.Fatal("deploy/pi-agent/Dockerfile must COPY settle-policy.mjs to /app/settle-policy.mjs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiAgentDockerBuildContext_RunnerModulesPresent(t *testing.T) {
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
t.Skip("docker CLI not available")
|
||||
}
|
||||
root := piAgentRepoRoot(t)
|
||||
dfBody, err := os.ReadFile(filepath.Join(root, "deploy", "pi-agent", "Dockerfile"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var copies []string
|
||||
for _, raw := range strings.Split(string(dfBody), "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if strings.HasPrefix(line, "COPY deploy/pi-agent/") && !strings.Contains(line, "--from=") {
|
||||
copies = append(copies, line)
|
||||
}
|
||||
}
|
||||
if len(copies) == 0 {
|
||||
t.Fatal("no deploy/pi-agent COPY lines to replay")
|
||||
}
|
||||
slim := "FROM debian:bookworm-slim\nWORKDIR /app\n" + strings.Join(copies, "\n") + "\n"
|
||||
df := filepath.Join(t.TempDir(), "Dockerfile")
|
||||
if err := os.WriteFile(df, []byte(slim), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tag := "agent-system-pi-runner-modules-smoke:local"
|
||||
build := exec.Command("docker", "build", "-f", df, "-t", tag, ".")
|
||||
build.Dir = root
|
||||
out, err := runCmdTimed(build, 3*time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("docker build context failed: %v\n%s", err, out)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = exec.Command("docker", "rmi", "-f", tag).Run()
|
||||
})
|
||||
for _, path := range []string{"/app/runner.mjs", "/app/settle-policy.mjs", "/app/extensions/tavily-search.mjs"} {
|
||||
cat := exec.Command("docker", "run", "--rm", "--entrypoint", "test", tag, "-f", path)
|
||||
if body, err := runCmdTimed(cat, 30*time.Second); err != nil {
|
||||
t.Fatalf("image missing %s: %v\n%s", path, err, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runCmdTimed(cmd *exec.Cmd, d time.Duration) ([]byte, error) {
|
||||
var buf strings.Builder
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
if err := cmd.Start(); err != nil {
|
||||
return []byte(buf.String()), err
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
select {
|
||||
case err := <-done:
|
||||
return []byte(buf.String()), err
|
||||
case <-time.After(d):
|
||||
_ = cmd.Process.Kill()
|
||||
return []byte(buf.String()), os.ErrDeadlineExceeded
|
||||
}
|
||||
}
|
||||
|
||||
var relativeFromRe = regexp.MustCompile(`(?m)from\s+["'](\./[^"']+)["']`)
|
||||
|
||||
func relativeModuleSpecs(src string) []string {
|
||||
var out []string
|
||||
for _, m := range relativeFromRe.FindAllStringSubmatch(src, -1) {
|
||||
if len(m) > 1 {
|
||||
out = append(out, m[1])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func runnerImportImagePath(repoRel string) string {
|
||||
const prefix = "deploy/pi-agent/"
|
||||
rel := filepath.ToSlash(repoRel)
|
||||
if strings.HasPrefix(rel, prefix) {
|
||||
return "/app/" + strings.TrimPrefix(rel, prefix)
|
||||
}
|
||||
return "/app/" + filepath.Base(rel)
|
||||
}
|
||||
|
||||
type dockerCopy struct {
|
||||
src string
|
||||
dest string
|
||||
}
|
||||
|
||||
func dockerCopyDestinations(df string) []dockerCopy {
|
||||
var out []dockerCopy
|
||||
for _, raw := range strings.Split(df, "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if !strings.HasPrefix(line, "COPY ") || strings.HasPrefix(line, "COPY --from=") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
src := filepath.ToSlash(fields[1])
|
||||
dest := filepath.ToSlash(fields[len(fields)-1])
|
||||
out = append(out, dockerCopy{src: src, dest: dest})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dockerCopyCovers(copies []dockerCopy, imagePath string) bool {
|
||||
imagePath = filepath.ToSlash(imagePath)
|
||||
for _, c := range copies {
|
||||
dest := c.dest
|
||||
src := c.src
|
||||
if dest == imagePath {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(dest, "/") && strings.HasPrefix(imagePath, dest) {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(src, "/") && strings.HasSuffix(dest, "/") {
|
||||
relSrc := strings.TrimPrefix(src, "deploy/pi-agent/")
|
||||
if strings.HasPrefix(imagePath, dest+relSrc) || strings.HasPrefix(imagePath, dest) {
|
||||
// /app/extensions/ + tavily-search.mjs
|
||||
base := strings.TrimPrefix(imagePath, dest)
|
||||
if base != imagePath && !strings.Contains(base, "..") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user