mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): guard agent inventory source residency
This commit is contained in:
1 parent
54f4fbb19f
commit
116d0b4176
8 files changed
+339
-15
No files matched your search
@@ -0,0 +1,5 @@
|
||||
//go:build !unix
|
||||
|
||||
package pluginpkg
|
||||
|
||||
const agentReadFlags = 0
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build unix
|
||||
|
||||
package pluginpkg
|
||||
|
||||
import "syscall"
|
||||
|
||||
// A nonregular source must not block the metadata check, even if it becomes a
|
||||
// named pipe before the root opens it.
|
||||
const agentReadFlags = syscall.O_NONBLOCK
|
||||
@@ -0,0 +1,53 @@
|
||||
package pluginpkg
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
fileencoding "reasonix/internal/base/fileutil/encoding"
|
||||
)
|
||||
|
||||
const maxAgentSourceBytes = 1 << 20
|
||||
|
||||
func applyClaudeAgentDirs(path string, manifest *Manifest) {
|
||||
root, err := os.OpenRoot(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer root.Close()
|
||||
for _, rel := range claudeConventionAgentDirs {
|
||||
if len(loadAgentRefs(root, filepath.FromSlash(rel))) > 0 && !containsPathEntry(manifest.Agents, rel) {
|
||||
manifest.Agents = append(manifest.Agents, rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func agentPathInfo(root *os.Root, path string) (os.FileInfo, *os.File) {
|
||||
file, err := root.OpenFile(path, os.O_RDONLY|agentReadFlags, 0)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil || (!info.IsDir() && !info.Mode().IsRegular()) {
|
||||
file.Close()
|
||||
return nil, nil
|
||||
}
|
||||
return info, file
|
||||
}
|
||||
|
||||
func agentSourceBody(root *os.Root, path string) []byte {
|
||||
info, file := agentPathInfo(root, path)
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
if !info.Mode().IsRegular() || info.Size() > maxAgentSourceBytes {
|
||||
return nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(file, maxAgentSourceBytes+1))
|
||||
if err != nil || len(body) > maxAgentSourceBytes {
|
||||
return nil
|
||||
}
|
||||
return fileencoding.DecodeToUTF8(body)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package pluginpkg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestAgentInventoryReadsOnlyResidentRegularSources(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlink creation requires host privileges")
|
||||
}
|
||||
for _, tc := range []struct{ label, manifest, body string }{
|
||||
{"native", NativeManifest, `{"apiVersion":"reasonix.io/plugin/v2","name":"resident-agents","contributes":{"agents":["agents"]}}`},
|
||||
{"Claude convention", ClaudeManifest, `{"name":"resident-agents"}`},
|
||||
} {
|
||||
t.Run(tc.label, func(t *testing.T) {
|
||||
root, outside := testenv.TempDir(t), testenv.TempDir(t)
|
||||
writeTestFile(t, filepath.Join(root, tc.manifest), tc.body)
|
||||
writeTestFile(t, filepath.Join(root, "agents", "ordinary.md"), "---\ndescription: Ordinary profile\n---\nORDINARY")
|
||||
writeTestFile(t, filepath.Join(root, "profiles", "resident.md"), "---\ndescription: Linked resident profile\n---\nRESIDENT")
|
||||
writeTestFile(t, filepath.Join(outside, "profile.md"), "---\ndescription: Nonresident profile\n---\nNONRESIDENT")
|
||||
if err := os.MkdirAll(filepath.Join(root, "empty"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, link := range []struct{ name, target string }{
|
||||
{"resident.md", filepath.Join("..", "profiles", "resident.md")},
|
||||
{"absolute.md", filepath.Join(root, "profiles", "resident.md")},
|
||||
{"nonresident.md", filepath.Join(outside, "profile.md")},
|
||||
{"nonregular.md", filepath.Join(root, "empty")},
|
||||
{"broken.md", filepath.Join(root, "missing.md")},
|
||||
} {
|
||||
if err := os.Symlink(link.target, filepath.Join(root, "agents", link.name)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Chdir(root)
|
||||
for _, inputRoot := range []string{root, "."} {
|
||||
pkg, _, err := ParseDir(inputRoot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agents := pkg.Inventory().Agents
|
||||
if len(agents) != 2 || pkg.AgentCount() != 2 || agents[0].Name != "ordinary" || agents[1].Name != "resident" {
|
||||
t.Fatalf("resident-only inventory=%+v, count=%d", agents, pkg.AgentCount())
|
||||
}
|
||||
if agents[1].Path != filepath.Join(inputRoot, "agents", "resident.md") || agents[1].Description != "Linked resident profile" {
|
||||
t.Fatalf("resident link projection=%+v", agents[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentInventorySkipsOversizedSources(t *testing.T) {
|
||||
root := testenv.TempDir(t)
|
||||
writeTestFile(t, filepath.Join(root, NativeManifest), `{"apiVersion":"reasonix.io/plugin/v2","name":"bounded-agents","contributes":{"agents":["agents"]}}`)
|
||||
writeTestFile(t, filepath.Join(root, "agents", "ordinary.md"), "---\ndescription: Ordinary profile\n---\nBODY")
|
||||
writeTestFile(t, filepath.Join(root, "agents", "large.md"), "---\ndescription: Large profile\n---\n"+strings.Repeat("x", 1<<20))
|
||||
pkg, _, err := ParseDir(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agents := pkg.Inventory().Agents
|
||||
if len(agents) != 1 || pkg.AgentCount() != 1 || agents[0].Name != "ordinary" {
|
||||
t.Fatalf("bounded inventory=%+v count=%d", agents, pkg.AgentCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentSourceBodyUsesRootHandle(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("moving an open root requires POSIX directory semantics")
|
||||
}
|
||||
parent := testenv.TempDir(t)
|
||||
rootPath := filepath.Join(parent, "package")
|
||||
writeTestFile(t, filepath.Join(rootPath, "profile.md"), "checked bytes")
|
||||
root, err := os.OpenRoot(rootPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
if err := os.Rename(rootPath, filepath.Join(parent, "moved")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTestFile(t, filepath.Join(rootPath, "profile.md"), "replacement bytes")
|
||||
t.Chdir(rootPath)
|
||||
if body := agentSourceBody(root, "profile.md"); string(body) != "checked bytes" {
|
||||
t.Fatalf("production source read=%q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentSourceBodyReadLimit(t *testing.T) {
|
||||
rootPath := testenv.TempDir(t)
|
||||
root, err := os.OpenRoot(rootPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
for _, size := range []int{maxAgentSourceBytes, maxAgentSourceBytes + 1} {
|
||||
writeTestFile(t, filepath.Join(rootPath, "profile.md"), strings.Repeat("x", size))
|
||||
body := agentSourceBody(root, "profile.md")
|
||||
if size == maxAgentSourceBytes && len(body) != size {
|
||||
t.Fatalf("exact-limit source size=%d", len(body))
|
||||
}
|
||||
if size > maxAgentSourceBytes && body != nil {
|
||||
t.Fatalf("oversized source size=%d", len(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRefsUseBorrowedRoot(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("moving an open root requires POSIX directory semantics")
|
||||
}
|
||||
parent := testenv.TempDir(t)
|
||||
rootPath := filepath.Join(parent, "package")
|
||||
writeTestFile(t, filepath.Join(rootPath, "agents", "original.md"), "---\ndescription: Original directory\n---\nBODY")
|
||||
root, err := os.OpenRoot(rootPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
if err := os.Rename(rootPath, filepath.Join(parent, "moved")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTestFile(t, filepath.Join(rootPath, "agents", "replacement.md"), "---\ndescription: Replacement directory\n---\nBODY")
|
||||
for i := range 2 {
|
||||
agents := loadAgentRefs(root, "agents")
|
||||
if len(agents) != 1 || agents[0].Name != "original" || agents[0].Description != "Original directory" || agents[0].Path != filepath.Join(rootPath, "agents", "original.md") {
|
||||
t.Fatalf("borrowed-root scan %d = %+v", i, agents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRefsRejectNonlocalDirectory(t *testing.T) {
|
||||
parent := testenv.TempDir(t)
|
||||
rootPath := filepath.Join(parent, "package")
|
||||
writeTestFile(t, filepath.Join(rootPath, "agents", "local.md"), "LOCAL")
|
||||
outside := filepath.Join(parent, "outside")
|
||||
writeTestFile(t, filepath.Join(outside, "outside.md"), "OUTSIDE")
|
||||
root, err := os.OpenRoot(rootPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
for _, dir := range []string{outside, filepath.Join("..", "outside")} {
|
||||
if agents := loadAgentRefs(root, dir); len(agents) != 0 {
|
||||
t.Fatalf("nonlocal directory %q projected %+v", dir, agents)
|
||||
}
|
||||
}
|
||||
if agents := loadAgentRefs(root, "agents"); len(agents) != 1 || agents[0].Name != "local" {
|
||||
t.Fatalf("local directory projected %+v", agents)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//go:build darwin || linux
|
||||
|
||||
package pluginpkg
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestAgentInventorySkipsNamedPipeSources(t *testing.T) {
|
||||
root := testenv.TempDir(t)
|
||||
writeTestFile(t, filepath.Join(root, NativeManifest), `{"apiVersion":"reasonix.io/plugin/v2","name":"pipe-agents","contributes":{"agents":["agents"]}}`)
|
||||
writeTestFile(t, filepath.Join(root, "agents", "ordinary.md"), "---\ndescription: Ordinary profile\n---\nORDINARY")
|
||||
if err := syscall.Mkfifo(filepath.Join(root, "agents", "pipe.md"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkg, _, err := ParseDir(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agents := pkg.Inventory().Agents
|
||||
if len(agents) != 1 || agents[0].Name != "ordinary" {
|
||||
t.Fatalf("inventory=%+v", agents)
|
||||
}
|
||||
}
|
||||
@@ -479,30 +479,54 @@ func compatibilityFor(pkg Package, issues []CompatibilityIssue) Compatibility {
|
||||
return Compatibility{Status: status, Mapped: mapped, Skipped: issues}
|
||||
}
|
||||
|
||||
func dirContainsAgentMd(dir string) bool { return len(loadAgentRefs(dir)) > 0 }
|
||||
|
||||
func (p Package) agentRefs() []AgentRef {
|
||||
root, err := os.OpenRoot(p.Root)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer root.Close()
|
||||
var dirs []string
|
||||
for _, raw := range p.Manifest.Agents {
|
||||
dir, err := cleanPortableRelativePath(raw)
|
||||
if err != nil || strings.TrimSpace(raw) == "" || !filepath.IsLocal(dir) {
|
||||
continue
|
||||
}
|
||||
dirs = append(dirs, filepath.FromSlash(dir))
|
||||
}
|
||||
slices.Sort(dirs)
|
||||
var out []AgentRef
|
||||
for _, root := range p.AgentRoots() {
|
||||
out = append(out, loadAgentRefs(root)...)
|
||||
for _, dir := range dirs {
|
||||
out = append(out, loadAgentRefs(root, dir)...)
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
func loadAgentRefs(dir string) []AgentRef {
|
||||
entries, err := os.ReadDir(dir)
|
||||
func loadAgentRefs(root *os.Root, dir string) []AgentRef {
|
||||
if !filepath.IsLocal(dir) {
|
||||
return nil
|
||||
}
|
||||
info, source := agentPathInfo(root, dir)
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
defer source.Close()
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
entries, err := source.ReadDir(-1)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
slices.SortFunc(entries, func(a, b os.DirEntry) int { return strings.Compare(a.Name(), b.Name()) })
|
||||
var out []AgentRef
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".md") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, entry.Name())
|
||||
body, err := fileencoding.ReadFileUTF8(path)
|
||||
if err != nil {
|
||||
path := filepath.Join(root.Name(), dir, entry.Name())
|
||||
body := agentSourceBody(root, filepath.Join(dir, entry.Name()))
|
||||
if body == nil {
|
||||
continue
|
||||
}
|
||||
fm, _ := frontmatter.SplitLegacy(string(body))
|
||||
|
||||
@@ -574,12 +574,7 @@ func applyClaudeConventionDirs(root string, manifest *Manifest) []string {
|
||||
manifest.Commands = append(manifest.Commands, rel)
|
||||
}
|
||||
}
|
||||
for _, rel := range claudeConventionAgentDirs {
|
||||
dir := filepath.Join(root, filepath.FromSlash(rel))
|
||||
if dirContainsAgentMd(dir) && !containsPathEntry(manifest.Agents, rel) {
|
||||
manifest.Agents = append(manifest.Agents, rel)
|
||||
}
|
||||
}
|
||||
applyClaudeAgentDirs(root, manifest)
|
||||
return warnings
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
)
|
||||
|
||||
func TestLinkedPackageAgentInventorySkipsNonresidentSources(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlink creation requires host privileges")
|
||||
}
|
||||
for _, tc := range []struct{ label, manifest, body string }{
|
||||
{"native", pluginpkg.NativeManifest, `{"apiVersion":"reasonix.io/plugin/v2","name":"resident-agents","contributes":{"agents":["agents"]}}`},
|
||||
{"Claude", pluginpkg.ClaudeManifest, `{"name":"resident-agents"}`},
|
||||
} {
|
||||
t.Run(tc.label, func(t *testing.T) {
|
||||
home, _, base := pluginHome(t)
|
||||
source, outside := testenv.TempDir(t), testenv.TempDir(t)
|
||||
writePluginFile(t, filepath.Join(source, tc.manifest), tc.body)
|
||||
writePluginFile(t, filepath.Join(source, "agents", "ordinary.md"), "---\ndescription: Resident fixture\n---\nBODY")
|
||||
writePluginFile(t, filepath.Join(outside, "profile.md"), "---\ndescription: External fixture\n---\nEXTERNAL")
|
||||
if err := os.Symlink(filepath.Join(outside, "profile.md"), filepath.Join(source, "agents", "outside.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkg, _, err := pluginpkg.ParseDir(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{Name: pkg.Manifest.Name, Root: source, ManifestKind: pkg.ManifestKind, Enabled: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := http.Get(base + "/plugins")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("installed inventory status=%d", resp.StatusCode)
|
||||
}
|
||||
plugins := getPlugins(t, base)
|
||||
if len(plugins) != 1 || len(plugins[0].Agents) != 1 || plugins[0].Agents[0].Name != "ordinary" {
|
||||
t.Fatalf("installed resident-only inventory=%+v", plugins)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user