Skip to content

basou: Refuse a session's events.jsonl or session.yaml that is not a regular file, instead of following it - #357

Merged
takashi-matsuyama merged 2 commits into
mainfrom
core/session-file-checks
Oct 16, 2026
Merged

takashi-matsuyama merged 2 commits into
mainfrom
core/session-file-checks

Conversation

@takashi-matsuyama

Copy link
Copy Markdown
Member

What

A session's events.jsonl and session.yaml (inside .basou/sessions/<id>/) were read through whatever stood at their names. A symlink was followed to a file outside the store, and a FIFO made basou verify, basou board measure and every reader of the session's events wait without end. basou never creates either form.

A shared reader in packages/core/src/storage/session-dir.ts now lstats the file first. A symlink, whatever it points to, or anything but a regular file (a directory, a FIFO) is refused with SessionFileNotAFileError (session id, file, kind) before anything is read; an absent file is handled as before. Every reader of the two files goes through it: replayEvents / readAllEvents, readSessionYaml and the session listing, verifyEventsChain, the chained append (so every note, decision record, task attach, approval resolution, and exec / run append and finalize), the in-place re-import, session rechain, session show, the running transition of exec and run, and the board's count of lost lines.

This is the second part of the reader checks; the first (the session's entry itself) was #356.

Behaviour

  • basou verify reports such a session tampered with a new reason, events_not_a_file or yaml_not_a_file, and event_count 0; it used to follow the file (an intact log behind a symlink was verified, exit 0) or wait on a FIFO. A session.yaml that is a regular file is still read beside an events.jsonl that is not, so a newer basou's session stays unsupported and session_yaml_invalid is still reported. session rechain skips such a session with the same reasons and exits non-zero; an in-place re-import skips it as a broken prior.
  • Commands that read the session by its id stop with <file> of session <id> is not a regular file; a symlink or anything else there is not followed. approval approve / reject stop for such a session.yaml (they used to go on for an unreadable one) and leave the approval pending.
  • Listings leave out a session whose session.yaml is such (session_yaml_not_a_file, full id); one whose events.jsonl is such is kept, its events unreadable (events_jsonl_unreadable). import warns that such a session may be imported again; task refresh-linkage keeps a link to it.
  • basou view: 404 for such a session.yaml; the session without its events for such an events.jsonl.
  • @basou/sdk: listSessions / getSession leave out a session whose session.yaml is such; readEvents / streamEvents return no events for such an events.jsonl and report it to onDiagnostic as events_jsonl_unreadable, the words stats() and the renderers use (compatibility.md, "The errors may grow" item 4).
  • basou board measure: integrity counts the session as verify judges it; trail, review_gaps and effort are not measured for a refused events.jsonl, and review_gaps, freshness and effort for a refused session.yaml. The five sections' method versions go from 1 to 2.
  • A directory at events.jsonl, which some tests used to provoke a read failure, is now one of these cases (verify reports tampered instead of stopping); those tests make the failure with chmod instead.

Docs

  • docs/spec/schemas.md section 7.5 lists the two reasons, each defined by its own file (which one is given when both apply is left open), and when session_yaml_invalid is left out.
  • docs/spec/workspace.md gains "A session's files are not followed", federated mirrors included.
  • docs/spec/compatibility.md gains "A layout basou never creates may come to be refused": the one way an invocation moves from 0 to a non-zero exit within 1.x, bounded by nothing changing on a layout basou creates, the refusal naming what it refuses, and the changelog listing it. This PR is such a change, as the symlink / not_a_directory reasons were.

Review

The second commit applies the findings of a convergent review and an unrequested divergent review (trigger: a published contract, the verify --json reason values and the SDK's reads), each review-only, one round, with per-finding approval: the version gate running after the layout check, session_yaml_invalid on those rows, the SDK diagnostic wording, documentation that overstated the board and the SDK, the compatibility bound, and four test gaps. Not adopted: counting the log on yaml_not_a_file rows (kept consistent with the other layout rows), and renaming the reasons (settled before the work).

Verification

  • pnpm -r build, pnpm -r typecheck, pnpm lint and pnpm -r test pass (core 2788, sdk 67, cli 1555).
  • New tests cover a symlink to an intact copy outside the store (a reader that followed it would succeed), a dangling symlink, a FIFO and a directory, for both files, across the readers, verify (including a newer basou's session.yaml), rechain, re-import, the appends and finalize, listings, board, view, approval, import, task linkage, exec / run and the SDK.
  • Against a fixture workspace, the built CLI of main blocks in basou verify --json on a FIFO session.yaml (killed by a 15 s alarm); this branch prints the two tampered rows and exits 1.
  • Mutation runs over both commits: 28 mutants of the new checks, all caught but one. The survivor replaces the in-place re-import's raw read with a following readFile, which only a swap between the verify gate and that read can reach.

…regular file, instead of following it

A session's events.jsonl and session.yaml were read through whatever stood at
their names: a symlink was followed to a file outside the store, and a FIFO
made basou verify, basou board measure and every reader of the session's
events wait without end. basou never creates either form.

A shared reader in session-dir.ts now lstats the file first. A symlink,
whatever it points to, or anything but a regular file (a directory, a FIFO)
is refused with SessionFileNotAFileError, which carries the session id, the
file and the kind, before anything is read; an absent file is handled as
before. replayEvents / readAllEvents, readSessionYaml and the listing,
verifyEventsChain, the chained append (and so every note, decision record,
task attach, approval resolution and exec / run append and finalize), the
in-place re-import, rechain, session show, the running transition of exec
and run, and the board's count of lost lines all go through it.

- basou verify reports such a session tampered with a new reason,
  events_not_a_file or yaml_not_a_file (events.jsonl first), and
  event_count 0. session rechain skips it with the same reason and exits
  non-zero; an in-place re-import skips it as a broken prior.
- Listings leave out a session whose session.yaml is such, as
  session_yaml_not_a_file, named by its full id; one whose events.jsonl is
  such is kept with its events unreadable. import warns that such a session
  may be imported again, and task refresh-linkage keeps a link to it.
  approval approve and reject stop for such a session.yaml instead of going
  on as for an unreadable one.
- basou view's session page answers 404 for such a session.yaml.
- The SDK treats such a file as a session entry that is not a directory:
  listSessions / getSession leave the session out, readEvents /
  streamEvents return no events, and each reports it to onDiagnostic.
- basou board measure raises the method of integrity, trail, review_gaps,
  freshness and effort to 2, as each counts such a session differently.

docs/spec/schemas.md lists the two reasons; docs/spec/workspace.md says what
each command does with such a file. A directory at events.jsonl, which tests
used to provoke a read failure, is now one of these cases, so those tests make
the failure with chmod instead.
… is refused, and correct what the docs say

Apply the review findings on the previous commit.

- verify read neither file when events.jsonl was not a regular file, so a
  session a newer basou wrote was tampered instead of unsupported, and
  session_yaml_invalid went missing. A session.yaml that is a regular file is
  now read beside such a log, so its version decides as for any other row;
  only a session.yaml that is not a regular file leaves the row to the layout
  alone.
- The SDK reports a refused events.jsonl as events_jsonl_unreadable, the
  words stats() and the renderers use for the same log.
- docs/spec/compatibility.md bounds a command coming to refuse a layout basou
  never creates, the one way an invocation moves from 0 to a non-zero exit
  within 1.x: nothing changes on a layout basou creates, the refusal names
  what it refuses, and the changelog lists it.
- The spec defines each new verify reason by its own file and leaves open
  which is given when both files are refused; it says which board sections
  read which file, what the SDK does, and that a federated mirror's sessions
  are covered too. The CHANGELOG no longer says the SDK treats such a file as
  a session entry that is not a directory, nor that the reason addition alone
  is what compatibility.md allows.
- Tests cover run's running transition, a FIFO in exec's, each rechain reason
  failing the run on its own, and the board's count of lost lines.
@takashi-matsuyama
takashi-matsuyama merged commit 92cd94b into main Oct 16, 2026
8 checks passed
@takashi-matsuyama
takashi-matsuyama deleted the core/session-file-checks branch October 16, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant