mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 14:34:32 +08:00
* fix(security): serialize exec approval mutations * fix(security): preserve additive approval writes * test(cli): expect normalized approval shape * fix(security): preserve exec approval compatibility * test(security): exercise locked approval initialization * test(security): mock serialized approval helpers * test(exec): derive enforced command path from plan * fix(gateway): always return approval CAS conflicts * fix(macos): serialize exec approvals writes * fix(security): repair approval build errors * fix(security): serialize exec approval mutations * fix(security): fail closed on approval persistence errors * test(security): cover detached approval persistence failures * fix(security): harden exec approval state * style(macos): format exec approval sources * fix(security): complete exec approval hardening Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com> * fix(macos): preserve approved login-shell semantics * fix(macos): keep login shell approvals one-shot * fix(security): linearize exec authorization Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com> * fix(security): preserve durable approval basis Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com> * fix(security): bind exec grants to current policy Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com> * test(security): fix exec revocation fixtures * test(security): align gateway approval fixtures * fix(macos): return approval decisions * chore(i18n): sync native approval strings * test(security): align approval hardening fixtures * test(node): authorize completed event fixture * test(security): fix approval decision fixtures * test(security): await durable approval visibility * fix(exec): preserve concurrent approval grants * fix(exec): address exact-head CI failures * fix(exec): preserve concurrent approval promotions * fix(exec): make Swift shutdown state explicit * test(macos): handle approval read failures * fix(macos): harden approval socket paths * fix(macos): preserve exact shell payload bytes * test(macos): make approval fixtures explicit * test(macos): fix approval suite compilation * fix(macos): bound approval socket JSONL reads * chore: move exec approval note to release process * chore: move exec approval note to release process --------- Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
152 lines
5.1 KiB
Swift
152 lines
5.1 KiB
Swift
import Foundation
|
|
import OpenClawIPC
|
|
|
|
enum ShellExecutor {
|
|
struct ShellResult: Sendable {
|
|
var stdout: String
|
|
var stderr: String
|
|
var exitCode: Int?
|
|
var timedOut: Bool
|
|
var success: Bool
|
|
var errorMessage: String?
|
|
}
|
|
|
|
private final class CompletionBox: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var finished = false
|
|
private let continuation: CheckedContinuation<ShellResult, Never>
|
|
|
|
init(continuation: CheckedContinuation<ShellResult, Never>) {
|
|
self.continuation = continuation
|
|
}
|
|
|
|
func finish(_ result: ShellResult) {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
guard !self.finished else { return }
|
|
self.finished = true
|
|
self.continuation.resume(returning: result)
|
|
}
|
|
}
|
|
|
|
private static func completedResult(
|
|
status: Int,
|
|
outTask: Task<Data, Never>,
|
|
errTask: Task<Data, Never>) async -> ShellResult
|
|
{
|
|
let out = await outTask.value
|
|
let err = await errTask.value
|
|
return ShellResult(
|
|
stdout: String(bytes: out, encoding: .utf8) ?? "",
|
|
stderr: String(bytes: err, encoding: .utf8) ?? "",
|
|
exitCode: status,
|
|
timedOut: false,
|
|
success: status == 0,
|
|
errorMessage: status == 0 ? nil : "exit \(status)")
|
|
}
|
|
|
|
static func runDetailed(
|
|
command: [String],
|
|
cwd: String?,
|
|
env: [String: String]?,
|
|
timeout: Double?) async -> ShellResult
|
|
{
|
|
guard !command.isEmpty else {
|
|
return ShellResult(
|
|
stdout: "",
|
|
stderr: "",
|
|
exitCode: nil,
|
|
timedOut: false,
|
|
success: false,
|
|
errorMessage: "empty command")
|
|
}
|
|
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
|
process.arguments = command
|
|
if let cwd {
|
|
process.currentDirectoryURL = URL(fileURLWithPath: cwd)
|
|
}
|
|
if let env {
|
|
process.environment = env
|
|
}
|
|
|
|
let stdoutPipe = Pipe()
|
|
let stderrPipe = Pipe()
|
|
process.standardOutput = stdoutPipe
|
|
process.standardError = stderrPipe
|
|
|
|
let outTask = Task { stdoutPipe.fileHandleForReading.readToEndSafely() }
|
|
let errTask = Task { stderrPipe.fileHandleForReading.readToEndSafely() }
|
|
|
|
if let timeout, timeout > 0 {
|
|
return await withCheckedContinuation { continuation in
|
|
let completion = CompletionBox(continuation: continuation)
|
|
|
|
process.terminationHandler = { terminatedProcess in
|
|
let status = Int(terminatedProcess.terminationStatus)
|
|
Task {
|
|
let result = await self.completedResult(
|
|
status: status,
|
|
outTask: outTask,
|
|
errTask: errTask)
|
|
completion.finish(result)
|
|
}
|
|
}
|
|
|
|
do {
|
|
try process.run()
|
|
} catch {
|
|
completion.finish(
|
|
ShellResult(
|
|
stdout: "",
|
|
stderr: "",
|
|
exitCode: nil,
|
|
timedOut: false,
|
|
success: false,
|
|
errorMessage: "failed to start: \(error.localizedDescription)"))
|
|
return
|
|
}
|
|
|
|
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + timeout) {
|
|
guard process.isRunning else { return }
|
|
process.terminate()
|
|
completion.finish(
|
|
ShellResult(
|
|
stdout: "",
|
|
stderr: "",
|
|
exitCode: nil,
|
|
timedOut: true,
|
|
success: false,
|
|
errorMessage: "timeout"))
|
|
}
|
|
}
|
|
}
|
|
|
|
do {
|
|
try process.run()
|
|
} catch {
|
|
return ShellResult(
|
|
stdout: "",
|
|
stderr: "",
|
|
exitCode: nil,
|
|
timedOut: false,
|
|
success: false,
|
|
errorMessage: "failed to start: \(error.localizedDescription)")
|
|
}
|
|
|
|
process.waitUntilExit()
|
|
return await self.completedResult(
|
|
status: Int(process.terminationStatus),
|
|
outTask: outTask,
|
|
errTask: errTask)
|
|
}
|
|
|
|
static func run(command: [String], cwd: String?, env: [String: String]?, timeout: Double?) async -> Response {
|
|
let result = await self.runDetailed(command: command, cwd: cwd, env: env, timeout: timeout)
|
|
let combined = result.stdout.isEmpty ? result.stderr : result.stdout
|
|
let payload = combined.isEmpty ? nil : Data(combined.utf8)
|
|
return Response(ok: result.success, message: result.errorMessage, payload: payload)
|
|
}
|
|
}
|