Skip to content

bug(host): a workflows folder that cannot be listed answers ok:true with an empty list #2614

Closed

Description

@Birfy

Written-by: Chief

A workflows/ folder that is a plain directory but cannot be listed is answered as "there are no workflows": ownFiles() swallows the readdirSync error, list() returns { ok: true, own: [] }, and save() spreads that empty list into its own ok: true. The page then shows a workspace with zero custom workflows while the file is on disk and still openable by name.

Where

core/src/host/workflow-store.js (main a754cc4ecab4122009aea2cc0b914097786bbf5d):

  • :78-86 ownFiles() — if (!folderReal()) return [] is the reviewed gate; the try { return readdirSync(dir)… } catch { return [] } behind it swallows every error, including EACCES/ENOENT.
  • :119 list() — ownFiles().flatMap(…) then return { ok: true, presets, own, default }.
  • :192 save() — writes the file, then return { ok: true, id, bytes, changed, ...list() }, so a failed listing becomes own: [] inside a success.

folderReal() (:48-52) only lstats the folder to prove it is a real directory, which needs the parent's +x, not the folder's +r. That is why the two halves of one read disagree: naming a file (open) needs +x and works, while listing needs +r and is reported as "empty" rather than "unreadable".

Reproduction (real output, main a754cc4e)

workflows/ created by the store, one workflow saved, then chmod 0o311 on the folder (write+execute, no read) — a workspace restored from a backup that kept another uid's mode, or a chmod -R accident:

import { createWorkflowStore } from './core/src/host/workflow-store.js'
const store = createWorkflowStore({ dir })            // dir = <tmp>/ws2614c/workflows
store.customize({ from: 'blank', id: 'mine', name: 'Mine' })
console.log('list-before own =', store.list().own.map(x => x.id))
chmodSync(dir, 0o311)
const op = store.open('mine')
const s = store.save({ definition: op.workflow.definition })
console.log('save(unlistable) ->', JSON.stringify({ ok: s.ok, changed: s.changed, own: s.own, presets: Array.isArray(s.presets) }))
const l = store.list()
console.log('list(unlistable) ->', JSON.stringify({ ok: l.ok, own: l.own }))
console.log('open(mine) ->', JSON.stringify({ ok: op.ok, source: op.workflow.source }))
chmodSync(dir, 0o755)
console.log('list after chmod ->', store.list().own.map(x => x.id))
list-before own = [ 'mine' ]
save(unlistable) -> {"ok":true,"changed":false,"own":[],"presets":true}
list(unlistable) -> {"ok":true,"own":[]}
open(mine) -> {"ok":true,"source":"workspace"}
list after chmod -> [ 'mine' ]

No warning is printed, skipped() is not called, and nothing in the answer distinguishes "no workflows" from "cannot read the folder".

Expected

An unlistable folder is a failure, not an empty result: list() answers ok: false, and a write that succeeded is never reported with a list the code could not read.

Fix (decided here, do not widen it)

  1. ownFiles(): keep if (!folderReal()) return [], delete the try/catch so a readdirSync failure propagates. The "one broken file must not take the list down" rule stays where it belongs — in readOwn()'s JSON.parse catch, which is untouched.
  2. list(): catch that failure and answer the store's existing refusal shape —
    { ok: false, code: 'workflow/unreadable', message: 'the workflow folder <dir> is there but cannot be listed, so its workflows cannot be read', problems: [] }.
  3. save() and setDefault(): the follow-up list() must not be spread into ok: true. When it fails, answer ok: false with code: 'workflow/unreadable' and wrote: true|false plus changed, so the caller learns both facts — the write happened, the folder cannot be listed. Never answer ok: true with an empty own.
  4. definitions() / rt.workflowDefinitions() (:303-305) keep their preset fallback: the board read must stay green. Changing that fallback is out of scope.

Scope

In: core/src/host/workflow-store.js; a red-first test in core/test/workflow-store.test.js (the chmod 0o311 case, skipped when process.getuid?.() === 0 because root ignores the mode; assert list → ok:false + workflow/unreadable, save → ok:false with wrote:true, open still works, and everything lists again after chmod 0o755); one line in docs/subsystems/core-host.md where the workflow-store row describes the folder gate; changelog.d/2614.fixed.md; regenerate patches/ds-bot/ (node scripts/ds-bot-patches.mjs generate + check) and keep the files/ mirrors byte-identical to core/.

Out: the write-side folderWritable/fileWritable rules (#2057, #2079), the readdir/lstat kind classification, how the settings page renders the new error, and any new join(dir, …) sink. If you do add a sink, add it to WORKFLOW_PROBES and to the #2068 probe test as that test demands.

Activity

  1. self-assigned this
    on Nov 7, 2026
  2. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    bugSomething isn't working
    area:automationScheduled tasks and automation
    agent-prPull request opened by an agent
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Nov 7, 2026
  3. added 2 commits that reference this issue on Nov 9, 2026
    bfc6295
    73edc8f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions