fix(studio): guard agent inventory source residency

This commit is contained in:
xiexian committed 2026-10-05 02:42:01 +08:00
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
+53
View File
@@ -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)
}
}
+33 -9
View File
@@ -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))
+1 -6
View File
@@ -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)
}
})
}
}