openclaw/apps/macos/Sources/OpenClaw/ExecSystemRunCommandValidator.swift
2026-03-19 14:27:05 +02:00

128 lines
4.8 KiB
Swift

import Foundation
enum ExecSystemRunCommandValidator {
struct ResolvedCommand {
let displayCommand: String
let evaluationRawCommand: String?
}
enum ValidationResult {
case ok(ResolvedCommand)
case invalid(message: String)
}
private static let posixOrPowerShellInlineWrapperNames = Set([
"ash",
"bash",
"dash",
"fish",
"ksh",
"powershell",
"pwsh",
"sh",
"zsh",
])
static func resolve(command: [String], rawCommand: String?) -> ValidationResult {
let normalizedRaw = self.normalizeRaw(rawCommand)
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
let shellCommand = shell.isWrapper ? self.trimmedNonEmpty(shell.command) : nil
let envManipulationBeforeShellWrapper = self.hasEnvManipulationBeforeShellWrapper(command)
let shellWrapperPositionalArgv = self.hasTrailingPositionalArgvAfterInlineCommand(command)
let mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv
let formattedArgv = ExecCommandFormatter.displayString(for: command)
let previewCommand: String? = if let shellCommand, !mustBindDisplayToFullArgv {
shellCommand
} else {
nil
}
if let raw = normalizedRaw, raw != formattedArgv, raw != previewCommand {
return .invalid(message: "INVALID_REQUEST: rawCommand does not match command")
}
return .ok(ResolvedCommand(
displayCommand: formattedArgv,
evaluationRawCommand: self.allowlistEvaluationRawCommand(
normalizedRaw: normalizedRaw,
shellIsWrapper: shell.isWrapper,
previewCommand: previewCommand)))
}
static func allowlistEvaluationRawCommand(command: [String], rawCommand: String?) -> String? {
let normalizedRaw = self.normalizeRaw(rawCommand)
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
let shellCommand = shell.isWrapper ? self.trimmedNonEmpty(shell.command) : nil
let envManipulationBeforeShellWrapper = self.hasEnvManipulationBeforeShellWrapper(command)
let shellWrapperPositionalArgv = self.hasTrailingPositionalArgvAfterInlineCommand(command)
let mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv
let previewCommand: String? = if let shellCommand, !mustBindDisplayToFullArgv {
shellCommand
} else {
nil
}
return self.allowlistEvaluationRawCommand(
normalizedRaw: normalizedRaw,
shellIsWrapper: shell.isWrapper,
previewCommand: previewCommand)
}
private static func normalizeRaw(_ rawCommand: String?) -> String? {
let trimmed = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
private static func trimmedNonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
private static func allowlistEvaluationRawCommand(
normalizedRaw: String?,
shellIsWrapper: Bool,
previewCommand: String?) -> String?
{
guard shellIsWrapper else {
return normalizedRaw
}
guard let normalizedRaw else {
return nil
}
return normalizedRaw == previewCommand ? normalizedRaw : nil
}
private static func hasEnvManipulationBeforeShellWrapper(_ argv: [String]) -> Bool {
return ExecWrapperResolution.hasEnvManipulationBeforeShellWrapper(argv)
}
private static func hasTrailingPositionalArgvAfterInlineCommand(_ argv: [String]) -> Bool {
let wrapperArgv = self.unwrapShellWrapperArgv(argv)
guard let token0 = self.trimmedNonEmpty(wrapperArgv.first) else {
return false
}
let wrapper = ExecWrapperResolution.normalizeExecutableToken(token0)
guard self.posixOrPowerShellInlineWrapperNames.contains(wrapper) else {
return false
}
let inlineCommandIndex = ExecWrapperResolution.resolveInlineCommandValueTokenIndex(
wrapperArgv,
normalizedWrapper: wrapper)
guard let inlineCommandIndex else {
return false
}
let start = inlineCommandIndex + 1
guard start < wrapperArgv.count else {
return false
}
return wrapperArgv[start...].contains { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
}
private static func unwrapShellWrapperArgv(_ argv: [String]) -> [String] {
ExecWrapperResolution.unwrapShellInspectionArgv(argv)
}
}