fix: guard resolveUserPath against undefined input (#10176)

* fix: guard resolveUserPath against undefined input

When subagent spawner omits workspaceDir, resolveUserPath receives
undefined and crashes on .trim().  Add a falsy guard that falls back
to process.cwd(), matching the behavior callers already expect.

Closes #10089

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden runner workspace fallback (#10176) (thanks @Yida-Dev)

* fix: harden workspace fallback scoping (#10176) (thanks @Yida-Dev)

* refactor: centralize workspace fallback classification and redaction (#10176) (thanks @Yida-Dev)

* test: remove explicit any from utils mock (#10176) (thanks @Yida-Dev)

* security: reject malformed agent session keys for workspace resolution (#10176) (thanks @Yida-Dev)

---------

Co-authored-by: Yida-Dev <reyifeijun@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
This commit is contained in:
Yida-Dev
2026-02-07 01:16:58 +07:00
committed by GitHub
parent 5842bcaaf7
commit 4216449405
22 changed files with 522 additions and 24 deletions

View File

@@ -0,0 +1,139 @@
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolveRunWorkspaceDir } from "./workspace-run.js";
import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js";
describe("resolveRunWorkspaceDir", () => {
it("resolves explicit workspace values without fallback", () => {
const explicit = path.join(process.cwd(), "tmp", "workspace-run-explicit");
const result = resolveRunWorkspaceDir({
workspaceDir: explicit,
sessionKey: "agent:main:subagent:test",
});
expect(result.usedFallback).toBe(false);
expect(result.agentId).toBe("main");
expect(result.workspaceDir).toBe(path.resolve(explicit));
});
it("falls back to configured per-agent workspace when input is missing", () => {
const defaultWorkspace = path.join(process.cwd(), "tmp", "workspace-default-main");
const researchWorkspace = path.join(process.cwd(), "tmp", "workspace-research");
const cfg = {
agents: {
defaults: { workspace: defaultWorkspace },
list: [{ id: "research", workspace: researchWorkspace }],
},
} satisfies OpenClawConfig;
const result = resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "agent:research:subagent:test",
config: cfg,
});
expect(result.usedFallback).toBe(true);
expect(result.fallbackReason).toBe("missing");
expect(result.agentId).toBe("research");
expect(result.workspaceDir).toBe(path.resolve(researchWorkspace));
});
it("falls back to default workspace for blank strings", () => {
const defaultWorkspace = path.join(process.cwd(), "tmp", "workspace-default-main");
const cfg = {
agents: {
defaults: { workspace: defaultWorkspace },
},
} satisfies OpenClawConfig;
const result = resolveRunWorkspaceDir({
workspaceDir: " ",
sessionKey: "agent:main:subagent:test",
config: cfg,
});
expect(result.usedFallback).toBe(true);
expect(result.fallbackReason).toBe("blank");
expect(result.agentId).toBe("main");
expect(result.workspaceDir).toBe(path.resolve(defaultWorkspace));
});
it("falls back to built-in main workspace when config is unavailable", () => {
const result = resolveRunWorkspaceDir({
workspaceDir: null,
sessionKey: "agent:main:subagent:test",
config: undefined,
});
expect(result.usedFallback).toBe(true);
expect(result.fallbackReason).toBe("missing");
expect(result.agentId).toBe("main");
expect(result.workspaceDir).toBe(path.resolve(DEFAULT_AGENT_WORKSPACE_DIR));
});
it("throws for malformed agent session keys", () => {
expect(() =>
resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "agent::broken",
config: undefined,
}),
).toThrow("Malformed agent session key");
});
it("uses explicit agent id for per-agent fallback when config is unavailable", () => {
const result = resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "definitely-not-a-valid-session-key",
agentId: "research",
config: undefined,
});
expect(result.agentId).toBe("research");
expect(result.agentIdSource).toBe("explicit");
expect(result.workspaceDir).toBe(path.resolve(os.homedir(), ".openclaw", "workspace-research"));
});
it("throws for malformed agent session keys even when config has a default agent", () => {
const mainWorkspace = path.join(process.cwd(), "tmp", "workspace-main-default");
const researchWorkspace = path.join(process.cwd(), "tmp", "workspace-research-default");
const cfg = {
agents: {
defaults: { workspace: mainWorkspace },
list: [
{ id: "main", workspace: mainWorkspace },
{ id: "research", workspace: researchWorkspace, default: true },
],
},
} satisfies OpenClawConfig;
expect(() =>
resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "agent::broken",
config: cfg,
}),
).toThrow("Malformed agent session key");
});
it("treats non-agent legacy keys as default, not malformed", () => {
const fallbackWorkspace = path.join(process.cwd(), "tmp", "workspace-default-legacy");
const cfg = {
agents: {
defaults: { workspace: fallbackWorkspace },
},
} satisfies OpenClawConfig;
const result = resolveRunWorkspaceDir({
workspaceDir: undefined,
sessionKey: "custom-main-key",
config: cfg,
});
expect(result.agentId).toBe("main");
expect(result.agentIdSource).toBe("default");
expect(result.workspaceDir).toBe(path.resolve(fallbackWorkspace));
});
});