- 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
112 lines
2.9 KiB
Go
112 lines
2.9 KiB
Go
// Package client implements the employee side: config loading, work-repo
|
|
// pull, inventory, manifest and deployment into the OpenCode target.
|
|
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
// ClientConfig is the admin-protected fleet configuration for sync.
|
|
type ClientConfig struct {
|
|
RepoURL string `json:"repo_url"`
|
|
Ref string `json:"ref,omitempty"`
|
|
Token string `json:"token,omitempty"`
|
|
TokenUser string `json:"token_user,omitempty"`
|
|
}
|
|
|
|
// ConfigEnv names the override environment variables.
|
|
const (
|
|
ConfigEnv = "AGENT_LIB_CONFIG"
|
|
StateDirEnv = "AGENT_LIB_STATE_DIR"
|
|
defaultConfig = `{
|
|
"repo_url": "",
|
|
"ref": "",
|
|
"token": "",
|
|
"token_user": ""
|
|
}`
|
|
)
|
|
|
|
// DefaultConfigPath returns the admin-protected config location per platform:
|
|
// ProgramData on Windows, /etc on Linux/macOS.
|
|
func DefaultConfigPath() string {
|
|
if runtime.GOOS == "windows" {
|
|
if pd := os.Getenv("ProgramData"); pd != "" {
|
|
return filepath.Join(pd, "agent-lib", "config.json")
|
|
}
|
|
return `C:\ProgramData\agent-lib\config.json`
|
|
}
|
|
return "/etc/agent-lib/config.json"
|
|
}
|
|
|
|
// LoadClientConfig reads the config from flagPath, the override env, or the
|
|
// platform default — in that order.
|
|
func LoadClientConfig(flagPath string) (*ClientConfig, error) {
|
|
path := flagPath
|
|
if path == "" {
|
|
path = os.Getenv(ConfigEnv)
|
|
}
|
|
if path == "" {
|
|
path = DefaultConfigPath()
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading client config %s: %w", path, err)
|
|
}
|
|
var cfg ClientConfig
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("client config %s is not valid JSON: %w", path, err)
|
|
}
|
|
if cfg.RepoURL == "" {
|
|
return nil, fmt.Errorf("client config %s: repo_url is required", path)
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
// StateDir returns the client state directory: the override env, then the
|
|
// platform default (LocalAppData on Windows, XDG state elsewhere).
|
|
func StateDir() (string, error) {
|
|
if d := os.Getenv(StateDirEnv); d != "" {
|
|
return d, nil
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
base := os.Getenv("LocalAppData")
|
|
if base == "" {
|
|
return "", fmt.Errorf("LocalAppData is not set")
|
|
}
|
|
return filepath.Join(base, "agent-lib"), nil
|
|
}
|
|
if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" {
|
|
return filepath.Join(xdg, "agent-lib"), nil
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, ".local", "state", "agent-lib"), nil
|
|
}
|
|
|
|
// StatePaths bundles the well-known files inside the state dir.
|
|
type StatePaths struct {
|
|
Root string
|
|
CacheDir string
|
|
Manifest string
|
|
LogFile string
|
|
}
|
|
|
|
func ResolveStatePaths() (StatePaths, error) {
|
|
root, err := StateDir()
|
|
if err != nil {
|
|
return StatePaths{}, err
|
|
}
|
|
return StatePaths{
|
|
Root: root,
|
|
CacheDir: filepath.Join(root, "work-repo"),
|
|
Manifest: filepath.Join(root, "manifest.json"),
|
|
LogFile: filepath.Join(root, "sync.log"),
|
|
}, nil
|
|
}
|