mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-20 03:01:34 +00:00
fix(sqlite): validate agent state before schema setup (#105179)
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
closeOpenClawAgentDatabaseByPath,
|
||||
closeOpenClawAgentDatabasesForTest,
|
||||
disposeOpenClawAgentDatabaseByPath,
|
||||
ensureOpenClawAgentDatabaseSchema,
|
||||
inspectOpenClawAgentDatabaseOwner,
|
||||
listOpenClawRegisteredAgentDatabases,
|
||||
OPENCLAW_AGENT_SCHEMA_VERSION,
|
||||
@@ -156,6 +157,34 @@ function seedVersion1MemoryAgentDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
function createUnsafeIndexDrift(databasePath: string): void {
|
||||
const { DatabaseSync } = requireNodeSqlite();
|
||||
const database = new DatabaseSync(databasePath);
|
||||
try {
|
||||
database.exec(`
|
||||
CREATE TABLE unsafe_index_records (
|
||||
id INTEGER PRIMARY KEY,
|
||||
indexed_value TEXT NOT NULL,
|
||||
alternate_value TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX unsafe_index_records_value ON unsafe_index_records(indexed_value);
|
||||
INSERT INTO unsafe_index_records (indexed_value, alternate_value)
|
||||
VALUES ('alpha', 'zeta'), ('beta', 'eta'), ('gamma', 'theta');
|
||||
`);
|
||||
database.enableDefensive?.(false);
|
||||
database.exec("PRAGMA writable_schema = ON;");
|
||||
database
|
||||
.prepare(
|
||||
"UPDATE sqlite_schema SET sql = 'CREATE INDEX unsafe_index_records_value ON unsafe_index_records(alternate_value)' WHERE name = 'unsafe_index_records_value'",
|
||||
)
|
||||
.run();
|
||||
const schemaVersion = readSqliteNumberPragma(database, "schema_version");
|
||||
database.exec(`PRAGMA writable_schema = OFF; PRAGMA schema_version = ${schemaVersion + 1};`);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
type AgentSchemaOpenerResult = { agentId: string; ok: boolean; error?: string };
|
||||
|
||||
function launchAgentSchemaOpener(params: {
|
||||
@@ -1354,6 +1383,73 @@ describe("openclaw agent database", () => {
|
||||
"size",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects stale secondary indexes before writable initialization", () => {
|
||||
const stateDir = createTempStateDir();
|
||||
const env = { OPENCLAW_STATE_DIR: stateDir };
|
||||
const created = openOpenClawAgentDatabase({ agentId: "worker-1", env });
|
||||
const databasePath = created.path;
|
||||
closeOpenClawAgentDatabasesForTest();
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
createUnsafeIndexDrift(databasePath);
|
||||
|
||||
const { DatabaseSync } = requireNodeSqlite();
|
||||
const before = new DatabaseSync(databasePath, { readOnly: true });
|
||||
let metadataBefore: unknown;
|
||||
try {
|
||||
expect(before.prepare("PRAGMA quick_check").get()).toEqual({ quick_check: "ok" });
|
||||
expect(before.prepare("PRAGMA integrity_check").all()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
integrity_check: expect.stringMatching(/missing from index unsafe_index_records_value/),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
metadataBefore = before
|
||||
.prepare(
|
||||
"SELECT schema_version, updated_at FROM schema_meta WHERE meta_key = 'primary' LIMIT 1",
|
||||
)
|
||||
.get();
|
||||
} finally {
|
||||
before.close();
|
||||
}
|
||||
|
||||
expect(() => openOpenClawAgentDatabase({ agentId: "worker-1", env })).toThrow(
|
||||
/integrity_check failed.*missing from index unsafe_index_records_value/iu,
|
||||
);
|
||||
const independentlyManaged = new DatabaseSync(databasePath);
|
||||
try {
|
||||
expect(() =>
|
||||
ensureOpenClawAgentDatabaseSchema(independentlyManaged, {
|
||||
agentId: "worker-1",
|
||||
env,
|
||||
}),
|
||||
).toThrow(/integrity_check failed.*missing from index unsafe_index_records_value/iu);
|
||||
} finally {
|
||||
independentlyManaged.close();
|
||||
}
|
||||
|
||||
const after = new DatabaseSync(databasePath, { readOnly: true });
|
||||
try {
|
||||
expect(
|
||||
after
|
||||
.prepare(
|
||||
"SELECT schema_version, updated_at FROM schema_meta WHERE meta_key = 'primary' LIMIT 1",
|
||||
)
|
||||
.get(),
|
||||
).toEqual(metadataBefore);
|
||||
expect(after.prepare("PRAGMA integrity_check").all()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
integrity_check: expect.stringMatching(/missing from index unsafe_index_records_value/),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
} finally {
|
||||
after.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses to open newer per-agent schema versions", () => {
|
||||
const stateDir = createTempStateDir();
|
||||
const databasePath = path.join(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "../infra/kysely-sync.js";
|
||||
import { requireNodeSqlite } from "../infra/node-sqlite.js";
|
||||
import { resolveSqliteDatabaseFilePaths } from "../infra/sqlite-files.js";
|
||||
import { assertSqliteIntegrity } from "../infra/sqlite-integrity.js";
|
||||
import {
|
||||
runSqliteImmediateTransactionSync,
|
||||
type SqliteTransactionOptions,
|
||||
@@ -608,6 +609,7 @@ export function ensureOpenClawAgentDatabaseSchema(
|
||||
const databaseOptions = { ...options, agentId };
|
||||
const pathname = resolveOpenClawAgentSqlitePath(databaseOptions);
|
||||
ensureOpenClawAgentDatabasePermissions(pathname, databaseOptions);
|
||||
assertAgentDatabaseIntegrityBeforeMutation(db, pathname);
|
||||
configureSqlitePreSchemaPragmas(db, {
|
||||
busyTimeoutMs: OPENCLAW_SQLITE_BUSY_TIMEOUT_MS,
|
||||
});
|
||||
@@ -803,6 +805,16 @@ export function inspectOpenClawAgentDatabaseOwner(
|
||||
}
|
||||
}
|
||||
|
||||
function assertAgentDatabaseIntegrityBeforeMutation(
|
||||
database: DatabaseSync,
|
||||
pathname: string,
|
||||
): void {
|
||||
database.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`);
|
||||
// Writable open permits interrupted journal recovery. Schema and connection
|
||||
// setup must wait until full table/index consistency is proven afterward.
|
||||
assertSqliteIntegrity(database, pathname);
|
||||
}
|
||||
|
||||
/** Open or return a cached per-agent database after schema and owner validation. */
|
||||
export function openOpenClawAgentDatabase(
|
||||
options: OpenClawAgentDatabaseOptions,
|
||||
@@ -833,6 +845,7 @@ export function openOpenClawAgentDatabase(
|
||||
const walMaintenance = (() => {
|
||||
let maintenance: SqliteWalMaintenance | undefined;
|
||||
try {
|
||||
assertAgentDatabaseIntegrityBeforeMutation(db, pathname);
|
||||
configureSqlitePreSchemaPragmas(db, {
|
||||
busyTimeoutMs: OPENCLAW_SQLITE_BUSY_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user