refactor: replace fmt.Errorf/errors.New with coreerr.E()
Some checks failed
Security Scan / security (push) Failing after 8s
Test / test (push) Successful in 1m57s

Replace all remaining fmt.Errorf and errors.New calls in production
code with coreerr.E("caller.Method", "message", err) from go-log.
This standardises error handling across 23 files using the structured
error convention already established in the plugin package.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-16 20:37:25 +00:00
parent c2c54f1abb
commit e9fc6902b1
23 changed files with 136 additions and 133 deletions

View file

@ -2,10 +2,10 @@
package agentci
import (
"errors"
"fmt"
"forge.lthn.ai/core/config"
coreerr "forge.lthn.ai/core/go-log"
)
// AgentConfig represents a single agent machine in the config file.
@ -43,7 +43,7 @@ func LoadAgents(cfg *config.Config) (map[string]AgentConfig, error) {
continue
}
if ac.Host == "" {
return nil, fmt.Errorf("agent %q: host is required", name)
return nil, coreerr.E("agentci.LoadAgents", "agent "+name+": host is required", nil)
}
if ac.QueueDir == "" {
ac.QueueDir = "/home/claude/ai-work/queue"
@ -126,10 +126,10 @@ func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error {
func RemoveAgent(cfg *config.Config, name string) error {
var agents map[string]AgentConfig
if err := cfg.Get("agentci.agents", &agents); err != nil {
return errors.New("no agents configured")
return coreerr.E("agentci.RemoveAgent", "no agents configured", nil)
}
if _, ok := agents[name]; !ok {
return fmt.Errorf("agent %q not found", name)
return coreerr.E("agentci.RemoveAgent", "agent not found: "+name, nil)
}
delete(agents, name)
return cfg.Set("agentci.agents", agents)

View file

@ -1,11 +1,12 @@
package agentci
import (
"fmt"
"os/exec"
"path/filepath"
"regexp"
"strings"
coreerr "forge.lthn.ai/core/go-log"
)
var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
@ -15,10 +16,10 @@ var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
func SanitizePath(input string) (string, error) {
base := filepath.Base(input)
if !safeNameRegex.MatchString(base) {
return "", fmt.Errorf("invalid characters in path element: %s", input)
return "", coreerr.E("agentci.SanitizePath", "invalid characters in path element: "+input, nil)
}
if base == "." || base == ".." || base == "/" {
return "", fmt.Errorf("invalid path element: %s", base)
return "", coreerr.E("agentci.SanitizePath", "invalid path element: "+base, nil)
}
return base, nil
}

View file

@ -10,6 +10,7 @@ import (
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
fg "forge.lthn.ai/core/go-scm/forge"
)
@ -64,7 +65,7 @@ func runSync(args []string) error {
if strings.HasPrefix(basePath, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to resolve home directory: %w", err)
return coreerr.E("forge.runSync", "failed to resolve home directory", err)
}
basePath = filepath.Join(home, basePath[2:])
}
@ -287,7 +288,7 @@ func syncConfigureForgeRemote(localPath, remoteURL string) error {
if existing != remoteURL {
cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "forge", remoteURL)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to update remote: %w", err)
return coreerr.E("forge.syncConfigureForgeRemote", "failed to update remote", err)
}
}
return nil
@ -295,7 +296,7 @@ func syncConfigureForgeRemote(localPath, remoteURL string) error {
cmd := exec.Command("git", "-C", localPath, "remote", "add", "forge", remoteURL)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to add remote: %w", err)
return coreerr.E("forge.syncConfigureForgeRemote", "failed to add remote", err)
}
return nil
@ -306,7 +307,7 @@ func syncPushUpstream(localPath, defaultBranch string) error {
cmd := exec.Command("git", "-C", localPath, "push", "--force", "forge", refspec)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
return coreerr.E("forge.syncPushUpstream", strings.TrimSpace(string(output)), err)
}
return nil
@ -316,7 +317,7 @@ func syncGitFetch(localPath, remote string) error {
cmd := exec.Command("git", "-C", localPath, "fetch", remote)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
return coreerr.E("forge.syncGitFetch", strings.TrimSpace(string(output)), err)
}
return nil
}
@ -327,7 +328,7 @@ func syncCreateMainFromUpstream(client *fg.Client, org, repo string) error {
OldBranchName: "upstream",
})
if err != nil {
return fmt.Errorf("create branch: %w", err)
return coreerr.E("forge.syncCreateMainFromUpstream", "create branch", err)
}
return nil

View file

