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
6 changes: 6 additions & 0 deletions docs/past-failures.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
- Cause: the node daemon reused the server session id as the ACPX session name, but a stale ACPX session could exist without the backing ACP metadata OpenClaw needs to initialize.
- Mitigation: the Go ACPX runner now treats that exact metadata-missing failure as recoverable both during `sessions ensure` and during the first prompt run, recreates the named ACPX session once with `sessions new --name <session>`, and retries. Regression tests cover the ensure path and the OpenClaw prompt-time failure shape.

## 2026-05-13: OpenClaw executable detection could accept a broken wrapper

- Symptom: `openclaw` was present on `PATH`, but `acpx openclaw` failed before ACP `initialize` because the first executable was a wrapper that depended on unavailable local state.
- Cause: detection treated executable presence as enough, while OpenClaw's ACPX target needs `openclaw acp` to run as a clean stdio ACP server from the daemon environment.
- Mitigation: OpenClaw detection now probes every `openclaw` executable directory on `PATH` with ACPX `sessions ensure`, persists the first PATH ordering that initializes successfully, and omits OpenClaw if none can start ACP. A regression test covers a broken wrapper before a working executable.

## 2026-05-12: Installer rejected valid Node 24 runtimes

- Symptom: remote bootstrap failed early with `could not determine Node.js major version` even though `node -v` reported `v24.13.1`.
Expand Down
66 changes: 65 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,20 +250,84 @@ func detectAgents(ctx context.Context, runner acpx.Runner) []nodeconfig.AgentCon
detectedCandidates := detectInstalledAgents(candidates)
agents := make([]nodeconfig.AgentConfig, 0, len(candidates))
for _, candidate := range detectedCandidates {
env := detectedAgentEnv(candidate)
if candidate.ACPXAgent == "openclaw" {
var ok bool
env, ok = verifiedOpenClawEnv(ctx, runner, env)
if !ok {
continue
}
}
agent := nodeconfig.AgentConfig{
ID: candidate.ID,
Name: candidate.Name,
ACPXAgent: candidate.ACPXAgent,
Command: defaultACPXCommand(),
Args: []string{},
Env: detectedAgentEnv(candidate),
Env: env,
Labels: []string{"detected"},
}
agents = append(agents, agent)
}
return agents
}

func verifiedOpenClawEnv(ctx context.Context, runner acpx.Runner, fallback map[string]string) (map[string]string, bool) {
candidateDirs := openClawPathDirs()
if len(candidateDirs) == 0 {
return nil, false
}

baseEntries := filepath.SplitList(os.Getenv("PATH"))
nodeDirs := lookPathDir("node")
for _, dir := range candidateDirs {
pathEntries := uniquePathEntries([]string{dir}, nodeDirs, baseEntries)
env := map[string]string{
"PATH": strings.Join(pathEntries, string(os.PathListSeparator)),
}
probeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
err := runner.Ensure(probeCtx, acpx.RunRequest{
Command: defaultACPXCommand(),
Agent: "openclaw",
Session: "amesh-detect-openclaw",
Env: envList(env),
})
cancel()
if err == nil {
return env, true
}
log.Printf("openclaw ACP readiness probe failed for PATH prefix %s: %v", dir, err)
}

if len(fallback) > 0 {
return fallback, false
}
return nil, false
}

func openClawPathDirs() []string {
seen := map[string]struct{}{}
dirs := make([]string, 0)
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
dir = strings.TrimSpace(dir)
if dir == "" {
continue
}
path := filepath.Join(dir, "openclaw")
info, err := os.Stat(path)
if err != nil || info.IsDir() || info.Mode()&0o111 == 0 {
continue
}
clean := filepath.Clean(dir)
if _, ok := seen[clean]; ok {
continue
}
seen[clean] = struct{}{}
dirs = append(dirs, clean)
}
return dirs
}

func detectedAgentEnv(candidate detectableAgent) map[string]string {
pathEntries := uniquePathEntries(
filepath.SplitList(os.Getenv("PATH")),
Expand Down
50 changes: 50 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
Expand Down Expand Up @@ -554,6 +555,55 @@ exit 1
}
}

func TestDetectAgentsVerifiesOpenClawACPReadinessAcrossPathCandidates(t *testing.T) {
home := t.TempDir()
badDir := filepath.Join(t.TempDir(), "bad-bin")
goodDir := filepath.Join(t.TempDir(), "good-bin")
nodeDir := filepath.Join(t.TempDir(), "node-bin")
t.Setenv("HOME", home)
t.Setenv("PATH", strings.Join([]string{badDir, goodDir, nodeDir}, string(os.PathListSeparator)))
t.Setenv("AMESH_ACPX_PATH", "")

managed := filepath.Join(home, ".local", "share", "amesh", "acpx", "bin", "acpx")
writeExecutable(t, managed, fmt.Sprintf(`#!/bin/sh
if [ "$1" = "--help" ]; then
cat <<'EOF'
Commands:
openclaw [options] [prompt...] Use openclaw agent
EOF
exit 0
fi
if [ "$1" = "openclaw" ] && [ "$2" = "sessions" ] && [ "$3" = "ensure" ]; then
if [ "$(command -v openclaw)" = "%s/openclaw" ]; then
exit 0
fi
echo "ACP agent exited before initialize completed: wrapper unavailable" >&2
exit 1
fi
exit 1
`, goodDir))
writeExecutable(t, filepath.Join(nodeDir, "node"), "#!/bin/sh\nexit 0\n")
writeExecutable(t, filepath.Join(badDir, "openclaw"), "#!/bin/sh\nexit 1\n")
writeExecutable(t, filepath.Join(goodDir, "openclaw"), "#!/bin/sh\nexit 0\n")

got := detectAgents(context.Background(), acpx.Runner{})
wantPath := strings.Join([]string{goodDir, nodeDir, badDir}, string(os.PathListSeparator))
want := []nodeconfig.AgentConfig{
{
ID: "agent-openclaw",
Name: "OpenClaw",
ACPXAgent: "openclaw",
Command: managed,
Args: []string{},
Env: map[string]string{"PATH": wantPath},
Labels: []string{"detected"},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("detectAgents() = %#v, want %#v", got, want)
}
}

type fakeDaemonClient struct {
mu sync.Mutex
connectErr error
Expand Down
Loading