mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 10:56:13 +00: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>
277 lines
8.8 KiB
Swift
277 lines
8.8 KiB
Swift
import Foundation
|
|
|
|
enum ExecInlineCommandParser {
|
|
struct Match {
|
|
let tokenIndex: Int
|
|
let inlineCommand: String?
|
|
let valueTokenOffset: Int
|
|
|
|
init(tokenIndex: Int, inlineCommand: String?, valueTokenOffset: Int = 1) {
|
|
self.tokenIndex = tokenIndex
|
|
self.inlineCommand = inlineCommand
|
|
self.valueTokenOffset = valueTokenOffset
|
|
}
|
|
}
|
|
|
|
private struct CombinedCommandFlag {
|
|
let attachedCommand: String?
|
|
let separateValueCount: Int
|
|
}
|
|
|
|
private static let posixShellOptionsWithSeparateValues = Set([
|
|
"--init-file",
|
|
"--rcfile",
|
|
"-O",
|
|
"-o",
|
|
"+O",
|
|
"+o",
|
|
])
|
|
|
|
static func hasPosixInteractiveStartupBeforeInlineCommand(
|
|
_ argv: [String],
|
|
flags: Set<String>) -> Bool
|
|
{
|
|
var idx = 1
|
|
var sawInteractiveMode = false
|
|
while idx < argv.count {
|
|
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if token.isEmpty {
|
|
idx += 1
|
|
continue
|
|
}
|
|
if token == "--" {
|
|
return false
|
|
}
|
|
if self.isPosixInteractiveModeOption(token) {
|
|
sawInteractiveMode = true
|
|
}
|
|
if flags.contains(token) || self.isCombinedCommandFlag(token) {
|
|
return sawInteractiveMode
|
|
}
|
|
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
|
return false
|
|
}
|
|
let combinedValueCount = self.combinedSeparateValueOptionCount(token)
|
|
if combinedValueCount > 0 {
|
|
idx += 1 + combinedValueCount
|
|
continue
|
|
}
|
|
if self.consumesSeparateValue(token) {
|
|
idx += 2
|
|
continue
|
|
}
|
|
idx += 1
|
|
}
|
|
return false
|
|
}
|
|
|
|
static func hasPosixLoginStartupBeforeInlineCommand(
|
|
_ argv: [String],
|
|
flags: Set<String>) -> Bool
|
|
{
|
|
var idx = 1
|
|
var sawLoginMode = false
|
|
while idx < argv.count {
|
|
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if token.isEmpty {
|
|
idx += 1
|
|
continue
|
|
}
|
|
if token == "--" {
|
|
return false
|
|
}
|
|
if token == "--login" || self.isPosixShortOption(token, containing: "l") {
|
|
sawLoginMode = true
|
|
}
|
|
if flags.contains(token) || self.isCombinedCommandFlag(token) {
|
|
return sawLoginMode
|
|
}
|
|
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
|
return false
|
|
}
|
|
let combinedValueCount = self.combinedSeparateValueOptionCount(token)
|
|
if combinedValueCount > 0 {
|
|
idx += 1 + combinedValueCount
|
|
continue
|
|
}
|
|
if self.consumesSeparateValue(token) {
|
|
idx += 2
|
|
continue
|
|
}
|
|
idx += 1
|
|
}
|
|
return false
|
|
}
|
|
|
|
static func hasFishInitCommandOption(_ argv: [String]) -> Bool {
|
|
var idx = 1
|
|
while idx < argv.count {
|
|
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if token.isEmpty {
|
|
idx += 1
|
|
continue
|
|
}
|
|
if token == "--" {
|
|
return false
|
|
}
|
|
if token == "-C" || token == "--init-command" {
|
|
return true
|
|
}
|
|
if token.hasPrefix("-C"), token != "-C" {
|
|
return true
|
|
}
|
|
if token.hasPrefix("--init-command=") {
|
|
return true
|
|
}
|
|
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
|
return false
|
|
}
|
|
idx += 1
|
|
}
|
|
return false
|
|
}
|
|
|
|
static func hasFishAttachedCommandOption(_ argv: [String]) -> Bool {
|
|
var idx = 1
|
|
while idx < argv.count {
|
|
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if token.isEmpty {
|
|
idx += 1
|
|
continue
|
|
}
|
|
if token == "--" {
|
|
return false
|
|
}
|
|
if token.hasPrefix("-c"), token != "-c" {
|
|
return true
|
|
}
|
|
if !token.hasPrefix("-"), !token.hasPrefix("+") {
|
|
return false
|
|
}
|
|
idx += 1
|
|
}
|
|
return false
|
|
}
|
|
|
|
static func findMatch(
|
|
_ argv: [String],
|
|
flags: Set<String>,
|
|
allowCombinedC: Bool) -> Match?
|
|
{
|
|
var idx = 1
|
|
while idx < argv.count {
|
|
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if token.isEmpty {
|
|
idx += 1
|
|
continue
|
|
}
|
|
if token == "--" { break }
|
|
let comparableToken = allowCombinedC ? token : token.lowercased()
|
|
if flags.contains(comparableToken) {
|
|
return Match(tokenIndex: idx, inlineCommand: nil)
|
|
}
|
|
if allowCombinedC, let combined = self.parseCombinedCommandFlag(token) {
|
|
if let attachedCommand = combined.attachedCommand {
|
|
return Match(tokenIndex: idx, inlineCommand: attachedCommand, valueTokenOffset: 0)
|
|
}
|
|
return Match(
|
|
tokenIndex: idx,
|
|
inlineCommand: nil,
|
|
valueTokenOffset: 1 + combined.separateValueCount)
|
|
}
|
|
if allowCombinedC, !token.hasPrefix("-"), !token.hasPrefix("+") {
|
|
break
|
|
}
|
|
let combinedValueCount = allowCombinedC ? self.combinedSeparateValueOptionCount(token) : 0
|
|
if combinedValueCount > 0 {
|
|
idx += 1 + combinedValueCount
|
|
continue
|
|
}
|
|
if allowCombinedC, self.consumesSeparateValue(token) {
|
|
idx += 2
|
|
continue
|
|
}
|
|
idx += 1
|
|
}
|
|
return nil
|
|
}
|
|
|
|
static func extractInlineCommand(
|
|
_ argv: [String],
|
|
flags: Set<String>,
|
|
allowCombinedC: Bool) -> String?
|
|
{
|
|
guard let match = self.findMatch(argv, flags: flags, allowCombinedC: allowCombinedC) else {
|
|
return nil
|
|
}
|
|
if let inlineCommand = match.inlineCommand {
|
|
return inlineCommand
|
|
}
|
|
let nextIndex = match.tokenIndex + match.valueTokenOffset
|
|
let payload = nextIndex < argv.count
|
|
? argv[nextIndex]
|
|
: ""
|
|
return payload.isEmpty ? nil : payload
|
|
}
|
|
|
|
private static func isCombinedCommandFlag(_ token: String) -> Bool {
|
|
self.parseCombinedCommandFlag(token) != nil
|
|
}
|
|
|
|
private static func parseCombinedCommandFlag(_ token: String) -> CombinedCommandFlag? {
|
|
let chars = Array(token)
|
|
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
|
|
return nil
|
|
}
|
|
let optionChars = Array(chars.dropFirst())
|
|
guard let commandFlagIndex = optionChars.firstIndex(of: "c") else {
|
|
return nil
|
|
}
|
|
if optionChars.contains("-") {
|
|
return nil
|
|
}
|
|
let suffix = String(optionChars.dropFirst(commandFlagIndex + 1))
|
|
if !suffix.isEmpty,
|
|
suffix.range(of: #"[^A-Za-z]"#, options: .regularExpression) != nil
|
|
{
|
|
return CombinedCommandFlag(attachedCommand: suffix, separateValueCount: 0)
|
|
}
|
|
let separateValueCount = optionChars.reduce(0) { count, char in
|
|
count + ((char == "o" || char == "O") ? 1 : 0)
|
|
}
|
|
return CombinedCommandFlag(attachedCommand: nil, separateValueCount: separateValueCount)
|
|
}
|
|
|
|
private static func combinedSeparateValueOptionCount(_ token: String) -> Int {
|
|
let chars = Array(token)
|
|
guard chars.count >= 2, chars[0] == "-" || chars[0] == "+", chars[1] != "-" else {
|
|
return 0
|
|
}
|
|
if chars.dropFirst().contains("-") {
|
|
return 0
|
|
}
|
|
return chars.dropFirst().reduce(0) { count, char in
|
|
count + ((char == "o" || char == "O") ? 1 : 0)
|
|
}
|
|
}
|
|
|
|
private static func consumesSeparateValue(_ token: String) -> Bool {
|
|
self.posixShellOptionsWithSeparateValues.contains(token)
|
|
}
|
|
|
|
private static func isPosixInteractiveModeOption(_ token: String) -> Bool {
|
|
token == "--interactive" || self.isPosixShortOption(token, containing: "i")
|
|
}
|
|
|
|
private static func isPosixShortOption(_ token: String, containing option: Character) -> Bool {
|
|
let chars = Array(token)
|
|
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
|
|
return false
|
|
}
|
|
if chars.dropFirst().contains("-") {
|
|
return false
|
|
}
|
|
return chars.dropFirst().contains(option)
|
|
}
|
|
}
|