@ -10,6 +10,7 @@ import (
"code.gitea.io/sdk/gitea"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
gt "forge.lthn.ai/core/go-scm/gitea"
)
@ -64,7 +65,7 @@ func runSync(args []string) error {
if strings.HasPrefix(basePath, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to resolve home directory: %w", err)
return coreerr.E("gitea.runSync", "failed to resolve home directory", err)
}
basePath = filepath.Join(home, basePath[2:])
}
@ -299,7 +300,7 @@ func configureGiteaRemote(localPath, remoteURL string) error {
if existing != remoteURL {
cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "gitea", remoteURL)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to update remote: %w", err)
return coreerr.E("gitea.configureGiteaRemote", "failed to update remote", err)
}
}
return nil
@ -308,7 +309,7 @@ func configureGiteaRemote(localPath, remoteURL string) error {
// Add new remote
cmd := exec.Command("git", "-C", localPath, "remote", "add", "gitea", remoteURL)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to add remote: %w", err)
return coreerr.E("gitea.configureGiteaRemote", "failed to add remote", err)
}
return nil
@ -321,7 +322,7 @@ func pushUpstream(localPath, defaultBranch string) error {
cmd := exec.Command("git", "-C", localPath, "push", "--force", "gitea", refspec)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
return coreerr.E("gitea.pushUpstream", strings.TrimSpace(string(output)), err)
}
return nil
@ -332,7 +333,7 @@ func gitFetch(localPath, remote string) error {
cmd := exec.Command("git", "-C", localPath, "fetch", remote)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
return coreerr.E("gitea.gitFetch", strings.TrimSpace(string(output)), err)
}
return nil
}
@ -344,7 +345,7 @@ func createMainFromUpstream(client *gt.Client, org, repo string) error {
OldBranchName: "upstream",
})
if err != nil {
return fmt.Errorf("create branch: %w", err)
return coreerr.E("gitea.createMainFromUpstream", "create branch", err)
}
return nil

View file

@ -1,7 +1,6 @@
package forge
import (
"fmt"
"strings"
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
@ -75,7 +74,7 @@ func (c *Client) GetLabelByName(owner, repo, name string) (*forgejo.Label, error
}
}
return nil, fmt.Errorf("forge.GetLabelByName: label %s not found in %s/%s", name, owner, repo)
return nil, log.E("forge.GetLabelByName", "label "+name+" not found in "+owner+"/"+repo, nil)
}
// EnsureLabel checks if a label exists, and creates it if it doesn't.

View file

@ -5,6 +5,7 @@ import (
"fmt"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-scm/forge"
"forge.lthn.ai/core/go-scm/jobrunner"
)
@ -47,11 +48,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
if signal.Success {
completeLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentComplete, ColorAgentComplete)
if err != nil {
return nil, fmt.Errorf("ensure label %s: %w", LabelAgentComplete, err)
return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentComplete, err)
}
if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{completeLabel.ID}); err != nil {
return nil, fmt.Errorf("add completed label: %w", err)
return nil, coreerr.E("completion.Execute", "add completed label", err)
}
if signal.Message != "" {
@ -60,11 +61,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
} else {
failedLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentFailed, ColorAgentFailed)
if err != nil {
return nil, fmt.Errorf("ensure label %s: %w", LabelAgentFailed, err)
return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentFailed, err)
}
if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{failedLabel.ID}); err != nil {
return nil, fmt.Errorf("add failed label: %w", err)
return nil, coreerr.E("completion.Execute", "add failed label", err)
}
msg := "Agent reported failure."

View file

