82 lines
2.8 KiB
Go
82 lines
2.8 KiB
Go
package definition
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// CuratedIDs reads the agent specs that ship with the deployment and returns
|
|
// the set of definition ids they declare.
|
|
//
|
|
// # WHY THIS EXISTS
|
|
//
|
|
// An agent is "built-in" when the product ships its spec. There is no column
|
|
// saying so and deliberately none is added here: `importagents` publishes these
|
|
// same files into `agent_definitions` as ordinary `organization` rows, so a
|
|
// curated agent and a tenant-authored shared agent are indistinguishable in the
|
|
// table. The distinguishing fact lives on disk, in the directory the importer
|
|
// publishes FROM — which is the same fact the frontend uses, where `isShipped`
|
|
// tests membership of the ids bundled from `src/agents/**/*.md`.
|
|
//
|
|
// Reading the directory rather than listing ids in configuration keeps the
|
|
// protected set and the published set the same set by construction. Adding a
|
|
// ninth agent protects it; removing one stops protecting it; neither needs a
|
|
// code change, and neither can drift.
|
|
//
|
|
// The ids are parsed out of the frontmatter with the same parser the importer
|
|
// uses, NOT taken from the filename. A file named `analytics-agent.md` whose
|
|
// frontmatter says `id: analytics` publishes as `analytics`, and protecting the
|
|
// filename would protect nothing.
|
|
//
|
|
// A missing directory returns an empty set and no error: development checkouts
|
|
// and test binaries run from working directories that have no `agents/`, and
|
|
// this is a protection list rather than something to serve from. A directory
|
|
// that exists but holds a spec that will not parse IS an error — that same file
|
|
// would fail the importer, and staying quiet about it would leave an agent
|
|
// unprotected for a reason nobody could see.
|
|
func CuratedIDs(dir string) (map[string]bool, error) {
|
|
entries, err := os.ReadDir(dir)
|
|
if os.IsNotExist(err) {
|
|
return map[string]bool{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read curated agents in %s: %w", dir, err)
|
|
}
|
|
|
|
ids := make(map[string]bool, len(entries))
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
// README.md is documentation, not a spec — skipped by name, the same
|
|
// way cmd/importagents skips it, so that a parse failure always means
|
|
// something is actually wrong.
|
|
if e.IsDir() || !strings.HasSuffix(name, ".md") || name == "README.md" {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, name))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", name, err)
|
|
}
|
|
parsed, err := ParseAgent(string(raw), Options{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
if parsed.ID != "" {
|
|
ids[parsed.ID] = true
|
|
}
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// SortedIDs renders a set as a stable list, for logging.
|
|
func SortedIDs(set map[string]bool) []string {
|
|
out := make([]string, 0, len(set))
|
|
for id := range set {
|
|
out = append(out, id)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|