mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 02:16:45 +08:00
* fix(daemon): link user Codex skills into codex-home instead of copying them Every Codex task got a full recursive copy of ~/.codex/skills/ inside its per-task CODEX_HOME. For a user with npm-backed skills that is ~100 MB per task directory, re-copied on every task start — hydrateCodexSkills wipes and re-seeds on both the Prepare and the Reuse path — and never reclaimed: the GC artifact sweep only matches node_modules / .next / .turbo, and whole-directory reclaim needs the issue to reach a terminal status first. Total cost scales with the number of live task directories. Link instead. The same per-task home already links its other two shared resources, sessions and the plugin cache, and a skill directory is read-only input to the CLI, so the write isolation a copy incidentally bought has no requester. EvalSymlinks still runs first, so an installer-shipped symlink (lark-cli into ~/.agents/skills/) resolves to the real directory rather than producing a link to a link. The three paths that could reach through the new link are unchanged in behavior: os.RemoveAll deletes a link and not its target, writeSkillFiles probes with os.Lstat so a workspace skill colliding with a linked user skill lands in a `-multica` sibling, and the GC refuses to walk into links. Tests pin all three, plus a windows-tagged test for the junction form of the link. Co-authored-by: multica-agent <[email protected]> * fix(daemon): stop GC walks from descending into Windows directory junctions The GC's artifact sweep and its size accounting both refuse to follow links out of a task directory, but they tested for os.ModeSymlink alone. That misses the link shape Windows actually produces: createDirLink falls back to a directory junction (mklink /J) whenever os.Symlink is denied, which is the default without Developer Mode, and since Go 1.23 os.Lstat reports a junction as ModeDir|ModeIrregular with no ModeSymlink bit while its DirEntry still answers IsDir() == true. filepath.WalkDir therefore descends straight into the link target, and the sweep would delete a node_modules / .next / .turbo directory that lives inside the user's real home. Match both mode bits in all three walks. The task directory links user content in three places today — the Codex session store, the plugin cache, and now the user's skills — so this is reachable on any Windows host running Codex tasks. Windows-tagged tests create the junction with mklink /J directly, so the junction shape is covered even on a runner where symlinks are permitted, and they run in the existing windows-execenv CI job. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]>
52 lines
1.7 KiB
Go
52 lines
1.7 KiB
Go
//go:build windows
|
|
|
|
package execenv
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// On Windows createDirLink falls back to a directory junction (mklink /J) when
|
|
// os.Symlink is denied — the default without Developer Mode. hydrateCodexSkills
|
|
// wipes codex-home/skills on every task start, so the wipe must delete the
|
|
// junction itself and never reach through it into the user's real skill files.
|
|
// os.RemoveAll drops a junction with RemoveDirectory, but only a real Windows
|
|
// filesystem proves it; mklink /J is used directly so the junction shape is
|
|
// exercised even on a runner where symlinks are permitted.
|
|
func TestHydrateCodexSkillsWipeKeepsJunctionTarget(t *testing.T) {
|
|
sharedHome := t.TempDir()
|
|
t.Setenv("CODEX_HOME", sharedHome)
|
|
userSkill := filepath.Join(sharedHome, "skills", "alpha")
|
|
writeUserSkill(t, userSkill, "alpha v1")
|
|
|
|
codexHome := t.TempDir()
|
|
junction := filepath.Join(codexHome, "skills", "alpha")
|
|
if err := os.MkdirAll(filepath.Dir(junction), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out, err := exec.Command("cmd", "/c", "mklink", "/J", junction, userSkill).CombinedOutput(); err != nil {
|
|
t.Fatalf("mklink /J: %s: %v", out, err)
|
|
}
|
|
if fi, err := os.Lstat(junction); err != nil {
|
|
t.Fatal(err)
|
|
} else if fi.Mode()&os.ModeSymlink != 0 {
|
|
t.Fatalf("expected a junction, got a symlink (mode %v)", fi.Mode())
|
|
}
|
|
|
|
if err := hydrateCodexSkills(codexHome, nil, nil, testLogger()); err != nil {
|
|
t.Fatalf("hydrateCodexSkills: %v", err)
|
|
}
|
|
|
|
body, err := os.ReadFile(filepath.Join(userSkill, "SKILL.md"))
|
|
if err != nil {
|
|
t.Fatalf("the skills wipe reached through the junction: %v", err)
|
|
}
|
|
if string(body) != "alpha v1" {
|
|
t.Errorf("user SKILL.md = %q, want %q", body, "alpha v1")
|
|
}
|
|
assertIsLink(t, junction)
|
|
}
|