fix(exec): import legacy approvals into sqlite

This commit is contained in:
Peter Steinberger
2026-05-15 17:14:12 +01:00
parent b2ff1f85a6
commit 3fe7fa663b
4 changed files with 112 additions and 3 deletions

View File

@@ -13,7 +13,19 @@ enum ExecApprovalsSQLiteStateStore {
}
static func readRawState() -> String? {
OpenClawSQLiteStateStore.readExecApprovalsRaw(configKey: self.configKey)
if let raw = OpenClawSQLiteStateStore.readExecApprovalsRaw(configKey: self.configKey) {
return raw
}
guard let raw = try? String(contentsOf: self.legacyURL(), encoding: .utf8) else {
return nil
}
do {
try self.writeRawState(raw)
try? FileManager.default.removeItem(at: self.legacyURL())
} catch {
return raw
}
return raw
}
static func writeRawState(_ raw: String) throws {
@@ -43,4 +55,8 @@ enum ExecApprovalsSQLiteStateStore {
}
return file
}
private static func legacyURL() -> URL {
OpenClawPaths.stateDirURL.appendingPathComponent("exec-approvals.json", isDirectory: false)
}
}

View File

@@ -35,6 +35,40 @@ struct ExecApprovalsStoreRefactorTests {
}
}
@Test
func `ensure state imports legacy json approvals before sqlite defaults`() async throws {
try await self.withTempStateDir { stateDir in
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
let legacyURL = stateDir.appendingPathComponent("exec-approvals.json")
try """
{
"version": 1,
"socket": { "path": "/tmp/legacy.sock", "token": "legacy-token" },
"defaults": { "security": "allowlist", "ask": "on-miss" },
"agents": {
"main": {
"allowlist": [
{ "id": "00000000-0000-0000-0000-000000000001", "pattern": "/usr/bin/rg" }
]
}
}
}
""".write(to: legacyURL, atomically: true, encoding: .utf8)
let ensured = ExecApprovalsStore.ensureState()
#expect(ensured.socket?.path == "/tmp/legacy.sock")
#expect(ensured.socket?.token == "legacy-token")
#expect(ensured.defaults?.security == .allowlist)
#expect(ensured.defaults?.ask == .onMiss)
#expect(ensured.agents?["main"]?.allowlist?.map(\.pattern) == ["/usr/bin/rg"])
#expect(!FileManager().fileExists(atPath: legacyURL.path))
let storedRaw = try Self.readStoredApprovalsRaw()
#expect(storedRaw?.contains("legacy-token") == true)
}
}
@Test
func `update allowlist accepts basename pattern`() async throws {
try await self.withTempStateDir { _ in

View File

@@ -196,6 +196,33 @@ describe("exec approvals store helpers", () => {
expect(readApprovalsFile().socket).toEqual(ensured.socket);
});
it("imports legacy JSON approvals before creating SQLite defaults", () => {
const dir = createHomeDir();
const legacyPath = path.join(dir, ".openclaw", "exec-approvals.json");
fs.mkdirSync(path.dirname(legacyPath), { recursive: true });
fs.writeFileSync(
legacyPath,
JSON.stringify(
{
version: 1,
socket: { path: "/tmp/legacy.sock", token: "legacy-token" },
defaults: { security: "allowlist", ask: "on-miss" },
agents: { main: { allowlist: [{ pattern: "/usr/bin/rg", id: "keep-id" }] } },
} satisfies ExecApprovalsFile,
null,
2,
) + "\n",
);
const ensured = ensureExecApprovals();
expect(ensured.socket).toEqual({ path: "/tmp/legacy.sock", token: "legacy-token" });
expect(ensured.defaults).toEqual({ security: "allowlist", ask: "on-miss" });
expect(ensured.agents?.main?.allowlist).toEqual([{ pattern: "/usr/bin/rg", id: "keep-id" }]);
expect(fs.existsSync(legacyPath)).toBe(false);
expect(readSqliteRaw()).toContain("legacy-token");
});
it("adds trimmed allowlist entries once and persists generated ids", () => {
createHomeDir();
vi.spyOn(Date, "now").mockReturnValue(123_456);

View File

@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import fs from "node:fs";
import type { Insertable } from "kysely";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import {
@@ -218,6 +219,7 @@ const DEFAULT_ASK: ExecAsk = "off";
export const DEFAULT_EXEC_APPROVAL_ASK_FALLBACK: ExecSecurity = "full";
const DEFAULT_AUTO_ALLOW_SKILLS = false;
const DEFAULT_SOCKET = "~/.openclaw/exec-approvals.sock";
const LEGACY_EXEC_APPROVALS_FILE = "~/.openclaw/exec-approvals.json";
const EXEC_APPROVALS_CONFIG_KEY = "current";
type ExecApprovalsDatabase = Pick<OpenClawStateKyselyDatabase, "exec_approvals_config">;
@@ -467,6 +469,36 @@ function readExecApprovalsRawFromSqlite(env: NodeJS.ProcessEnv = process.env): s
return row?.raw_json ?? null;
}
function readLegacyExecApprovalsRaw(env: NodeJS.ProcessEnv = process.env): {
raw: string | null;
exists: boolean;
path: string;
} {
const filePath = resolveLegacyExecApprovalsPath(env);
if (!fs.existsSync(filePath)) {
return { raw: null, exists: false, path: filePath };
}
return { raw: fs.readFileSync(filePath, "utf8"), exists: true, path: filePath };
}
function resolveLegacyExecApprovalsPath(env: NodeJS.ProcessEnv = process.env): string {
return expandHomePrefix(LEGACY_EXEC_APPROVALS_FILE, { env });
}
function readExecApprovalsRaw(env: NodeJS.ProcessEnv = process.env): string | null {
const sqliteRaw = readExecApprovalsRawFromSqlite(env);
if (sqliteRaw !== null) {
return sqliteRaw;
}
const legacy = readLegacyExecApprovalsRaw(env);
if (!legacy.exists || legacy.raw === null) {
return null;
}
writeExecApprovalsRawToSqlite(legacy.raw, env);
fs.rmSync(legacy.path, { force: true });
return legacy.raw;
}
export function writeExecApprovalsRawToSqlite(
raw: string,
env: NodeJS.ProcessEnv = process.env,
@@ -523,7 +555,7 @@ function parseExecApprovalsRaw(raw: string | null): ExecApprovalsFile {
}
export function readExecApprovalsSnapshot(): ExecApprovalsSnapshot {
const sqliteRaw = readExecApprovalsRawFromSqlite();
const sqliteRaw = readExecApprovalsRaw();
return {
path: resolveExecApprovalsStoreLocationForDisplay(),
exists: sqliteRaw !== null,
@@ -534,7 +566,7 @@ export function readExecApprovalsSnapshot(): ExecApprovalsSnapshot {
}
export function loadExecApprovals(): ExecApprovalsFile {
return parseExecApprovalsRaw(readExecApprovalsRawFromSqlite());
return parseExecApprovalsRaw(readExecApprovalsRaw());
}
export function saveExecApprovals(file: ExecApprovalsFile) {