File size: 18,051 Bytes
b8370f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
import Foundation
import Testing
@testable import OpenClawNativeState
#if canImport(Darwin)
import Darwin
import Darwin.membership
#endif

struct OpenClawNativeStateSQLiteTests {
    @Test
    func `version zero composes exact canonical tables`() throws {
        try self.withDatabase { database in
            try database.withImmediateTransaction {
                try database.ensureCanonicalTable(.macosPortGuardianRecords)
                try database.execute("""
                INSERT INTO macos_port_guardian_records (pid, port, command, mode, timestamp)
                VALUES (4242, 18789, '/usr/bin/ssh', 'remote', 42.5)
                """)
                try database.ensureCanonicalTable(.deviceIdentities)
            }

            #expect(try database.scalarInt64("PRAGMA user_version") == 0)
            #expect(try database.scalarInt64(
                "SELECT COUNT(*) FROM macos_port_guardian_records WHERE pid = 4242") == 1)
            #expect(try database.scalarInt64(
                "SELECT COUNT(*) FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'") == 4)
        }
    }

    @Test
    func `version zero rejects incomplete known sibling schema`() throws {
        try self.withDatabase { database in
            try database.execute("""
            CREATE TABLE 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
            ) STRICT
            """)

            #expect(throws: OpenClawNativeStateError.self) {
                try database.ensureCanonicalTable(.deviceIdentities)
            }
        }
    }

    @Test
    func `versioned database never synthesizes a missing canonical table`() throws {
        try self.withDatabase { database in
            try database.execute("""
            CREATE TABLE schema_meta (
              meta_key TEXT NOT NULL PRIMARY KEY,
              role TEXT NOT NULL,
              schema_version INTEGER NOT NULL
            ) STRICT;
            INSERT INTO schema_meta (meta_key, role, schema_version)
            VALUES ('primary', 'global', 3);
            PRAGMA user_version = 3;
            """)

            #expect(throws: OpenClawNativeStateError.self) {
                try database.ensureCanonicalTable(.deviceIdentities)
            }
            #expect(try database.schemaObjectExists(type: "table", name: "device_identities") == false)
        }
    }

    @Test
    func `immediate transaction rolls back all writes`() throws {
        try self.withDatabase { database in
            try database.withImmediateTransaction {
                try database.ensureCanonicalTable(.macosPortGuardianRecords)
            }

            #expect(throws: TestError.self) {
                try database.withImmediateTransaction {
                    try database.execute("""
                    INSERT INTO macos_port_guardian_records (pid, port, command, mode, timestamp)
                    VALUES (4242, 18789, '/usr/bin/ssh', 'remote', 42.5)
                    """)
                    throw TestError.expected
                }
            }
            #expect(try database.scalarInt64("SELECT COUNT(*) FROM macos_port_guardian_records") == 0)
        }
    }

    @Test
    func `concurrent version zero stores compose different tables`() async throws {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString, isDirectory: true)
        defer { try? FileManager.default.removeItem(at: directory) }
        let databaseURL = directory.appendingPathComponent("openclaw.sqlite", isDirectory: false)

        try await withThrowingTaskGroup(of: Void.self) { group in
            for table in [
                OpenClawNativeStateCanonicalTable.deviceIdentities,
                .macosPortGuardianRecords,
            ] {
                group.addTask {
                    let database = try OpenClawNativeStateSQLite(databaseURL: databaseURL)
                    try database.withImmediateTransaction {
                        try database.ensureCanonicalTable(table)
                    }
                }
            }
            try await group.waitForAll()
        }

        let database = try OpenClawNativeStateSQLite(databaseURL: databaseURL)
        #expect(try database.scalarInt64(
            "SELECT COUNT(*) FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'") == 4)
    }

    @Test(arguments: [
        NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError),
        NSError(domain: NSPOSIXErrorDomain, code: Int(POSIXErrorCode.ENOENT.rawValue)),
    ])
    func `vanished transient sidecar does not fail committed write cleanup`(missing: NSError) throws {
        let databaseURL = URL(fileURLWithPath: "/tmp/openclaw-native-state.sqlite")
        var attemptedPaths: [String] = []

        try OpenClawNativeStateSQLite.secureDatabaseFiles(
            databaseURL,
            fileExists: { _ in true },
            setAttributes: { _, path in
                attemptedPaths.append(path)
                if path.hasSuffix("-wal") {
                    throw missing
                }
            })

        #expect(attemptedPaths == [
            databaseURL.path,
            databaseURL.path + "-wal",
            databaseURL.path + "-shm",
            databaseURL.path + "-journal",
        ])
    }

    #if canImport(Darwin)
    @Test
    func `new private state removes inherited grants without changing ancestors`() throws {
        let ancestor = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: ancestor, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: ancestor) }
        try self.installACL(at: ancestor)
        let ancestorACL = try self.aclText(at: ancestor)
        let sibling = ancestor.appendingPathComponent("unrelated")
        try Data("unchanged".utf8).write(to: sibling)
        let siblingACL = try self.aclText(at: sibling)

        let parent = ancestor.appendingPathComponent("state")
        let databaseURL = parent.appendingPathComponent("openclaw.sqlite")
        let database = try OpenClawNativeStateSQLite(databaseURL: databaseURL)
        try database.execute("PRAGMA journal_mode=WAL")
        try database.withImmediateTransaction {
            try database.ensureCanonicalTable(.deviceIdentities)
        }

        try self.expectPrivateMetadata(at: parent, mode: 0o700)
        for suffix in ["", "-wal", "-shm"] {
            try self.expectPrivateMetadata(at: URL(fileURLWithPath: databaseURL.path + suffix), mode: 0o600)
        }
        #expect(try self.aclText(at: ancestor) == ancestorACL)
        #expect(try self.aclText(at: sibling) == siblingACL)
    }

    @Test(arguments: [false, true], ["WAL", "PERSIST"])
    func `existing state removes explicit grants and preserves contents`(
        createIfMissing: Bool,
        journalMode: String) throws
    {
        let parent = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        defer { try? FileManager.default.removeItem(at: parent) }
        let databaseURL = parent.appendingPathComponent("openclaw.sqlite")
        do {
            let database = try OpenClawNativeStateSQLite(databaseURL: databaseURL)
            try database.withImmediateTransaction {
                try database.ensureCanonicalTable(.deviceIdentities)
                try database.ensureCanonicalTable(.deviceAuthTokens)
                try database.execute("""
                INSERT INTO device_identities VALUES ('profile', 'device', 'public', 'private', 10, 20);
                INSERT INTO device_auth_tokens VALUES ('device', 'operator', 'token', '["read"]', 30);
                """)
            }
        }
        try self.installACL(at: parent)
        try self.installACL(at: databaseURL)
        try FileManager.default.setAttributes([.posixPermissions: 0o750], ofItemAtPath: parent.path)
        let parentACL = try self.aclText(at: parent)

        let database = try OpenClawNativeStateSQLite(databaseURL: databaseURL, createIfMissing: createIfMissing)
        try self.expectPrivateMetadata(at: databaseURL, mode: 0o600)
        try database.execute("PRAGMA journal_mode=\(journalMode)")
        try database.withImmediateTransaction {
            try database.execute("UPDATE device_identities SET updated_at_ms = 20")
        }
        let suffixes = journalMode == "WAL" ? ["", "-wal", "-shm"] : ["", "-journal"]
        for suffix in suffixes {
            try self.installACL(at: URL(fileURLWithPath: databaseURL.path + suffix))
        }
        try database.withImmediateTransaction {
            try database.execute("UPDATE device_auth_tokens SET updated_at_ms = 30")
        }
        for suffix in suffixes {
            try self.expectPrivateMetadata(at: URL(fileURLWithPath: databaseURL.path + suffix), mode: 0o600)
        }
        #expect(try database.scalarInt64("""
        SELECT COUNT(*) FROM device_identities
        WHERE identity_key = 'profile' AND device_id = 'device' AND public_key_pem = 'public'
          AND private_key_pem = 'private' AND created_at_ms = 10 AND updated_at_ms = 20
        """) == 1)
        #expect(try database.scalarInt64("""
        SELECT COUNT(*) FROM device_auth_tokens
        WHERE device_id = 'device' AND role = 'operator' AND token = 'token'
          AND scopes_json = '["read"]' AND updated_at_ms = 30
        """) == 1)
        #expect(try database.scalarInt64("PRAGMA user_version") == 0)
        if createIfMissing {
            try self.expectPrivateMetadata(at: parent, mode: 0o700)
        } else {
            #expect(try self.aclText(at: parent) == parentACL)
            #expect(try FileManager.default.attributesOfItem(atPath: parent.path)[.posixPermissions] as? Int == 0o750)
        }
    }

    @Test(arguments: [GrantPrincipal.owner, .group, .everyone])
    func `mixed ACL preserves ordering and owner access`(principal: GrantPrincipal) throws {
        try self.withDatabaseURL { database, databaseURL in
            try database.withImmediateTransaction {
                try database.ensureCanonicalTable(.deviceIdentities)
            }
            try self.installACL(
                at: databaseURL,
                tags: [ACL_EXTENDED_ALLOW, ACL_EXTENDED_DENY],
                principal: principal)
            let originalACL = try self.aclText(at: databaseURL)
            let originalBytes = try Data(contentsOf: databaseURL)

            try OpenClawNativeStateSQLite.secureDatabaseFiles(databaseURL)

            #expect(try self.aclText(at: databaseURL) == originalACL)
            #expect(try Data(contentsOf: databaseURL) == originalBytes)
            try database.withImmediateTransaction {
                try database.execute("""
                INSERT INTO device_identities VALUES ('profile', 'device', 'public', 'private', 10, 20)
                """)
            }
            #expect(try database.scalarInt64("SELECT COUNT(*) FROM device_identities") == 1)
            #expect(try self.aclText(at: databaseURL) == originalACL)
        }
    }

    @Test(arguments: [ACL_FLAG_NO_INHERIT, ACL_FLAG_DEFER_INHERIT])
    func `flagged ACL remains unchanged`(flag: acl_flag_t) throws {
        try self.withDatabaseURL { _, databaseURL in
            try self.installACL(at: databaseURL, flag: flag)
            let originalACL = try self.aclText(at: databaseURL)

            try OpenClawNativeStateSQLite.secureDatabaseFiles(databaseURL)

            #expect(try self.aclText(at: databaseURL) == originalACL)
            guard let acl = acl_get_file(databaseURL.path, ACL_TYPE_EXTENDED) else { throw self.aclError() }
            defer { acl_free(UnsafeMutableRawPointer(acl)) }
            var flags: acl_flagset_t?
            guard acl_get_flagset_np(UnsafeMutableRawPointer(acl), &flags) == 0 else { throw self.aclError() }
            #expect(acl_get_flag_np(flags, flag) == 1)
        }
    }

    enum GrantPrincipal: UInt8, Sendable {
        case owner = 0x0A
        case everyone = 0x0C
        case group = 0x10
    }

    private func installACL(
        at url: URL,
        tags: [acl_tag_t] = [ACL_EXTENDED_ALLOW, ACL_EXTENDED_ALLOW],
        principal: GrantPrincipal = .everyone,
        flag: acl_flag_t? = nil) throws
    {
        var acl = acl_init(Int32(tags.count))
        guard acl != nil else { throw self.aclError() }
        defer { acl_free(UnsafeMutableRawPointer(acl!)) }
        if let flag {
            var flags: acl_flagset_t?
            guard acl_get_flagset_np(UnsafeMutableRawPointer(acl!), &flags) == 0,
                  acl_add_flag_np(flags, flag) == 0
            else { throw self.aclError() }
        }
        for tag in tags {
            // Darwin's well-known principals keep the fixture independent of local accounts.
            var qualifier: uuid_t = (
                0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF, 0xAB, 0xCD,
                0xEF, 0xAB, 0xCD, 0xEF, 0, 0, 0,
                tag == ACL_EXTENDED_ALLOW ? principal.rawValue : GrantPrincipal.everyone.rawValue)
            if tag == ACL_EXTENDED_ALLOW, principal == .owner || principal == .group {
                let result = principal == .owner
                    ? mbr_uid_to_uuid(geteuid(), &qualifier)
                    : mbr_gid_to_uuid(getegid(), &qualifier)
                guard result == 0 else { throw NSError(domain: NSPOSIXErrorDomain, code: Int(result)) }
            }
            var entry: acl_entry_t?
            guard acl_create_entry(&acl, &entry) == 0,
                  acl_set_tag_type(entry, tag) == 0,
                  acl_set_qualifier(entry, &qualifier) == 0
            else { throw self.aclError() }
            var permissions: acl_permset_t?
            guard acl_get_permset(entry, &permissions) == 0 else { throw self.aclError() }
            for permission in tag == ACL_EXTENDED_ALLOW
                ? [ACL_READ_DATA, ACL_WRITE_DATA, ACL_APPEND_DATA, ACL_EXECUTE, ACL_READ_ATTRIBUTES,
                   ACL_WRITE_ATTRIBUTES, ACL_READ_EXTATTRIBUTES, ACL_WRITE_EXTATTRIBUTES, ACL_READ_SECURITY,
                   ACL_WRITE_SECURITY]
                : [ACL_READ_DATA]
            {
                guard acl_add_perm(permissions, permission) == 0 else { throw self.aclError() }
            }
            var flags: acl_flagset_t?
            guard acl_get_flagset_np(UnsafeMutableRawPointer(entry), &flags) == 0,
                  acl_add_flag_np(flags, ACL_ENTRY_FILE_INHERIT) == 0,
                  acl_add_flag_np(flags, ACL_ENTRY_DIRECTORY_INHERIT) == 0
            else { throw self.aclError() }
        }
        guard acl_set_file(url.path, ACL_TYPE_EXTENDED, acl) == 0 else { throw self.aclError() }
    }

    private func aclText(at url: URL) throws -> String {
        guard let acl = acl_get_file(url.path, ACL_TYPE_EXTENDED) else { throw self.aclError() }
        defer { acl_free(UnsafeMutableRawPointer(acl)) }
        guard let text = acl_to_text(acl, nil) else { throw self.aclError() }
        defer { acl_free(text) }
        return String(cString: text)
    }

    private func expectPrivateMetadata(at url: URL, mode: Int) throws {
        #expect(try FileManager.default.attributesOfItem(atPath: url.path)[.posixPermissions] as? Int == mode)
        guard let acl = acl_get_file(url.path, ACL_TYPE_EXTENDED) else {
            let code = errno
            _ = try FileManager.default.attributesOfItem(atPath: url.path)
            #expect(code == ENOENT)
            return
        }
        defer { acl_free(UnsafeMutableRawPointer(acl)) }
        var index: Int32 = 0
        var entry: acl_entry_t?
        while acl_get_entry(acl, index, &entry) == 0 {
            var tag = ACL_UNDEFINED_TAG
            guard acl_get_tag_type(entry, &tag) == 0 else { throw self.aclError() }
            #expect(tag != ACL_EXTENDED_ALLOW)
            index += 1
        }
        guard errno == EINVAL else { throw self.aclError() }
        #expect(index == 0)
    }

    private func aclError() -> NSError {
        NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
    }
    #endif

    @Test
    func `missing database or nonmissing sidecar failure remains fatal`() {
        let databaseURL = URL(fileURLWithPath: "/tmp/openclaw-native-state.sqlite")
        let missing = NSError(domain: NSPOSIXErrorDomain, code: Int(POSIXErrorCode.ENOENT.rawValue))
        let denied = NSError(domain: NSPOSIXErrorDomain, code: Int(POSIXErrorCode.EACCES.rawValue))

        #expect(throws: NSError.self) {
            try OpenClawNativeStateSQLite.secureDatabaseFiles(
                databaseURL,
                fileExists: { _ in true },
                setAttributes: { _, path in
                    if path == databaseURL.path { throw missing }
                })
        }
        #expect(throws: NSError.self) {
            try OpenClawNativeStateSQLite.secureDatabaseFiles(
                databaseURL,
                fileExists: { _ in true },
                setAttributes: { _, path in
                    if path.hasSuffix("-wal") { throw denied }
                })
        }
    }

    private enum TestError: Error {
        case expected
    }

    private func withDatabase(
        body: (OpenClawNativeStateSQLite) throws -> Void) throws
    {
        try self.withDatabaseURL { database, _ in try body(database) }
    }

    private func withDatabaseURL(
        body: (OpenClawNativeStateSQLite, URL) throws -> Void) throws
    {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString, isDirectory: true)
        defer { try? FileManager.default.removeItem(at: directory) }
        let databaseURL = directory.appendingPathComponent("openclaw.sqlite", isDirectory: false)
        let database = try OpenClawNativeStateSQLite(databaseURL: databaseURL)
        try body(database, databaseURL)
    }
}