Skip to content

basou: Have the event readers check the session entry themselves, and refuse it with a typed error - #356

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

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

Conversation

@takashi-matsuyama

Copy link
Copy Markdown
Member

What

replayEvents and readAllEvents in @basou/core now take the store's paths and a session id instead of a session directory, and check the entry at the session's name, without following it, before reading:

Entry at .basou/sessions/<id> Before Now
a directory read read
nothing nothing yielded nothing yielded
a symlink, whatever it points to read through it SessionNotADirectoryError, kind symlink
a file, or anything else that is not a directory Failed to read events.jsonl (ENOTDIR) SessionNotADirectoryError, kind not_a_directory
a name that is not a session id joined into a path Invalid session id

A failed lstat of the entry gives Failed to read events.jsonl, without a path, with the native error as its cause, as the readers' other I/O failures do. The readers do not check .basou/sessions itself, so a federated mirror whose store is a symlink is still read; the local store is checked by the listing or resolver that produced the id.

SessionNotADirectoryError is new and exported from @basou/core, with sessionId and kind. assertSessionDirSafe, resolveSessionId and the readers throw it, and basou view's 404 takes its message, which is unchanged: Session <id> is not a directory; a symlink or a file there is not followed. The resolver takes the kind from the listing, so enumerateSessionEntries also returns symlinks, the entries of notDirectories that are symlinks.

@basou/sdk maps the refusal to null by the error's class instead of matching its message with a regular expression.

All 19 callers move to the new signature; the federated orientation passes each entry's own store.

Why

The readers relied on a convention: every caller had to pass an id from a listing or a check first. Every caller followed it, but nothing enforced it, and a new caller that forgot would read through a symlink. The refusal's message was also built in three places, and the SDK depended on it matching a regular expression.

User-visible behaviour

None in the CLI or the SDK, since every caller already passed a checked id. The readers' signature in @basou/core changes; core is not a semver-guaranteed API.

Docs

  • CHANGELOG: three Unreleased "Changed" entries.

Verification

  • pnpm -r build, pnpm -r typecheck, pnpm lint (the same 5 warnings and 1 info as main), pnpm lint:lang, pnpm lint:paths, and pnpm -r run test (core 2756 passed and 1 skipped, sdk 66, cli 1538): all exit 0.
  • A separate review ran the CLI built from main and from this branch on the same fixtures (real sessions, a symlink to a copy of one, a file, a dangling symlink, a symlink to a file, pending approvals) across 88 invocations, and found no difference other than relative times and ports. The SDK across 12 inputs, and orient over a federated mirror whose store is a symlink, showed none either.
  • Of 20 mutations tried against the first commit, the 4 that survived (checking the entry only after looking for events.jsonl, rethrowing the native lstat error, putting a path in its message, reversing symlinks) are caught by the tests the second commit adds.

Not in this PR

A symlink or a file that is not a regular file at events.jsonl or session.yaml inside a real session directory is still read through. A follow-up checks each file before reading it.

… refuse it with a typed error

replayEvents and readAllEvents took a session directory and relied on their
caller to have checked it; every caller passed an id from a listing,
resolveSessionId, assertSessionDirSafe or inspectSessionEntry. They now take
the store's paths and a session id, and lstat the entry before reading:
a symlink or a file is refused with SessionNotADirectoryError, and a name that
is not a session id with "Invalid session id". They do not check
.basou/sessions itself, so a federated mirror whose store is a symlink is
still read.

SessionNotADirectoryError carries the session id and the kind of entry, with
the message assertSessionDirSafe, resolveSessionId and basou view's 404 gave.
The resolver takes the kind from the listing, which now also returns which of
its entries that are not directories are symlinks. The SDK maps the refusal to
null by its class instead of matching its message, and its tests now cover
readEvents and streamEvents on such an entry.
…at failure, and correct two descriptions

The reader tests now refuse a dangling symlink and one to an empty
directory, where a reader that looked for events.jsonl first would yield
nothing instead, and check that a failed lstat of the entry gives "Failed to
read events.jsonl", without a path, with EACCES as its cause. The listing test
checks that symlinks is ascending by name, not by creation.

loadEntriesFromRoot's doc no longer says a listed session is never checked
again: a running session's events go through replayEvents, which checks the
entry. The CHANGELOG no longer says the event readers threw a plain Error
with the same message before; they followed a symlink and gave another
message for a file.
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