Skip to content

A symlinked memory or workflows folder reads another folder's files as this team's own #2057

Closed

Description

@HugoXK

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 text

Reproduce (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 content

Expected

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 around dirOf(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 around dirFor().
  • Tests in the existing core/test/memory.test.js and core/test/workflow-store.test.js (or the file that already covers each list).
  • No client change, no host API change, no decisions/ or journal/ change (those are #2055 and #2056). generate re-emits the patches/ds-bot/files/ mirrors.

Four cases at least (two per consumer):

  1. 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.
  2. 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).
  3. a memory or workflow folder that is a plain file lists nothing — both consumers answer empty, nothing throws.
  4. a real memory folder and a real workflow folder keep listing and reading their files — the control pair: a real memory/team/x.md and a real workflows/one.json behave 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

  1. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    Sequencing correction (owner policy via Chief): do not start this until #2055 has merged (and it may follow #2056 — same helper, serial, no parallel copies). The directory check comes from #2055's shared helper (isRealDir or equivalent in a host-side module); this issue calls it for memory/team, memory/bots/<id> and <workspace>/workflows and 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.

  2. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    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 main was 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-ok comment 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.

  3. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    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's core/src/host/real-dir.js (dirKind / dirKindSync, three states: real, absent, other) and #2056's writer-side use of it. The Base: 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-ok comment.

  4. HugoXK commented on Nov 2, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: a27589d6562d4b0aa8f3f451f37e9121
    Reason: Work merged; issue closed.
    At: 2026-11-02T02:15:10Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions