Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions agentci/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions agentci/security.go
Original file line number Diff line number Diff line change
@@ -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\-\_\.]+$`)
Expand All @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/forge/cmd_forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ package forge

import (
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-scm/locales"
)

func init() {
cli.RegisterCommands(AddForgeCommands)
cli.RegisterCommands(AddForgeCommands, locales.FS)
}

// Style aliases from shared package.
Expand Down
13 changes: 7 additions & 6 deletions cmd/forge/cmd_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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:])
}
Expand Down Expand Up @@ -287,15 +288,15 @@ 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
}

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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down
13 changes: 7 additions & 6 deletions cmd/gitea/cmd_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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:])
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down
102 changes: 102 additions & 0 deletions cmd/scm/cmd_compile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package scm

import (
"crypto/ed25519"
"encoding/hex"
"os/exec"
"strings"

"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/manifest"
)

func addCompileCommand(parent *cli.Command) {
var (
dir string
signKey string
builtBy string
)

cmd := &cli.Command{
Use: "compile",
Short: "Compile manifest.yaml into core.json",
Long: "Read .core/manifest.yaml, attach build metadata (commit, tag), and write core.json to the project root.",
RunE: func(cmd *cli.Command, args []string) error {
return runCompile(dir, signKey, builtBy)
},
}

cmd.Flags().StringVarP(&dir, "dir", "d", ".", "Project root directory")
cmd.Flags().StringVar(&signKey, "sign-key", "", "Hex-encoded ed25519 private key for signing")
cmd.Flags().StringVar(&builtBy, "built-by", "core scm compile", "Builder identity")

parent.AddCommand(cmd)
}

func runCompile(dir, signKeyHex, builtBy string) error {
medium, err := io.NewSandboxed(dir)
if err != nil {
return cli.WrapVerb(err, "open", dir)
}

m, err := manifest.Load(medium, ".")
if err != nil {
return cli.WrapVerb(err, "load", "manifest")
}

opts := manifest.CompileOptions{
Commit: gitCommit(dir),
Tag: gitTag(dir),
BuiltBy: builtBy,
}

if signKeyHex != "" {
keyBytes, err := hex.DecodeString(signKeyHex)
if err != nil {
return cli.WrapVerb(err, "decode", "sign key")
}
opts.SignKey = ed25519.PrivateKey(keyBytes)
}
Comment on lines +54 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing key length validation before cast.

The hex-decoded bytes are cast directly to ed25519.PrivateKey without validating the length. An ed25519 private key should be 64 bytes (or 32 bytes for seed-only). Invalid lengths will cause cryptographic operations to fail or behave unexpectedly.

🛡️ Proposed fix to add validation
 	if signKeyHex != "" {
 		keyBytes, err := hex.DecodeString(signKeyHex)
 		if err != nil {
 			return cli.WrapVerb(err, "decode", "sign key")
 		}
+		if len(keyBytes) != ed25519.PrivateKeySize {
+			return cli.WrapVerb(fmt.Errorf("expected %d bytes, got %d", ed25519.PrivateKeySize, len(keyBytes)), "validate", "sign key")
+		}
 		opts.SignKey = ed25519.PrivateKey(keyBytes)
 	}

Note: This requires adding "fmt" to the imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/scm/cmd_compile.go` around lines 54 - 60, Validate the decoded signing
key length before casting: in the block handling signKeyHex (where keyBytes, err
:= hex.DecodeString(signKeyHex) and opts.SignKey = ed25519.PrivateKey(keyBytes)
are used), check that len(keyBytes) is either 32 or 64 and return a wrapped
error (using cli.WrapVerb) if not; if valid, proceed to set opts.SignKey; add
"fmt" to imports if you need to format the error message.


cm, err := manifest.Compile(m, opts)
if err != nil {
return err
}

if err := manifest.WriteCompiled(medium, ".", cm); err != nil {
return err
}

cli.Blank()
cli.Print(" %s %s\n", successStyle.Render("compiled"), valueStyle.Render(m.Code))
cli.Print(" %s %s\n", dimStyle.Render("version:"), valueStyle.Render(m.Version))
if opts.Commit != "" {
cli.Print(" %s %s\n", dimStyle.Render("commit:"), valueStyle.Render(opts.Commit))
}
if opts.Tag != "" {
cli.Print(" %s %s\n", dimStyle.Render("tag:"), valueStyle.Render(opts.Tag))
}
cli.Print(" %s %s\n", dimStyle.Render("output:"), valueStyle.Render("core.json"))
cli.Blank()

return nil
}

// gitCommit returns the current HEAD commit hash, or empty on error.
func gitCommit(dir string) string {
out, err := exec.Command("git", "-C", dir, "rev-parse", "HEAD").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}

// gitTag returns the tag pointing at HEAD, or empty if none.
func gitTag(dir string) string {
out, err := exec.Command("git", "-C", dir, "describe", "--tags", "--exact-match", "HEAD").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
59 changes: 59 additions & 0 deletions cmd/scm/cmd_export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package scm

import (
"fmt"

"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/manifest"
)

func addExportCommand(parent *cli.Command) {
var dir string

cmd := &cli.Command{
Use: "export",
Short: "Export compiled manifest as JSON",
Long: "Read core.json from the project root and print it to stdout. Falls back to compiling .core/manifest.yaml if core.json is not found.",
RunE: func(cmd *cli.Command, args []string) error {
return runExport(dir)
},
}

cmd.Flags().StringVarP(&dir, "dir", "d", ".", "Project root directory")

parent.AddCommand(cmd)
}

func runExport(dir string) error {
medium, err := io.NewSandboxed(dir)
if err != nil {
return cli.WrapVerb(err, "open", dir)
}

// Try core.json first.
cm, err := manifest.LoadCompiled(medium, ".")
if err != nil {
// Fall back to compiling from source.
m, loadErr := manifest.Load(medium, ".")
if loadErr != nil {
return cli.WrapVerb(loadErr, "load", "manifest")
}
cm, err = manifest.Compile(m, manifest.CompileOptions{
Commit: gitCommit(dir),
Tag: gitTag(dir),
BuiltBy: "core scm export",
})
if err != nil {
return err
}
}

data, err := manifest.MarshalJSON(cm)
if err != nil {
return cli.WrapVerb(err, "marshal", "manifest")
}

fmt.Println(string(data))
return nil
}
Loading