mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-20 02:17:24 +00:00
fix(exec): surface real cwd error + auto-normalize stale workspace paths (#77)
* fix(exec): surface chdir error instead of misleading fork/exec Linux Go forkAndExecInChild conflates chdir + execve failures into one PathError naming only argv0. When agent workspace points to a stale directory, exec reports "fork/exec /usr/bin/gh: no such file" even though the binary exists — the real failure is chdir on cmd.Dir. Two fixes: - loop_context.go: fall back to l.workspace when per-user MkdirAll fails (3 sites: user workspace, dispatched team, auto-resolved team) - shell.go + credentialed_exec.go: preflight cwd via validateExecCwd before exec.Command, so the error names the directory, not the binary Regression test TestValidateExecCwd covers empty / existing / missing / file-instead-of-dir. * feat(upgrade): auto-normalize stale agent workspace paths Detects non-portable workspace values left by deployment migrations (Docker → bare-metal, host path drift) and rewrites them to the current configured base. Registered as data hook 072_normalize_agent_workspaces; runs automatically after `migrate up`. Stale patterns: - /app/workspace/* (Docker container path persisted on bare-metal) - ~ prefix (Go doesn't expand tildes; MkdirAll creates literal ~ dir) Base resolved with gateway precedence: GOCLAW_WORKSPACE env > config.Agents.Defaults.Workspace > default ~/.goclaw/workspace. Idempotent (skips rows already at proposed value). Conservative (leaves absolute non-stale paths untouched as intentional custom config).
This commit is contained in:
1 parent
93461e9863
commit
d4463cde6f
7 files changed
+324
-6
No files matched your search
@@ -177,7 +177,15 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
ctx = store.WithSharedSessions(ctx)
|
||||
}
|
||||
if err := os.MkdirAll(effectiveWorkspace, 0755); err != nil {
|
||||
slog.Warn("failed to create user workspace directory", "workspace", effectiveWorkspace, "user", req.UserID, "error", err)
|
||||
// Stale stored workspace (e.g. Docker-era /app/workspace/ on bare-metal host)
|
||||
// would propagate as cmd.Dir into exec tools, where Linux's clone+chdir+execve
|
||||
// failure surfaces as a misleading "fork/exec PATH: no such file or directory"
|
||||
// — same message users would see for a missing binary. Fall back to the system
|
||||
// default workspace (already created at startup) so tools keep working while
|
||||
// the warning surfaces the data drift for operators.
|
||||
slog.Warn("failed to create user workspace directory; falling back to system default",
|
||||
"workspace", effectiveWorkspace, "fallback", l.workspace, "user", req.UserID, "error", err)
|
||||
effectiveWorkspace = l.workspace
|
||||
}
|
||||
ctx = tools.WithToolWorkspace(ctx, effectiveWorkspace)
|
||||
} else if l.workspace != "" {
|
||||
@@ -187,10 +195,15 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
// Team workspace: dispatched task overrides default workspace.
|
||||
if req.TeamWorkspace != "" {
|
||||
if err := os.MkdirAll(req.TeamWorkspace, 0755); err != nil {
|
||||
slog.Warn("failed to create team workspace directory", "workspace", req.TeamWorkspace, "error", err)
|
||||
// See note above on loop_context user workspace fallback. A broken
|
||||
// req.TeamWorkspace would otherwise become cmd.Dir and surface as
|
||||
// "fork/exec PATH: no such file or directory" from any tool exec.
|
||||
slog.Warn("failed to create team workspace directory; keeping previous workspace",
|
||||
"workspace", req.TeamWorkspace, "error", err)
|
||||
} else {
|
||||
ctx = tools.WithToolTeamWorkspace(ctx, req.TeamWorkspace)
|
||||
ctx = tools.WithToolWorkspace(ctx, req.TeamWorkspace)
|
||||
}
|
||||
ctx = tools.WithToolTeamWorkspace(ctx, req.TeamWorkspace)
|
||||
ctx = tools.WithToolWorkspace(ctx, req.TeamWorkspace)
|
||||
}
|
||||
if req.TeamID != "" {
|
||||
ctx = tools.WithToolTeamID(ctx, req.TeamID)
|
||||
@@ -235,9 +248,14 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
tools.UserChatLayer(wsChat, shared),
|
||||
)
|
||||
if err := os.MkdirAll(wsDir, 0750); err != nil {
|
||||
slog.Warn("failed to create team workspace directory", "workspace", wsDir, "error", err)
|
||||
// See note above on loop_context user workspace fallback. Skip the
|
||||
// team workspace context-set so downstream tools don't inherit a
|
||||
// path that would fail with a misleading "fork/exec PATH: ENOENT".
|
||||
slog.Warn("failed to create team workspace directory; skipping team workspace ctx",
|
||||
"workspace", wsDir, "error", err)
|
||||
} else {
|
||||
ctx = tools.WithToolTeamWorkspace(ctx, wsDir)
|
||||
}
|
||||
ctx = tools.WithToolTeamWorkspace(ctx, wsDir)
|
||||
// Team root (no UserChatLayer): lets any team agent — leader or member —
|
||||
// read files produced by peers under different chat/user scopes within
|
||||
// the same team. Writes still default to wsDir above; team root only
|
||||
|
||||
Reference in new issue
Block a user