chore(core): move database permissions to separate PR

This commit is contained in:
Aiden Cline
2026-09-29 14:06:09 -05:00
parent 1a26fdc297
commit d07becdc71
2 changed files with 3 additions and 62 deletions
+3 -23
View File
@@ -5,8 +5,6 @@ import { sqliteLayer, supportsForeignKeyToggle, supportsTuningPragmas } from "#s
import { Context, Effect, Layer, Schema, Semaphore } from "effect"
import type { SqlClient } from "effect/unstable/sql"
import { Global } from "@opencode/util/global"
import { closeSync, existsSync, openSync } from "node:fs"
import { chmod } from "node:fs/promises"
import { isAbsolute, join } from "path"
import { DatabaseMigration } from "./migration.js"
import { makeGlobalNode } from "@opencode/util/effect/app-node"
@@ -72,31 +70,13 @@ export function layer(options: Options = { path: ":memory:" }) {
Layer.provide(sqliteLayer({ filename })),
)
const filename = options.path ?? ":memory:"
if (filename === ":memory:") return provide(filename)
const file = isAbsolute(filename) ? filename : join((yield* Global.Service).data, filename)
yield* Effect.promise(() => restrictToOwner(file))
return provide(file)
if (filename === ":memory:" || isAbsolute(filename)) return provide(filename)
const global = yield* Global.Service
return provide(join(global.data, filename))
}),
)
}
// SQLite creates new sidecars with the database's mode, but does not tighten existing sidecars.
// Windows relies on the user profile directory's inherited ACLs instead of POSIX modes.
async function restrictToOwner(filename: string) {
if (process.platform === "win32") return
// Opening and closing an existing database can release another connection's POSIX locks.
// Create missing files synchronously so another fiber cannot open one before it is restricted.
if (!existsSync(filename)) closeSync(openSync(filename, "a", 0o600))
await chmod(filename, 0o600)
await Promise.all(
[`${filename}-wal`, `${filename}-shm`].map((file) =>
chmod(file, 0o600).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "ENOENT") throw error
}),
),
)
}
// The database service over an injected SqlClient, for runtimes that receive
// database storage instead of opening a filesystem path. Any client provided
// here still goes through the pragma guards and migrations; Global is required
-39
View File
@@ -1,39 +0,0 @@
import { describe, expect, test } from "bun:test"
import { chmod, stat, writeFile } from "node:fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
import { Database } from "@opencode/core/database/database"
import { Global } from "@opencode/util/global"
import { tmpdir } from "./fixture/tmpdir"
// Read while the connection is open: SQLite may remove the sidecars on close.
const openModes = (filename: string) =>
Effect.runPromise(
Effect.gen(function* () {
yield* Layer.build(Database.layer({ path: filename }))
return yield* Effect.promise(() =>
Promise.all(["", "-wal", "-shm"].map(async (suffix) => (await stat(filename + suffix)).mode & 0o777)),
)
}).pipe(Effect.scoped, Effect.provideService(Global.Service, Global.make({ data: path.dirname(filename) }))),
)
describe.skipIf(process.platform === "win32")("Database file permissions", () => {
test("creates the database and sidecars readable only by the owner", async () => {
await using tmp = await tmpdir()
expect(await openModes(path.join(tmp.path, "opencode.db"))).toEqual([0o600, 0o600, 0o600])
})
test("tightens an existing database and sidecars", async () => {
await using tmp = await tmpdir()
const filename = path.join(tmp.path, "opencode.db")
await Promise.all(
["", "-wal", "-shm"].map(async (suffix) => {
await writeFile(filename + suffix, "")
await chmod(filename + suffix, 0o644)
}),
)
expect(await openModes(filename)).toEqual([0o600, 0o600, 0o600])
})
})