Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawSQLiteStateStore.swift
2026-05-28 00:46:34 +01:00

678 lines
28 KiB
Swift

import Foundation
import OSLog
import SQLite3
public struct OpenClawSQLiteDeviceIdentityRow: Sendable {
public let deviceId: String
public let publicKeyPem: String
public let privateKeyPem: String
public let createdAtMs: Int
public init(deviceId: String, publicKeyPem: String, privateKeyPem: String, createdAtMs: Int) {
self.deviceId = deviceId
self.publicKeyPem = publicKeyPem
self.privateKeyPem = privateKeyPem
self.createdAtMs = createdAtMs
}
}
public struct OpenClawSQLiteDeviceAuthTokenRow: Sendable {
public let deviceId: String
public let role: String
public let token: String
public let scopesJSON: String
public let updatedAtMs: Int
public init(deviceId: String, role: String, token: String, scopesJSON: String, updatedAtMs: Int) {
self.deviceId = deviceId
self.role = role
self.token = token
self.scopesJSON = scopesJSON
self.updatedAtMs = updatedAtMs
}
}
public struct OpenClawSQLitePortGuardianRecord: Sendable {
public let port: Int
public let pid: Int32
public let command: String
public let mode: String
public let timestamp: TimeInterval
public init(port: Int, pid: Int32, command: String, mode: String, timestamp: TimeInterval) {
self.port = port
self.pid = pid
self.command = command
self.mode = mode
self.timestamp = timestamp
}
}
public enum OpenClawSQLiteStateStore {
private static let logger = Logger(subsystem: "ai.openclaw", category: "sqlite-state")
private static let secureStateDirPermissions = 0o700
public static func databaseURL() -> URL {
DeviceIdentityPaths.stateDirURL()
.appendingPathComponent("state", isDirectory: true)
.appendingPathComponent("openclaw.sqlite")
}
public static func tableLocationForDisplay(table: String, key: String) -> String {
"\(self.databaseURL().path)#table/\(table)/\(key)"
}
public static func readDeviceIdentity(key: String = "default") -> OpenClawSQLiteDeviceIdentityRow? {
do {
return try self.readDeviceIdentityChecked(key: key)
} catch {
self.logger.warning("SQLite device identity read failed: \(error.localizedDescription, privacy: .public)")
return nil
}
}
static func readDeviceIdentityChecked(key: String = "default") throws -> OpenClawSQLiteDeviceIdentityRow? {
let db = try self.openStateDatabase()
defer { sqlite3_close(db) }
let sql = """
SELECT device_id, public_key_pem, private_key_pem, created_at_ms
FROM device_identities
WHERE identity_key = ?
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: key)
let status = sqlite3_step(statement)
if status == SQLITE_ROW,
let deviceId = self.columnString(statement, index: 0),
let publicKeyPem = self.columnString(statement, index: 1),
let privateKeyPem = self.columnString(statement, index: 2)
{
return OpenClawSQLiteDeviceIdentityRow(
deviceId: deviceId,
publicKeyPem: publicKeyPem,
privateKeyPem: privateKeyPem,
createdAtMs: Int(sqlite3_column_int64(statement, 3)))
}
if status == SQLITE_DONE { return nil }
throw self.sqliteError(db, context: "SQLite device identity read failed")
}
public static func writeDeviceIdentity(
key: String = "default",
identity: OpenClawSQLiteDeviceIdentityRow,
updatedAtMs: Int = Int(Date().timeIntervalSince1970 * 1000)) throws
{
try self.withWriteTransaction { db in
let sql = """
INSERT INTO device_identities (
identity_key, device_id, public_key_pem, private_key_pem, created_at_ms, updated_at_ms
)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(identity_key) DO UPDATE SET
device_id = excluded.device_id,
public_key_pem = excluded.public_key_pem,
private_key_pem = excluded.private_key_pem,
created_at_ms = excluded.created_at_ms,
updated_at_ms = excluded.updated_at_ms
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: key)
self.bindText(statement, index: 2, value: identity.deviceId)
self.bindText(statement, index: 3, value: identity.publicKeyPem)
self.bindText(statement, index: 4, value: identity.privateKeyPem)
sqlite3_bind_int64(statement, 5, Int64(identity.createdAtMs))
sqlite3_bind_int64(statement, 6, Int64(updatedAtMs))
guard sqlite3_step(statement) == SQLITE_DONE else {
throw self.sqliteError(db, context: "SQLite device identity write failed")
}
}
}
public static func readDeviceAuthToken(deviceId: String, role: String) -> OpenClawSQLiteDeviceAuthTokenRow? {
do {
let db = try self.openStateDatabase()
defer { sqlite3_close(db) }
let sql = """
SELECT device_id, role, token, scopes_json, updated_at_ms
FROM device_auth_tokens
WHERE device_id = ? AND role = ?
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: deviceId)
self.bindText(statement, index: 2, value: role)
let status = sqlite3_step(statement)
if status == SQLITE_ROW,
let rowDeviceId = self.columnString(statement, index: 0),
let rowRole = self.columnString(statement, index: 1),
let token = self.columnString(statement, index: 2),
let scopesJSON = self.columnString(statement, index: 3)
{
return OpenClawSQLiteDeviceAuthTokenRow(
deviceId: rowDeviceId,
role: rowRole,
token: token,
scopesJSON: scopesJSON,
updatedAtMs: Int(sqlite3_column_int64(statement, 4)))
}
if status == SQLITE_DONE { return nil }
throw self.sqliteError(db, context: "SQLite device auth read failed")
} catch {
self.logger.warning("SQLite device auth read failed: \(error.localizedDescription, privacy: .public)")
return nil
}
}
public static func readLatestDeviceAuthDeviceId() -> String? {
do {
let db = try self.openStateDatabase()
defer { sqlite3_close(db) }
let sql = """
SELECT device_id
FROM device_auth_tokens
ORDER BY updated_at_ms DESC, device_id ASC
LIMIT 1
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
let status = sqlite3_step(statement)
if status == SQLITE_ROW { return self.columnString(statement, index: 0) }
if status == SQLITE_DONE { return nil }
throw self.sqliteError(db, context: "SQLite device auth latest-device read failed")
} catch {
self.logger.warning(
"SQLite device auth latest-device read failed: \(error.localizedDescription, privacy: .public)")
return nil
}
}
public static func upsertDeviceAuthToken(_ row: OpenClawSQLiteDeviceAuthTokenRow) throws {
try self.withWriteTransaction { db in
let sql = """
INSERT INTO device_auth_tokens (device_id, role, token, scopes_json, updated_at_ms)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(device_id, role) DO UPDATE SET
token = excluded.token,
scopes_json = excluded.scopes_json,
updated_at_ms = excluded.updated_at_ms
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: row.deviceId)
self.bindText(statement, index: 2, value: row.role)
self.bindText(statement, index: 3, value: row.token)
self.bindText(statement, index: 4, value: row.scopesJSON)
sqlite3_bind_int64(statement, 5, Int64(row.updatedAtMs))
guard sqlite3_step(statement) == SQLITE_DONE else {
throw self.sqliteError(db, context: "SQLite device auth write failed")
}
}
}
public static func deleteDeviceAuthToken(deviceId: String, role: String) throws {
try self.withWriteTransaction { db in
let sql = "DELETE FROM device_auth_tokens WHERE device_id = ? AND role = ?"
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: deviceId)
self.bindText(statement, index: 2, value: role)
guard sqlite3_step(statement) == SQLITE_DONE else {
throw self.sqliteError(db, context: "SQLite device auth delete failed")
}
}
}
public static func deleteAllDeviceAuthTokens() throws {
try self.withWriteTransaction { db in
try self.exec(db, "DELETE FROM device_auth_tokens")
}
}
public static func execApprovalsLocationForDisplay(configKey: String = "current") -> String {
self.tableLocationForDisplay(table: "exec_approvals_config", key: configKey)
}
public static func readExecApprovalsRaw(configKey: String = "current") -> String? {
do {
let db = try self.openStateDatabase()
defer { sqlite3_close(db) }
let sql = "SELECT raw_json FROM exec_approvals_config WHERE config_key = ?"
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: configKey)
let status = sqlite3_step(statement)
if status == SQLITE_ROW { return self.columnString(statement, index: 0) }
if status == SQLITE_DONE { return nil }
throw self.sqliteError(db, context: "SQLite exec approvals read failed")
} catch {
self.logger.warning("SQLite exec approvals read failed: \(error.localizedDescription, privacy: .public)")
return nil
}
}
public static func writeExecApprovalsConfig(
configKey: String = "current",
rawJSON: String,
socketPath: String?,
hasSocketToken: Bool,
defaultSecurity: String?,
defaultAsk: String?,
defaultAskFallback: String?,
autoAllowSkills: Bool?,
agentCount: Int,
allowlistCount: Int,
updatedAtMs: Int = Int(Date().timeIntervalSince1970 * 1000)) throws
{
try self.withWriteTransaction { db in
let sql = """
INSERT INTO exec_approvals_config (
config_key, raw_json, socket_path, has_socket_token, default_security,
default_ask, default_ask_fallback, auto_allow_skills,
agent_count, allowlist_count, updated_at_ms
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(config_key) DO UPDATE SET
raw_json = excluded.raw_json,
socket_path = excluded.socket_path,
has_socket_token = excluded.has_socket_token,
default_security = excluded.default_security,
default_ask = excluded.default_ask,
default_ask_fallback = excluded.default_ask_fallback,
auto_allow_skills = excluded.auto_allow_skills,
agent_count = excluded.agent_count,
allowlist_count = excluded.allowlist_count,
updated_at_ms = excluded.updated_at_ms
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: configKey)
self.bindText(statement, index: 2, value: rawJSON)
self.bindNullableText(statement, index: 3, value: socketPath)
sqlite3_bind_int(statement, 4, hasSocketToken ? 1 : 0)
self.bindNullableText(statement, index: 5, value: defaultSecurity)
self.bindNullableText(statement, index: 6, value: defaultAsk)
self.bindNullableText(statement, index: 7, value: defaultAskFallback)
if let autoAllowSkills {
sqlite3_bind_int(statement, 8, autoAllowSkills ? 1 : 0)
} else {
sqlite3_bind_null(statement, 8)
}
sqlite3_bind_int(statement, 9, Int32(agentCount))
sqlite3_bind_int(statement, 10, Int32(allowlistCount))
sqlite3_bind_int64(statement, 11, Int64(updatedAtMs))
guard sqlite3_step(statement) == SQLITE_DONE else {
throw self.sqliteError(db, context: "SQLite exec approvals write failed")
}
}
}
public static func readConfigHealthState() -> [String: Any] {
do {
let db = try self.openStateDatabase()
defer { sqlite3_close(db) }
let sql = """
SELECT config_path, last_known_good_json, last_promoted_good_json, last_observed_suspicious_signature
FROM config_health_entries
ORDER BY config_path ASC
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
var entries: [String: Any] = [:]
while true {
let status = sqlite3_step(statement)
if status == SQLITE_DONE { break }
guard status == SQLITE_ROW, let configPath = self.columnString(statement, index: 0) else {
throw self.sqliteError(db, context: "SQLite config health read failed")
}
var entry: [String: Any] = [:]
if let lastKnownGood = self.columnJSONDictionary(statement, index: 1) {
entry["lastKnownGood"] = lastKnownGood
}
if let lastPromotedGood = self.columnJSONDictionary(statement, index: 2) {
entry["lastPromotedGood"] = lastPromotedGood
}
if let signature = self.columnString(statement, index: 3) {
entry["lastObservedSuspiciousSignature"] = signature
}
entries[configPath] = entry
}
return entries.isEmpty ? [:] : ["entries": entries]
} catch {
self.logger.warning("SQLite config health read failed: \(error.localizedDescription, privacy: .public)")
return [:]
}
}
public static func writeConfigHealthState(_ state: [String: Any]) throws {
let entries = state["entries"] as? [String: Any] ?? [:]
let updatedAtMs = Int(Date().timeIntervalSince1970 * 1000)
try self.withWriteTransaction { db in
try self.exec(db, "DELETE FROM config_health_entries")
for (configPath, rawEntry) in entries {
guard let entry = rawEntry as? [String: Any] else { continue }
try self.insertConfigHealthEntry(
db,
configPath: configPath,
entry: entry,
updatedAtMs: updatedAtMs)
}
}
}
public static func readPortGuardianRecords() -> [OpenClawSQLitePortGuardianRecord] {
do {
let db = try self.openStateDatabase()
defer { sqlite3_close(db) }
let sql = """
SELECT port, pid, command, mode, timestamp
FROM macos_port_guardian_records
ORDER BY timestamp ASC, pid ASC
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
var rows: [OpenClawSQLitePortGuardianRecord] = []
while true {
let status = sqlite3_step(statement)
if status == SQLITE_DONE { break }
guard status == SQLITE_ROW else {
throw self.sqliteError(db, context: "SQLite port guardian read failed")
}
guard let command = self.columnString(statement, index: 2),
let mode = self.columnString(statement, index: 3)
else { continue }
rows.append(OpenClawSQLitePortGuardianRecord(
port: Int(sqlite3_column_int(statement, 0)),
pid: sqlite3_column_int(statement, 1),
command: command,
mode: mode,
timestamp: sqlite3_column_double(statement, 4)))
}
return rows
} catch {
self.logger.warning("SQLite port guardian read failed: \(error.localizedDescription, privacy: .public)")
return []
}
}
public static func replacePortGuardianRecords(_ records: [OpenClawSQLitePortGuardianRecord]) throws {
try self.withWriteTransaction { db in
try self.exec(db, "DELETE FROM macos_port_guardian_records")
for record in records {
try self.insertPortGuardianRecord(db, record)
}
}
}
private static func openStateDatabase() throws -> OpaquePointer? {
self.ensureSecureStateDirectory()
let url = self.databaseURL()
try FileManager().createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try? FileManager().setAttributes(
[.posixPermissions: self.secureStateDirPermissions],
ofItemAtPath: url.deletingLastPathComponent().path)
var db: OpaquePointer?
guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) == SQLITE_OK
else {
defer { sqlite3_close(db) }
throw self.sqliteError(db, context: "SQLite state open failed")
}
try self.configureStateDatabase(db)
self.hardenStateDatabaseFiles()
return db
}
private static func configureStateDatabase(_ db: OpaquePointer?) throws {
try self.exec(db, "PRAGMA journal_mode = WAL")
try self.exec(db, "PRAGMA synchronous = NORMAL")
try self.exec(db, "PRAGMA busy_timeout = 30000")
try self.exec(db, "PRAGMA foreign_keys = ON")
try self.exec(
db,
"""
CREATE TABLE IF NOT EXISTS device_identities (
identity_key TEXT NOT NULL PRIMARY KEY,
device_id TEXT NOT NULL,
public_key_pem TEXT NOT NULL,
private_key_pem TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
)
""")
try self.exec(
db,
"CREATE INDEX IF NOT EXISTS idx_device_identities_device ON device_identities(device_id, updated_at_ms DESC)")
try self.exec(
db,
"""
CREATE TABLE IF NOT EXISTS device_auth_tokens (
device_id TEXT NOT NULL,
role TEXT NOT NULL,
token TEXT NOT NULL,
scopes_json TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (device_id, role)
)
""")
try self.exec(
db,
"CREATE INDEX IF NOT EXISTS idx_device_auth_tokens_updated ON device_auth_tokens(updated_at_ms DESC, device_id, role)")
try self.exec(
db,
"""
CREATE TABLE IF NOT EXISTS exec_approvals_config (
config_key TEXT NOT NULL PRIMARY KEY,
raw_json TEXT NOT NULL,
socket_path TEXT,
has_socket_token INTEGER NOT NULL,
default_security TEXT,
default_ask TEXT,
default_ask_fallback TEXT,
auto_allow_skills INTEGER,
agent_count INTEGER NOT NULL,
allowlist_count INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
)
""")
try self.exec(
db,
"""
CREATE TABLE IF NOT EXISTS macos_port_guardian_records (
pid INTEGER NOT NULL PRIMARY KEY,
port INTEGER NOT NULL,
command TEXT NOT NULL,
mode TEXT NOT NULL,
timestamp REAL NOT NULL
)
""")
try self.exec(
db,
"CREATE INDEX IF NOT EXISTS idx_macos_port_guardian_records_port ON macos_port_guardian_records(port, timestamp DESC)")
try self.exec(
db,
"""
CREATE TABLE IF NOT EXISTS config_health_entries (
config_path TEXT NOT NULL PRIMARY KEY,
last_known_good_json TEXT,
last_promoted_good_json TEXT,
last_observed_suspicious_signature TEXT,
updated_at_ms INTEGER NOT NULL
)
""")
}
private static func prepare(_ db: OpaquePointer?, _ sql: String, _ statement: inout OpaquePointer?) throws {
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
throw self.sqliteError(db, context: "SQLite state prepare failed")
}
}
private static func insertPortGuardianRecord(
_ db: OpaquePointer?,
_ record: OpenClawSQLitePortGuardianRecord) throws
{
let sql = """
INSERT INTO macos_port_guardian_records (pid, port, command, mode, timestamp)
VALUES (?, ?, ?, ?, ?)
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
sqlite3_bind_int(statement, 1, record.pid)
sqlite3_bind_int(statement, 2, Int32(record.port))
self.bindText(statement, index: 3, value: record.command)
self.bindText(statement, index: 4, value: record.mode)
sqlite3_bind_double(statement, 5, record.timestamp)
guard sqlite3_step(statement) == SQLITE_DONE else {
throw self.sqliteError(db, context: "SQLite port guardian write failed")
}
}
private static func exec(_ db: OpaquePointer?, _ sql: String) throws {
var errorMessage: UnsafeMutablePointer<CChar>?
if sqlite3_exec(db, sql, nil, nil, &errorMessage) != SQLITE_OK {
let message = errorMessage.map { String(cString: $0) }
sqlite3_free(errorMessage)
throw NSError(
domain: "OpenClawSQLiteStateStore",
code: Int(sqlite3_errcode(db)),
userInfo: [
NSLocalizedDescriptionKey: message ?? sqlite3ErrorMessage(db),
])
}
}
private static func bindText(_ statement: OpaquePointer?, index: Int32, value: String) {
let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
sqlite3_bind_text(statement, index, value, -1, transient)
}
private static func bindNullableText(_ statement: OpaquePointer?, index: Int32, value: String?) {
guard let value else {
sqlite3_bind_null(statement, index)
return
}
self.bindText(statement, index: index, value: value)
}
private static func columnString(_ statement: OpaquePointer?, index: Int32) -> String? {
guard let raw = sqlite3_column_text(statement, index) else { return nil }
return String(cString: UnsafeRawPointer(raw).assumingMemoryBound(to: CChar.self))
}
private static func columnJSONDictionary(_ statement: OpaquePointer?, index: Int32) -> [String: Any]? {
guard let raw = self.columnString(statement, index: index),
let data = raw.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return nil }
return object
}
private static func jsonString(_ value: Any?) -> String? {
guard let value, !(value is NSNull), JSONSerialization.isValidJSONObject(value),
let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
else { return nil }
return String(data: data, encoding: .utf8)
}
private static func insertConfigHealthEntry(
_ db: OpaquePointer?,
configPath: String,
entry: [String: Any],
updatedAtMs: Int) throws
{
let sql = """
INSERT INTO config_health_entries (
config_path, last_known_good_json, last_promoted_good_json,
last_observed_suspicious_signature, updated_at_ms
)
VALUES (?, ?, ?, ?, ?)
"""
var statement: OpaquePointer?
try self.prepare(db, sql, &statement)
defer { sqlite3_finalize(statement) }
self.bindText(statement, index: 1, value: configPath)
self.bindNullableText(statement, index: 2, value: self.jsonString(entry["lastKnownGood"]))
self.bindNullableText(statement, index: 3, value: self.jsonString(entry["lastPromotedGood"]))
self.bindNullableText(
statement,
index: 4,
value: entry["lastObservedSuspiciousSignature"] as? String)
sqlite3_bind_int64(statement, 5, Int64(updatedAtMs))
guard sqlite3_step(statement) == SQLITE_DONE else {
throw self.sqliteError(db, context: "SQLite config health write failed")
}
}
private static func withWriteTransaction(_ body: (OpaquePointer?) throws -> Void) throws {
let db = try self.openStateDatabase()
defer { sqlite3_close(db) }
try self.exec(db, "BEGIN IMMEDIATE")
do {
try body(db)
try self.exec(db, "COMMIT")
} catch {
try? self.exec(db, "ROLLBACK")
throw error
}
self.hardenStateDatabaseFiles()
}
private static func sqliteError(_ db: OpaquePointer?, context: String) -> NSError {
NSError(
domain: "OpenClawSQLiteStateStore",
code: Int(sqlite3_errcode(db)),
userInfo: [
NSLocalizedDescriptionKey: "\(context): \(self.sqlite3ErrorMessage(db))",
])
}
private static func sqlite3ErrorMessage(_ db: OpaquePointer?) -> String {
guard let message = sqlite3_errmsg(db) else {
return "unknown SQLite error"
}
return String(cString: message)
}
private static func hardenStateDatabaseFiles() {
let path = self.databaseURL().path
for suffix in ["", "-wal", "-shm"] {
let candidate = "\(path)\(suffix)"
if FileManager().fileExists(atPath: candidate) {
try? FileManager().setAttributes([.posixPermissions: 0o600], ofItemAtPath: candidate)
}
}
}
private static func ensureSecureStateDirectory() {
let url = DeviceIdentityPaths.stateDirURL()
do {
try FileManager().createDirectory(at: url, withIntermediateDirectories: true)
try FileManager().setAttributes(
[.posixPermissions: self.secureStateDirPermissions],
ofItemAtPath: url.path)
} catch {
self.logger.warning(
"SQLite state dir permission hardening failed: \(error.localizedDescription, privacy: .public)")
}
}
}