Files
agent-lib/internal/client/inventory.go
T
m3ta-chiron 49dbcf469e feat: client sync core — config, pull, inventory, manifest, OpenCode target
- admin-protected client config (flag > AGENT_LIB_CONFIG > platform default),
  token used for basic auth only, redacted from all errors and files
- work-repo pull via go-git (bare cache clone + fetch, remote re-pointed on
  config change), clean failure keeps previous state
- inventory: own items + external areas from lockfile; MCP inventoried but
  never deployed
- manifest v1 records name/type/origin/revision/content-hash per item,
  deterministic bytes; folder hashes over sorted relpath+filehash lines so
  tree and disk hashes agree
- shared deploy package (atomic temp+rename, exec-bit preserving) now backs
  both curator materialization and client deployment
- sync validates and hashes everything before the first mutation; second run
  is a byte-level no-op
2026-08-23 10:31:00 +02:00

94 lines
2.5 KiB
Go

package client
import (
"fmt"
"sort"
"github.com/m3tam3re/agent-lib/internal/discovery"
"github.com/m3tam3re/agent-lib/internal/gitsource"
"github.com/m3tam3re/agent-lib/internal/lockfile"
)
// InventoryItem is one deployable item found in the work repository.
type InventoryItem struct {
Type string
Name string
Origin string
RelPath string
Hash string
Warnings []string
}
// Inventory is the full content of a work-repo revision: own contributions
// plus every external area. MCP fragments are inventoried but filtered at
// deployment time.
type Inventory struct {
Revision string
Items []InventoryItem
}
// BuildInventory reads the work repository tree and the lockfile (when
// present) and resolves the flat deployed namespace.
func BuildInventory(tree *gitsource.GitTree, rev string) (*Inventory, error) {
inv := &Inventory{Revision: rev}
own, err := discovery.Scan(tree, discovery.Config{})
if err != nil {
return nil, fmt.Errorf("scanning own items: %w", err)
}
for _, it := range own {
inv.Items = append(inv.Items, InventoryItem{
Type: it.Type, Name: it.UpstreamID, Origin: "own", RelPath: it.RelPath,
})
}
var lf *lockfile.Lockfile
if data, err := tree.ReadFile(lockfile.FileName); err == nil {
lf, err = lockfile.ParseBytes(data)
if err != nil {
return nil, fmt.Errorf("work repository lockfile is invalid: %w", err)
}
} else {
lf = lockfile.New()
}
sources := make([]string, 0, len(lf.Sources))
for name := range lf.Sources {
sources = append(sources, name)
}
sort.Strings(sources)
for _, name := range sources {
sub := &discovery.PrefixTree{Tree: tree, Prefix: "external/" + name}
items, err := discovery.Scan(sub, discovery.Config{})
if err != nil {
return nil, fmt.Errorf("scanning external area of %q: %w", name, err)
}
for _, it := range items {
inv.Items = append(inv.Items, InventoryItem{
Type: it.Type, Name: it.UpstreamID, Origin: name, RelPath: "external/" + name + "/" + it.RelPath,
Warnings: it.Warnings,
})
}
}
sort.Slice(inv.Items, func(i, j int) bool {
if inv.Items[i].Type != inv.Items[j].Type {
return inv.Items[i].Type < inv.Items[j].Type
}
return inv.Items[i].Name < inv.Items[j].Name
})
return inv, nil
}
// Deployable filters the inventory down to the types the target deploys.
func (inv *Inventory) Deployable() []InventoryItem {
var out []InventoryItem
for _, it := range inv.Items {
if it.Type == lockfile.TypeMcp {
continue
}
out = append(out, it)
}
return out
}