@ -8,10 +8,10 @@ import (
"path/filepath"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-scm/agentci"
"forge.lthn.ai/core/go-scm/forge"
"forge.lthn.ai/core/go-scm/jobrunner"
"forge.lthn.ai/core/go-log"
)
const (
@ -83,23 +83,23 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
agentName, agent, ok := h.spinner.FindByForgejoUser(signal.Assignee)
if !ok {
return nil, fmt.Errorf("unknown agent: %s", signal.Assignee)
return nil, coreerr.E("dispatch.Execute", "unknown agent: "+signal.Assignee, nil)
}
// Sanitize inputs to prevent path traversal.
safeOwner, err := agentci.SanitizePath(signal.RepoOwner)
if err != nil {
return nil, fmt.Errorf("invalid repo owner: %w", err)
return nil, coreerr.E("dispatch.Execute", "invalid repo owner", err)
}
safeRepo, err := agentci.SanitizePath(signal.RepoName)
if err != nil {
return nil, fmt.Errorf("invalid repo name: %w", err)
return nil, coreerr.E("dispatch.Execute", "invalid repo name", err)
}
// Ensure in-progress label exists on repo.
inProgressLabel, err := h.forge.EnsureLabel(safeOwner, safeRepo, LabelInProgress, ColorInProgress)
if err != nil {
return nil, fmt.Errorf("ensure label %s: %w", LabelInProgress, err)
return nil, coreerr.E("dispatch.Execute", "ensure label "+LabelInProgress, err)
}
// Check if already in progress to prevent double-dispatch.
@ -107,7 +107,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
if err == nil {
for _, l := range issue.Labels {
if l.Name == LabelInProgress || l.Name == LabelAgentComplete {
log.Info("issue already processed, skipping", "issue", signal.ChildNumber, "label", l.Name)
coreerr.Info("issue already processed, skipping", "issue", signal.ChildNumber, "label", l.Name)
return &jobrunner.ActionResult{
Action: "dispatch",
Success: true,
@ -120,11 +120,11 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
// Assign agent and add in-progress label.
if err := h.forge.AssignIssue(safeOwner, safeRepo, int64(signal.ChildNumber), []string{signal.Assignee}); err != nil {
log.Warn("failed to assign agent, continuing", "err", err)
coreerr.Warn("failed to assign agent, continuing", "err", err)
}
if err := h.forge.AddIssueLabels(safeOwner, safeRepo, int64(signal.ChildNumber), []int64{inProgressLabel.ID}); err != nil {
return nil, fmt.Errorf("add in-progress label: %w", err)
return nil, coreerr.E("dispatch.Execute", "add in-progress label", err)
}
// Remove agent-ready label if present.
@ -164,13 +164,13 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin
ticketJSON, err := json.MarshalIndent(ticket, "", " ")
if err != nil {
h.failDispatch(signal, "Failed to marshal ticket JSON")
return nil, fmt.Errorf("marshal ticket: %w", err)
return nil, coreerr.E("dispatch.Execute", "marshal ticket", err)
}
// Check if ticket already exists on agent (dedup).
ticketName := fmt.Sprintf("ticket-%s-%s-%d.json", safeOwner, safeRepo, signal.ChildNumber)
if h.ticketExists(ctx, agent, ticketName) {
log.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee)
coreerr.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee)
return &jobrunner.ActionResult{
Action: "dispatch",
RepoOwner: safeOwner,
@ -263,7 +263,7 @@ func (h *DispatchHandler) secureTransfer(ctx context.Context, agent agentci.Agen
output, err := cmd.CombinedOutput()
if err != nil {
return log.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err)
return coreerr.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err)
}
return nil
}

View file

@ -7,6 +7,7 @@ import (
forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-scm/forge"
"forge.lthn.ai/core/go-scm/jobrunner"
)
@ -39,7 +40,7 @@ func (h *DismissReviewsHandler) Execute(ctx context.Context, signal *jobrunner.P
reviews, err := h.forge.ListPRReviews(signal.RepoOwner, signal.RepoName, int64(signal.PRNumber))
if err != nil {
return nil, fmt.Errorf("dismiss_reviews: list reviews: %w", err)
return nil, coreerr.E("dismiss_reviews.Execute", "list reviews", err)
}
var dismissErrors []string

View file

@ -8,6 +8,7 @@ import (
forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-scm/forge"
"forge.lthn.ai/core/go-scm/jobrunner"
)
@ -41,7 +42,7 @@ func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.Pipel
// Fetch the epic issue body.
epic, err := h.forge.GetIssue(signal.RepoOwner, signal.RepoName, int64(signal.EpicNumber))
if err != nil {
return nil, fmt.Errorf("tick_parent: fetch epic: %w", err)
return nil, coreerr.E("tick_parent.Execute", "fetch epic", err)
}
oldBody := epic.Body

View file

@ -2,14 +2,13 @@ package jobrunner
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
coreerr "forge.lthn.ai/core/go-log"
coreio "forge.lthn.ai/core/go-io"
)
@ -55,7 +54,7 @@ type Journal struct {
// NewJournal creates a new Journal rooted at baseDir.
func NewJournal(baseDir string) (*Journal, error) {
if baseDir == "" {
return nil, errors.New("journal base directory is required")
return nil, coreerr.E("jobrunner.NewJournal", "base directory is required", nil)
}
return &Journal{baseDir: baseDir}, nil
}
@ -66,12 +65,12 @@ func NewJournal(baseDir string) (*Journal, error) {
func sanitizePathComponent(name string) (string, error) {
// Reject empty or whitespace-only values.
if name == "" || strings.TrimSpace(name) == "" {
return "", fmt.Errorf("invalid path component: %q", name)
return "", coreerr.E("jobrunner.sanitizePathComponent", "invalid path component: "+name, nil)
}
// Reject inputs containing path separators (directory traversal attempt).
if strings.ContainsAny(name, `/\`) {
return "", fmt.Errorf("path component contains directory separator: %q", name)
return "", coreerr.E("jobrunner.sanitizePathComponent", "path component contains directory separator: "+name, nil)
}
// Use filepath.Clean to normalize (e.g., collapse redundant dots).
@ -79,12 +78,12 @@ func sanitizePathComponent(name string) (string, error) {
// Reject traversal components.
if clean == "." || clean == ".." {
return "", fmt.Errorf("invalid path component: %q", name)
return "", coreerr.E("jobrunner.sanitizePathComponent", "invalid path component: "+name, nil)
}
// Validate against the safe character set.
if !validPathComponent.MatchString(clean) {
return "", fmt.Errorf("path component contains invalid characters: %q", name)
return "", coreerr.E("jobrunner.sanitizePathComponent", "path component contains invalid characters: "+name, nil)
}
return clean, nil
@ -93,10 +92,10 @@ func sanitizePathComponent(name string) (string, error) {
// Append writes a journal entry for the given signal and result.
func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
if signal == nil {
return errors.New("signal is required")
return coreerr.E("jobrunner.Journal.Append", "signal is required", nil)
}
if result == nil {
return errors.New("result is required")
return coreerr.E("jobrunner.Journal.Append", "result is required", nil)
}
entry := JournalEntry{
@ -124,18 +123,18 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
data, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("marshal journal entry: %w", err)
return coreerr.E("jobrunner.Journal.Append", "marshal journal entry", err)
}
data = append(data, '\n')
// Sanitize path components to prevent path traversal (CVE: issue #46).
owner, err := sanitizePathComponent(signal.RepoOwner)
if err != nil {
return fmt.Errorf("invalid repo owner: %w", err)
return coreerr.E("jobrunner.Journal.Append", "invalid repo owner", err)
}
repo, err := sanitizePathComponent(signal.RepoName)
if err != nil {
return fmt.Errorf("invalid repo name: %w", err)
return coreerr.E("jobrunner.Journal.Append", "invalid repo name", err)
}
date := result.Timestamp.UTC().Format("2006-01-02")
@ -144,27 +143,27 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
// Resolve to absolute path and verify it stays within baseDir.
absBase, err := filepath.Abs(j.baseDir)
if err != nil {
return fmt.Errorf("resolve base directory: %w", err)
return coreerr.E("jobrunner.Journal.Append", "resolve base directory", err)
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("resolve journal directory: %w", err)
return coreerr.E("jobrunner.Journal.Append", "resolve journal directory", err)
}
if !strings.HasPrefix(absDir, absBase+string(filepath.Separator)) {
return fmt.Errorf("journal path %q escapes base directory %q", absDir, absBase)
return coreerr.E("jobrunner.Journal.Append", "journal path escapes base directory", nil)
}
j.mu.Lock()
defer j.mu.Unlock()
if err := coreio.Local.EnsureDir(dir); err != nil {
return fmt.Errorf("create journal directory: %w", err)
return coreerr.E("jobrunner.Journal.Append", "create journal directory", err)
}
path := filepath.Join(dir, date+".jsonl")
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("open journal file: %w", err)
return coreerr.E("jobrunner.Journal.Append", "open journal file", err)
}
defer func() { _ = f.Close() }()

View file

@ -3,10 +3,10 @@ package manifest
import (
"crypto/ed25519"
"encoding/json"
"fmt"
"path/filepath"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-io"
)
@ -35,19 +35,19 @@ type CompileOptions struct {
// options. If opts.SignKey is provided the manifest is signed first.
func Compile(m *Manifest, opts CompileOptions) (*CompiledManifest, error) {
if m == nil {
return nil, fmt.Errorf("manifest.Compile: nil manifest")
return nil, coreerr.E("manifest.Compile", "nil manifest", nil)
}
if m.Code == "" {
return nil, fmt.Errorf("manifest.Compile: missing code")
return nil, coreerr.E("manifest.Compile", "missing code", nil)
}
if m.Version == "" {
return nil, fmt.Errorf("manifest.Compile: missing version")
return nil, coreerr.E("manifest.Compile", "missing version", nil)
}
// Sign if a key is supplied.
if opts.SignKey != nil {
if err := Sign(m, opts.SignKey); err != nil {
return nil, fmt.Errorf("manifest.Compile: %w", err)
return nil, coreerr.E("manifest.Compile", "sign failed", err)
}
}
@ -69,7 +69,7 @@ func MarshalJSON(cm *CompiledManifest) ([]byte, error) {
func ParseCompiled(data []byte) (*CompiledManifest, error) {
var cm CompiledManifest
if err := json.Unmarshal(data, &cm); err != nil {
return nil, fmt.Errorf("manifest.ParseCompiled: %w", err)
return nil, coreerr.E("manifest.ParseCompiled", "unmarshal failed", err)
}
return &cm, nil
}
@ -81,7 +81,7 @@ const compiledPath = "core.json"
func WriteCompiled(medium io.Medium, root string, cm *CompiledManifest) error {
data, err := MarshalJSON(cm)
if err != nil {
return fmt.Errorf("manifest.WriteCompiled: %w", err)
return coreerr.E("manifest.WriteCompiled", "marshal failed", err)
}
path := filepath.Join(root, compiledPath)
return medium.Write(path, string(data))
@ -92,7 +92,7 @@ func LoadCompiled(medium io.Medium, root string) (*CompiledManifest, error) {
path := filepath.Join(root, compiledPath)
data, err := medium.Read(path)
if err != nil {
return nil, fmt.Errorf("manifest.LoadCompiled: %w", err)
return nil, coreerr.E("manifest.LoadCompiled", "read failed", err)
}
return ParseCompiled([]byte(data))
}

View file

@ -2,9 +2,9 @@ package manifest
import (
"crypto/ed25519"
"fmt"
"path/filepath"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-io"
"gopkg.in/yaml.v3"
)
@ -21,7 +21,7 @@ func Load(medium io.Medium, root string) (*Manifest, error) {
path := filepath.Join(root, manifestPath)
data, err := medium.Read(path)
if err != nil {
return nil, fmt.Errorf("manifest.Load: %w", err)
return nil, coreerr.E("manifest.Load", "read failed", err)
}
return Parse([]byte(data))
}
@ -34,10 +34,10 @@ func LoadVerified(medium io.Medium, root string, pub ed25519.PublicKey) (*Manife
}
ok, err := Verify(m, pub)
if err != nil {
return nil, fmt.Errorf("manifest.LoadVerified: %w", err)
return nil, coreerr.E("manifest.LoadVerified", "verification error", err)
}
if !ok {
return nil, fmt.Errorf("manifest.LoadVerified: signature verification failed for %q", m.Code)
return nil, coreerr.E("manifest.LoadVerified", "signature verification failed for "+m.Code, nil)
}
return m, nil
}

View file

@ -1,8 +1,7 @@
package manifest
import (
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"gopkg.in/yaml.v3"
)
@ -66,7 +65,7 @@ type DaemonSpec struct {
func Parse(data []byte) (*Manifest, error) {
var m Manifest
if err := yaml.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("manifest.Parse: %w", err)
return nil, coreerr.E("manifest.Parse", "unmarshal failed", err)
}
return &m, nil
}

View file

@ -3,9 +3,8 @@ package manifest
import (
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
coreerr "forge.lthn.ai/core/go-log"
"gopkg.in/yaml.v3"
)
@ -20,7 +19,7 @@ func signable(m *Manifest) ([]byte, error) {
func Sign(m *Manifest, priv ed25519.PrivateKey) error {
msg, err := signable(m)
if err != nil {
return fmt.Errorf("manifest.Sign: marshal: %w", err)
return coreerr.E("manifest.Sign", "marshal failed", err)
}
sig := ed25519.Sign(priv, msg)
m.Sign = base64.StdEncoding.EncodeToString(sig)
@ -30,15 +29,15 @@ func Sign(m *Manifest, priv ed25519.PrivateKey) error {
// Verify checks the ed25519 signature in m.Sign against the public key.
func Verify(m *Manifest, pub ed25519.PublicKey) (bool, error) {
if m.Sign == "" {
return false, errors.New("manifest.Verify: no signature present")
return false, coreerr.E("manifest.Verify", "no signature present", nil)
}
sig, err := base64.StdEncoding.DecodeString(m.Sign)
if err != nil {
return false, fmt.Errorf("manifest.Verify: decode: %w", err)
return false, coreerr.E("manifest.Verify", "decode failed", err)
}
msg, err := signable(m)
if err != nil {
return false, fmt.Errorf("manifest.Verify: marshal: %w", err)
return false, coreerr.E("manifest.Verify", "marshal failed", err)
}
return ed25519.Verify(pub, msg, sig), nil
}

View file

@ -2,12 +2,12 @@ package marketplace
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
coreerr "forge.lthn.ai/core/go-log"
coreio "forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/manifest"
)
@ -40,7 +40,7 @@ func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) {
if os.IsNotExist(err) {
continue
}
return nil, fmt.Errorf("marketplace.Builder: read %s: %w", dir, err)
return nil, coreerr.E("marketplace.Builder.BuildFromDirs", "read "+dir, err)
}
for _, e := range entries {
@ -115,11 +115,11 @@ func BuildFromManifests(manifests []*manifest.Manifest) *Index {
// WriteIndex serialises an Index to JSON and writes it to the given path.
func WriteIndex(path string, idx *Index) error {
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
return fmt.Errorf("marketplace.WriteIndex: mkdir: %w", err)
return coreerr.E("marketplace.WriteIndex", "mkdir failed", err)
}
data, err := json.MarshalIndent(idx, "", " ")
if err != nil {
return fmt.Errorf("marketplace.WriteIndex: marshal: %w", err)
return coreerr.E("marketplace.WriteIndex", "marshal failed", err)
}
return coreio.Local.Write(path, string(data))
}
@ -131,7 +131,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) {
if raw, err := coreio.Local.Read(coreJSON); err == nil {
cm, err := manifest.ParseCompiled([]byte(raw))
if err != nil {
return nil, fmt.Errorf("parse core.json: %w", err)
return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse core.json", err)
}
return &cm.Manifest, nil
}
@ -145,7 +145,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) {
m, err := manifest.Parse([]byte(raw))
if err != nil {
return nil, fmt.Errorf("parse manifest.yaml: %w", err)
return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse manifest.yaml", err)
}
return m, nil
}

View file

@ -1,11 +1,11 @@
package marketplace
import (
"fmt"
"log"
"os"
"path/filepath"
coreerr "forge.lthn.ai/core/go-log"
coreio "forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/manifest"
"gopkg.in/yaml.v3"
@ -30,7 +30,7 @@ func DiscoverProviders(dir string) ([]DiscoveredProvider, error) {
if os.IsNotExist(err) {
return nil, nil // No providers directory — not an error.
}
return nil, fmt.Errorf("marketplace.DiscoverProviders: %w", err)
return nil, coreerr.E("marketplace.DiscoverProviders", "read directory", err)
}
var providers []DiscoveredProvider
@ -93,12 +93,12 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) {
Providers: make(map[string]ProviderRegistryEntry),
}, nil
}
return nil, fmt.Errorf("marketplace.LoadProviderRegistry: %w", err)
return nil, coreerr.E("marketplace.LoadProviderRegistry", "read failed", err)
}
var reg ProviderRegistryFile
if err := yaml.Unmarshal([]byte(raw), &reg); err != nil {
return nil, fmt.Errorf("marketplace.LoadProviderRegistry: %w", err)
return nil, coreerr.E("marketplace.LoadProviderRegistry", "parse failed", err)
}
if reg.Providers == nil {
@ -111,12 +111,12 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) {
// SaveProviderRegistry writes the registry to the given path.
func SaveProviderRegistry(path string, reg *ProviderRegistryFile) error {
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
return fmt.Errorf("marketplace.SaveProviderRegistry: %w", err)
return coreerr.E("marketplace.SaveProviderRegistry", "ensure directory", err)
}
data, err := yaml.Marshal(reg)
if err != nil {
return fmt.Errorf("marketplace.SaveProviderRegistry: %w", err)
return coreerr.E("marketplace.SaveProviderRegistry", "marshal failed", err)
}
return coreio.Local.Write(path, string(data))

View file

@ -4,12 +4,12 @@ import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/manifest"
"forge.lthn.ai/core/go-io/store"
@ -49,15 +49,15 @@ type InstalledModule struct {
func (i *Installer) Install(ctx context.Context, mod Module) error {
// Check if already installed
if _, err := i.store.Get(storeGroup, mod.Code); err == nil {
return fmt.Errorf("marketplace: module %q already installed", mod.Code)
return coreerr.E("marketplace.Installer.Install", "module already installed: "+mod.Code, nil)
}
dest := filepath.Join(i.modulesDir, mod.Code)
if err := i.medium.EnsureDir(i.modulesDir); err != nil {
return fmt.Errorf("marketplace: mkdir: %w", err)
return coreerr.E("marketplace.Installer.Install", "mkdir", err)
}
if err := gitClone(ctx, mod.Repo, dest); err != nil {
return fmt.Errorf("marketplace: clone %s: %w", mod.Repo, err)
return coreerr.E("marketplace.Installer.Install", "clone "+mod.Repo, err)
}
// On any error after clone, clean up the directory
@ -70,7 +70,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error {
medium, err := io.NewSandboxed(dest)
if err != nil {
return fmt.Errorf("marketplace: medium: %w", err)
return coreerr.E("marketplace.Installer.Install", "medium", err)
}
m, err := loadManifest(medium, mod.SignKey)
@ -92,11 +92,11 @@ func (i *Installer) Install(ctx context.Context, mod Module) error {
data, err := json.Marshal(installed)
if err != nil {
return fmt.Errorf("marketplace: marshal: %w", err)
return coreerr.E("marketplace.Installer.Install", "marshal", err)
}
if err := i.store.Set(storeGroup, mod.Code, string(data)); err != nil {
return fmt.Errorf("marketplace: store: %w", err)
return coreerr.E("marketplace.Installer.Install", "store", err)
}
cleanup = false
@ -106,7 +106,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error {
// Remove uninstalls a module by deleting its files and store entry.
func (i *Installer) Remove(code string) error {
if _, err := i.store.Get(storeGroup, code); err != nil {
return fmt.Errorf("marketplace: module %q not installed", code)
return coreerr.E("marketplace.Installer.Remove", "module not installed: "+code, nil)
}
dest := filepath.Join(i.modulesDir, code)
@ -119,29 +119,29 @@ func (i *Installer) Remove(code string) error {
func (i *Installer) Update(ctx context.Context, code string) error {
raw, err := i.store.Get(storeGroup, code)
if err != nil {
return fmt.Errorf("marketplace: module %q not installed", code)
return coreerr.E("marketplace.Installer.Update", "module not installed: "+code, nil)
}
var installed InstalledModule
if err := json.Unmarshal([]byte(raw), &installed); err != nil {
return fmt.Errorf("marketplace: unmarshal: %w", err)
return coreerr.E("marketplace.Installer.Update", "unmarshal", err)
}
dest := filepath.Join(i.modulesDir, code)
cmd := exec.CommandContext(ctx, "git", "-C", dest, "pull", "--ff-only")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("marketplace: pull: %s: %w", strings.TrimSpace(string(output)), err)
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 fmt.Errorf("marketplace: medium: %w", mErr)
return coreerr.E("marketplace.Installer.Update", "medium", mErr)
}
m, mErr := loadManifest(medium, installed.SignKey)
if mErr != nil {
return fmt.Errorf("marketplace: reload manifest: %w", mErr)
return coreerr.E("marketplace.Installer.Update", "reload manifest", mErr)
}
// Update stored metadata
@ -151,7 +151,7 @@ func (i *Installer) Update(ctx context.Context, code string) error {
data, err := json.Marshal(installed)
if err != nil {
return fmt.Errorf("marketplace: marshal: %w", err)
return coreerr.E("marketplace.Installer.Update", "marshal", err)
}
return i.store.Set(storeGroup, code, string(data))
@ -161,7 +161,7 @@ func (i *Installer) Update(ctx context.Context, code string) error {
func (i *Installer) Installed() ([]InstalledModule, error) {
all, err := i.store.GetAll(storeGroup)
if err != nil {
return nil, fmt.Errorf("marketplace: list: %w", err)
return nil, coreerr.E("marketplace.Installer.Installed", "list", err)
}
var modules []InstalledModule
@ -180,7 +180,7 @@ func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error)
if signKey != "" {
pubBytes, err := hex.DecodeString(signKey)
if err != nil {
return nil, fmt.Errorf("marketplace: decode sign key: %w", err)
return nil, coreerr.E("marketplace.loadManifest", "decode sign key", err)
}
return manifest.LoadVerified(medium, ".", pubBytes)
}
@ -191,7 +191,7 @@ func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error)
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 fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
return coreerr.E("marketplace.gitClone", strings.TrimSpace(string(output)), err)
}
return nil
}

View file

@ -2,8 +2,9 @@ package marketplace
import (
"encoding/json"
"fmt"
"strings"
coreerr "forge.lthn.ai/core/go-log"
)
// Module is a marketplace entry pointing to a module's Git repo.
@ -26,7 +27,7 @@ type Index struct {
func ParseIndex(data []byte) (*Index, error) {
var idx Index
if err := json.Unmarshal(data, &idx); err != nil {
return nil, fmt.Errorf("marketplace.ParseIndex: %w", err)
return nil, coreerr.E("marketplace.ParseIndex", "unmarshal failed", err)
}
return &idx, nil
}

View file

@ -159,7 +159,7 @@ func (i *Installer) cloneRepo(ctx context.Context, org, repo, version, dest stri
cmd := exec.CommandContext(ctx, "gh", args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(output)))
return coreerr.E("plugin.Installer.cloneRepo", strings.TrimSpace(string(output)), err)
}
return nil

View file

@ -1,10 +1,10 @@
package repos
import (
"fmt"
"path/filepath"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-io"
"gopkg.in/yaml.v3"
)
@ -44,12 +44,12 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) {
content, err := m.Read(path)
if err != nil {
return nil, fmt.Errorf("failed to read git state: %w", err)
return nil, coreerr.E("repos.LoadGitState", "failed to read git state", err)
}
var gs GitState
if err := yaml.Unmarshal([]byte(content), &gs); err != nil {
return nil, fmt.Errorf("failed to parse git state: %w", err)
return nil, coreerr.E("repos.LoadGitState", "failed to parse git state", err)
}
if gs.Repos == nil {
@ -66,17 +66,17 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) {
func SaveGitState(m io.Medium, root string, gs *GitState) error {
coreDir := filepath.Join(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
return fmt.Errorf("failed to create .core directory: %w", err)
return coreerr.E("repos.SaveGitState", "failed to create .core directory", err)
}
data, err := yaml.Marshal(gs)
if err != nil {
return fmt.Errorf("failed to marshal git state: %w", err)
return coreerr.E("repos.SaveGitState", "failed to marshal git state", err)
}
path := filepath.Join(coreDir, "git.yaml")
if err := m.Write(path, string(data)); err != nil {
return fmt.Errorf("failed to write git state: %w", err)
return coreerr.E("repos.SaveGitState", "failed to write git state", err)
}
return nil

View file

@ -4,6 +4,7 @@ import (
"fmt"
"path/filepath"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-io"
"gopkg.in/yaml.v3"
)
@ -74,12 +75,12 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
content, err := m.Read(path)
if err != nil {
return nil, fmt.Errorf("failed to read kb config: %w", err)
return nil, coreerr.E("repos.LoadKBConfig", "failed to read kb config", err)
}
kb := DefaultKBConfig()
if err := yaml.Unmarshal([]byte(content), kb); err != nil {
return nil, fmt.Errorf("failed to parse kb config: %w", err)
return nil, coreerr.E("repos.LoadKBConfig", "failed to parse kb config", err)
}
return kb, nil
@ -89,17 +90,17 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error {
coreDir := filepath.Join(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
return fmt.Errorf("failed to create .core directory: %w", err)
return coreerr.E("repos.SaveKBConfig", "failed to create .core directory", err)
}
data, err := yaml.Marshal(kb)
if err != nil {
return fmt.Errorf("failed to marshal kb config: %w", err)
return coreerr.E("repos.SaveKBConfig", "failed to marshal kb config", err)
}
path := filepath.Join(coreDir, "kb.yaml")
if err := m.Write(path, string(data)); err != nil {
return fmt.Errorf("failed to write kb config: %w", err)
return coreerr.E("repos.SaveKBConfig", "failed to write kb config", err)
}
return nil

View file

@ -4,12 +4,11 @@
package repos
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-io"
"gopkg.in/yaml.v3"
)
@ -67,13 +66,13 @@ type Repo struct {
func LoadRegistry(m io.Medium, path string) (*Registry, error) {
content, err := m.Read(path)
if err != nil {
return nil, fmt.Errorf("failed to read registry file: %w", err)
return nil, coreerr.E("repos.LoadRegistry", "failed to read registry file", err)
}
data := []byte(content)
var reg Registry
if err := yaml.Unmarshal(data, &reg); err != nil {
return nil, fmt.Errorf("failed to parse registry file: %w", err)
return nil, coreerr.E("repos.LoadRegistry", "failed to parse registry file", err)
}
reg.medium = m
@ -147,7 +146,7 @@ func FindRegistry(m io.Medium) (string, error) {
}
}
return "", errors.New("repos.yaml not found")
return "", coreerr.E("repos.FindRegistry", "repos.yaml not found", nil)
}
// ScanDirectory creates a Registry by scanning a directory for git repos.
@ -156,7 +155,7 @@ func FindRegistry(m io.Medium) (string, error) {
func ScanDirectory(m io.Medium, dir string) (*Registry, error) {
entries, err := m.List(dir)
if err != nil {
return nil, fmt.Errorf("failed to read directory: %w", err)
return nil, coreerr.E("repos.ScanDirectory", "failed to read directory", err)
}
reg := &Registry{
@ -282,12 +281,12 @@ func (r *Registry) TopologicalOrder() ([]*Repo, error) {
return nil
}
if visiting[name] {
return fmt.Errorf("circular dependency detected: %s", name)
return coreerr.E("repos.Registry.TopologicalOrder", "circular dependency detected: "+name, nil)
}
repo, ok := r.Repos[name]
if !ok {
return fmt.Errorf("unknown repo: %s", name)
return coreerr.E("repos.Registry.TopologicalOrder", "unknown repo: "+name, nil)
}
visiting[name] = true

View file

@ -1,10 +1,10 @@
package repos
import (
"fmt"
"path/filepath"
"time"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-io"
"gopkg.in/yaml.v3"
)
@ -63,12 +63,12 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) {
content, err := m.Read(path)
if err != nil {
return nil, fmt.Errorf("failed to read work config: %w", err)
return nil, coreerr.E("repos.LoadWorkConfig", "failed to read work config", err)
}
wc := DefaultWorkConfig()
if err := yaml.Unmarshal([]byte(content), wc); err != nil {
return nil, fmt.Errorf("failed to parse work config: %w", err)
return nil, coreerr.E("repos.LoadWorkConfig", "failed to parse work config", err)
}
return wc, nil
@ -78,17 +78,17 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) {
func SaveWorkConfig(m io.Medium, root string, wc *WorkConfig) error {
coreDir := filepath.Join(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
return fmt.Errorf("failed to create .core directory: %w", err)
return coreerr.E("repos.SaveWorkConfig", "failed to create .core directory", err)
}
data, err := yaml.Marshal(wc)
if err != nil {
return fmt.Errorf("failed to marshal work config: %w", err)
return coreerr.E("repos.SaveWorkConfig", "failed to marshal work config", err)
}
path := filepath.Join(coreDir, "work.yaml")
if err := m.Write(path, string(data)); err != nil {
return fmt.Errorf("failed to write work config: %w", err)
return coreerr.E("repos.SaveWorkConfig", "failed to write work config", err)
}
return nil