go-scm/marketplace/installer.go
Virgil c42cc4a6ce
Some checks failed
Security Scan / security (push) Failing after 10s
Test / test (push) Successful in 2m4s
chore(ax): gofmt exported declaration comments
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-30 05:44:09 +00:00

222 lines
6.6 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package marketplace
import (
"context"
filepath "dappco.re/go/core/scm/internal/ax/filepathx"
json "dappco.re/go/core/scm/internal/ax/jsonx"
strings "dappco.re/go/core/scm/internal/ax/stringsx"
"encoding/hex"
exec "golang.org/x/sys/execabs"
"time"
"dappco.re/go/core/io"
"dappco.re/go/core/io/store"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/scm/agentci"
"dappco.re/go/core/scm/manifest"
)
const storeGroup = "_modules"
// Installer handles module installation from Git repos.
type Installer struct {
medium io.Medium
modulesDir string
store *store.Store
}
// NewInstaller creates a new module installer.
func NewInstaller(m io.Medium, modulesDir string, st *store.Store) *Installer {
return &Installer{
medium: m,
modulesDir: modulesDir,
store: st,
}
}
// InstalledModule holds stored metadata about an installed module.
type InstalledModule struct {
Code string `json:"code"`
Name string `json:"name"`
Version string `json:"version"`
Repo string `json:"repo"`
EntryPoint string `json:"entry_point"`
Permissions manifest.Permissions `json:"permissions"`
SignKey string `json:"sign_key,omitempty"`
InstalledAt string `json:"installed_at"`
}
// Install clones a module repo, verifies its manifest signature, and registers it.
func (i *Installer) Install(ctx context.Context, mod Module) error {
safeCode, dest, err := i.resolveModulePath(mod.Code)
if err != nil {
return coreerr.E("marketplace.Installer.Install", "invalid module code", err)
}
// Check if already installed
if _, err := i.store.Get(storeGroup, safeCode); err == nil {
return coreerr.E("marketplace.Installer.Install", "module already installed: "+safeCode, nil)
}
if err := i.medium.EnsureDir(i.modulesDir); err != nil {
return coreerr.E("marketplace.Installer.Install", "mkdir", err)
}
if err := gitClone(ctx, mod.Repo, dest); err != nil {
return coreerr.E("marketplace.Installer.Install", "clone "+mod.Repo, err)
}
// On any error after clone, clean up the directory
cleanup := true
defer func() {
if cleanup {
_ = i.medium.DeleteAll(dest)
}
}()
medium, err := io.NewSandboxed(dest)
if err != nil {
return coreerr.E("marketplace.Installer.Install", "medium", err)
}
m, err := loadManifest(medium, mod.SignKey)
if err != nil {
return err
}
entryPoint := filepath.Join(dest, "main.ts")
installed := InstalledModule{
Code: safeCode,
Name: m.Name,
Version: m.Version,
Repo: mod.Repo,
EntryPoint: entryPoint,
Permissions: m.Permissions,
SignKey: mod.SignKey,
InstalledAt: time.Now().UTC().Format(time.RFC3339),
}
data, err := json.Marshal(installed)
if err != nil {
return coreerr.E("marketplace.Installer.Install", "marshal", err)
}
if err := i.store.Set(storeGroup, safeCode, string(data)); err != nil {
return coreerr.E("marketplace.Installer.Install", "store", err)
}
cleanup = false
return nil
}
// Remove uninstalls a module by deleting its files and store entry.
func (i *Installer) Remove(code string) error {
safeCode, dest, err := i.resolveModulePath(code)
if err != nil {
return coreerr.E("marketplace.Installer.Remove", "invalid module code", err)
}
if _, err := i.store.Get(storeGroup, safeCode); err != nil {
return coreerr.E("marketplace.Installer.Remove", "module not installed: "+safeCode, nil)
}
if err := i.medium.DeleteAll(dest); err != nil {
return coreerr.E("marketplace.Installer.Remove", "delete module files", err)
}
return i.store.Delete(storeGroup, safeCode)
}
// Update pulls latest changes and re-verifies the manifest.
func (i *Installer) Update(ctx context.Context, code string) error {
safeCode, dest, err := i.resolveModulePath(code)
if err != nil {
return coreerr.E("marketplace.Installer.Update", "invalid module code", err)
}
raw, err := i.store.Get(storeGroup, safeCode)
if err != nil {
return coreerr.E("marketplace.Installer.Update", "module not installed: "+safeCode, nil)
}
var installed InstalledModule
if err := json.Unmarshal([]byte(raw), &installed); err != nil {
return coreerr.E("marketplace.Installer.Update", "unmarshal", err)
}
cmd := exec.CommandContext(ctx, "git", "-C", dest, "pull", "--ff-only")
if output, err := cmd.CombinedOutput(); err != nil {
return coreerr.E("marketplace.Installer.Update", "pull: "+strings.TrimSpace(string(output)), err)
}
// Reload and re-verify manifest with the same key used at install time
medium, mErr := io.NewSandboxed(dest)
if mErr != nil {
return coreerr.E("marketplace.Installer.Update", "medium", mErr)
}
m, mErr := loadManifest(medium, installed.SignKey)
if mErr != nil {
return coreerr.E("marketplace.Installer.Update", "reload manifest", mErr)
}
// Update stored metadata
installed.Code = safeCode
installed.Name = m.Name
installed.Version = m.Version
installed.Permissions = m.Permissions
data, err := json.Marshal(installed)
if err != nil {
return coreerr.E("marketplace.Installer.Update", "marshal", err)
}
return i.store.Set(storeGroup, safeCode, string(data))
}
// Installed returns all installed module metadata.
func (i *Installer) Installed() ([]InstalledModule, error) {
all, err := i.store.GetAll(storeGroup)
if err != nil {
return nil, coreerr.E("marketplace.Installer.Installed", "list", err)
}
var modules []InstalledModule
for _, raw := range all {
var m InstalledModule
if err := json.Unmarshal([]byte(raw), &m); err != nil {
continue
}
modules = append(modules, m)
}
return modules, nil
}
// loadManifest loads and optionally verifies a module manifest.
func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error) {
if signKey != "" {
pubBytes, err := hex.DecodeString(signKey)
if err != nil {
return nil, coreerr.E("marketplace.loadManifest", "decode sign key", err)
}
return manifest.LoadVerified(medium, ".", pubBytes)
}
return manifest.Load(medium, ".")
}
// gitClone clones a repository with --depth=1.
func gitClone(ctx context.Context, repo, dest string) error {
cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", repo, dest)
if output, err := cmd.CombinedOutput(); err != nil {
return coreerr.E("marketplace.gitClone", strings.TrimSpace(string(output)), err)
}
return nil
}
func (i *Installer) resolveModulePath(code string) (string, string, error) {
safeCode, dest, err := agentci.ResolvePathWithinRoot(i.modulesDir, code)
if err != nil {
return "", "", coreerr.E("marketplace.Installer.resolveModulePath", "resolve module path", err)
}
return safeCode, dest, nil
}