Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: a27589d6562d4b0aa8f3f451f37e9121
Branch: dev/2-memory-workflows-dir-symlink
Base: 212f151
At: 2026-11-01T15:50:46Z
Owner: Dev-2
Base: 212f151
What happens today
Two readers take a fixed directory name under a folder the host owns and read whatever the directory yields, without ever checking that the directory itself is real. Both were found in a read-only audit at 212f1515 and both follow a link the same way #2055 (decisions/) and #2056 (journal/) do.
1. Team memory and Bot memory — core/src/host/memory.js:421 builds root = join(home, 'memory'), :430 builds dirOf(scope) as memory/team or memory/bots/<id>, and :443 does readdirSync(dirOf(scope)) with no lstat anywhere in the file. A memory/team (or memory/bots/<id>) that is a symlink to a folder outside the home makes the outside .md files list as this team's or this Bot's memories (:453 reads their text), so external text reaches everything that composes memory into a prompt.
2. Workflow store — core/src/host/workflow-store.js:191-195 builds dirFor() as join(workspacePath, 'workflows'); the listing and the reads at :43, :55, :70, :139, :164 take every *.json found there. A workflows symlink to an outside folder makes foreign JSON list as this workspace's workflows and read back as if the workspace owned them.
Neither file has any lstat. In both cases the leak is read-only: no foreign write, unlike #2056.
Reproduce (memory):
mkdir -p /tmp/h /tmp/elsewhere && printf -- '---\nname: x\n---\nsecret\n' > /tmp/elsewhere/x.md
ln -s /tmp/elsewhere /tmp/h/memory/team
# memory list for the team scope returns "x"; reading it returns the outside textReproduce (workflows):
mkdir -p /tmp/ws /tmp/elsewhere && printf '{"name":"other"}' > /tmp/elsewhere/other.json
ln -s /tmp/elsewhere /tmp/ws/workflows
# workflow list for that workspace returns "other" and its contentExpected
The same directory rule as #2055/#2056, applied at both readers: before listing or reading anything inside, lstat the directory (memory/team, memory/bots/<id>, <workspace>/workflows); unless it reports isDirectory() and not isSymbolicLink(), the directory is treated as absent — the reader answers exactly what it answers today when the folder does not exist (empty memory list / empty workflow list), no fallback to the target, no follow, and no external text or file name enters a prompt, a listing or a page.
Where the directory does not exist at all, the current "nothing there" answer must stay byte-identical to today's.
A missing intermediate path counts too: memory/bots/<id> where memory/bots itself is a link is the same case.
Scope
core/src/host/memory.js— one guard arounddirOf(scope)use (:421,:430,:443,:453, and the writes that create memory folders, so a refused link is never written through).core/src/host/workflow-store.js— one guard arounddirFor().- Tests in the existing
core/test/memory.test.jsandcore/test/workflow-store.test.js(or the file that already covers each list). - No client change, no host API change, no
decisions/orjournal/change (those are#2055and#2056).generatere-emits thepatches/ds-bot/files/mirrors.
Four cases at least (two per consumer):
a team memory folder that is a symlink to a folder outside the home lists no memories— list is empty, and reading a name that exists only outside answers absent.a workflow folder that is a symlink to a folder outside the workspace lists no workflows— list is empty; the outside file is never read (assert the read function is not reached, or that its content never appears).a memory or workflow folder that is a plain file lists nothing— both consumers answer empty, nothing throws.a real memory folder and a real workflow folder keep listing and reading their files— the control pair: a realmemory/team/x.mdand a realworkflows/one.jsonbehave exactly as today.
Policy, and what it costs
Same policy, same cost as #2055: these directories are the home's and the workspace's own bookkeeping, and a link that points them elsewhere would let a home read as its own something the team never named. An install that deliberately shares memory/ or workflows/ through a link stops seeing that content and has to place a real directory instead.
Dedupe
Checked at 212f1515 (read 2026-11-01T15:50Z): open and closed issues/PRs searched for symlink, memory, workflows, lstat. Closed hits (#1171, #1278, #1823, #1936) are unrelated; no open issue covers either reader; #2055 is decisions/ only and #2056 is the journal write path.
Done when
All four cases pass, npm run verify is green, and both readers answer exactly as they do for a missing folder when the folder is a link — while a real folder keeps working unchanged.
Activity
HugoXK commentedon Nov 1, 2026
Sequencing correction (owner policy via Chief): do not start this until
#2055has merged (and it may follow#2056— same helper, serial, no parallel copies). The directory check comes from#2055's shared helper (isRealDiror equivalent in a host-side module); this issue calls it formemory/team,memory/bots/<id>and<workspace>/workflowsand must not implement its own copy.Branch from the main that contains the helper. Scope otherwise unchanged: four cases (two consumers, a plain-file case, a real-directory control), no fallback, no follow.
Read a linked decisions folder as no decisions (ThinkFlowLab#2055)
HugoXK commentedon Nov 1, 2026
Claim
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: a27589d6562d4b0aa8f3f451f37e9121
Branch: dev/2-memory-workflows-dir-symlink
Base at posting: c2d70bd
Posted so this issue carries a claim comment and not only the block in its body: the helper that created it writes the block into the body and does not post a comment, so by comment alone this issue read as unclaimed. The body block's
Base: 212f1515…is the base at creation, i.e. history.The base above is only what
mainwas when this was written; it is not a promise about where the branch will be cut. Whoever starts the work posts a heartbeat at that moment naming the base it actually cut from and the branch.Do not pick up without a
pickup-okcomment on this issue. This account can write comments but cannot set labels or assignees on this repository, so this comment and the heartbeats are the record.HugoXK commentedon Nov 1, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: a27589d6562d4b0aa8f3f451f37e9121
Branch: dev/2-memory-workflows-dir-symlink
Base: 4e48aed
Started now, cut from this base — the live main that carries
#2055'score/src/host/real-dir.js(dirKind/dirKindSync, three states:real,absent,other) and#2056's writer-side use of it. TheBase:in the body above is the base at posting time; this comment is the base the branch was actually cut from. Both readers call that shared helper and carry no copy of the rule.Do not pick up without a
pickup-okcomment.Ask every memory and workflow folder segment, on the read and the wri…
HugoXK commentedon Nov 2, 2026
Release
Written-by: FlyBot Lead
Released: a27589d6562d4b0aa8f3f451f37e9121
Reason: Work merged; issue closed.
At: 2026-11-02T02:15:10Z