From 2e3bbe3f9881298cd079896c3e29332a32c7488a Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 9 Aug 2026 17:00:03 +0530 Subject: [PATCH 1/4] fix(workflow): scope CLI runs and validate options --- src/workflow-cli.ts | 95 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/src/workflow-cli.ts b/src/workflow-cli.ts index 6100332c..fb3548b9 100644 --- a/src/workflow-cli.ts +++ b/src/workflow-cli.ts @@ -1,5 +1,6 @@ import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { resolveCliWorkspaceScope } from "./cli-workspace.js"; import type { ServerConfig } from "./config.js"; import { parseWorkflowArgFlagsResult } from "./workflow-files.js"; import { @@ -27,6 +28,7 @@ import { spawnWorkflowWorker, spawnWorkflowWorkerFromCli, } from "./workflow-worker.js"; +import { isPathInsideRoot } from "./roots.js"; export { runWorkflowWorker, spawnWorkflowWorker, spawnWorkflowWorkerFromCli }; @@ -104,6 +106,8 @@ export function printWorkflowHelp(): void { async function runWorkflowRun(args: string[], config: ServerConfig): Promise { const { flags } = splitFlags(args); + assertKnownFlags(flags, ["follow", "script-path", "file", "name", "resume", "arg"], + "Usage: devspace workflow run [--file|--script-path | --name ] [--resume ] [--arg key=value]... [--follow]"); const follow = flags.has("follow"); const file = flagValue(flags, "script-path") ?? flagValue(flags, "file"); const name = flagValue(flags, "name"); @@ -126,10 +130,15 @@ async function runWorkflowRun(args: string[], config: ServerConfig): Promise { - const follow = args.includes("--follow"); - const runId = args.find((a) => !a.startsWith("-")); + const { flags, positionals } = splitFlags(args); + assertKnownFlags(flags, ["follow"], "Usage: devspace workflow status [--follow]"); + const follow = flags.has("follow"); + const runId = positionals[0]; if (!runId) { throw new InvalidWorkflowInputError({ code: "invalid_argument", @@ -199,6 +210,7 @@ async function runWorkflowStatus(args: string[], config: ServerConfig): Promise< if (runResult.isErr()) throw runResult.error; const run = runResult.value; if (!run) throw new WorkflowNotFoundError(runId); + assertWorkflowInScope(run, resolveCliWorkspaceScope(config.allowedRoots)); console.log(formatRunLine(run)); console.log(formatCallSummary(store.listAgentCalls(runId))); if (follow) { @@ -213,7 +225,9 @@ async function runWorkflowStatus(args: string[], config: ServerConfig): Promise< } async function runWorkflowCancel(args: string[], config: ServerConfig): Promise { - const runId = args[0]; + const { flags, positionals } = splitFlags(args); + assertKnownFlags(flags, [], "Usage: devspace workflow cancel "); + const runId = positionals[0]; if (!runId) { throw new InvalidWorkflowInputError({ code: "invalid_argument", @@ -223,6 +237,9 @@ async function runWorkflowCancel(args: string[], config: ServerConfig): Promise< const store = createWorkflowStore(config); try { reapStaleWorkflows(store); + const run = store.getRun(runId); + if (!run) throw new WorkflowNotFoundError(runId); + assertWorkflowInScope(run, resolveCliWorkspaceScope(config.allowedRoots)); console.log(formatRunLine(await cancelWorkflowRun(store, runId))); } finally { store.close(); @@ -233,7 +250,8 @@ async function runWorkflowList(config: ServerConfig): Promise { const store = createWorkflowStore(config); try { reapStaleWorkflows(store); - const runs = store.listRuns(50); + const scope = resolveCliWorkspaceScope(config.allowedRoots); + const runs = store.listRunsForWorkspace(scope.workspaceRoot, { limit: 50 }); if (runs.length === 0) { console.log("No workflow runs."); return; @@ -245,7 +263,9 @@ async function runWorkflowList(config: ServerConfig): Promise { } async function runWorkflowCalls(args: string[], config: ServerConfig): Promise { - const runId = args[0]; + const { flags, positionals } = splitFlags(args); + assertKnownFlags(flags, [], "Usage: devspace workflow calls "); + const runId = positionals[0]; if (!runId) { throw new InvalidWorkflowInputError({ code: "invalid_argument", @@ -254,7 +274,9 @@ async function runWorkflowCalls(args: string[], config: ServerConfig): Promise { - const runId = args[0]; - const callIndex = Number(args[1]); + const { flags, positionals } = splitFlags(args); + assertKnownFlags(flags, [], "Usage: devspace workflow call "); + const runId = positionals[0]; + const callIndex = Number(positionals[1]); if (!runId || !Number.isInteger(callIndex) || callIndex < 0) { throw new InvalidWorkflowInputError({ code: "invalid_argument", @@ -277,7 +301,9 @@ async function runWorkflowCall(args: string[], config: ServerConfig): Promise { let sinceSeq = 0; for (;;) { + reapStaleWorkflows(store); const page = store.drainEvents(runId, sinceSeq, WORKFLOW_LIMITS.eventDrainDefault); for (const event of page.events) printEvent(event); sinceSeq = page.nextSeq; @@ -440,6 +467,50 @@ function splitFlags(args: string[]): { return { flags, positionals }; } +function assertKnownFlags( + flags: Map, + allowed: string[], + usage: string, +): void { + const allowedSet = new Set(allowed); + const unknown = [...flags.keys()].filter((flag) => !allowedSet.has(flag)); + if (unknown.length > 0) { + throw new InvalidWorkflowInputError({ + code: "invalid_argument", + message: `${usage}\nUnknown option: --${unknown[0]}`, + }); + } +} + +function resolveWorkflowFilePath(path: string, workspaceRoot: string): string { + const resolvedPath = resolve(workspaceRoot, path); + if (!isPathInsideRoot(resolvedPath, workspaceRoot)) { + throw new InvalidWorkflowInputError({ + code: "invalid_path", + message: `Workflow file must be inside the workspace: ${workspaceRoot}`, + }); + } + return resolvedPath; +} + +function assertWorkflowInScope( + run: Pick, + scope: { workspaceRoot: string; workspaceId?: string }, +): void { + if (resolve(run.workspaceRoot) !== resolve(scope.workspaceRoot)) { + throw new InvalidWorkflowInputError({ + code: "invalid_argument", + message: `Workflow run belongs to a different workspace: ${scope.workspaceRoot}`, + }); + } + if (scope.workspaceId && run.workspaceId && run.workspaceId !== scope.workspaceId) { + throw new InvalidWorkflowInputError({ + code: "invalid_argument", + message: `Workflow run belongs to a different workspaceId: ${scope.workspaceId}`, + }); + } +} + function flagValue(flags: Map, key: string): string | undefined { const value = flags.get(key); return typeof value === "string" ? value : undefined; From 5aceb53a748f7baa2a2259a39ca09c6e8820e3e4 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 9 Aug 2026 17:00:03 +0530 Subject: [PATCH 2/4] fix(agents): close stores and clean prompt handoffs --- src/cli.ts | 83 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index a9797458..9fb69a77 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,9 +3,9 @@ import { createRequire } from "node:module"; import { stdin as input, stdout as output } from "node:process"; import { spawn } from "node:child_process"; import { mkdtempSync, writeFileSync } from "node:fs"; -import { readFile } from "node:fs/promises"; +import { readFile, unlink } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { basename, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import * as prompts from "@clack/prompts"; import { getShellConfig } from "@earendil-works/pi-coding-agent"; @@ -75,9 +75,10 @@ async function main(argv: string[]): Promise { runConfigCommand(args); return; case "agents": - if (!loadConfig().subagents) { + const config = loadConfig(); + if (!config.subagents && !config.workflows) { throw new Error( - "Subagents are disabled. Set DEVSPACE_SUBAGENTS=1 to enable the experimental feature.", + "Subagents and Dynamic Workflows are disabled. Set DEVSPACE_SUBAGENTS=1 or DEVSPACE_WORKFLOWS=1 to enable agent tooling.", ); } await runAgentsCommand(args); @@ -495,27 +496,31 @@ async function runAgentsShow(args: string[]): Promise { const config = loadConfig(); const store = createLocalAgentStore(config); - let record = store.get(id); - if (!record) throw new Error(`Unknown subagent id: ${id}`); - assertAgentInScope(record, resolveCurrentWorkspaceScope(config)); - - const deadline = Date.now() + 15_000; - while ((record.status === "starting" || record.status === "running") && Date.now() < deadline) { - await sleep(500); - record = store.get(id) ?? record; - } + try { + let record = store.get(id); + if (!record) throw new Error(`Unknown subagent id: ${id}`); + assertAgentInScope(record, resolveCurrentWorkspaceScope(config)); + + const deadline = Date.now() + 15_000; + while ((record.status === "starting" || record.status === "running") && Date.now() < deadline) { + await sleep(500); + record = store.get(id) ?? record; + } - console.log(formatAgentLine(record)); - if (record.latestResponse) { - console.log(record.latestResponse); - return; - } - if (record.error) { - console.log(record.error); - return; - } - if (record.status === "starting" || record.status === "running") { - console.log(`No final response yet. Call \`devspace agents show ${record.id}\` again later.`); + console.log(formatAgentLine(record)); + if (record.latestResponse) { + console.log(record.latestResponse); + return; + } + if (record.error) { + console.log(record.error); + return; + } + if (record.status === "starting" || record.status === "running") { + console.log(`No final response yet. Call \`devspace agents show ${record.id}\` again later.`); + } + } finally { + store.close(); } } @@ -527,11 +532,11 @@ async function runAgentsWorker(args: string[]): Promise { const config = loadConfig(); const store = createLocalAgentStore(config); - const record = store.get(id); - if (!record) throw new Error(`Unknown subagent id: ${id}`); - - store.update(record.id, { status: "running", error: undefined }); try { + const record = store.get(id); + if (!record) throw new Error(`Unknown subagent id: ${id}`); + + store.update(record.id, { status: "running", error: undefined }); const profiles = await loadLocalAgentProfiles(config, record.workspaceRoot); const prompt = await readFile(promptFile, "utf8"); const target = resolveLocalAgentExecution({ @@ -557,10 +562,18 @@ async function runAgentsWorker(args: string[]): Promise { error: undefined, }); } catch (error) { - store.update(record.id, { - status: "error", - error: error instanceof Error ? error.message : String(error), - }); + const record = store.get(id); + if (record) { + store.update(record.id, { + status: "error", + error: error instanceof Error ? error.message : String(error), + }); + } + } finally { + if (isGeneratedPromptFile(promptFile)) { + await unlink(promptFile).catch(() => undefined); + } + store.close(); } } @@ -588,6 +601,12 @@ function writeAgentPromptFile(prompt: string): string { return filePath; } +function isGeneratedPromptFile(filePath: string): boolean { + const resolvedPath = resolve(filePath); + const prefix = `${resolve(tmpdir())}${process.platform === "win32" ? "\\" : "/"}devspace-agent-prompt-`; + return resolvedPath.startsWith(prefix) && basename(resolvedPath) === "prompt.txt"; +} + function resolveCurrentWorkspaceRoot(config: ReturnType): string { return resolveCliWorkspaceScope(config.allowedRoots).workspaceRoot; } From debad175b2aee84b8db13655371c29222ed24860 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 9 Aug 2026 17:00:03 +0530 Subject: [PATCH 3/4] fix(agents): reject unknown run options --- src/local-agent-targets.test.ts | 5 +++++ src/local-agent-targets.ts | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/local-agent-targets.test.ts b/src/local-agent-targets.test.ts index 737f970a..81bbee09 100644 --- a/src/local-agent-targets.test.ts +++ b/src/local-agent-targets.test.ts @@ -81,6 +81,11 @@ assert.throws( /Missing value for --effort/, ); +assert.throws( + () => parseLocalAgentRunArgs(["codex", "--unknown", "hello"]), + /Unknown option: --unknown/, +); + { const target = resolveLocalAgentTarget("reviewer", profiles); assert.equal(target?.kind, "profile"); diff --git a/src/local-agent-targets.ts b/src/local-agent-targets.ts index c794511b..a2c4a72f 100644 --- a/src/local-agent-targets.ts +++ b/src/local-agent-targets.ts @@ -64,6 +64,9 @@ export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs effort = value; continue; } + if (part?.startsWith("--")) { + throw new Error(`Unknown option: ${part}\n${USAGE}`); + } promptParts.push(part ?? ""); } From 3020b712bf8c02675bce74a03883351dbe010000 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 9 Aug 2026 17:00:48 +0530 Subject: [PATCH 4/4] refactor(mcp): remove workflow execution tools --- src/server.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/server.ts b/src/server.ts index cac9f374..427f8f7f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -47,7 +47,6 @@ import { formatPathForPrompt } from "./skills.js"; import { createWorkspaceStore } from "./workspace-store.js"; import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js"; import { buildLocalAgentCatalog } from "./local-agent-catalog.js"; -import { registerWorkflowTools } from "./workflow-tools.js"; import { startWorkflowReaper } from "./workflow-lifecycle.js"; import { createWorkflowStore } from "./workflow-store.js"; import { loadActiveWorkflowSummaries } from "./workflow-ui.js"; @@ -1628,10 +1627,6 @@ function createMcpServer( registerCodexProcessTools(server, config, workspaces, processSessions); } - if (config.workflows) { - registerWorkflowTools(server, config, workspaces); - } - return server; }