Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/containerization.git", from: "0.30.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.7.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "5.0.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"5.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.12.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.97.0"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.6.4"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.33.1"),
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -66,6 +68,8 @@ let package = Package(
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "Logging", package: "swift-log"),
.product(
name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux])),
]
),
.executableTarget(
Expand All @@ -79,6 +83,9 @@ let package = Package(
name: "AgentIsolationDockerRuntime", condition: .when(traits: ["ContainerRuntimeDocker"])),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Subprocess", package: "swift-subprocess"),
.product(
name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux])),
]
),
.executableTarget(
Expand Down Expand Up @@ -112,7 +119,10 @@ let package = Package(
.testTarget(
name: "AgentcIntegrationTests",
dependencies: [
.product(name: "Crypto", package: "swift-crypto")
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "Subprocess", package: "swift-subprocess"),
.product(
name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux])),
]
),
]
Expand Down
16 changes: 1 addition & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,7 @@ agentc run -c copilot

### Project Settings

Place a `.agentc/settings.json` file in your project root to set default agent options for the project.

```json
{
"agent": {
"image": "ghcr.io/my-org/dev:latest",
"configurations": ["claude"],
"cpus": 4,
"memoryMiB": 4096,
"excludes": [".git", "secrets"]
}
}
```

CLI flags override project settings; some fields (like `excludes` and `additionalMounts`) are merged.
Use `agentc init` to place a `.agentc/settings.json` file in your project root to set default agent options for the project. CLI flags override project settings; some fields (like `excludes` and `additionalMounts`) are merged.

See [docs/project-settings.md](./docs/project-settings.md) for the full schema and override rules.

