Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
216 lines
8.0 KiB
Swift
216 lines
8.0 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized)
|
|
struct ExecApprovalsStoreRefactorTests {
|
|
private var realTemporaryDirectory: URL {
|
|
let path = FileManager().temporaryDirectory.path
|
|
if path.hasPrefix("/var/") {
|
|
return URL(fileURLWithPath: "/private\(path)", isDirectory: true)
|
|
}
|
|
return FileManager().temporaryDirectory.resolvingSymlinksInPath()
|
|
}
|
|
|
|
private func withLockedEnv(
|
|
_ values: [String: String?],
|
|
_ body: () async throws -> Void) async throws
|
|
{
|
|
func restoreEnv(_ values: [String: String?]) {
|
|
for (key, value) in values {
|
|
if let value {
|
|
setenv(key, value, 1)
|
|
} else {
|
|
unsetenv(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
await TestIsolationLock.shared.acquire()
|
|
var previousEnv: [String: String?] = [:]
|
|
for (key, value) in values {
|
|
previousEnv[key] = getenv(key).map { String(cString: $0) }
|
|
if let value {
|
|
setenv(key, value, 1)
|
|
} else {
|
|
unsetenv(key)
|
|
}
|
|
}
|
|
|
|
do {
|
|
try await body()
|
|
restoreEnv(previousEnv)
|
|
await TestIsolationLock.shared.release()
|
|
} catch {
|
|
restoreEnv(previousEnv)
|
|
await TestIsolationLock.shared.release()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private func withTempStateDir(
|
|
_ body: @escaping @Sendable (URL) async throws -> Void) async throws
|
|
{
|
|
let root = self.realTemporaryDirectory
|
|
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
|
|
let home = root.appendingPathComponent("home", isDirectory: true)
|
|
let stateDir = root.appendingPathComponent("state", isDirectory: true)
|
|
defer { try? FileManager().removeItem(at: root) }
|
|
try Self.seedCurrentApprovalsFile(in: stateDir)
|
|
|
|
try await self.withLockedEnv([
|
|
"OPENCLAW_HOME": home.path,
|
|
"OPENCLAW_STATE_DIR": stateDir.path,
|
|
]) {
|
|
try await body(stateDir)
|
|
}
|
|
}
|
|
|
|
private func withTempHomeAndStateDir(
|
|
_ body: @escaping @Sendable (URL, URL) async throws -> Void) async throws
|
|
{
|
|
let root = self.realTemporaryDirectory
|
|
.appendingPathComponent("openclaw-home-state-\(UUID().uuidString)", isDirectory: true)
|
|
let home = root.appendingPathComponent("home", isDirectory: true)
|
|
let stateDir = root.appendingPathComponent("state", isDirectory: true)
|
|
defer { try? FileManager().removeItem(at: root) }
|
|
|
|
try await self.withLockedEnv([
|
|
"OPENCLAW_HOME": home.path,
|
|
"OPENCLAW_STATE_DIR": stateDir.path,
|
|
]) {
|
|
try await body(home, stateDir)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `ensure file skips rewrite when unchanged`() async throws {
|
|
try await self.withTempStateDir { _ in
|
|
_ = ExecApprovalsStore.ensureFile()
|
|
let url = ExecApprovalsStore.fileURL()
|
|
let firstIdentity = try Self.fileIdentity(at: url)
|
|
|
|
_ = ExecApprovalsStore.ensureFile()
|
|
let secondIdentity = try Self.fileIdentity(at: url)
|
|
|
|
#expect(firstIdentity == secondIdentity)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `ensure file migrates default approvals into custom state dir`() async throws {
|
|
try await self.withTempHomeAndStateDir { home, stateDir in
|
|
let legacyDir = home.appendingPathComponent(".openclaw", isDirectory: true)
|
|
try FileManager().createDirectory(
|
|
at: legacyDir,
|
|
withIntermediateDirectories: true)
|
|
let legacySocket = legacyDir.appendingPathComponent("exec-approvals.sock").path
|
|
let legacyFile = legacyDir.appendingPathComponent("exec-approvals.json")
|
|
let legacyJson = """
|
|
{
|
|
"version": 1,
|
|
"socket": {
|
|
"path": "\(legacySocket)",
|
|
"token": "legacy-token"
|
|
},
|
|
"defaults": {
|
|
"security": "deny",
|
|
"ask": "always"
|
|
},
|
|
"agents": {
|
|
"main": {
|
|
"allowlist": [{ "pattern": "git status" }]
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
try Data(legacyJson.utf8).write(to: legacyFile)
|
|
|
|
let file = ExecApprovalsStore.ensureFile()
|
|
let targetURL = ExecApprovalsStore.fileURL()
|
|
|
|
#expect(targetURL.path == stateDir.appendingPathComponent("exec-approvals.json").path)
|
|
#expect(FileManager().fileExists(atPath: targetURL.path))
|
|
#expect(file.socket?.path == stateDir.appendingPathComponent("exec-approvals.sock").path)
|
|
#expect(file.socket?.token == "legacy-token")
|
|
#expect(file.defaults?.security == .deny)
|
|
#expect(file.defaults?.ask == .always)
|
|
#expect(file.agents?["main"]?.allowlist?.map(\.pattern) == ["git status"])
|
|
#expect(!FileManager().fileExists(atPath: legacyFile.path))
|
|
#expect(FileManager().fileExists(atPath: "\(legacyFile.path).migrated"))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `update allowlist accepts basename pattern`() async throws {
|
|
try await self.withTempStateDir { _ in
|
|
let rejected = ExecApprovalsStore.updateAllowlist(
|
|
agentId: "main",
|
|
allowlist: [
|
|
ExecAllowlistEntry(pattern: "echo"),
|
|
ExecAllowlistEntry(pattern: "/bin/echo"),
|
|
])
|
|
#expect(rejected.isEmpty)
|
|
|
|
let resolved = ExecApprovalsStore.resolve(agentId: "main")
|
|
#expect(resolved.allowlist.map(\.pattern) == ["echo", "/bin/echo"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `update allowlist migrates legacy pattern from resolved path`() async throws {
|
|
try await self.withTempStateDir { _ in
|
|
let rejected = ExecApprovalsStore.updateAllowlist(
|
|
agentId: "main",
|
|
allowlist: [
|
|
ExecAllowlistEntry(
|
|
pattern: "echo",
|
|
lastUsedAt: nil,
|
|
lastUsedCommand: nil,
|
|
lastResolvedPath: " /usr/bin/echo "),
|
|
])
|
|
#expect(rejected.isEmpty)
|
|
|
|
let resolved = ExecApprovalsStore.resolve(agentId: "main")
|
|
#expect(resolved.allowlist.map(\.pattern) == ["/usr/bin/echo"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `ensure file hardens state directory permissions`() async throws {
|
|
try await self.withTempStateDir { stateDir in
|
|
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
|
|
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: stateDir.path)
|
|
|
|
_ = ExecApprovalsStore.ensureFile()
|
|
let attrs = try FileManager().attributesOfItem(atPath: stateDir.path)
|
|
let permissions = (attrs[.posixPermissions] as? NSNumber)?.intValue ?? -1
|
|
#expect(permissions & 0o777 == 0o700)
|
|
}
|
|
}
|
|
|
|
private static func fileIdentity(at url: URL) throws -> Int {
|
|
let attributes = try FileManager().attributesOfItem(atPath: url.path)
|
|
guard let identifier = (attributes[.systemFileNumber] as? NSNumber)?.intValue else {
|
|
struct MissingIdentifierError: Error {}
|
|
throw MissingIdentifierError()
|
|
}
|
|
return identifier
|
|
}
|
|
|
|
private static func seedCurrentApprovalsFile(in stateDir: URL) throws {
|
|
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
|
|
let file = ExecApprovalsFile(
|
|
version: 1,
|
|
socket: ExecApprovalsSocketConfig(
|
|
path: stateDir.appendingPathComponent("exec-approvals.sock").path,
|
|
token: "test-token"),
|
|
defaults: nil,
|
|
agents: [:])
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
try encoder.encode(file)
|
|
.write(to: stateDir.appendingPathComponent("exec-approvals.json"))
|
|
}
|
|
}
|