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
39 changes: 25 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ Run AI coding agents in isolated containers with persistent profiles and per-pro

Supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [GitHub Copilot CLI](https://gh.io/copilot-install), and more — with pluggable agent configurations via the [agent-isolation-configurations](https://github.com/laosb/agent-isolation-configurations) repo. Contributions for additional agents are welcome!

Available container runtimes: [Apple Containerization](https://apple.github.io/containerization/) (macOS) and Docker (macOS/Linux).

## Install

### Prerequisites
Expand All @@ -20,8 +18,6 @@ Available container runtimes: [Apple Containerization](https://apple.github.io/c
curl -fsSL https://raw.githubusercontent.com/laosb/agentc/main/install.sh | sh
```

This detects your platform, downloads the latest release from GitHub, and installs to `~/.agentc/bin/` with a symlink in `~/.local/bin/`.

## Quick Start

```sh
Expand All @@ -33,24 +29,22 @@ agentc sh -- ls -la /home/agent # run a command inside the container
agentc version # print version info
```

On first run, `agentc` clones the [agent-isolation-configurations](https://github.com/laosb/agent-isolation-configurations) repo and runs each configuration's `prepare.sh` to install required tools. Subsequent runs reuse the existing install.

Use `agentc --help` and `agentc <subcommand> --help` for full CLI reference.

### Profiles

A profile is a persistent `/home/agent` directory that survives container restarts — keeping agent auth, memory, settings, and MCP servers.
A profile contains a persistent `/home/agent` directory that survives container restarts — keeping agent auth, memory, settings, and MCP servers.

```sh
agentc run -p work # use a named profile
agentc run --profile-dir ~/my-prof # use a custom directory
```

Profiles are stored at `~/.agentc/profiles/<name>/home/`.
Profiles are stored at `~/.agentc/profiles/<name>/`.

### Configurations

Agent configurations are modular setup recipes. Each configuration provides a `prepare.sh` script, optional additional PATH entries, and an entrypoint command. The last configuration's entrypoint is used.
Agent configurations are modular setup recipes. Each configuration provides a `prepare.sh` script and optional additional settings. The last configuration's entrypoint is used.

```sh
# makes sure both Claude Code + GitHub Copilot CLI installed, but invokes GitHub Copilot CLI
Expand All @@ -60,6 +54,26 @@ agentc run -c claude,copilot
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.

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

### Container Images

`agentc` works with any standard container image — it automatically sets up the agent user, sudo, and required tools at container start via an embedded bootstrap script. The default image is pre-configured for faster startup, but you can use any base image:
Expand All @@ -84,11 +98,12 @@ agentc (CLI)
└─ AgentIsolation (runtime-agnostic orchestration)
└─ AgentIsolationAppleContainerRuntime (Apple Containerization, macOS)
└─ AgentIsolationDockerRuntime (Docker Engine API, macOS/Linux)
agentc-bootstrap (In-container bootstrap program)
```

`AgentIsolation` depends only on Foundation and [swift-crypto](https://github.com/apple/swift-crypto). Runtime backends are conditionally compiled via Swift package traits.

The `agentc-bootstrap` binary is a standalone statically linked Linux executable that runs as the container entrypoint. It creates `agent` user and does the rest of agent initialization as needed. It is distributed as a separate release artifact and installed alongside the `agentc` binary.
The `agentc-bootstrap` binary is a standalone statically-linked Linux executable that runs as the container entrypoint. It creates `agent` user and does the rest of agent initialization as needed.

## Development

Expand Down Expand Up @@ -131,11 +146,7 @@ agentc migrate-from-claudec
If you have scripts or muscle memory that use the `claudec` command, you can set up a shell alias:

```sh
# bash / zsh
alias claudec='agentc run --'

# fish
alias claudec 'agentc run --'
```

## License
Expand Down
65 changes: 65 additions & 0 deletions Sources/AgentIsolation/ProjectSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Foundation

/// 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 var agent: AgentSettings?

public struct AgentSettings: Decodable, Sendable, Equatable {
public var image: String?
public var profile: String?
public var excludes: [String]?
public var configurations: [String]?
public var additionalMounts: [String]?
public var defaultArguments: [String]?
public var additionalArguments: [String]?
public var cpus: Int?
public var memoryMiB: Int?
public var bootstrap: String?
public var respectImageEntrypoint: Bool?
}

/// The folder names to probe at each directory level, in priority order.
private static let folderNames = [".boite", ".agentc"]

/// Searches upward from `startDir` for a settings file.
///
/// At each directory level, checks for `settings.json` inside the candidate
/// folders (in priority order). Returns the first successfully decoded
/// settings, or `nil` if the filesystem root is reached without a match.
public static func find(from startDir: URL) -> ProjectSettings? {
var dir = startDir.standardizedFileURL
while true {
for folderName in folderNames {
let settingsURL = dir
.appendingPathComponent(folderName)
.appendingPathComponent("settings.json")
if let settings = load(from: settingsURL) {
return settings
}
}
let parent = dir.deletingLastPathComponent()
if parent.path == dir.path { break }
dir = parent
}
return nil
}

/// Loads settings from a specific folder (e.g. the path given via `--agentc-folder`).
public static func load(fromFolder folder: URL) -> ProjectSettings? {
load(from: folder.appendingPathComponent("settings.json"))
}

/// Loads and decodes a settings file at the given URL.
private static func load(from url: URL) -> ProjectSettings? {
guard let data = try? Data(contentsOf: url),
let settings = try? JSONDecoder().decode(ProjectSettings.self, from: data)
else {
return nil
}
return settings
}
}
25 changes: 16 additions & 9 deletions Sources/agentc/RunCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ struct RunCommand: AsyncParsableCommand {
// Check for legacy claudec data before proceeding
try MigrationCheck.checkIfNeeded(suppress: options.suppressMigrationFromClaudec)

let (_, profileDir) = options.resolveProfile()
let projectSettings = options.loadProjectSettings()

let (_, profileDir) = options.resolveProfile(projectSettings: projectSettings)
let profileHomeDir = profileDir.appending(path: "home")
let workspace = options.resolveWorkspace()
let configurationsDir = options.resolveConfigurationsDir()
let configNames = options.resolveConfigurations(
positional: options.configurationsFlag, profileDir: profileDir)
let excludeFolders = options.resolveExcludeFolders()
positional: options.configurationsFlag, profileDir: profileDir,
projectSettings: projectSettings)
let excludeFolders = options.resolveExcludeFolders(projectSettings: projectSettings)
let allocateTTY = isatty(STDIN_FILENO) == 1 && isatty(STDOUT_FILENO) == 1

// Ensure configurations repo
Expand All @@ -51,19 +54,23 @@ struct RunCommand: AsyncParsableCommand {
updateInterval: options.configurationsUpdateInterval
)

let resolvedImage = options.resolveImage(projectSettings: projectSettings)
let resolvedArguments = options.resolveArguments(
entrypointArguments: entrypointArguments, projectSettings: projectSettings)

let isolationConfig = IsolationConfig(
image: options.image,
image: resolvedImage,
profileHomeDir: profileHomeDir,
workspace: workspace,
excludeFolders: excludeFolders,
configurationsDir: configurationsDir,
configurations: configNames,
bootstrapMode: try options.resolveBootstrapMode(),
arguments: entrypointArguments,
bootstrapMode: try options.resolveBootstrapMode(projectSettings: projectSettings),
arguments: resolvedArguments,
allocateTTY: allocateTTY,
cpuCount: options.cpuCount,
memoryLimitMiB: options.memoryLimitMiB,
additionalHostMounts: options.additionalMount.map { URL(fileURLWithPath: $0) },
cpuCount: options.resolveCpuCount(projectSettings: projectSettings),
memoryLimitMiB: options.resolveMemoryLimitMiB(projectSettings: projectSettings),
additionalHostMounts: options.resolveAdditionalMounts(projectSettings: projectSettings),
verbose: options.verbose
)

Expand Down
Loading
Loading