diff --git a/.github/actions/setup-swift/action.yml b/.github/actions/setup-swift/action.yml index b5d79a5..310af93 100644 --- a/.github/actions/setup-swift/action.yml +++ b/.github/actions/setup-swift/action.yml @@ -13,17 +13,17 @@ runs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.3.2" + swift-version: "6.3.3" - name: Cache Static Linux SDK if: inputs.static-sdk == 'true' id: cache-static-sdk - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.swiftpm/swift-sdks - key: static-sdk-6.3.2-${{ runner.arch }} + key: static-sdk-6.3.3-${{ runner.arch }} - name: Install Static Linux SDK if: inputs.static-sdk == 'true' && steps.cache-static-sdk.outputs.cache-hit != 'true' shell: bash - run: swift sdk install https://download.swift.org/swift-6.3.2-release/static-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_static-linux-0.1.0.artifactbundle.tar.gz --checksum 3fd798bef6f4408f1ea5a6f94ce4d4052830c4326ab85ebc04f983f01b3da407 + run: swift sdk install https://download.swift.org/swift-6.3.3-release/static-sdk/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE_static-linux-0.1.0.artifactbundle.tar.gz --checksum 87c3eaf908e67c0e13a84367119e12273cec1d2cd3d81f7d74bb36722d6b607b diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index d9164de..67858b3 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -19,13 +19,13 @@ jobs: uses: actions/checkout@v6 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -64,7 +64,7 @@ jobs: echo "Computed tag: ${VERSION}" - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5c1ef3..0276f56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: tar czf "agentc-${{ matrix.variant }}.tar.gz" agentc - name: Upload agentc artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: agentc-${{ matrix.variant }} path: agentc-${{ matrix.variant }}.tar.gz @@ -112,7 +112,7 @@ jobs: tar czf "agentc-bootstrap-${{ matrix.variant }}.tar.gz" agentc-bootstrap - name: Upload bootstrap artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: agentc-bootstrap-${{ matrix.variant }} path: agentc-bootstrap-${{ matrix.variant }}.tar.gz @@ -125,13 +125,13 @@ jobs: steps: - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts merge-multiple: true - name: Create release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: draft: true files: artifacts/*.tar.gz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3518736..c6ba93c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: uses: ./.github/actions/setup-swift - name: Cache SPM build artifacts - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: .build key: spm-unit-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('Package.swift', 'Package.resolved') }} @@ -82,7 +82,7 @@ jobs: uses: ./.github/actions/setup-swift - name: Cache SPM build artifacts - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: .build key: spm-runtime-e2e-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('Package.swift', 'Package.resolved') }} @@ -184,7 +184,7 @@ jobs: run: docker save ghcr.io/laosb/claudec:latest | gzip > claudec-image.tar.gz - name: Upload image artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: claudec-image-${{ matrix.arch }} path: claudec-image.tar.gz @@ -214,7 +214,7 @@ jobs: static-sdk: "true" - name: Cache SPM build artifacts - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: .build key: spm-integration-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('Package.swift', 'Package.resolved') }} @@ -222,7 +222,7 @@ jobs: spm-integration-${{ matrix.os }}-${{ matrix.arch }}- - name: Download base image - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: claudec-image-${{ matrix.arch }} @@ -253,7 +253,7 @@ jobs: touch ~/.agentc/configurations/.agentc-last-pull - name: Cache shared test profiles - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | ~/.agentc/profiles/__TEST_agentc_shared @@ -288,7 +288,7 @@ jobs: static-sdk: "true" - name: Cache SPM build artifacts - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: .build key: spm-static-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('Package.swift', 'Package.resolved') }} @@ -296,7 +296,7 @@ jobs: spm-static-${{ matrix.os }}-${{ matrix.arch }}- - name: Download base image - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: claudec-image-${{ matrix.arch }} @@ -331,7 +331,7 @@ jobs: touch ~/.agentc/configurations/.agentc-last-pull - name: Cache shared test profiles - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | ~/.agentc/profiles/__TEST_agentc_shared diff --git a/.github/workflows/toolkit.yml b/.github/workflows/toolkit.yml index 9c937d4..3419568 100644 --- a/.github/workflows/toolkit.yml +++ b/.github/workflows/toolkit.yml @@ -72,7 +72,7 @@ jobs: ./check/bin/curl -fsS -o /dev/null https://github.com/ - name: Publish release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ steps.manifest.outputs.tag }} name: agentc Toolkit v${{ steps.manifest.outputs.version }} diff --git a/.swift-version b/.swift-version index 91e4a9f..7849b73 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -6.3.2 +6.3.3 diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..102e555 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,148 @@ +# Building agentc + +This document covers local development and release-style builds. For installation and day-to-day usage, see [README.md](./README.md). + +## Requirements + +- **Swift 6.3+**. CI currently uses Swift 6.3.2. +- **macOS 15+** to build the Apple Containerization backend. +- **macOS or Linux** to build the Docker backend. +- A Docker-compatible daemon is only required when running Docker-backed integration tests or using a Docker build locally. +- A Swift **Static Linux SDK** is required to build the statically linked Linux `agentc` binary or `agentc-bootstrap`. + +The package exposes two Swift package traits: + +- `ContainerRuntimeAppleContainer` — Apple Containerization, macOS only. +- `ContainerRuntimeDocker` — Docker Engine API, macOS and Linux. + +## Development builds + +On macOS, the package defaults to both runtime traits: + +```sh +swift build +``` + +For a Docker-only build, including on Linux: + +```sh +swift build --disable-default-traits --traits ContainerRuntimeDocker +``` + +To build with both runtimes explicitly on macOS: + +```sh +swift build \ + --disable-default-traits \ + --traits ContainerRuntimeAppleContainer,ContainerRuntimeDocker +``` + +## Tests + +Run the runtime-independent and Docker unit tests with: + +```sh +swift test \ + --disable-default-traits \ + --traits ContainerRuntimeDocker \ + --filter 'AgentIsolationTests|AgentIsolationDockerRuntimeTests' +``` + +On macOS, enable both runtime traits when working on Apple Containerization code: + +```sh +swift test \ + --disable-default-traits \ + --traits ContainerRuntimeAppleContainer,ContainerRuntimeDocker +``` + +The integration test suite also needs a working container runtime, a locally available `agentc-bootstrap`, test images, and agent configurations. See [`.github/workflows/test.yml`](./.github/workflows/test.yml) for the CI setup used by the project. + +## Release-style `agentc` builds + +`build.sh` builds the `agentc` executable, selects runtime traits, and copies the result to `./agentc`. + +```sh +./build.sh # release build; platform-default runtimes +./build.sh --debug # debug build +./build.sh --runtimes docker # Docker only +./build.sh --runtimes apple-container,docker # both runtimes on macOS +``` + +Defaults are: + +- macOS: `apple-container,docker` +- Linux: `docker` + +When Apple Containerization is included on macOS, `build.sh` ad-hoc signs the executable with the virtualization entitlement. + +Set build metadata through environment variables: + +```sh +BUILD_VERSION=1.2.3 BUILD_GIT_SHA=$(git rev-parse HEAD) ./build.sh +``` + +These values are reported by `agentc version`. If omitted, the build uses `dev` and the current Git SHA when available. + +## Static Linux builds + +Install a Swift Static Linux SDK matching your toolchain, then pass its SDK name to `build.sh`: + +```sh +./build.sh --runtimes docker --swift-sdk x86_64-swift-linux-musl +./build.sh --runtimes docker --swift-sdk aarch64-swift-linux-musl +``` + +The CI-pinned Static Linux SDK installation is defined in [`.github/actions/setup-swift/action.yml`](./.github/actions/setup-swift/action.yml). + +## Building `agentc-bootstrap` + +`agentc-bootstrap` is the in-container entrypoint and is built separately as a statically linked Linux executable: + +```sh +# x64 +swift build \ + --product agentc-bootstrap \ + -c release \ + --swift-sdk x86_64-swift-linux-musl + +# arm64 +swift build \ + --product agentc-bootstrap \ + -c release \ + --swift-sdk aarch64-swift-linux-musl +``` + +For local development, either install the binary where agentc looks for it: + +```sh +mkdir -p ~/.agentc/bin +cp .build//release/agentc-bootstrap ~/.agentc/bin/bootstrap +chmod +x ~/.agentc/bin/bootstrap +``` + +or pass it explicitly: + +```sh +./agentc run --bootstrap /path/to/agentc-bootstrap +``` + +Released versions of agentc download the matching bootstrap binary automatically on first use. + +## Toolkit + +The agentc Toolkit is released independently from the main executable. Its contents are defined by [`scripts/toolkit/manifest.sh`](./scripts/toolkit/manifest.sh), and its release workflow lives in [`.github/workflows/toolkit.yml`](./.github/workflows/toolkit.yml). + +## Architecture + +```text +agentc (CLI) + ├─ AgentIsolation runtime-agnostic orchestration + ├─ AgentIsolationAppleContainerRuntime Apple Containerization backend + └─ AgentIsolationDockerRuntime Docker Engine backend + +agentc-bootstrap in-container initialization/entrypoint +agentc Toolkit curl, jq, ripgrep, CA bundle +``` + +`AgentIsolation` depends only on Foundation and `swift-crypto`; runtime-specific dependencies are isolated behind Swift package traits. diff --git a/Package.resolved b/Package.resolved index 400ca95..cfb8319 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "ef32d353270b320f22cd06b5428f8e6e4d21ef7cee654f3b90ba727de34babf8", + "originHash" : "10b2896706a17a8663e01b2c526c5e5784d778452544a2fd7256e34075934c5d", "pins" : [ { "identity" : "async-http-client", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "7744c2a035c68ec14726c709f031835e3e30bde1", - "version" : "1.34.0" + "revision" : "9544287b9416c0bc71e58b9f3aead8dd14b16103", + "version" : "1.36.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/containerization.git", "state" : { - "revision" : "44bec8b9933bc491d0cbf44abac90a1f6aaebf6b", - "version" : "0.35.0" + "revision" : "5427fd21ded4b84034126caef5b3182900b4776d", + "version" : "0.41.0" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift-nio-transport.git", "state" : { - "revision" : "2ca31f06658ed288a2560e23ad649acbb3d6b3a3", - "version" : "2.9.0" + "revision" : "eaad084d6c26ff1f2e96f9c2ab76ef84d7165ab6", + "version" : "2.9.2" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-algorithms.git", "state" : { - "revision" : "d0b4a06d0f173a2f3be27d3ea21b3c3aa18db440", - "version" : "1.1.4" + "revision" : "3da39bbc4e687d4192af7c9cf4eab805745a0b9c", + "version" : "1.1.5" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-certificates.git", "state" : { - "revision" : "eaa10d47d19919979dd8df22ff4fd6349b1108d4", - "version" : "1.19.2" + "revision" : "449dbbecd0f31e82b510ada227ca152caa8b5e98", + "version" : "1.19.4" } }, { @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "a878e7f8f46cfc0e1125e565b5c08e7d5272dc9a", - "version" : "1.14.0" + "revision" : "3ffafb9722d5d918c614feb496c8789a3b59d222", + "version" : "1.15.0" } }, { @@ -168,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "cd3e1152083706d77b223fb29110e590efcc70c0", - "version" : "2.101.2" + "revision" : "0b18836bd8b0162e7e17a995a3fbee20ed8f3b2b", + "version" : "2.101.3" } }, { @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "d2eeec0339074034f11a040a74aa2a341a2c4506", - "version" : "1.34.1" + "revision" : "88a51340f59cf181ebde888bd1b749296b3ec029", + "version" : "1.34.3" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "61d1b44f6e4e118792be1cff88ee2bc0267c6f9a", - "version" : "1.44.0" + "revision" : "45bdf670248be5f16ec0340e125dca285536f0fb", + "version" : "1.45.0" } }, { @@ -195,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "407d82d5b6cc00e1c3fb83a81b1539b70c788c5e", - "version" : "2.37.1" + "revision" : "d930168b86f46ca51a4bc09c5ca45c1833db8067", + "version" : "2.37.2" } }, { @@ -238,10 +238,10 @@ { "identity" : "swift-service-lifecycle", "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "location" : "https://github.com/swift-server/swift-service-lifecycle", "state" : { - "revision" : "9829955b385e5bb88128b73f1b8389e9b9c3191a", - "version" : "2.11.0" + "revision" : "7f9326b0326ff86e3646295ea6e891f68c471c5e", + "version" : "2.12.0" } }, { @@ -249,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-subprocess.git", "state" : { - "revision" : "11633673a41f509f8945f23c96c7acd4adafd679", - "version" : "0.5.0" + "revision" : "b3937ab85dd32f6e9435914599c1519074769c1a", + "version" : "1.0.0" } }, { @@ -258,8 +258,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "7502b711c92a17741fa625d722b0ccbd595d8ed1", - "version" : "1.7.2" + "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", + "version" : "1.8.1" } }, { diff --git a/Package.swift b/Package.swift index d92ca9b..e7344ba 100644 --- a/Package.swift +++ b/Package.swift @@ -25,14 +25,15 @@ 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-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.5.0"), + .package(url: "https://github.com/apple/containerization.git", from: "0.41.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.8.2"), + // Containerization 0.41.0 still constrains swift-crypto to the 3.x series. + .package(url: "https://github.com/apple/swift-crypto.git", from: "3.15.1"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.15.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.101.3"), + .package(url: "https://github.com/apple/swift-system.git", from: "1.8.1"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.36.0"), + .package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "1.0.0"), ], targets: [ .target( @@ -45,6 +46,7 @@ let package = Package( name: "AgentIsolationAppleContainerRuntime", dependencies: [ "AgentIsolation", + .product(name: "Crypto", package: "swift-crypto"), .product( name: "Containerization", package: "containerization", condition: .when(platforms: [.macOS])), diff --git a/README.md b/README.md index d033f95..aaffb6b 100644 --- a/README.md +++ b/README.md @@ -2,208 +2,150 @@ Run AI coding agents in isolated containers with persistent profiles and per-project memory isolation. -Supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [ChatGPT Codex CLI](https://learn.chatgpt.com/docs/codex/cli), 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! +`agentc` works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [OpenAI Codex CLI](https://learn.chatgpt.com/docs/codex/cli), [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli), and other agents through pluggable configurations from [agent-isolation-configurations](https://github.com/laosb/agent-isolation-configurations). + +## Highlights + +- **Isolated execution** — use Apple Containerization on macOS or a Docker-compatible runtime on macOS/Linux. On Docker, agentc prefers Kata Containers or gVisor over `runc` when available. +- **Persistent profiles** — keep agent authentication, settings, MCP servers, and memory across sessions without mixing every project into one workspace. +- **Pluggable agents** — switch or combine agent configurations without rebuilding agentc. +- **Bring your own image** — run on normal or minimal container images; the bootstrap and Toolkit provide the basics needed to get started. +- **Project-aware defaults** — store agent, resource, environment, mount, and image settings in `.agentc/settings.json`. ## Install ### Prerequisites -**macOS (Apple Container runtime):** macOS 15+, Apple Silicon or Intel. +**macOS with Apple Containerization:** macOS 15+, Apple Silicon or Intel. -**macOS / Linux (Docker runtime):** x64 or arm64, Docker Engine API v1.44+ (Docker, Podman with Docker compatibility, etc.). +**macOS / Linux with Docker:** x64 or arm64, with a Docker Engine API v1.44+ compatible daemon such as Docker or Podman in Docker-compatible mode. > [!IMPORTANT] -> Standard Docker containers share the host kernel. Because agents run untrusted code, -> `agentc` automatically prefers Kata Containers or gVisor when available and warns when -> only standard `runc` isolation is available. See [*Safer Docker Isolation*](./docs/docker-runtimes.md). +> Standard Docker containers share the host kernel. Because coding agents run code you did not write, agentc automatically prefers Kata Containers or gVisor when available and warns when only standard `runc` isolation is available. See [Safer Docker Isolation](./docs/docker-runtimes.md). -### Install +Install the latest release: ```sh curl -fsSL https://raw.githubusercontent.com/laosb/agentc/main/install.sh | sh ``` -## Quick Start +## Quick start + +Run the default agent, Claude Code, in the current directory: ```sh -agentc run # start default agent (claude) in $PWD -agentc run -c codex # use a different agent configuration -agentc run "explain this code" # forward args to the agent entrypoint -agentc run -e TZ=Europe/Berlin # set a container environment variable -agentc sh # open a shell in the container -agentc sh -- ls -la /home/agent # run a command inside the container +agentc run ``` -Use `agentc --help` and `agentc --help` for full CLI reference. +Choose another agent configuration or forward arguments to the agent: -### Profiles +```sh +agentc run -c codex +agentc run -c copilot +agentc run -c claude -- --model opus +agentc run -- "explain this code" +``` -A profile contains a persistent `/home/agent` directory that survives container restarts — keeping agent auth, memory, settings, and MCP servers. +Open a shell in the same kind of isolated environment: ```sh -agentc run -p work # use a named profile -agentc run --profile-dir ~/my-prof # use a custom directory +agentc sh +agentc sh -- ls -la /home/agent ``` -Profiles are stored at `~/.agentc/profiles//`. +Use `agentc --help` and `agentc --help` for the full CLI reference. + +## Agent configurations -### Configurations +Agent configurations are modular setup recipes. They install or prepare an agent and can depend on other configurations. -Agent configurations are modular setup recipes. Each configuration provides a `prepare.sh` script and optional additional settings. A configuration may declare `dependsOn` to pull in other configurations, which are always activated before it. The last activated configuration that defines an entrypoint provides the command to run. +Pass a comma-separated list with `-c` / `--configurations`: ```sh -# makes sure both Claude Code + GitHub Copilot CLI installed, but invokes GitHub Copilot CLI +# Prepare Claude Code and GitHub Copilot CLI, then launch Copilot. agentc run -c claude,copilot - -# just GitHub Copilot CLI -agentc run -c copilot ``` -### Toolkit +Configurations are activated in order; the last activated configuration that defines an entrypoint provides the command that runs. See [agent-isolation-configurations](https://github.com/laosb/agent-isolation-configurations) for the available configurations and their definitions. -Every session mounts the **agentc Toolkit** read-only at `/agent-isolation/toolkit`: a -small set of static binaries — `curl`, `jq`, `ripgrep` — and a CA bundle. It exists so -agents always have some basic tools to work with even in `alpine`-like slim images. +## Persistent profiles -The toolkit is designed in such a way that they would not override what the image itself -provides. +A profile keeps a persistent agent home directory across container restarts. Its `home` directory is mounted as `/home/agent`, so agent authentication, settings, memory, and MCP configuration survive between sessions. ```sh -agentc run --no-toolkit # only what the image itself provides -agentc run --toolkit ~/my-toolkit # a locally built bundle -``` +agentc run -p work +agentc run -p personal -The toolkit is versioned independently of agentc and downloaded once, to -`~/.agentc/toolkit/v`. Its contents are defined by -[`scripts/toolkit/manifest.sh`](./scripts/toolkit/manifest.sh). +agentc profiles +agentc profiles list work -### Project Settings - -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. +agentc run --profile-dir ~/my-agent-profile +``` -See [docs/project-settings.md](./docs/project-settings.md) for the full schema and override rules. +Profiles are stored under `~/.agentc/profiles//`. You can also use a custom directory. -### Mount Paths +## Project settings -By default, host-backed paths use the `workspace` scheme. The workspace and every -`--additional-mount` receive stable destinations beneath `/workspace`: +Initialize a project to create `.agentc/settings.json` and prepare its container environment: -```text -/Users/me/project → /workspace/project- +```sh +agentc init ``` -Use `--mount-path-scheme host` when a script or external protocol requires the same -absolute path inside and outside the container: +You can set useful defaults while initializing: ```sh -agentc run \ - --mount-path-scheme host \ - --additional-mount /Users/me/shared +agentc init -c codex --cpus 4 --memory-mib 4096 ``` -In this mode, the workspace working directory remains `/Users/me/project`, and the -additional mount remains `/Users/me/shared`. The bind source is still canonicalized, -but the destination preserves the standardized caller-visible path without resolving -symlinks. For example, a requested macOS path under `/tmp/project` remains -`/tmp/project` in the container even when its bind source is `/private/tmp/project`. - -Path preservation applies only to resources agentc mounts: the workspace, -`--additional-mount`, and `agent.additionalMounts`. It does not expose arbitrary host -executables, sockets, or files. Configuration-repository `additionalMounts` are -profile-backed container paths and retain their existing semantics. Host mode rejects -`/` and exact collisions with agentc-owned destinations such as `/home/agent`, -`/agent-isolation/agents`, `/agent-isolation/toolkit`, and `/entrypoint-bootstrap`. - -Set the project default with `agent.mountPathScheme`; the CLI flag overrides it. The -default remains `workspace`. - -### Scripted Execution I/O +CLI flags override project settings, while mergeable fields such as excludes and additional mounts are combined. See [Project Settings](./docs/project-settings.md) for the full schema and resolution rules. -For `agentc run` and `agentc sh`, stdout belongs to the launched workload. Agentc -progress, setup output, warnings, and verbose diagnostics go to stderr, including -output produced by configuration `prepare.sh` scripts. This makes non-interactive -output safe to pipe or parse while preserving the existing automatic TTY behavior. +## Container images and Toolkit -### 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. Images that ship no tooling of their own are covered by the [toolkit](#toolkit). The default image is pre-configured for faster startup, but you can use any base image: - -```sh -agentc run -i debian:latest # stock Debian -agentc run -i alpine:latest # Alpine Linux -agentc run -i buildpack-deps:scm # Debian + git, curl, etc. -agentc run -i my-custom-image:latest # your own image -``` - -To skip the bootstrap and use the image's own entrypoint: +agentc can use any standard container image. By default, its bootstrap prepares the agent user and environment before launching the configured agent. ```sh -agentc run --respect-image-entrypoint -i my-image:latest +agentc run -i debian:latest +agentc run -i alpine:latest +agentc run -i buildpack-deps:scm +agentc run -i my-custom-image:latest ``` -### Docker Isolation +By default, agentc mounts an **agentc Toolkit** into the container, which provides `curl`, `jq`, `ripgrep` in `$PATH`, if they are not supplied by the image. This allows you to use coding agents on virtually any kind of Docker images. -Agents run code you did not write, so on the Docker backend `agentc` asks the daemon which -runtimes it has and prefers the strongest isolation available: **Kata Containers** (a VM per -container) over **gVisor** (`runsc`) over **`runc`** (shares the host kernel). If only `runc` -is available, `agentc` uses it and prints a warning with setup instructions. +## Workspaces and additional mounts -Pick one yourself — including `runc`, which also silences the warning: +By default, host-backed paths use the `workspace` mount scheme: the project workspace and additional mounts receive stable destinations under `/workspace`. ```sh -agentc run --docker-runtime runsc -agentc run --docker-runtime runc # "I know, runc is fine here" +agentc run --additional-mount ~/shared ``` -See [Safer Docker Isolation](./docs/docker-runtimes.md). The Apple Container backend already -gives every container its own VM, so runtime selection applies only to the Docker backend. +When an editor, protocol, or script requires the same absolute path inside and outside the container, use the `host` scheme: -## Architecture - -``` -agentc (CLI) - └─ AgentIsolation (runtime-agnostic orchestration) - └─ AgentIsolationAppleContainerRuntime (Apple Containerization, macOS) - └─ AgentIsolationDockerRuntime (Docker Engine API, macOS/Linux) -agentc-bootstrap (In-container bootstrap program) +```sh +agentc run \ + --mount-path-scheme host \ + --additional-mount /Users/me/shared ``` -`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. +The project default can be set with `agent.mountPathScheme` in `.agentc/settings.json`; the CLI flag overrides it. -The agentc Toolkit is built separately from `scripts/toolkit/manifest.sh` and released on its own -schedule; see [Toolkit](#toolkit). +## Scripted execution -## Development +For `agentc run` and `agentc sh`, stdout belongs to the launched workload. agentc progress, setup output, warnings, and verbose diagnostics go to stderr, including configuration `prepare.sh` output. -Swift 6.1+. Tested on Swift 6.3. +That makes non-interactive output safe to pipe or parse: ```sh -swift build # debug build (default traits) -swift build --traits ContainerRuntimeDocker # Docker-only -swift test --filter AgentIsolationTests # unit tests -./build.sh # release build + codesign -./build.sh --runtimes docker # Docker-only release +agentc run -- "summarize this project" > summary.txt ``` -Set `BUILD_VERSION` and `BUILD_GIT_SHA` environment variables before `build.sh` to inject version info into the `agentc version` output. - -### Bootstrap binary - -The `agentc-bootstrap` binary is the container entrypoint. It must be built separately as a statically linked Linux binary: +This allows you to use agentc to run stdio-based MCP or ACP. Automatic TTY behavior is preserved for interactive sessions. -```sh -# Build for the current architecture (requires Static Linux SDK) -swift build --product agentc-bootstrap -c release --swift-sdk x86_64-swift-linux-musl # x64 -swift build --product agentc-bootstrap -c release --swift-sdk aarch64-swift-linux-musl # arm64 - -# Install to the expected location -mkdir -p ~/.agentc/bin -cp .build//release/agentc-bootstrap ~/.agentc/bin/bootstrap -``` +## Development -For released versions, `agentc` automatically downloads the matching bootstrap binary on first run. During development, you can also use `--bootstrap ` to specify a custom bootstrap binary or shell script, or `--respect-image-entrypoint` to skip the bootstrap entirely. +Building agentc, running tests, creating static Linux binaries, and building `agentc-bootstrap` are documented in [BUILD.md](./BUILD.md). ## License diff --git a/Sources/AgentIsolationAppleContainerRuntime/AppleContainerDefaultKernel.swift b/Sources/AgentIsolationAppleContainerRuntime/AppleContainerDefaultKernel.swift new file mode 100644 index 0000000..c145982 --- /dev/null +++ b/Sources/AgentIsolationAppleContainerRuntime/AppleContainerDefaultKernel.swift @@ -0,0 +1,58 @@ +import Crypto +import Foundation + +struct LinuxKernelVersion: Comparable, Sendable { + let components: [Int] + + init?(filename: String) { + guard let marker = filename.range(of: "vmlinux-") else { return nil } + let suffix = filename[marker.upperBound...] + let versionPrefix = suffix.prefix { character in + character.isNumber || character == "." || character == "-" + } + let components = versionPrefix.split { !$0.isNumber }.compactMap { Int($0) } + guard components.count >= 3 else { return nil } + self.components = components + } + + static func < (lhs: Self, rhs: Self) -> Bool { + let componentCount = max(lhs.components.count, rhs.components.count) + for index in 0.. Bool { + guard let installedVersion = LinuxKernelVersion(filename: filename) else { return false } + return installedVersion >= version + } + + static func sha256Digest(of file: URL) throws -> String { + let handle = try FileHandle(forReadingFrom: file) + defer { try? handle.close() } + + var hasher = SHA256() + while let chunk = try handle.read(upToCount: 1_048_576), !chunk.isEmpty { + hasher.update(data: chunk) + } + return "sha256:" + hasher.finalize().map { String(format: "%02x", $0) }.joined() + } +} diff --git a/Sources/AgentIsolationAppleContainerRuntime/AppleContainerRuntime.swift b/Sources/AgentIsolationAppleContainerRuntime/AppleContainerRuntime.swift index 571006f..3bed2cd 100644 --- a/Sources/AgentIsolationAppleContainerRuntime/AppleContainerRuntime.swift +++ b/Sources/AgentIsolationAppleContainerRuntime/AppleContainerRuntime.swift @@ -50,7 +50,7 @@ self.manager = try await ContainerManager( kernel: kernel, - initfsReference: "ghcr.io/apple/containerization/vminit:0.29.0", + initfsReference: "ghcr.io/apple/containerization/vminit:0.41.0", imageStore: store, network: network ) @@ -317,40 +317,66 @@ // MARK: - Kernel private func getOrDownloadKernel() async throws -> Kernel { - // 1. Try the container app's installed kernel + // 1. Reuse Apple Container's installed kernel when it is at least as new as + // Apple's current default. Older kernels are skipped and upgraded in our cache. let appKernelLink = Self.containerAppDataRoot .appendingPathComponent("kernels") .appendingPathComponent("default.kernel-arm64") let appKernelResolved = appKernelLink.resolvingSymlinksInPath() - if FileManager.default.fileExists(atPath: appKernelResolved.path) { + let appKernelExists = FileManager.default.fileExists(atPath: appKernelResolved.path) + if appKernelExists, + AppleContainerDefaultKernel.isCurrentOrNewer( + filename: appKernelResolved.lastPathComponent) + { return Kernel(path: appKernelResolved, platform: .linuxArm) } - // 2. Try our own cached kernel + // 2. Try our own current cached kernel. Using Apple's versioned filename makes + // an agentc update automatically bypass any older cached kernel. let ourKernelDir = storagePath.appendingPathComponent("kernels") let ourKernelLink = ourKernelDir.appendingPathComponent("default.kernel-arm64") - let ourKernelResolved = ourKernelLink.resolvingSymlinksInPath() - if FileManager.default.fileExists(atPath: ourKernelResolved.path) { - return Kernel(path: ourKernelResolved, platform: .linuxArm) + let kernelBinary = ourKernelDir.appendingPathComponent(AppleContainerDefaultKernel.filename) + if FileManager.default.fileExists(atPath: kernelBinary.path) { + try? FileManager.default.removeItem(at: ourKernelLink) + try FileManager.default.createSymbolicLink( + at: ourKernelLink, withDestinationURL: kernelBinary) + return Kernel(path: kernelBinary, platform: .linuxArm) } - // 3. Download kernel from kata-containers - fputs("agentc: downloading kernel (one-time setup)...\n", stderr) - let tarURL = URL( - string: - "https://github.com/kata-containers/kata-containers/releases/download/3.26.0/kata-static-3.26.0-arm64.tar.zst" - )! - let kernelPathInArchive = "opt/kata/share/kata-containers/vmlinux-6.18.5-177" - - let (tempFile, _) = try await URLSession.shared.download(from: tarURL) + // 3. Download and verify Apple's current default kernel. + let existingCachedKernel = ourKernelLink.resolvingSymlinksInPath() + let isUpgrade = + appKernelExists + || FileManager.default.fileExists(atPath: existingCachedKernel.path) + let action = isUpgrade ? "upgrading to" : "installing" + fputs( + "agentc: \(action) Apple Containers default kernel " + + "(Kata \(AppleContainerDefaultKernel.kataVersion))...\n", + stderr) + let tarURL = URL(string: AppleContainerDefaultKernel.archiveURL)! + + let (tempFile, response) = try await URLSession.shared.download(from: tarURL) defer { try? FileManager.default.removeItem(at: tempFile) } + guard let response = response as? HTTPURLResponse, + (200..<300).contains(response.statusCode) + else { + throw AppleContainerRuntimeError.kernelDownloadFailed( + statusCode: (response as? HTTPURLResponse)?.statusCode) + } + + let actualDigest = try AppleContainerDefaultKernel.sha256Digest(of: tempFile) + guard actualDigest == AppleContainerDefaultKernel.archiveDigest else { + throw AppleContainerRuntimeError.unexpectedKernelArchiveDigest( + expected: AppleContainerDefaultKernel.archiveDigest, + actual: actualDigest) + } let archiveReader = try ArchiveReader(file: tempFile) - let (_, kernelData) = try archiveReader.extractFile(path: kernelPathInArchive) + let (_, kernelData) = + try archiveReader.extractFile(path: AppleContainerDefaultKernel.binaryPath) try FileManager.default.createDirectory(at: ourKernelDir, withIntermediateDirectories: true) - let kernelBinary = ourKernelDir.appendingPathComponent("vmlinux-6.18.5-177") try kernelData.write(to: kernelBinary, options: .atomic) try? FileManager.default.removeItem(at: ourKernelLink) @@ -435,11 +461,21 @@ public enum AppleContainerRuntimeError: LocalizedError { case notPrepared + case kernelDownloadFailed(statusCode: Int?) + case unexpectedKernelArchiveDigest(expected: String, actual: String) public var errorDescription: String? { switch self { case .notPrepared: return "Container runtime has not been prepared. Call prepare() first." + case .kernelDownloadFailed(let statusCode): + if let statusCode { + return "Failed to download the Apple Containers kernel (HTTP \(statusCode))." + } + return "Failed to download the Apple Containers kernel: invalid HTTP response." + case .unexpectedKernelArchiveDigest(let expected, let actual): + return + "Apple Containers kernel archive digest mismatch: expected \(expected), got \(actual)." } } } diff --git a/Sources/agentc/Commands/ImagesCommand.swift b/Sources/agentc/Commands/ImagesCommand.swift index b22d10c..dc6f6f6 100644 --- a/Sources/agentc/Commands/ImagesCommand.swift +++ b/Sources/agentc/Commands/ImagesCommand.swift @@ -1,7 +1,7 @@ import AgentIsolation import ArgumentParser -#if ContainerRuntimeAppleContainer +#if os(macOS) && ContainerRuntimeAppleContainer import AgentIsolationAppleContainerRuntime #endif #if ContainerRuntimeDocker @@ -46,7 +46,7 @@ struct ImageRuntimeOptions: ParsableArguments { switch RuntimeChoice.resolve(explicit: runtime) { case .appleContainer: - #if ContainerRuntimeAppleContainer + #if os(macOS) && ContainerRuntimeAppleContainer return try await operation(AppleContainerRuntime(config: config)) #else throw AgentcError.runtimeNotAvailable("apple-container") diff --git a/Sources/agentc/ConfigurationsManager.swift b/Sources/agentc/ConfigurationsManager.swift index 3e2a6ba..a9e4cca 100644 --- a/Sources/agentc/ConfigurationsManager.swift +++ b/Sources/agentc/ConfigurationsManager.swift @@ -90,7 +90,7 @@ enum ConfigurationsManager { error: .discarded ) // Update marker regardless of pull success (avoid repeated failures) - FileManager.default.createFile(atPath: markerFile.path, contents: nil) + _ = FileManager.default.createFile(atPath: markerFile.path, contents: nil) } } diff --git a/Sources/agentc/SessionRunner.swift b/Sources/agentc/SessionRunner.swift index c9e2cbe..8e3e3d3 100644 --- a/Sources/agentc/SessionRunner.swift +++ b/Sources/agentc/SessionRunner.swift @@ -6,7 +6,7 @@ import AgentIsolation import Foundation #endif -#if ContainerRuntimeAppleContainer +#if os(macOS) && ContainerRuntimeAppleContainer import AgentIsolationAppleContainerRuntime #endif #if ContainerRuntimeDocker @@ -118,7 +118,7 @@ enum SessionRunner { throw AgentcError.runtimeNotAvailable("docker") #endif case .appleContainer: - #if ContainerRuntimeAppleContainer + #if os(macOS) && ContainerRuntimeAppleContainer try await executeSession( runtime: AppleContainerRuntime(config: runtimeConfig), config: config, diff --git a/Tests/AgentIsolationAppleContainerRuntimeTests/AppleContainerDefaultKernelTests.swift b/Tests/AgentIsolationAppleContainerRuntimeTests/AppleContainerDefaultKernelTests.swift new file mode 100644 index 0000000..a0d914a --- /dev/null +++ b/Tests/AgentIsolationAppleContainerRuntimeTests/AppleContainerDefaultKernelTests.swift @@ -0,0 +1,61 @@ +#if ContainerRuntimeAppleContainer + import Foundation + import Testing + + @testable import AgentIsolationAppleContainerRuntime + + @Suite("Apple Containers default kernel") + struct AppleContainerDefaultKernelTests { + @Test("metadata matches Apple's Kata 3.32.0 default") + func defaultMetadata() { + #expect(AppleContainerDefaultKernel.kataVersion == "3.32.0") + #expect(AppleContainerDefaultKernel.filename == "vmlinux-6.18.35-197-debug") + #expect( + AppleContainerDefaultKernel.archiveDigest + == "sha256:8736c054d9223974735394f822000823baef509e1c33405ec798240fa9b6e4b5") + } + + @Test( + "older kernels are upgraded", + arguments: [ + "vmlinux-6.18.5-177", + "vmlinux-6.18.15-186", + "vmlinux-6.18.35", + "vmlinux-6.18.35-196-debug", + ]) + func olderKernels(filename: String) { + #expect(!AppleContainerDefaultKernel.isCurrentOrNewer(filename: filename)) + } + + @Test("Apple's current kernel is reused") + func currentKernel() { + #expect( + AppleContainerDefaultKernel.isCurrentOrNewer( + filename: "vmlinux-6.18.35-197-debug")) + } + + @Test("newer installed kernels are not downgraded") + func newerKernel() { + #expect( + AppleContainerDefaultKernel.isCurrentOrNewer( + filename: "vmlinux-6.19.1-200-debug")) + } + + @Test("unversioned kernels are refreshed") + func unversionedKernel() { + #expect(!AppleContainerDefaultKernel.isCurrentOrNewer(filename: "default.kernel-arm64")) + } + + @Test("download digest is computed as SHA-256") + func archiveDigest() throws { + let file = FileManager.default.temporaryDirectory + .appendingPathComponent("agentc-kernel-digest-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: file) } + try Data("abc".utf8).write(to: file) + + #expect( + try AppleContainerDefaultKernel.sha256Digest(of: file) + == "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + } + } +#endif diff --git a/Tests/AgentIsolationAppleContainerRuntimeTests/AppleContainerRuntimeTests.swift b/Tests/AgentIsolationAppleContainerRuntimeTests/AppleContainerRuntimeTests.swift index bc26526..3fd10a1 100644 --- a/Tests/AgentIsolationAppleContainerRuntimeTests/AppleContainerRuntimeTests.swift +++ b/Tests/AgentIsolationAppleContainerRuntimeTests/AppleContainerRuntimeTests.swift @@ -67,7 +67,7 @@ @Test("ghcr.io reference is unchanged") func ghcrReference() { - let ref = "ghcr.io/apple/containerization/vminit:0.29.0" + let ref = "ghcr.io/apple/containerization/vminit:0.41.0" let result = AppleContainerRuntime.normalizedDockerHubRef(ref) #expect(result == ref) } diff --git a/Tests/AgentcIntegrationTests/AgentcHelpers.swift b/Tests/AgentcIntegrationTests/AgentcHelpers.swift index 79552de..0b8b534 100644 --- a/Tests/AgentcIntegrationTests/AgentcHelpers.swift +++ b/Tests/AgentcIntegrationTests/AgentcHelpers.swift @@ -52,7 +52,7 @@ func runAgentc( case .signaled(let sig): exitCode = sig } return ProcessOutput( - exitCode: exitCode, stdout: result.standardOutput ?? "", stderr: result.standardError ?? "") + exitCode: exitCode, stdout: result.standardOutput, stderr: result.standardError) } catch { return ProcessOutput(exitCode: -1, stdout: "", stderr: "launch error: \(error)") }