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
2 changes: 1 addition & 1 deletion go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ set; the load-bearing ones:
| `AGENT_CALLBACK_URL` | Public URL the control plane calls the node back on. **Required for any containerized/remote deploy that isn't this compose file** (compose sets it per service) — without it the CP gets `504 agent_unreachable` |
| `NODE_ID` | Node ID (`swe-planner` / `swe-fast`) |
| `PORT` | Listen port (`8005` / `8006`) |
| `SWE_PRO_ENGINE` | Route per-issue coding through the bundled high-performance coding engine (beta). **Set to `1` for you by `af install` / Desktop; unset (off) everywhere else** — clone, fork, compose, bare binary. `1`/`true`/`yes`/`on` enables, `0`/`false` disables |
| `SWE_PRO_ENGINE` | Route per-issue coding through the bundled high-performance coding engine (beta), vendored for darwin-arm64, darwin-amd64, linux-amd64, and linux-arm64. Windows is not yet supported by the engine; the node logs a warning and falls back to the classic loop there. **Set to `1` for you by `af install` / Desktop; unset (off) everywhere else** — clone, fork, compose, bare binary. `1`/`true`/`yes`/`on` enables, `0`/`false` disables |
| `SWE_PRO_VARIANT` | Engine reasoning-effort variant (e.g. `low` for fastest turnaround, `high` for depth). Unset keeps the engine's own default |
| `SWE_PRO_MAX_COST` | Per-run cost ceiling in USD forwarded to the engine on every dispatch. Unset: no SWE-AF-side ceiling |
| `SWE_PRO_PUBLIC_URL` | Callback base URL for the engine, mirroring `AGENT_CALLBACK_URL` on the nodes. **In Docker this must be set to a container-reachable URL**, otherwise the control plane can't call the engine back |
Expand Down
Binary file added go/bin/swe-pro-darwin-amd64
Binary file not shown.
Binary file added go/bin/swe-pro-linux-arm64
Binary file not shown.
14 changes: 8 additions & 6 deletions go/docs/pro-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

The Go node runs a high-performance coding engine, shipped as prebuilt
binaries — one per supported platform, vendored at `go/bin` as
`swe-pro-darwin-arm64` and `swe-pro-linux-amd64`, because one checkout is
installed on macOS and Linux alike and the node picks the matching build at
startup.
`swe-pro-darwin-arm64`, `swe-pro-darwin-amd64`, `swe-pro-linux-amd64`, and
`swe-pro-linux-arm64`, because one checkout is installed on macOS and Linux
alike and the node picks the matching build at startup. The engine does not yet
support Windows; there the node logs a warning and falls back to the classic
coding loop.

It is **on by default for nodes installed with `af install` or AgentField
Desktop**: `agentfield-package.yaml` declares `SWE_PRO_ENGINE` with
Expand Down Expand Up @@ -59,8 +61,8 @@ to switch back. Two things differ from the classic loop, both additive:
The engine never pushes or opens PRs — branch, push and PR creation stay with
the standard pipeline, so the deliverables are unchanged.

