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: 3715e54f5468494999c0e69737d278cc
Branch: dev/2-journal-dir-symlink
Base: 212f151
At: 2026-11-01T15:48:37Z
Owner: Dev-2
Base: 212f151
What happens today
openJournal() (core/src/host/journal.js:241) starts with
await mkdir(dirname(path), { recursive: true, mode: 0o700 })
await chmod(dirname(path), 0o700)and only afterwards lstats the journal file (:246). The directory above the journal is never checked. When that directory is a symlink to a directory outside the home, all three steps follow it: the mkdir is a no-op (the target exists), chmod applies 0o700 to the target directory, and every later write — appendFile (:292), the temp file plus rename on prune (:281/:284) — lands inside the target. So turning stateJournal on in a home whose journal folder is a link silently changes a foreign directory's permissions and writes the team's state journal into it.
The journal file itself is guarded (lstat + isFile() at :246/:251), which is the leaf rule; the directory above it has no rule at all.
Reproduce (a home whose journal folder links at another real directory):
mkdir -p /tmp/h /tmp/elsewhere && chmod 755 /tmp/elsewhere
ln -s /tmp/elsewhere /tmp/h/journal
# start a host with stateJournal on and a journal path under /tmp/h/journal
stat -c '%a' /tmp/elsewhere # became 700
ls /tmp/h/journal # the journal file is really in /tmp/elsewhereExpected
The directory the journal lives in must be a real directory, checked before anything is created, chmod-ed or written. lstat(dirname(path)) must report isDirectory() and not isSymbolicLink(); when it does not (a link to a real folder, a link to a path that does not exist, or a plain file), recording stops exactly the way any other journal problem stops it today: one warning naming the path, stopped = true, and no mkdir, no chmod, no write through the link. Saving the team must keep working, as :227 already promises for every other journal failure.
This is the same rule #2055 sets for decisions/: a directory that is a link is treated as not there, no fallback and no follow.
The leaf rule at :246 stays as it is; this adds the directory-shaped case above it.
Scope
core/src/host/journal.js— theopen()prologue only.store.js's call site (config.stateJournal) is unchanged; the writer keeps its interface and its "never stop the team from saving" behaviour.- Tests in the existing
core/test/journal.test.js(or the file that already coversopenJournal, if it lives elsewhere). - No client change;
generatere-emits thepatches/ds-bot/files/mirror.
Four cases at least:
a journal folder that is a symlink to a real folder outside the home records nothing and stays silent on disk— warning once,stoppedtrue, the target directory's mode is unchanged (assert0o755stayed0o755), no journal file appears inside it.a journal folder that is a symlink to a missing path records nothing— same, and nothing is created.a journal folder that is a plain file records nothing— same; no throw escapes to the caller.a real journal folder still records— the control: entries are written, replay still works, prune still works, mode0o700on its own directory and0o600on the file.
Policy, and what it costs
Same policy as #2055: the host's own bookkeeping directory is the host's, and a link that points it elsewhere would let a home write into (and chmod) a folder the team never named. Refusing the link is the accepted cost: an install that deliberately keeps its journal folder elsewhere has to place a real directory in the home instead.
Because this one has a write and a permission side effect on a foreign directory, it is the highest-risk item of the folder-symlink family found in the audit (the other two — memory/ and workflows/ — are read-only and get their own issue).
Dedupe
Checked at 212f1515 (read 2026-11-01T15:48Z): open and closed issues/PRs searched for symlink, journal, lstat, stateJournal. Only #317 (unrelated interop docs) mentions a journal symlink; nothing covers the journal directory, and #2055 is scoped to decisions/ only.
Done when
All four cases pass, npm run verify is green, and a home whose journal folder is a link records nothing while the team keeps saving.
Activity
HugoXK commentedon Nov 1, 2026
Sequencing correction (owner policy via Chief): do not start this until
#2055has merged. This family uses one shared helper (isRealDir/realDirOrMissing, introduced by#2055in a host-side module); this issue must call it, not carry its own copy of the check. Branch from the main that contains#2055's helper.Two things this issue must keep once it starts:
0o755folder stays0o755) — that is the evidence that distinguishes this write-path case from the read-only ones, and it is why this one is ranked first in the family;mkdir,chmodor a write — it goes through the existingstop()path with one warning.No other change to scope.
#2057follows on the same helper.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: 3715e54f5468494999c0e69737d278cc
Branch: dev/2-journal-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: 3715e54f5468494999c0e69737d278cc
Branch: dev/2-journal-dir-symlink
Base: c2de64d
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). TheBase:in the body above is the base at posting time; this comment is the base the branch was actually cut from. This issue calls that shared helper and does not carry its own copy of the check.HugoXK commentedon Nov 1, 2026
Release
Written-by: FlyBot Lead
Released: 3715e54f5468494999c0e69737d278cc
Reason: merged as 4e48aed (#2065)
At: 2026-11-01T17:19:31Z
fix(host): the state journal refuses a hard link at its own path (Thi…