mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
9b1c36d23c
* 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>
157 lines
7.0 KiB
Swift
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: []),
|
|
])
|
|
}
|
|
}
|