If the flag is set but no *runnable* engine binary is found — missing, or
present without its execute bit — the node logs a warning naming the path and
If the flag is set but no *runnable* engine binary is found — missing, or on
Unix present without its execute bit — the node logs a warning naming the path and
comes up on the classic coding loop: `pro_execute` is not registered and
nothing is routed to an engine node that never joined. The binary is searched
for at `SWE_PRO_BIN` when set (authoritative — no fallback), else
Expand All @@ -75,7 +77,7 @@ what keeps a macOS install from exec'ing the Linux build.
| Variable | Default | Purpose |
|---|---|---|
| `SWE_PRO_ENGINE` | `1` via the manifest on `af install` / Desktop; unset (off) for a clone, fork, compose stack or bare binary | Truthy (`1`/`true`/`yes`/`on`) enables; `0`/`false` opts out |
| `SWE_PRO_BIN` | `/usr/local/bin/swe-pro`, else a `swe-pro-<GOOS>-<GOARCH>` / `swe-pro` sibling | Engine binary path (authoritative when set) |
| `SWE_PRO_BIN` | `/usr/local/bin/swe-pro`, else a `swe-pro-<GOOS>-<GOARCH>` / `swe-pro` sibling (`.exe` names first on Windows) | Engine binary path (authoritative when set); must have an execute bit on Unix |
| `SWE_PRO_NODE_ID` | `swe-pro` | Engine's control-plane node id |
| `SWE_PRO_PORT` | `8801` | Engine's listen port |
| `SWE_PRO_PUBLIC_URL` | `http://localhost:8801` (engine default) | Callback base URL — **must** be set to a container-reachable address in Docker, otherwise the control plane cannot reach the engine |
Expand Down
29 changes: 22 additions & 7 deletions go/internal/pro/pro.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,23 @@ func Enabled() bool {
func BinPath() string { return envOr(EnvBin, DefaultBin) }

// runnable reports whether path is an existing regular file we could actually
// spawn. Mere existence is not enough: a copy that lost its execute bit (some
// installers create destination files with a fresh 0644 mode) would otherwise
// look available and then fail at exec time, which is exactly the state the
// availability gate exists to avoid.
// spawn. On Unix, mere existence is not enough: a copy that lost its execute
// bit (some installers create destination files with a fresh 0644 mode) would
// otherwise look available and then fail at exec time. Windows does not expose
// execute bits through os.Stat, so a regular file is sufficient there.
func runnable(path string) bool {
info, err := os.Stat(path)
if err != nil || info.IsDir() {
if err != nil {
return false
}
return runnableMode(runtime.GOOS, info.Mode())
}

func runnableMode(goos string, mode os.FileMode) bool {
if !mode.IsRegular() {
return false
}
return info.Mode().Perm()&0o111 != 0
return goos == "windows" || mode.Perm()&0o111 != 0
}

// osExecutable is os.Executable, indirected so tests can point the sibling
Expand All @@ -126,7 +133,15 @@ var osExecutable = os.Executable
// plain name stays as a fallback for layouts that place one hand-built engine
// beside the node (an unpacked image, a local engine build).
func siblingNames() []string {
return []string{"swe-pro-" + runtime.GOOS + "-" + runtime.GOARCH, "swe-pro"}
return siblingNamesFor(runtime.GOOS, runtime.GOARCH)
}

func siblingNamesFor(goos, goarch string) []string {
platformName := "swe-pro-" + goos + "-" + goarch
if goos == "windows" {
return []string{platformName + ".exe", "swe-pro.exe", platformName, "swe-pro"}
}
return []string{platformName, "swe-pro"}
}

// ResolveBin returns the first runnable engine binary on disk. An explicit
Expand Down
81 changes: 67 additions & 14 deletions go/internal/pro/pro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,49 @@ func TestDefaults(t *testing.T) {
}
}

func TestSiblingNamesFor(t *testing.T) {
tests := []struct {
goos, goarch string
want []string
}{
{"linux", "amd64", []string{"swe-pro-linux-amd64", "swe-pro"}},
{"darwin", "arm64", []string{"swe-pro-darwin-arm64", "swe-pro"}},
{"windows", "amd64", []string{"swe-pro-windows-amd64.exe", "swe-pro.exe", "swe-pro-windows-amd64", "swe-pro"}},
}
for _, tt := range tests {
t.Run(tt.goos+"/"+tt.goarch, func(t *testing.T) {
got := siblingNamesFor(tt.goos, tt.goarch)
if strings.Join(got, "\x00") != strings.Join(tt.want, "\x00") {
t.Errorf("siblingNamesFor(%q, %q) = %q, want %q", tt.goos, tt.goarch, got, tt.want)
}
})
}
}

func TestRunnableMode(t *testing.T) {
tests := []struct {
name string
goos string
mode os.FileMode
want bool
}{
{"linux regular 0644", "linux", 0o644, false},
{"linux regular 0755", "linux", 0o755, true},
{"darwin regular 0644", "darwin", 0o644, false},
{"darwin regular 0755", "darwin", 0o755, true},
{"windows regular 0666", "windows", 0o666, true},
{"linux directory", "linux", os.ModeDir | 0o755, false},
{"windows directory", "windows", os.ModeDir | 0o777, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := runnableMode(tt.goos, tt.mode); got != tt.want {
t.Errorf("runnableMode(%q, %v) = %v, want %v", tt.goos, tt.mode, got, tt.want)
}
})
}
}

