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
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user