fix: filter non-parent events in main NDJSON handler and fix workspace creation path

Bug 1: Subagent events from gateway broadcasts were processed as parent
events because the sessionKey filter was accidentally removed during the
subagent decoupling refactor. Re-add the filter in wireChildProcess.

Bug 2: Creating workspaces at custom paths failed because:
- mkdir API rejected absolute paths outside workspace root
- Directory picker started at workspace root, not home
- Error responses from mkdir were silently swallowed
Add absolute path support to mkdir, handle errors in picker UI,
start picker at home dir, and normalize init route paths.
This commit is contained in:
kumarabhirup 2026-02-21 13:45:11 -08:00
parent 109b88b93c
commit 92fadd6700
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
6 changed files with 45 additions and 9 deletions

View File

@ -1,5 +1,6 @@
import { readdirSync, statSync, type Dirent } from "node:fs";
import { join, dirname, resolve } from "node:path";
import { homedir } from "node:os";
import { resolveWorkspaceRoot } from "@/lib/workspace";
export const dynamic = "force-dynamic";
@ -116,6 +117,10 @@ export async function GET(req: Request) {
);
}
if (dir.startsWith("~")) {
dir = join(homedir(), dir.slice(1));
}
const resolved = resolve(dir);
const entries = buildBrowseTree(resolved, 3, 0, showHidden);

View File

@ -248,6 +248,7 @@ export async function POST(req: Request) {
if (workspaceDir.startsWith("~")) {
workspaceDir = join(homedir(), workspaceDir.slice(1));
}
workspaceDir = resolve(workspaceDir);
} else {
const stateDir = resolveOpenClawStateDir();
if (profileName && profileName !== "default") {

View File

@ -1,4 +1,5 @@
import { mkdirSync, existsSync } from "node:fs";
import { resolve, normalize } from "node:path";
import { safeResolveNewPath } from "@/lib/workspace";
export const dynamic = "force-dynamic";
@ -6,27 +7,44 @@ export const runtime = "nodejs";
/**
* POST /api/workspace/mkdir
* Body: { path: string }
* Body: { path: string; absolute?: boolean }
*
* Creates a new directory in the workspace.
* Creates a new directory. By default paths are resolved relative to the
* workspace root. When `absolute` is true the path is treated as a
* filesystem-absolute path (used by the directory picker for workspace
* creation outside the current workspace).
*/
export async function POST(req: Request) {
let body: { path?: string };
let body: { path?: string; absolute?: boolean };
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
}
const { path: relPath } = body;
if (!relPath || typeof relPath !== "string") {
const { path: rawPath, absolute: useAbsolute } = body;
if (!rawPath || typeof rawPath !== "string") {
return Response.json(
{ error: "Missing 'path' field" },
{ status: 400 },
);
}
const absPath = safeResolveNewPath(relPath);
let absPath: string | null;
if (useAbsolute) {
const normalized = normalize(rawPath);
if (normalized.includes("/../") || normalized.includes("/..")) {
return Response.json(
{ error: "Path traversal rejected" },
{ status: 400 },
);
}
absPath = resolve(normalized);
} else {
absPath = safeResolveNewPath(rawPath);
}
if (!absPath) {
return Response.json(
{ error: "Invalid path or path traversal rejected" },
@ -43,7 +61,7 @@ export async function POST(req: Request) {
try {
mkdirSync(absPath, { recursive: true });
return Response.json({ ok: true, path: relPath });
return Response.json({ ok: true, path: absPath });
} catch (err) {
return Response.json(
{ error: err instanceof Error ? err.message : "mkdir failed" },

View File

@ -369,6 +369,7 @@ export function CreateWorkspaceDialog({ isOpen, onClose, onCreated }: CreateWork
open={showDirPicker}
onClose={() => setShowDirPicker(false)}
onSelect={(path) => setCustomPath(path)}
startDir="~"
/>
</div>
);

View File

@ -151,11 +151,16 @@ export function DirectoryPickerModal({
if (!newFolderName.trim() || !displayDir) {return;}
const folderPath = `${displayDir}/${newFolderName.trim()}`;
try {
await fetch("/api/workspace/mkdir", {
const res = await fetch("/api/workspace/mkdir", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: folderPath }),
body: JSON.stringify({ path: folderPath, absolute: true }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError((data as { error?: string }).error || "Failed to create folder");
return;
}
setCreatingFolder(false);
setNewFolderName("");
void fetchDir(currentDir);

View File

@ -803,6 +803,12 @@ function wireChildProcess(run: ActiveRun): void {
return;
}
// Skip events from other sessions (e.g. subagent broadcasts that
// the gateway delivers on the same WS connection).
if (ev.sessionKey && ev.sessionKey !== parentSessionKey) {
return;
}
// Track the global event cursor from the gateway for replay on handoff.
const gSeq = typeof (ev as Record<string, unknown>).globalSeq === "number"
? (ev as Record<string, unknown>).globalSeq as number