Skip to content

TaskFlow: restore managed substrate#58930

Merged
mbelinky merged 7 commits into
mainfrom
workspace/clawflow-repack-v1
Apr 2, 2026
Merged

TaskFlow: restore managed substrate#58930
mbelinky merged 7 commits into
mainfrom
workspace/clawflow-repack-v1

Conversation

@mbelinky

@mbelinky mbelinky commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • restore TaskFlow as the durable job substrate above tasks
  • keep tasks as the execution units and TaskFlow as the owned wrapper around one or more task runs
  • add the core persistence, lifecycle, and operator surfaces without adding any DSL or workflow engine semantics

What This PR Does

TaskFlow comes back into core as the parent job record for detached work.

Tasks still do the actual work and remain the core execution substrate.
TaskFlow adds the higher-level record that can own one or more tasks, keep shared state, and be inspected or cancelled as a unit.

This PR adds:

  • durable flow records with task_mirrored vs managed sync modes
  • managed-flow state for controller, revision, opaque state/wait blobs, and cancel intent
  • task-to-flow linkage in the task layer
  • openclaw flows inspection and operator recovery surfaces
  • owner-scoped flow access alongside the hardened task model

What This PR Does Not Do

  • no DAG executor
  • no branching DSL
  • no cycle or dependency-graph engine
  • no plugin-facing orchestration API yet
  • no authoring helper or skill layer

Tasks vs TaskFlow

  • Tasks: tracked executions such as ACP runs, subagent runs, cron runs, and other detached work
  • TaskFlow: the durable parent job that groups one or more tasks into one owned unit with shared lifecycle and operator controls

Validation

  • mb-server: pnpm build
  • mb-server: focused Vitest slice across flow/task/doctor surfaces

