Files
openclaw/apps/macos/Tests/OpenClawIPCTests/ExecSkillBinTrustTests.swift
Peter Steinberger 9b1c36d23c fix: prevent exec approval revocation races (#103515)
* 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>
2026-07-10 21:35:05 +01:00

157 lines
7.0 KiB
Swift

import Foundation
import Testing
@testable import OpenClaw
struct ExecSkillBinTrustTests {
@Test func `build trust index resolves skill bin paths`() throws {
let fixture = try Self.makeExecutable(named: "jq")
defer { try? FileManager.default.removeItem(at: fixture.root) }
let trust = SkillBinsCache._testBuildTrustIndex(
report: Self.makeReport(bins: ["jq"]),
searchPaths: [fixture.root.path])
#expect(trust.names == ["jq"])
#expect(trust.pathsByName["jq"] == [fixture.path])
}
@Test func `skill auto allow accepts trusted resolved skill bin path`() throws {
let fixture = try Self.makeExecutable(named: "jq")
defer { try? FileManager.default.removeItem(at: fixture.root) }
let trust = SkillBinsCache._testBuildTrustIndex(
report: Self.makeReport(bins: ["jq"]),
searchPaths: [fixture.root.path])
let resolution = ExecCommandResolution(
rawExecutable: "jq",
resolvedPath: fixture.path,
executableName: "jq",
cwd: nil)
#expect(ExecApprovalEvaluator._testIsSkillAutoAllowed([resolution], trustedBinsByName: trust.pathsByName))
}
@Test func `skill auto allow rejects same basename at different path`() throws {
let trusted = try Self.makeExecutable(named: "jq")
let untrusted = try Self.makeExecutable(named: "jq")
defer {
try? FileManager.default.removeItem(at: trusted.root)
try? FileManager.default.removeItem(at: untrusted.root)
}
let trust = SkillBinsCache._testBuildTrustIndex(
report: Self.makeReport(bins: ["jq"]),
searchPaths: [trusted.root.path])
let resolution = ExecCommandResolution(
rawExecutable: "jq",
resolvedPath: untrusted.path,
executableName: "jq",
cwd: nil)
#expect(!ExecApprovalEvaluator._testIsSkillAutoAllowed([resolution], trustedBinsByName: trust.pathsByName))
}
@Test func `skill auto allow rejects path scoped invocation`() throws {
let fixture = try Self.makeExecutable(named: "jq")
defer { try? FileManager.default.removeItem(at: fixture.root) }
let trust = SkillBinsCache._testBuildTrustIndex(
report: Self.makeReport(bins: ["jq"]),
searchPaths: [fixture.root.path])
let resolution = ExecCommandResolution(
rawExecutable: fixture.path,
resolvedPath: fixture.path,
resolvedRealPath: fixture.path,
executableName: "jq",
cwd: nil)
#expect(!ExecApprovalEvaluator._testIsSkillAutoAllowed([resolution], trustedBinsByName: trust.pathsByName))
}
@Test func `skill auto allow rejects retargeted PATH symlink`() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("openclaw-skill-symlink-\(UUID().uuidString)", isDirectory: true)
.resolvingSymlinksInPath()
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let first = root.appendingPathComponent("first")
let second = root.appendingPathComponent("second")
let alias = root.appendingPathComponent("jq")
try "#!/bin/sh\nexit 0\n".write(to: first, atomically: true, encoding: .utf8)
try "#!/bin/sh\nexit 0\n".write(to: second, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: first.path)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: second.path)
try FileManager.default.createSymbolicLink(at: alias, withDestinationURL: first)
let trust = SkillBinsCache._testBuildTrustIndex(
report: Self.makeReport(bins: ["jq"]),
searchPaths: [root.path])
try FileManager.default.removeItem(at: alias)
try FileManager.default.createSymbolicLink(at: alias, withDestinationURL: second)
let resolution = try #require(ExecCommandResolution.resolve(
command: ["jq"],
cwd: nil,
env: ["PATH": root.path]))
#expect(!ExecApprovalEvaluator._testIsSkillAutoAllowed([resolution], trustedBinsByName: trust.pathsByName))
}
@Test func `skill auto allow rejects an alias to a shell carrier`() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("openclaw-skill-shell-alias-\(UUID().uuidString)", isDirectory: true)
.resolvingSymlinksInPath()
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let alias = root.appendingPathComponent("skill-shell")
try FileManager.default.createSymbolicLink(
at: alias,
withDestinationURL: URL(fileURLWithPath: "/bin/sh"))
let trust = SkillBinsCache._testBuildTrustIndex(
report: Self.makeReport(bins: ["skill-shell"]),
searchPaths: [root.path])
let resolution = try #require(ExecCommandResolution.resolve(
command: ["skill-shell", "-c", "/usr/bin/printf ok"],
cwd: nil,
env: ["PATH": root.path]))
#expect(!ExecApprovalEvaluator._testIsSkillAutoAllowed([resolution], trustedBinsByName: trust.pathsByName))
}
private static func makeExecutable(named name: String) throws -> (root: URL, path: String) {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("openclaw-skill-bin-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let file = root.appendingPathComponent(name)
try "#!/bin/sh\nexit 0\n".write(to: file, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o755))],
ofItemAtPath: file.path)
return (root, file.path)
}
private static func makeReport(bins: [String]) -> SkillsStatusReport {
SkillsStatusReport(
workspaceDir: "/tmp/workspace",
managedSkillsDir: "/tmp/skills",
skills: [
SkillStatus(
name: "test-skill",
description: "test",
source: "local",
filePath: "/tmp/skills/test-skill/SKILL.md",
baseDir: "/tmp/skills/test-skill",
skillKey: "test-skill",
primaryEnv: nil,
emoji: nil,
homepage: nil,
always: false,
disabled: false,
eligible: true,
requirements: SkillRequirements(bins: bins, env: [], config: []),
missing: SkillMissing(bins: [], env: [], config: []),
configChecks: [],
install: []),
])
}
}