Add opt-in GRDB memory with frozen conversation snapshots, background learning, session search, and a user-only Destroy action on both platforms. Persist and synchronize complete skill packages with the new AgentKit management tools. Pin signed AgentKit 0.12.3 and GRDB 7.11.1. Add the V18 package migration, regression coverage, bilingual UI snapshots, and architecture records. Validation: AgentKit and iOS suites, focused macOS skills/memory/architecture/localization tests, Swift formatting, and Chinese localization checks passed. Full macOS runs exposed intermittent existing waveform-rendering and metrics-timing failures.
96 lines
4.7 KiB
Swift
96 lines
4.7 KiB
Swift
import AgentKit
|
|
import Darwin
|
|
import Foundation
|
|
|
|
/// Reading a skill off disk, kept out of the view so the parsing decisions are
|
|
/// testable without a panel.
|
|
public enum SkillImporter {
|
|
public struct Outcome {
|
|
public var document: AgentSkillDocument
|
|
public var origin: String
|
|
public var warnings: [String] = []
|
|
}
|
|
|
|
public static func read(_ url: URL) throws -> Outcome {
|
|
let accessed = url.startAccessingSecurityScopedResource()
|
|
defer { if accessed { url.stopAccessingSecurityScopedResource() } }
|
|
|
|
let values = try url.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey])
|
|
guard values.isSymbolicLink != true else { throw AgentSkillImportError.notReadable }
|
|
let isDirectory = values.isDirectory == true
|
|
let file = isDirectory ? url.appending(path: "SKILL.md") : url
|
|
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
throw AgentSkillImportError.noSkillFile
|
|
}
|
|
|
|
let data = try readBounded(file, root: isDirectory ? url : nil, limit: AgentSkill.maximumBodyBytes)
|
|
guard let text = String(data: data, encoding: .utf8) else {
|
|
throw AgentSkillImportError.notReadable
|
|
}
|
|
|
|
// The folder name is the skill name for anything written against the
|
|
// standard, where the directory *is* the identifier.
|
|
let source = isDirectory ? url : url.deletingPathExtension()
|
|
var document = try AgentSkillDocument.parse(
|
|
text, fallbackName: source.lastPathComponent
|
|
)
|
|
if isDirectory {
|
|
let root = url.standardizedFileURL
|
|
let keys: [URLResourceKey] = [.isRegularFileKey, .isDirectoryKey, .isSymbolicLinkKey, .fileSizeKey]
|
|
guard let enumerator = FileManager.default.enumerator(at: root, includingPropertiesForKeys: keys) else {
|
|
throw AgentSkillImportError.notReadable
|
|
}
|
|
for case let resource as URL in enumerator {
|
|
let values = try resource.resourceValues(forKeys: Set(keys))
|
|
guard values.isSymbolicLink != true else { throw AgentSkillImportError.notReadable }
|
|
if values.isDirectory == true { continue }
|
|
guard values.isRegularFile == true else { throw AgentSkillImportError.notReadable }
|
|
let path = String(resource.standardizedFileURL.path.dropFirst(root.path.count + 1))
|
|
if path == "SKILL.md" { continue }
|
|
try AgentSkillPackage.validatePath(path)
|
|
document.package.files[path] = try readBounded(
|
|
resource, root: root, limit: AgentSkillPackage.maximumFileBytes)
|
|
try document.package.validate()
|
|
}
|
|
}
|
|
return Outcome(document: document, origin: file.lastPathComponent, warnings: [])
|
|
}
|
|
|
|
public static func readResource(_ url: URL) throws -> Data {
|
|
let access = url.startAccessingSecurityScopedResource()
|
|
defer { if access { url.stopAccessingSecurityScopedResource() } }
|
|
return try readBounded(url, root: nil, limit: AgentSkillPackage.maximumFileBytes)
|
|
}
|
|
|
|
private static func readBounded(_ url: URL, root: URL?, limit: Int) throws -> Data {
|
|
var descriptor: Int32
|
|
if let root {
|
|
let path = String(url.standardizedFileURL.path.dropFirst(root.standardizedFileURL.path.count + 1))
|
|
try AgentSkillPackage.validatePath(path)
|
|
descriptor = open(root.path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC)
|
|
guard descriptor >= 0 else { throw AgentSkillImportError.notReadable }
|
|
let parts = path.components(separatedBy: "/")
|
|
for (index, part) in parts.enumerated() {
|
|
let flags = O_RDONLY | O_NOFOLLOW | O_CLOEXEC | (index == parts.count - 1 ? 0 : O_DIRECTORY)
|
|
let next = openat(descriptor, part, flags)
|
|
close(descriptor)
|
|
guard next >= 0 else { throw AgentSkillImportError.notReadable }
|
|
descriptor = next
|
|
}
|
|
} else {
|
|
descriptor = open(url.path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC)
|
|
guard descriptor >= 0 else { throw AgentSkillImportError.notReadable }
|
|
}
|
|
let handle = FileHandle(fileDescriptor: descriptor, closeOnDealloc: true)
|
|
defer { try? handle.close() }
|
|
var info = stat()
|
|
guard fstat(descriptor, &info) == 0, info.st_mode & S_IFMT == S_IFREG else {
|
|
throw AgentSkillImportError.notReadable
|
|
}
|
|
guard info.st_size <= limit else { throw AgentSkillImportError.tooLarge }
|
|
let data = try handle.read(upToCount: limit + 1) ?? Data()
|
|
guard data.count <= limit else { throw AgentSkillImportError.tooLarge }
|
|
return data
|
|
}
|
|
}
|