@openclaw-barnacle openclaw-barnacle Bot added cli CLI command changes commands Command implementations size: XL maintainer Maintainer-authored PR labels Apr 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0ede2adbb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +542 to +543
normalizeComparableText(task.parentFlowId) ===
normalizeComparableText(params.parentFlowId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep runId dedupe independent of parent flow linkage

Including parentFlowId in runScopeMatches breaks idempotent createQueuedTaskRun/createRunningTaskRun behavior for ACP/subagent runs. The first create call typically has no parentFlowId, then ensureSingleTaskFlow links it afterward; a retry with the same runId still arrives without parentFlowId, so it no longer matches the existing task and creates a duplicate task (and often a duplicate one-task flow). That can produce duplicate status updates, notifications, and cancellation handling for one logical run.

Useful? React with 👍 / 👎.

Comment on lines +197 to +199
restoreAttempted = true;
const restored = getFlowRegistryStore().loadSnapshot();
flows.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle restore errors before marking flow registry initialized

restoreAttempted is flipped before loadSnapshot() and this function does not catch restore failures. If loading SQLite throws (for example from a transient permission/corruption issue), the first call fails and subsequent calls will skip restore entirely because the flag is already set, leaving the in-memory registry empty for the rest of the process and hiding persisted flows.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR restores the core flow substrate that underpins background orchestration in openclaw. It introduces a FlowRecord model with task_mirrored vs managed sync modes, CAS-based mutation helpers (revision-checked updates), durable SQLite persistence, an owner-scoped access seam (flow-owner-access.ts), an import-boundary enforcement test, and openclaw flows CLI inspection/recovery commands.

Key changes:

  • src/tasks/flow-registry.types.ts — new FlowRecord, FlowSyncMode, FlowStatus, and JsonValue types
  • src/tasks/flow-registry.ts — in-memory registry with CAS helpers, lazy restore, hook events, and syncFlowFromTask for mirrored mode
  • src/tasks/flow-registry.store.sqlite.ts — SQLite persistence with incremental migrations for all new columns, WAL mode, and strict file/dir permissions (0o700/0o600)
  • src/tasks/task-registry.tsparentFlowId linkage with owner/scope validation and per-flow task index; syncFlowFromTask wired into create/update paths
  • src/tasks/task-executor.ts — auto-creates task_mirrored flows for eligible single-task runs, adds cancelFlowById/cancelFlowByIdForOwner, and retryBlockedFlowAsQueuedTaskRun
  • src/commands/flows.ts — new flows list/show/cancel operator commands
  • src/commands/doctor-workspace-status.ts — recovery hints for broken/blocked managed flows

Findings:

  • cancelFlowById in task-executor.ts checks isTerminalFlowStatus(flow.status) against the pre-cancellation flow snapshot rather than the refreshed one. For task_mirrored flows, child task cancellation triggers syncFlowFromTask which may already advance the flow to "cancelled" before the guard runs, leading to a redundant CAS write and extra revision bump. No data integrity risk (the CAS is correct), but worth tightening.
  • retryBlockedFlowAsQueuedTaskRun creates the retry task and links it to the flow, but does not update the flow's status field for managed flows (only task_mirrored flows benefit from syncFlowFromTask being triggered on task creation). Callers working with managed flows should be aware they need to advance the flow status separately.
  • Minor double structuredClone call per JSON field in buildFlowRecord.

Confidence Score: 5/5

Safe to merge — all findings are P2; no data corruption, security bypass, or runtime error risk identified.

The security boundaries (owner-scoped access, cross-owner task/flow link rejection, import boundary enforcement) are all correctly implemented and tested. The SQLite migrations are incremental and safe. The two flagged issues are both minor: stale-snapshot terminal-status check in cancelFlowById causes redundant writes but not incorrect state, and the managed-flow retry gap is a documented design boundary rather than a bug. All other code paths have corresponding test coverage.

src/tasks/task-executor.ts — cancelFlowById stale terminal check and retryBlockedFlowAsQueuedTaskRun managed-flow gap.

Comments Outside Diff (3)

  1. src/tasks/task-executor.ts, line 3224-3232 (link)

    P2 Stale terminal-status check after task cancellations

    isTerminalFlowStatus(flow.status) uses the snapshot captured at the start of the function, before the active-task cancellation loop. For task_mirrored flows, each cancelTaskById call triggers updateTask → syncFlowFromTask, which may already advance the flow into a terminal state. After the loop, refreshedFlow is re-read with the current revision, so the CAS succeeds and writes "cancelled" a second time — resulting in a redundant write and a bumped revision. No data integrity issue (the CAS handles the race), but moving the terminal check to use refreshedFlow would be cleaner.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/tasks/task-executor.ts
    Line: 3224-3232
    
    Comment:
    **Stale terminal-status check after task cancellations**
    
    `isTerminalFlowStatus(flow.status)` uses the snapshot captured at the start of the function, before the active-task cancellation loop. For `task_mirrored` flows, each `cancelTaskById` call triggers `updateTask → syncFlowFromTask`, which may already advance the flow into a terminal state. After the loop, `refreshedFlow` is re-read with the current revision, so the CAS succeeds and writes "cancelled" a second time — resulting in a redundant write and a bumped revision. No data integrity issue (the CAS handles the race), but moving the terminal check to use `refreshedFlow` would be cleaner.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/tasks/task-executor.ts, line 3157-3163 (link)

    P2 retryBlockedFlowAsQueuedTaskRun does not update flow status for managed flows

    When called on a managed flow, retryBlockedFlowTask creates a new task and links it via parentFlowId, but syncFlowFromTask is a no-op for managed flows. After the call returns retried: true, the flow record still shows status: "blocked". Consider documenting this or explicitly advancing the managed flow status inside retryBlockedFlowTask.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/tasks/task-executor.ts
    Line: 3157-3163
    
    Comment:
    **`retryBlockedFlowAsQueuedTaskRun` does not update flow status for managed flows**
    
    When called on a `managed` flow, `retryBlockedFlowTask` creates a new task and links it via `parentFlowId`, but `syncFlowFromTask` is a no-op for managed flows. After the call returns `retried: true`, the flow record still shows `status: "blocked"`. Consider documenting this or explicitly advancing the managed flow status inside `retryBlockedFlowTask`.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. src/tasks/flow-registry.ts, line 2135-2145 (link)

    P2 normalizeJsonBlob called twice per JSON field in buildFlowRecord

    Both the guard check and the spread value call normalizeJsonBlob(params.stateJson) separately, triggering two structuredClone calls per non-undefined JSON field. Capture the result once to avoid double cloning.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/tasks/flow-registry.ts
    Line: 2135-2145
    
    Comment:
    **`normalizeJsonBlob` called twice per JSON field in `buildFlowRecord`**
    
    Both the guard check and the spread value call `normalizeJsonBlob(params.stateJson)` separately, triggering two `structuredClone` calls per non-undefined JSON field. Capture the result once to avoid double cloning.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 3224-3232

Comment:
**Stale terminal-status check after task cancellations**

`isTerminalFlowStatus(flow.status)` uses the snapshot captured at the start of the function, before the active-task cancellation loop. For `task_mirrored` flows, each `cancelTaskById` call triggers `updateTask → syncFlowFromTask`, which may already advance the flow into a terminal state. After the loop, `refreshedFlow` is re-read with the current revision, so the CAS succeeds and writes "cancelled" a second time — resulting in a redundant write and a bumped revision. No data integrity issue (the CAS handles the race), but moving the terminal check to use `refreshedFlow` would be cleaner.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 3157-3163

Comment:
**`retryBlockedFlowAsQueuedTaskRun` does not update flow status for managed flows**

When called on a `managed` flow, `retryBlockedFlowTask` creates a new task and links it via `parentFlowId`, but `syncFlowFromTask` is a no-op for managed flows. After the call returns `retried: true`, the flow record still shows `status: "blocked"`. Consider documenting this or explicitly advancing the managed flow status inside `retryBlockedFlowTask`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/tasks/flow-registry.ts
Line: 2135-2145

Comment:
**`normalizeJsonBlob` called twice per JSON field in `buildFlowRecord`**

Both the guard check and the spread value call `normalizeJsonBlob(params.stateJson)` separately, triggering two `structuredClone` calls per non-undefined JSON field. Capture the result once to avoid double cloning.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Tasks/flows: fix flow patch nullability" | Re-trigger Greptile

@mbelinky mbelinky force-pushed the workspace/clawflow-repack-v1 branch from c0ede2a to 7c6f535 Compare April 2, 2026 09:32
@mbelinky mbelinky changed the title Tasks/flows: restore managed flow substrate TaskFlow: restore managed substrate Apr 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c6f535b84

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

tasks: refreshedTasks,
};
}
if (isTerminalFlowStatus(flow.status)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard cancellation against stale flow status

cancelFlowById checks terminality using the initial flow snapshot from before child-task cancellation. If one of those tasks finishes and syncs the flow to failed/succeeded while this function is awaiting cancels, this stale check still falls through and the subsequent CAS patch overwrites the terminal status to cancelled, losing the real outcome. Re-check terminal status on the refreshed flow snapshot before applying the cancellation patch.

Useful? React with 👍 / 👎.

Comment on lines +315 to +316
const flow = getFlowById(params.flowId);
if (!flow) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate blocked state before retry task creation

After resolveRetryableBlockedFlowTask confirms the flow is blocked, retryBlockedFlowTask fetches the flow again but only checks for existence. A concurrent retry/cancel can move the flow out of blocked between these two reads, and this path will still create another retry task (especially when runId is omitted), producing duplicate retries for a single blocked step. Re-check flow.status === "blocked" on the second read before calling createTaskRecord.

Useful? React with 👍 / 👎.

@aisle-research-bot

aisle-research-bot Bot commented Apr 2, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 5 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Symlink/TOCTOU risk when chmod-ing SQLite registry files derived from OPENCLAW_STATE_DIR
2 🟡 Medium Sensitive data exposure via openclaw flows --json and flows show --json dumping full flow/task records
3 🟡 Medium Terminal escape sequence injection in flows CLI output (untrusted flow/task fields logged unsanitized)
4 🟡 Medium Missing owner authorization when cancelling flows (cancelFlowById used by CLI)
5 🔵 Low Terminal/log injection in doctor workspace status recovery hints (flow IDs & blockedTaskId in notes)
1. 🟡 Symlink/TOCTOU risk when chmod-ing SQLite registry files derived from OPENCLAW_STATE_DIR
Property Value
Severity Medium
CWE CWE-367
Location src/tasks/flow-registry.store.sqlite.ts:315-325

Description

The flow registry SQLite store attempts to harden file permissions by calling chmodSync on paths derived from the environment-controlled state directory.

  • Input/control surface: resolveFlowRegistryDir() / resolveFlowRegistrySqlitePath() ultimately depend on OPENCLAW_STATE_DIR (via resolveStateDir()), which can be set via environment.
  • Dangerous operations: ensureFlowRegistryPermissions() uses mkdirSync(..., {recursive:true}) and then chmodSync() on the directory and on each sidecar file (registry.sqlite, registry.sqlite-wal, registry.sqlite-shm).
  • Symlink + TOCTOU issue: it checks existsSync(candidate) and then separately calls chmodSync(candidate). An attacker who can write within the chosen state directory (or influence it to point somewhere writable) could replace candidate with a symlink between the check and the chmod, causing chmodSync to act on an unexpected target (chmod follows symlinks). This can result in unintended permission changes to arbitrary files accessible to the running user, and becomes a privilege escalation risk if the process is ever run with elevated privileges (e.g., as a service or via sudo) while using an attacker-influenced OPENCLAW_STATE_DIR.

Vulnerable code:

for (const suffix of FLOW_REGISTRY_SIDECAR_SUFFIXES) {
  const candidate = `${pathname}${suffix}`;
  if (!existsSync(candidate)) {
    continue;
  }
  chmodSync(candidate, FLOW_REGISTRY_FILE_MODE);
}

Recommendation

Avoid following symlinks and remove the TOCTOU window.

Recommended mitigations:

  1. Validate the resolved paths stay within the expected state directory using realpathSync() (or fs.promises.realpath) for both the directory and each candidate, and ensure they have the expected prefix.
  2. Refuse to chmod symlinks by using lstatSync() and checking isSymbolicLink() and isFile().
  3. Prefer operating on already-open file descriptors (where possible) to avoid races.

Example hardening (best-effort in Node):

import { chmodSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
import path from "node:path";

function safeChmodFile(candidate: string, rootDir: string, mode: number) {
  const resolvedRoot = realpathSync(rootDir);
  const resolvedCandidate = realpathSync(candidate);
  if (!resolvedCandidate.startsWith(resolvedRoot + path.sep)) {
    throw new Error("Refusing to chmod path outside state dir");
  }
  const st = lstatSync(candidate);
  if (st.isSymbolicLink() || !st.isFile()) {
    throw new Error("Refusing to chmod non-regular file");
  }
  chmodSync(candidate, mode);
}

Also consider treating OPENCLAW_STATE_DIR as trusted configuration only (do not allow untrusted callers to influence it) and documenting that the process must not be run with elevated privileges when using user-controlled state directories.

2. 🟡 Sensitive data exposure via `openclaw flows --json` and `flows show --json` dumping full flow/task records
Property Value
Severity Medium
CWE CWE-200
Location src/commands/flows.ts:125-183

Description

The CLI commands flowsListCommand and flowsShowCommand support --json output that serializes and prints entire flow records and linked task records directly to stdout.

This is risky because these structures can contain sensitive data and identifiers, including:

  • FlowRecord.requesterOrigin (delivery context such as channel/accountId/threadId) which may be PII
  • FlowRecord.stateJson / FlowRecord.waitJson which are generic JSON blobs and may carry credentials, tokens, message contents, or other secrets depending on workflow/controller implementation
  • Task records returned by listTasksForFlowId(...) which include requesterSessionKey, ownerKey, runId, error, etc.

In environments where CLI output is captured (CI logs, support bundles, telemetry, shell history, redirected output), this can lead to unintended disclosure.

Vulnerable code:

flows: flows.map((flow) => ({
  ...flow,
  tasks: listTasksForFlowId(flow.flowId),
  taskSummary: getFlowTaskSummary(flow.flowId),
})),

And:

JSON.stringify({ ...flow, tasks, taskSummary }, null, 2)

Recommendation

Redact or minimize sensitive fields in JSON output by default.

Options (can be combined):

  1. Default-safe JSON schema: only emit non-sensitive metadata (ids, status, timestamps, counts), and exclude stateJson, waitJson, requesterOrigin, and session keys unless explicitly requested.
  2. Add an explicit --include-private / --unsafe flag to include full records.
  3. If full JSON must be supported, implement a redaction function that scrubs common secret keys (e.g., token, authorization, apiKey, secret, password) recursively.

Example (default-safe):

function toPublicFlow(flow: FlowRecord) {
  return {
    flowId: flow.flowId,
    syncMode: flow.syncMode,
    status: flow.status,
    revision: flow.revision,
    controllerId: flow.controllerId ?? null,
    goal: flow.goal,
    currentStep: flow.currentStep ?? null,
    blockedTaskId: flow.blockedTaskId ?? null,
    blockedSummary: flow.blockedSummary ?? null,
    cancelRequestedAt: flow.cancelRequestedAt ?? null,
    createdAt: flow.createdAt,
    updatedAt: flow.updatedAt,
    endedAt: flow.endedAt ?? null,
  };
}// usage
flows: flows.map((flow) => ({
  ...toPublicFlow(flow),
  taskSummary: getFlowTaskSummary(flow.flowId),
})),

If task details are required, similarly emit a minimal task projection (taskId/status/label) rather than full task records.

3. 🟡 Terminal escape sequence injection in `flows` CLI output (untrusted flow/task fields logged unsanitized)
Property Value
Severity Medium
CWE CWE-117
Location src/commands/flows.ts:72-81

Description

The openclaw flows CLI commands print externally-influenced strings directly to the terminal via runtime.log().

An attacker who can influence task metadata (e.g., task.label, task.task) can inject ANSI escape sequences/control characters into flow.goal (derived from task label/task) and into per-task output lines. This can:

  • Spoof or hide terminal output (e.g., overwrite lines, clear screen)
  • Trick operators into executing malicious commands by rendering fake prompts/commands
  • Corrupt logs (control characters, CRLF)

Although the repo contains sanitizeTerminalText() (which strips ANSI and control characters), it is not used here.

Vulnerable code:

lines.push([
  shortToken(flow.flowId).padEnd(ID_PAD),
  ...,
  truncate(flow.controllerId ?? "n/a", CTRL_PAD).padEnd(CTRL_PAD),
  counts.padEnd(14),
  truncate(flow.goal, 80),
].join(" "));
...
runtime.log(`goal: ${flow.goal}`);
...
runtime.log(`- ${task.taskId} ${task.status} ${task.runId ?? "n/a"} ${task.label ?? task.task}`);

Recommendation

Sanitize any untrusted text before rendering to the terminal.

Use the existing helper sanitizeTerminalText() (strips ANSI + control chars) for user/external fields such as flow.goal, flow.controllerId, task.label, and task.task.

Example fix:

import { sanitizeTerminalText } from "../terminal/safe-text.js";

const safeGoal = sanitizeTerminalText(flow.goal);
const safeController = sanitizeTerminalText(flow.controllerId ?? "n/a");

runtime.log(`goal: ${safeGoal}`);
...
runtime.log(
  `- ${sanitizeTerminalText(task.taskId)} ${task.status} ${task.runId ?? "n/a"} ` +
  `${sanitizeTerminalText(task.label ?? task.task)}`,
);

Optionally, update truncate() (or create truncateSafe()) to sanitize before truncation so all table cells are safe by default.

4. 🟡 Missing owner authorization when cancelling flows (cancelFlowById used by CLI)
Property Value
Severity Medium
CWE CWE-862
Location src/tasks/task-executor.ts:389-440

Description

The flow cancellation API has an owner-scoped variant (cancelFlowByIdForOwner) but the unscoped cancelFlowById remains exported and is used directly by the CLI flowsCancelCommand.

  • cancelFlowById cancels tasks and marks the flow cancelled solely based on a provided flowId.
  • No verification is performed that the caller is authorized for the flow’s ownerKey.
  • flowsCancelCommand resolves the target flow via resolveFlowForLookupToken, which accepts either a flow id or an owner key (returns latest flow for that owner). This allows cancelling another owner’s flow if an attacker can supply/guess a victim flowId or ownerKey and can reach this command surface.

Vulnerable code:

export async function cancelFlowById(params: { cfg: OpenClawConfig; flowId: string }) {
  const flow = getFlowById(params.flowId);// ... cancels tasks and updates flow status without owner check
}
const flow = resolveFlowForLookupToken(opts.lookup);
const result = await cancelFlowById({ cfg: loadConfig(), flowId: flow.flowId });

Recommendation

Enforce owner scoping at the call boundary(s) that can be influenced by untrusted input.

Option A (preferred): require callerOwnerKey for cancellation and remove/limit the unscoped export:

export async function cancelFlowByIdForOwner(params: {
  cfg: OpenClawConfig;
  flowId: string;
  callerOwnerKey: string;
}) {
  const flow = getFlowByIdForOwner({ flowId: params.flowId, callerOwnerKey: params.callerOwnerKey });
  if (!flow) return { found: false, cancelled: false, reason: "Flow not found." };
  return cancelFlowById({ cfg: params.cfg, flowId: flow.flowId });
}

Then update flowsCancelCommand (and any other external surface) to use an owner-scoped resolver and cancellation:

const flow = resolveFlowForLookupTokenForOwner({ token: opts.lookup, callerOwnerKey });
const result = await cancelFlowByIdForOwner({ cfg: loadConfig(), flowId: flow.flowId, callerOwnerKey });

Option B: if unscoped cancellation is required for internal/admin use, make it non-exported or clearly mark it as privileged and ensure only admin-only paths can call it.

5. 🔵 Terminal/log injection in doctor workspace status recovery hints (flow IDs & blockedTaskId in notes)
Property Value
Severity Low
CWE CWE-117
Location src/commands/doctor-workspace-status.ts:21-33

Description

noteWorkspaceStatus() now emits TaskFlow recovery hints via note() that include flow.flowId and flow.blockedTaskId directly in the note body.

If an attacker can influence these identifiers (or other linked metadata persisted in the flow registry), they can embed control characters / ANSI escape sequences, resulting in terminal escape injection within the interactive doctor output.

note() currently wraps text but does not sanitize ANSI/control characters, and @​clack/prompts notes render directly to terminal.

Vulnerable code:

findings.push(
  `${flow.flowId}: blocked flow points at missing task ${flow.blockedTaskId}; inspect before retrying.`,
);
...
note([...suspicious.slice(0, 5), ...].join("\n"), "TaskFlow recovery");

Recommendation

Sanitize any flow/task identifiers before embedding them in terminal notes.

Example:

import { sanitizeTerminalText } from "../terminal/safe-text.js";

findings.push(
  `${sanitizeTerminalText(flow.flowId)}: blocked flow points at missing task ` +
  `${sanitizeTerminalText(flow.blockedTaskId)}; inspect before retrying.`,
);

Alternatively, enforce sanitization inside note()/wrapNoteMessage() so all note content is safe by default (but ensure you don't break intentional formatting for trusted strings).


Analyzed PR: #58930 at commit 9c946d5

Last updated on: 2026-04-02T11:06:22Z

@mbelinky mbelinky force-pushed the workspace/clawflow-repack-v1 branch from 7c6f535 to 9c946d5 Compare April 2, 2026 09:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e5122946b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +73 to +77
} catch (error) {
log.warn("Failed to create one-task flow for detached run", {
taskId: params.task.taskId,
runId: params.task.runId,
error,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up provisional flow when task-linking throws

If createFlowForTask succeeds but linkTaskToFlowById throws (for example, a transient write failure while updating the task row), this catch path returns the original task without deleting the newly created flow. Unlike the explicit !linked/mismatch branches, this leaves an orphan task_mirrored flow with no linked task, which can pollute flow listings and cancellation/retry behavior on subsequent operations.

Useful? React with 👍 / 👎.

@mbelinky mbelinky force-pushed the workspace/clawflow-repack-v1 branch from 1e51229 to c990938 Compare April 2, 2026 10:17
@mbelinky mbelinky merged commit 2fa4c7c into main Apr 2, 2026
9 checks passed
@mbelinky mbelinky deleted the workspace/clawflow-repack-v1 branch April 2, 2026 10:17
@mbelinky

mbelinky commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Merged via squash.

Thanks @mbelinky!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c99093838f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +761 to +763
try {
syncFlowFromTask(next);
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ignore stale task updates when mirroring flow state

Every task update now calls syncFlowFromTask, even updates that only touch delivery metadata. In flows that accumulate retries (old blocked task + new queued/running task), a later update on the older task (for example setTaskRunDeliveryStatusByRunId from subagent cleanup) will re-sync the shared task_mirrored flow from that stale task and can flip the flow back to blocked/succeeded while a newer retry task is active. This produces incorrect flow status and can drive wrong cancel/retry decisions; flow sync should be gated to the newest linked task (or at least lifecycle-changing patches).

Useful? React with 👍 / 👎.

ngutman pushed a commit that referenced this pull request Apr 3, 2026
Merged via squash.

Prepared head SHA: c990938
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
steipete pushed a commit to duncanita/openclaw that referenced this pull request Apr 4, 2026
Merged via squash.

Prepared head SHA: c990938
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
Merged via squash.

Prepared head SHA: c990938
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
Merged via squash.

Prepared head SHA: c990938
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Merged via squash.

Prepared head SHA: c990938
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Merged via squash.

Prepared head SHA: c990938
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes commands Command implementations maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant