Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions scripts/proof-cloud-setup-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const requested = {
cwd: { base: 'repository', path: 'src' },
}
const credential = randomBytes(24).toString('hex')
const credentialB = randomBytes(24).toString('hex')
const requests = []
const server = createServer((request, response) => {
requests.push({
Expand Down Expand Up @@ -77,6 +78,7 @@ async function key(value) {
}
function capture(label) {
assert.ok(!raw.includes(credential), 'credential appeared in terminal output')
assert.ok(!raw.includes(credentialB), 'B credential appeared in terminal output')
frames.push({ label, screen: screen.trim() })
}
async function snapshot() {
Expand All @@ -95,7 +97,7 @@ async function snapshot() {
}, 'SIGUSR2 state snapshot')
return value
}
async function openSetup() {
async function openSetup(connectionKeys = ['\r']) {
await key('\u000b')
await expect(/Setup/u)
capture('setup menu')
Expand All @@ -104,7 +106,7 @@ async function openSetup() {
await expect(/choose an AgentProfile/u)
await key('\r')
await expect(/choose a connection/u)
await key('\r')
for (const choiceKey of connectionKeys) await key(choiceKey)
await expect(/workspace · cloud sandbox/u)
}
async function start(endpoint, phase) {
Expand Down Expand Up @@ -213,6 +215,19 @@ try {
updatedAt: '2026-09-29T00:00:00.000Z',
lastHealth: { status: 'unknown' },
},
{
id: 'connection-tangle-sandbox-b',
kind: 'tangle-sandbox',
name: 'Tangle Sandbox B (loopback refusal)',
endpoint,
providerOptions: {
transport: 'local',
capabilityHints: ['stream', 'placement', 'usage'],
},
createdAt: '2026-09-29T00:00:00.000Z',
updatedAt: '2026-09-29T00:00:00.000Z',
lastHealth: { status: 'unknown' },
},
],
databaseKeyFile: keyPath,
})}\n`,
Expand Down Expand Up @@ -269,21 +284,58 @@ try {
const afterSave = await snapshot()
assert.equal(afterSave.state.runs.length, 0)
await key('\u001b')
await expect(/new message/u)
await reopenedWorkspace('reopened in same process')
assert.deepEqual(await readFile(configPath), savedBytes)
assert.equal(requests.length, 0)
await openSetup(['\u001b[B', '\r'])
await key('\u000c')
await expect(/files · lifetime/u)
await key('\r')
await expect(/credential · Tangle Sandbox B/u)
await key(credentialB)
await key('\r')
await expect(/review and/u)
await key('\r')
await expect(/selection applied/u)
const selectedBBytes = await readFile(configPath)
const selectedB = JSON.parse(selectedBBytes.toString('utf8'))
assert.equal(selectedB.connectionId, 'connection-tangle-sandbox-b')
assert.ok(!selectedBBytes.includes(credentialB))
const afterB = await snapshot()
assert.equal(afterB.state.runConfiguration.connectionId, 'connection-tangle-sandbox-b')
await key('\u001b')
await expect(/new message/u)
await openSetup(['\u001b[A', '\r'])
await key('\u000c')
await expect(/files · lifetime/u)
await key('\r')
await key('\r')
await expect(/review and/u)
await key('\r')
await expect(/selection applied/u)
const selectedABytes = await readFile(configPath)
const selectedA = JSON.parse(selectedABytes.toString('utf8'))
assert.equal(selectedA.connectionId, 'connection-tangle-sandbox')
const afterA = await snapshot()
assert.equal(afterA.state.runConfiguration.connectionId, 'connection-tangle-sandbox')
await key('\u001b')
assert.deepEqual(await readFile(configPath), selectedABytes)
assert.equal(requests.length, 0)
// A later user submission must reach the existing authenticated provider preflight.
await key('CLOUD_SETUP_LATER_TASK\r')
await expect(/SERVER_ERROR/u)
assert.ok(requests.length > 0)
assert.ok(requests.some((request) => request.credentialReceived))
const afterTask = await snapshot()
assert.equal(afterTask.state.runConfiguration.connectionId, 'connection-tangle-sandbox')
capture('later explicit submission; unchanged provider refusal')
await key('\u001b')
await stop()
const beforeReloadRequests = requests.length
await start(endpoint, 'disk-reloaded-cli')
await reopenedWorkspace('reopened after encrypted disk reload')
assert.deepEqual(await readFile(configPath), savedBytes)
assert.deepEqual(await readFile(configPath), selectedABytes)
assert.equal(requests.length, beforeReloadRequests)
await key('CLOUD_SETUP_RELOADED_TASK\r')
await expect(/SERVER_ERROR/u)
Expand All @@ -294,7 +346,7 @@ try {
evidence.savedWorkspaceRequest = saved.workspaceRequest
evidence.savedLifecycle = connection.providerOptions.lifecycle
evidence.result =
'save, same-process reopen, cancel, disk reload and authenticated preflight verified; no cloud task completed'
'save, A→B→A, same-process reopen, encrypted reload and authenticated A preflight verified; no cloud task completed'
}
await stop()
evidence.exit = exit
Expand Down
64 changes: 61 additions & 3 deletions src/adapters/storage/sqlite-bound-open.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { closeSync, constants, fchmodSync, fstatSync, openSync, readdirSync } from 'node:fs'
import { basename, dirname, resolve } from 'node:path'
import { descriptorPath, openAt, unlinkAt } from '../persistence/posix-at.js'
import type { SqliteDatabase, SqliteDatabaseFactory } from './sqlite-driver.js'
import {
isNativeCipherDatabaseFactory,
type SqliteDatabase,
type SqliteDatabaseFactory,
} from './sqlite-driver.js'
import { StorageError } from './sqlite-errors.js'

export interface BoundSqliteDatabase {
Expand All @@ -13,6 +17,10 @@ export interface BoundSqliteDatabase {
const REQUIRED_PARENT_FLAGS = constants.O_DIRECTORY | (constants.O_NOFOLLOW ?? 0)
const REQUIRED_FILE_FLAGS = constants.O_RDWR | (constants.O_NOFOLLOW ?? 0)

// SQLite's Unix VFS may retain a proven descriptor to preserve POSIX locks and
// reuse it for a later connection without a new open(2) call.
const witnessedNativeDescriptors = new Map<number, ReturnType<typeof fstatSync>>()

function unsupported(message: string): StorageError {
return new StorageError('STORAGE_PATH_RACE_UNSUPPORTED', message)
}
Expand Down Expand Up @@ -40,19 +48,69 @@ function liveFileDescriptors(): ReadonlySet<number> {
return descriptors
}

function witnessedNativeDescriptor(expected: ReturnType<typeof fstatSync>): boolean {
let found = false
for (const [descriptor, known] of witnessedNativeDescriptors) {
try {
const current = fstatSync(descriptor)
if (!sameInode(known, current)) {
witnessedNativeDescriptors.delete(descriptor)
continue
}
if (sameInode(expected, current)) found = true
} catch {
witnessedNativeDescriptors.delete(descriptor)
}
}
return found
}

function nativeDatabaseNamesExpectedFile(database: SqliteDatabase, path: string): boolean {
if (!database.open) return false
try {
const list: unknown = database.pragma('database_list')
return (
Array.isArray(list) &&
list.some(
(row) =>
row !== null &&
typeof row === 'object' &&
(row as { readonly name?: unknown }).name === 'main' &&
(row as { readonly file?: unknown }).file === path,
)
)
} catch {
return false
}
}

function assertDatabaseDescriptorIdentity(
before: ReadonlySet<number>,
expected: ReturnType<typeof fstatSync>,
path: string,
factory: SqliteDatabaseFactory,
database: SqliteDatabase,
): void {
const nativeFactory = isNativeCipherDatabaseFactory(factory)
for (const descriptor of liveFileDescriptors()) {
if (before.has(descriptor)) continue
try {
if (sameInode(expected, fstatSync(descriptor))) return
const current = fstatSync(descriptor)
if (!sameInode(expected, current)) continue
if (nativeFactory) witnessedNativeDescriptors.set(descriptor, current)
return
} catch {
// A concurrent close cannot establish the required identity.
}
}
if (
process.platform === 'linux' &&
nativeFactory &&
witnessedNativeDescriptor(expected) &&
nativeDatabaseNamesExpectedFile(database, path)
) {
return
}
throw new StorageError(
'STORAGE_PATH_RACE',
`SQLite did not open the validated database file: ${path}`,
Expand Down Expand Up @@ -165,7 +223,7 @@ export function openBoundSqliteDatabase(
fchmodSync(fileDescriptor, 0o600)
const descriptorsBefore = liveFileDescriptors()
database = factory(descriptorPath(fileDescriptor), { timeout })
assertDatabaseDescriptorIdentity(descriptorsBefore, metadata, normalizedPath)
assertDatabaseDescriptorIdentity(descriptorsBefore, metadata, normalizedPath, factory, database)
return { database, fileDescriptor, newDatabase: opened.newDatabase }
} catch (error) {
try {
Expand Down
9 changes: 8 additions & 1 deletion src/adapters/storage/sqlite-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StorageError } from './sqlite-errors.js'
export const SQLITE_DRIVER_PACKAGE = 'better-sqlite3-multiple-ciphers'
export const SQLITE_DRIVER_VERSION = '13.0.3'
const RAW_KEY_PREFIX = Buffer.from('raw:', 'ascii')
const nativeCipherFactories = new WeakSet<SqliteDatabaseFactory>()

export type SqliteValue = string | number | bigint | Buffer | null

Expand Down Expand Up @@ -88,7 +89,7 @@ export function loadCipherDatabaseFactory(): SqliteDatabaseFactory {
'The encrypted SQLite module has no constructor',
)
}
return (filename, options) => {
const factory: SqliteDatabaseFactory = (filename, options) => {
try {
return new (Constructor as new (path: string, options: unknown) => SqliteDatabase)(
filename,
Expand All @@ -102,6 +103,12 @@ export function loadCipherDatabaseFactory(): SqliteDatabaseFactory {
)
}
}
nativeCipherFactories.add(factory)
return factory
}

export function isNativeCipherDatabaseFactory(factory: SqliteDatabaseFactory): boolean {
return nativeCipherFactories.has(factory)
}

export function configureCipherDatabase(
Expand Down
46 changes: 45 additions & 1 deletion test/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ import {
openBoundSqliteDatabase,
} from '../src/adapters/storage/sqlite-bound-open.js'
import { assertPersistablePayload } from '../src/adapters/storage/sqlite-crypto.js'
import type { SqliteDatabase } from '../src/adapters/storage/sqlite-driver.js'
import {
loadCipherDatabaseFactory,
type SqliteDatabase,
} from '../src/adapters/storage/sqlite-driver.js'
import { StorageError } from '../src/adapters/storage/sqlite-errors.js'
import { prepareConversationImport } from '../src/app/conversation-import-document.js'
import { providerEventFor } from '../src/app/run-event-mapper.js'
Expand Down Expand Up @@ -1192,3 +1195,44 @@ test('backup and restore enforce the approved root, descriptor identity, and no-
)
await storage.close()
})

test('native SQLite reuses a verified file descriptor on a third open', async () => {
if (process.platform !== 'linux') return
const root = await mkdtemp(join(tmpdir(), 'braid-sqlite-native-reopen-'))
const path = join(root, 'braid.sqlite')
let first: ReturnType<typeof openBoundSqliteDatabase> | undefined
let second: ReturnType<typeof openBoundSqliteDatabase> | undefined
let third: ReturnType<typeof openBoundSqliteDatabase> | undefined
try {
const factory = loadCipherDatabaseFactory()
first = openBoundSqliteDatabase(path, factory, 5_000)
first.database.pragma('journal_mode = WAL')
first.database.exec('CREATE TABLE probe (value TEXT)')
first.database.exec("INSERT INTO probe (value) VALUES ('bound')")
second = openBoundSqliteDatabase(path, factory, 5_000)
second.database.pragma('journal_mode = WAL')
closeBoundSqliteDatabase(first)
first = undefined
assert.throws(
() =>
openBoundSqliteDatabase(
path,
() =>
({
open: true,
pragma: () => [{ name: 'main', file: path }],
close: () => undefined,
}) as unknown as SqliteDatabase,
5_000,
),
(error: unknown) => error instanceof StorageError && error.code === 'STORAGE_PATH_RACE',
)
third = openBoundSqliteDatabase(path, factory, 5_000)
assert.deepEqual(third.database.prepare('SELECT value FROM probe').all(), [{ value: 'bound' }])
} finally {
if (third) closeBoundSqliteDatabase(third)
if (second) closeBoundSqliteDatabase(second)
if (first) closeBoundSqliteDatabase(first)
await rm(root, { recursive: true, force: true })
}
})
Loading