Expand Down
6 changes: 5 additions & 1 deletion Sources/AgentIsolation/AgentSession.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Foundation
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// Settings from an agent configuration's settings.json.
private struct AgentConfigurationSettings: Decodable {
Expand Down
6 changes: 5 additions & 1 deletion Sources/AgentIsolation/IsolationConfig.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Foundation
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// Determines how the container entrypoint is configured.
public enum BootstrapMode: Sendable {
Expand Down
7 changes: 6 additions & 1 deletion Sources/AgentIsolation/PathUtils.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import Crypto
import Foundation

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

public enum AgentIsolationPathUtils {
/// Resolve symlinks with platform consideration.
Expand Down
43 changes: 39 additions & 4 deletions Sources/AgentIsolation/ProjectSettings.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import Foundation
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// Project-level settings loaded from `.agentc/settings.json`.
///
/// Place this file in your project root (or any ancestor directory) to set defaults
/// for `agentc` invocations within the project tree. All fields are optional;
/// only the values you specify take effect.
public struct ProjectSettings: Decodable, Sendable, Equatable {
public struct ProjectSettings: Codable, Sendable, Equatable {
public var agent: AgentSettings?

public struct AgentSettings: Decodable, Sendable, Equatable {
public init(agent: AgentSettings? = nil) {
self.agent = agent
}

public struct AgentSettings: Codable, Sendable, Equatable {
public var image: String?
public var profile: String?
public var excludes: [String]?
Expand All @@ -20,6 +28,32 @@ public struct ProjectSettings: Decodable, Sendable, Equatable {
public var memoryMiB: Int?
public var bootstrap: String?
public var respectImageEntrypoint: Bool?

public init(
image: String? = nil,
profile: String? = nil,
excludes: [String]? = nil,
configurations: [String]? = nil,
additionalMounts: [String]? = nil,
defaultArguments: [String]? = nil,
additionalArguments: [String]? = nil,
cpus: Int? = nil,
memoryMiB: Int? = nil,
bootstrap: String? = nil,
respectImageEntrypoint: Bool? = nil
) {
self.image = image
self.profile = profile
self.excludes = excludes
self.configurations = configurations
self.additionalMounts = additionalMounts
self.defaultArguments = defaultArguments
self.additionalArguments = additionalArguments
self.cpus = cpus
self.memoryMiB = memoryMiB
self.bootstrap = bootstrap
self.respectImageEntrypoint = respectImageEntrypoint
}
}

/// The folder names to probe at each directory level, in priority order.
Expand All @@ -34,7 +68,8 @@ public struct ProjectSettings: Decodable, Sendable, Equatable {
var dir = startDir.standardizedFileURL
while true {
for folderName in folderNames {
let settingsURL = dir
let settingsURL =
dir
.appendingPathComponent(folderName)
.appendingPathComponent("settings.json")
if let settings = load(from: settingsURL) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ContainerizationOS
import Foundation
import Logging
import System

// MARK: - AppleContainerRuntime

Expand Down Expand Up @@ -164,9 +165,9 @@
containerConfig.process.setTerminalIO(terminal: t)
}
case .standardIO:
containerConfig.process.stdin = FileHandleReader(.standardInput)
containerConfig.process.stdout = FileHandleWriter(.standardOutput)
containerConfig.process.stderr = FileHandleWriter(.standardError)
containerConfig.process.stdin = FileDescriptorReader(.standardInput)
containerConfig.process.stdout = FileDescriptorWriter(.standardOutput)
containerConfig.process.stderr = FileDescriptorWriter(.standardError)
case .custom(let stdin, let stdout, let stderr, let isTerminal):
containerConfig.process.terminal = isTerminal
containerConfig.process.stdin = ContainerizationReaderStream(stdin)
Expand Down
62 changes: 62 additions & 0 deletions Sources/AgentIsolationAppleContainerRuntime/FileDescriptorIO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#if canImport(Containerization)
import Containerization
import Foundation
import System

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#endif

/// `ReaderStream` backed by a `FileDescriptor` (e.g. stdin).
struct FileDescriptorReader: ReaderStream {
private let fd: FileDescriptor

init(_ fd: FileDescriptor) {
self.fd = fd
}

func stream() -> AsyncStream<Data> {
let rawFD = fd.rawValue
return AsyncStream { continuation in
let source = DispatchSource.makeReadSource(
fileDescriptor: rawFD,
queue: DispatchQueue.global(qos: .userInteractive))
source.setEventHandler {
var buffer = [UInt8](repeating: 0, count: 4096)
let bytesRead = read(rawFD, &buffer, buffer.count)
if bytesRead <= 0 {
source.cancel()
} else {
continuation.yield(Data(buffer[0..<bytesRead]))
}
}
source.setCancelHandler {
continuation.finish()
}
continuation.onTermination = { _ in
source.cancel()
}
source.resume()
}
}
}

/// `Writer` backed by a `FileDescriptor` (e.g. stdout / stderr).
struct FileDescriptorWriter: Writer {
private let fd: FileDescriptor

init(_ fd: FileDescriptor) {
self.fd = fd
}

func write(_ data: Data) throws {
_ = try data.withUnsafeBytes { try fd.writeAll($0) }
}

func close() throws {
try fd.close()
}
}
#endif
44 changes: 0 additions & 44 deletions Sources/AgentIsolationAppleContainerRuntime/FileHandleIO.swift

This file was deleted.

32 changes: 26 additions & 6 deletions Sources/AgentIsolationDockerRuntime/DockerAPIClient.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import AsyncHTTPClient
import Foundation
import NIOCore
import NIOFoundationCompat

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// HTTP client for Docker Engine API v1.44.
///
/// Supports Unix domain socket and TCP connections.
Expand Down Expand Up @@ -286,12 +291,27 @@ final class DockerAPIClient: Sendable {
}

/// Percent-encode a string for use as a URL path component.
/// Encodes everything except unreserved characters (RFC 3986 section 2.3).
///
/// Allows unreserved characters (RFC 3986 §2.3) plus the sub-delimiter and
/// pchar extras that are legal in path segments — but NOT `/`, which must be
/// encoded when it appears inside a Docker image reference.
static func pathEncodeComponent(_ value: String) -> String {
// .urlPathAllowed includes '/' which we must also encode for Docker image refs
var allowed = CharacterSet.urlPathAllowed
allowed.remove("/")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
// Characters allowed in a URL path segment (RFC 3986) minus '/'.
let allowed: Set<Character> = Set(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:@!$&'()*+,;="
)
var result = ""
result.reserveCapacity(value.count)
for char in value {
if allowed.contains(char) {
result.append(char)
} else {
for byte in String(char).utf8 {
result += String(format: "%%%02X", byte)
}
}
}
return result
}
}

Expand Down
6 changes: 5 additions & 1 deletion Sources/AgentIsolationDockerRuntime/DockerModels.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Foundation
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

// MARK: - Image Types

Expand Down
Loading
Loading