cfceb8452b
The parent implementation introduced sandboxMode and approvalPolicy as generic SessionHeader fields, then propagated those fields through both persistence backends, session-query indexes, collision checks, policy-specific seed-boundary folds, catalogs, and a broad test matrix. That storage plane is unnecessary: Session already accepts a validated constructor seed, and persistence captures that seed when the session is announced before committing its first batch. Capture each parent override synchronously at delegation, append source-tagged sandbox/mode and approval/policy records after the optional fork prefix, and create the child with that combined seed. Keeping header.seedLength at the original fork-prefix length preserves lineage while ordinary last-event-wins folds make the inherited records outrank stale parent history and remain subordinate to later child switches. Unswitched parents still stamp nothing, so children continue to follow deployment defaults. Remove the generic header fields and every persistence/query/schema branch built around them. Collapse the inheritance suite from ten leaking scenarios to four owned-context cases covering real filesystem confinement, stale fork precedence, delegation-time capture, and the no-override path. The assembled headless snapshot now asserts the persisted inheritance event directly. This keeps the security behavior while restoring policy ownership to the existing event log and deleting the speculative durability machinery that the original tests did not exercise.
171 lines
5.8 KiB
TypeScript
171 lines
5.8 KiB
TypeScript
/** SQLite schema for the disposable session full-text read model. */
|
|
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
import { mkdir, open } from 'node:fs/promises'
|
|
import { dirname, resolve } from 'node:path'
|
|
|
|
/** Current derived-index schema version. Incompatible versions reset in place. */
|
|
export const SESSION_QUERY_SQLITE_SCHEMA_VERSION = 5
|
|
|
|
/** SQLite application id protecting unrelated databases from derived resets. */
|
|
export const SESSION_QUERY_SQLITE_APPLICATION_ID = 0x44534851
|
|
|
|
/** Supported SQLite journal modes. */
|
|
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
|
|
|
|
const DERIVED_USER_TABLES = new Set([
|
|
'search_state',
|
|
'persisted_sessions',
|
|
'persisted_docs',
|
|
'persisted_docs_data',
|
|
'persisted_docs_idx',
|
|
'persisted_docs_content',
|
|
'persisted_docs_docsize',
|
|
'persisted_docs_config',
|
|
])
|
|
|
|
/**
|
|
* Exclusively create a missing database file with owner-only permissions.
|
|
* Existing files retain their modes, and errors other than `EEXIST` propagate.
|
|
*/
|
|
async function createDatabaseFile(path: string): Promise<void> {
|
|
try {
|
|
const handle = await open(path, 'wx', 0o600)
|
|
await handle.close()
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open, validate, and initialize persistent and connection-local schemas.
|
|
* @param path - dedicated derived-index path or `:memory:`; missing filesystem paths are created owner-only.
|
|
* @param journalMode - validated SQLite journal mode.
|
|
* @returns initialized database handle owned by the search service.
|
|
*/
|
|
export async function openSearchDatabase(path: string, journalMode: JournalMode): Promise<DatabaseSync> {
|
|
const actual = path === ':memory:' ? path : resolve(path)
|
|
if (actual !== ':memory:') {
|
|
await mkdir(dirname(actual), { recursive: true, mode: 0o700 })
|
|
await createDatabaseFile(actual)
|
|
}
|
|
const db = new DatabaseSync(actual)
|
|
try {
|
|
const { application_id: applicationId } = db.prepare('PRAGMA application_id').get() as { application_id: number }
|
|
const { user_version: version } = db.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
const userTables = listUserTables(db)
|
|
if (applicationId !== 0 && applicationId !== SESSION_QUERY_SQLITE_APPLICATION_ID) {
|
|
throw new Error(`session-search database at "${actual}" belongs to another application`)
|
|
}
|
|
if (applicationId === 0 && userTables.length > 0) {
|
|
throw new Error(`session-search database at "${actual}" is not an empty or recognized derived index`)
|
|
}
|
|
if (applicationId === SESSION_QUERY_SQLITE_APPLICATION_ID) {
|
|
assertDerivedUserTables(actual, userTables)
|
|
if (version !== SESSION_QUERY_SQLITE_SCHEMA_VERSION) resetDerivedSchema(db, userTables)
|
|
}
|
|
// Apply mutating pragmas only after refusing foreign or canonical files.
|
|
// journalMode is a validated closed union, not caller-controlled SQL.
|
|
db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`)
|
|
ensurePersistentSchema(db)
|
|
ensureTemporarySchema(db)
|
|
return db
|
|
} catch (error: unknown) {
|
|
db.close()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
function listUserTables(db: DatabaseSync): string[] {
|
|
const rows = db.prepare(
|
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT GLOB 'sqlite_*' ORDER BY name",
|
|
).all() as Array<{ name: string }>
|
|
return rows.map(row => row.name)
|
|
}
|
|
|
|
function assertDerivedUserTables(path: string, userTables: readonly string[]): void {
|
|
const unknownTables = userTables.filter(name => !DERIVED_USER_TABLES.has(name))
|
|
if (unknownTables.length > 0) {
|
|
throw new Error(
|
|
`session-search database at "${path}" has unrecognized user tables: ${unknownTables.join(', ')}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
function resetDerivedSchema(db: DatabaseSync, userTables: readonly string[]): void {
|
|
for (const name of userTables) {
|
|
db.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(name)}`)
|
|
}
|
|
db.exec('PRAGMA user_version = 0')
|
|
}
|
|
|
|
function ensurePersistentSchema(db: DatabaseSync): void {
|
|
db.exec(`PRAGMA application_id = ${SESSION_QUERY_SQLITE_APPLICATION_ID}`)
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS search_state (
|
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
global_generation INTEGER NOT NULL
|
|
) STRICT
|
|
`)
|
|
db.exec('INSERT OR IGNORE INTO search_state (singleton, global_generation) VALUES (1, 0)')
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS persisted_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
version INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
cwd TEXT,
|
|
parent_session TEXT,
|
|
seed_length INTEGER,
|
|
delegation_depth INTEGER,
|
|
revision TEXT NOT NULL,
|
|
generation INTEGER NOT NULL
|
|
) STRICT
|
|
`)
|
|
db.exec(`
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS persisted_docs USING fts5(
|
|
text,
|
|
session_id UNINDEXED,
|
|
seq UNINDEXED,
|
|
type UNINDEXED,
|
|
time UNINDEXED,
|
|
surface UNINDEXED,
|
|
codepoint_length UNINDEXED,
|
|
tokenize = 'unicode61'
|
|
)
|
|
`)
|
|
db.exec(`PRAGMA user_version = ${SESSION_QUERY_SQLITE_SCHEMA_VERSION}`)
|
|
}
|
|
|
|
function ensureTemporarySchema(db: DatabaseSync): void {
|
|
db.exec(`
|
|
CREATE TEMP TABLE IF NOT EXISTS live_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
version INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
cwd TEXT,
|
|
parent_session TEXT,
|
|
seed_length INTEGER,
|
|
delegation_depth INTEGER,
|
|
fingerprint TEXT NOT NULL,
|
|
persisted INTEGER NOT NULL CHECK (persisted IN (0, 1)),
|
|
generation INTEGER NOT NULL
|
|
) STRICT
|
|
`)
|
|
db.exec(`
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS temp.live_docs USING fts5(
|
|
text,
|
|
session_id UNINDEXED,
|
|
seq UNINDEXED,
|
|
type UNINDEXED,
|
|
time UNINDEXED,
|
|
surface UNINDEXED,
|
|
codepoint_length UNINDEXED,
|
|
tokenize = 'unicode61'
|
|
)
|
|
`)
|
|
}
|
|
|
|
function quoteIdentifier(value: string): string {
|
|
return `"${value.replaceAll('"', '""')}"`
|
|
}
|