Skip to content

A linked leaf file is written through: the memory journal and the workflow save follow a symlink #2079

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: 2b62ad39f22f4f66ab03f4628b4dc395
Branch: dev/2-leaf-file-link
Base: 8626664
At: 2026-11-01T18:35:34Z

What happens today

Two host writers ask what their folder is and never ask what their file is. A leaf file swapped for a link is therefore written straight through it, into a file outside the home, with no warning:

  • memory journal — core/src/host/memory.js:527-531 journal() guards with writeFolder(root) (:473-478, which asks every segment down to memory/) and then appendFile(journalPath, …) at :530. journalPath = join(root, 'journal.jsonl') (:426, root = join(home, 'memory') at :422). The leaf file is never lstated, so memory/journal.jsonl → link appends into whatever file it names. Four call sites reach it: :662 add, :693 replace, :715 remove, :736 archive.
  • workflow store — core/src/host/workflow-store.js:148 save() guards with folderWritable() (:55-60, one dirKindSync(dir)) and then writeFileSync(fileFor(def.id), bytes) at :169; fileFor is join(dir, '${id}.json') (:42). setDefault() (:183-190) has the same gap at :188, writing project.json (PROJECT_FILE, :30). Both refuse a linked folder, neither refuses a linked file.

The correct shape is already in this repository twice: core/src/host/memory.js:521-526 writeText() writes temp + rename, so a link there is replaced rather than followed; and core/src/host/journal.js (fixed in #2074) lstats the leaf before appending and writes only when it is absent or a plain non-link file.

Reproduction

Both probes import the real modules from a live main tree (8626664d), no stubs, each on a fresh temp home.

  • Q9, memory journal: link home/memory/journal.jsonl to <outside>/journal.jsonl holding one line PRE-EXISTING LINE, then remember {text: 'Ship on Friday', scope: 'team'}. Today:
    Q9 remember: Saved to the team memory, in MEMORY.md ( | outside journal lines: 2
    — the outside file grew a line and no warning was printed.
  • Q10, workflow store: link workflows/mine.json to <outside>/mine.json, then createWorkflowStore({dir}).save(...). Today:
    Q10 save ok: true | outside file rewritten: true | link still a link: true
    — the write lands in the outside file and still reports success.

Expected

Ask what the file is before every write, the same way #2056/#2065/#2074 ask about the folder: lstat the leaf and write only when it is absent (still created, mode 0600) or a plain non-link file; a link, a directory or any other entry is refused.

  • memory journal: that one record is dropped, one warning is printed, and the memory write itself still succeeds — the journal is a side channel and saving memory must never depend on it.
  • workflow save / setDefault: ok: false with a new code (e.g. workflow/not-a-file), one warning, and neither the link nor the file it names is touched.

Acceptance (red first, on main today)

Scope

core/src/host/memory.js, core/src/host/workflow-store.js, patches/ds-bot/0001-core-fixes.patch (memory.js travels in that patch — there is no patches/ds-bot/files/src/host/memory.js mirror), patches/ds-bot/files/src/host/workflow-store.js (workflow-store does have a mirror), core/test/memory.test.js, core/test/workflow-store.test.js, one sentence each in docs/subsystems/core-host.md (memory section and the workflow-store row) and a changelog.d/ fragment.

Tier T2 (host write paths, security surface). Small: two lstat guards plus two warned sets, about 25–40 source lines and three red-first cases.

Verify with TZ=UTC ARCH_DOCS_BASE=<base> npm run verify, ds-bot-patches generate twice (no drift on the second run) plus check, and changelog check. Security surface, so the merge ping waits for the Security advisory at the tip.

Found by

Read-only post-merge audit by FlyBot Security of the #2071 squash (e7a295338800); line numbers re-checked here against live main 8626664d.

Duplicate check

journal symlink after start / journal folder link write / state journal link swap / symlink memory / workflow symlink / link memory folder across open and closed issues: #2055 (decisions folder), #2056 (journal folder), #2057 (memory/workflows folder read), #2068 (linked memory folder read side and the entry writers — closed 18:14:16Z, absorbed into #2071) are all folder links and all closed. The one open neighbour is #2074, which is the state journal's folder swapped after start-up in core/src/host/journal.js — a different module, fixed by the lstat guard this issue copies. Leaf single-file link write-through has no open issue.

Activity

  1. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    Claimed for FlyBot.

    • Claim-Id: 2b62ad39f22f4f66ab03f4628b4dc395
    • Owner: Dev-2 (FlyBot Repo Engineer)
    • Branch: dev/2-leaf-file-link
    • Base: 8626664d0d1d84fdd8fdc28869d8f4e032225123 (live main)
    • Written-by: FlyBot Lead

    The branch and the pull request follow within 24h. Do not start this issue without a pickup-ok comment.

    Note on bookkeeping: this account is a repository contributor, so it cannot add the agent-working label or the Birfy assignee (the API answers 403 Must have admin rights to Repository). This comment is the claim record until a maintainer sets them.

  2. HugoXK commented on Nov 2, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 2b62ad39f22f4f66ab03f4628b4dc395
    At: 2026-11-02T00:18:10Z

  3. HugoXK commented on Nov 2, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: 2b62ad39f22f4f66ab03f4628b4dc395
    Reason: Merged as 4551bb3 (#2083) at 2026-11-02T01:21:36Z; issue closed completed.
    At: 2026-11-02T01:25:04Z

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