// TestResolveBin covers the three-step search: explicit SWE_PRO_BIN is
// authoritative (found or not — no fall-through), and Available() is the
// flag AND binary-presence conjunction.
Expand Down Expand Up @@ -110,12 +153,18 @@ func TestResolveBin(t *testing.T) {
t.Fatal(err)
}
t.Setenv(EnvBin, nonExec)
if _, ok := ResolveBin(); ok {
t.Error("ResolveBin() reported a non-executable file as usable")
}
t.Setenv(EnvEnabled, "1")
if Available() {
t.Error("Available() = true for a non-executable binary — must degrade to the classic loop")
if runtime.GOOS == "windows" {
if _, ok := ResolveBin(); !ok || !Available() {
t.Error("regular Windows binary reported unusable because os.Stat exposes no execute bits")
}
} else {
if _, ok := ResolveBin(); ok {
t.Error("ResolveBin() reported a non-executable file as usable")
}
if Available() {
t.Error("Available() = true for a non-executable binary — must degrade to the classic loop")
}
}

// A directory at the binary path is likewise not runnable (os.Stat alone
Expand Down Expand Up @@ -152,24 +201,28 @@ func TestResolveBinSiblings(t *testing.T) {
if runnable(DefaultBin) {
t.Skipf("%s exists on this host and short-circuits the sibling search", DefaultBin)
}
suffixed := "swe-pro-" + runtime.GOOS + "-" + runtime.GOARCH
names := siblingNames()
suffixed, plain := names[0], names[1]

cases := []struct {
type testCase struct {
name string
// present maps sibling file name to its mode; 0o644 is the
// present-but-unusable case the availability gate must reject.
present map[string]os.FileMode
want string // sibling name, or "" for "no usable engine"
wantOK bool
}{
{"suffixed preferred over plain", map[string]os.FileMode{suffixed: 0o755, "swe-pro": 0o755}, suffixed, true},
}
cases := []testCase{
{"suffixed preferred over plain", map[string]os.FileMode{suffixed: 0o755, plain: 0o755}, suffixed, true},
{"suffixed alone", map[string]os.FileMode{suffixed: 0o755}, suffixed, true},
{"plain alone is the fallback", map[string]os.FileMode{"swe-pro": 0o755}, "swe-pro", true},
{"plain alone is the fallback", map[string]os.FileMode{plain: 0o755}, plain, true},
{"neither present", nil, "", false},
{"plain usable, suffixed not", map[string]os.FileMode{suffixed: 0o644, "swe-pro": 0o755}, "swe-pro", true},
// Both unusable: the warning must name the suffixed candidate, the one
// this platform was meant to run.
{"both unusable names the best candidate", map[string]os.FileMode{suffixed: 0o644, "swe-pro": 0o644}, suffixed, false},
}
if runtime.GOOS != "windows" {
cases = append(cases,
testCase{"plain usable, suffixed not", map[string]os.FileMode{suffixed: 0o644, plain: 0o755}, plain, true},
testCase{"both unusable names the best candidate", map[string]os.FileMode{suffixed: 0o644, plain: 0o644}, suffixed, false},
)
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
Loading