docs(session-persistence): tighten SQLite permission prose

This commit is contained in:
Tianyi Cui
2026-07-19 11:37:41 +08:00
parent 7ba5b95d8e
commit 85fc58943e
4 changed files with 20 additions and 15 deletions

View File

@@ -656,10 +656,11 @@ Requires: `sessions`
export interface Config { export interface Config {
/** /**
* Filesystem path to the SQLite database file. The special value `:memory:` * Filesystem path to the SQLite database file. The special value `:memory:`
* opens an in-process database (tests). Missing directories and the database * opens an in-process database (tests). On filesystems with POSIX modes,
* are created with owner-only permissions; existing path modes are preserved. * missing directories and databases are created owner-only; existing path
* Parent directories writable by another principal are outside the backend's * modes are preserved. Filesystem setup errors other than an existing database
* database-integrity boundary. * fail initialization. The backend does not protect integrity when another
* principal can replace the database entry in its parent directory.
*/ */
path: string path: string
/** /**
@@ -682,7 +683,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
``` ```
Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:53`](../packages/session-persistence/session-persistence-sqlite/src/index.ts) Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:54`](../packages/session-persistence/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-query` ## `@deepseek-ai/dsh-session-query`

View File

@@ -10,7 +10,9 @@ A SQLite durable session-persistence backend — a second `SessionPersistence` i
Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../docs/rfc/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`) lives in a `sessions` row. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row), so no separate column is needed. Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../docs/rfc/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`) lives in a `sessions` row. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row), so no separate column is needed.
The repository's Node range supports unflagged `node:sqlite`. The database enables foreign keys and uses the configured journal mode (`wal` by default; use a rollback mode where WAL shared-memory files are unsuitable). On filesystems with POSIX modes, missing directories are created as `0700` and a missing database is exclusively created as `0600` before SQLite opens it, causing new WAL sidecars to inherit owner-only access. Existing directories, database files, and sidecars keep their modes; ordinary filesystem access errors still fail initialization. This default prevents incidental exposure through the process umask; it does not protect database integrity when another principal can modify entries in an existing parent directory. `PRAGMA user_version` stores the table-layout version; databases with any other version are rejected because this unreleased format has no migrations. The repository's Node range supports unflagged `node:sqlite`. The database enables foreign keys and uses the configured journal mode (`wal` by default; use a rollback mode where WAL shared-memory files are unsuitable). `PRAGMA user_version` stores the table-layout version; databases with any other version are rejected because this unreleased format has no migrations.
On filesystems with POSIX modes, the backend creates missing directories as `0700` and exclusively creates a missing database as `0600` before SQLite opens it. New WAL sidecars receive the database's owner-only mode. Existing directories, database files, and sidecars keep their modes; filesystem setup errors other than an existing database fail initialization. These defaults prevent incidental exposure through the process umask, but do not protect database integrity when another principal can replace the database entry in its parent directory.
## Contract semantics over rows ## Contract semantics over rows

View File

@@ -36,9 +36,10 @@ function surfaceBindings(event: SessionEvent): [string | null, string | null] {
} }
/** /**
* Create a missing database owner-only while preserving an existing file's * Exclusively create a missing database file with owner-only permissions.
* mode. `DatabaseSync` cannot adopt this handle, so a parent directory writable * Existing files retain their modes, and errors other than `EEXIST` propagate.
* by another principal is outside the backend's database-integrity boundary. * `DatabaseSync` reopens by path, so this does not protect integrity when
* another principal can replace the database entry in its parent directory.
*/ */
async function createDatabaseFile(path: string): Promise<void> { async function createDatabaseFile(path: string): Promise<void> {
try { try {
@@ -53,10 +54,11 @@ async function createDatabaseFile(path: string): Promise<void> {
export interface Config { export interface Config {
/** /**
* Filesystem path to the SQLite database file. The special value `:memory:` * Filesystem path to the SQLite database file. The special value `:memory:`
* opens an in-process database (tests). Missing directories and the database * opens an in-process database (tests). On filesystems with POSIX modes,
* are created with owner-only permissions; existing path modes are preserved. * missing directories and databases are created owner-only; existing path
* Parent directories writable by another principal are outside the backend's * modes are preserved. Filesystem setup errors other than an existing database
* database-integrity boundary. * fail initialization. The backend does not protect integrity when another
* principal can replace the database entry in its parent directory.
*/ */
path: string path: string
/** /**

View File

@@ -390,7 +390,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
}) })
describe('SessionPersistenceSqlite: edge cases', () => { describe('SessionPersistenceSqlite: edge cases', () => {
it('creates a new database and WAL sidecars owner-only without changing an existing directory mode', async () => { it('creates a new database and WAL sidecars with owner-only modes without changing its parent mode', async () => {
if (process.platform === 'win32') return if (process.platform === 'win32') return
const path = await freshDbPath() const path = await freshDbPath()
const dir = dirname(path) const dir = dirname(path)
@@ -421,7 +421,7 @@ describe('SessionPersistenceSqlite: edge cases', () => {
await fiber.dispose() await fiber.dispose()
}) })
it('surfaces database pre-creation errors independently of process privileges', async () => { it('surfaces an invalid database path during pre-creation', async () => {
const path = await freshDbPath() const path = await freshDbPath()
const b = await backend(`${path}\0`) const b = await backend(`${path}\0`)