--- adr_number: 43 title: Devcontainer Integration and Container-Project Association status_history: - - '2026-03-01' - Proposed - Jeffrey Phillips Freeman - - '2026-03-01' - Accepted - Jeffrey Phillips Freeman tier: 4 authors: - Jeffrey Phillips Freeman superseded_by: null related_adrs: - number: 8 title: Resource System relationship: Devcontainer-instance extends the built-in resource type registry - number: 9 title: Project Model relationship: Execution environment routing adds configuration to the project model - number: 15 title: Sandbox and Checkpoint relationship: Devcontainer sandbox coordination builds on the sandbox model - number: 36 title: Resource DAG Operational Semantics relationship: Devcontainer instances create new sandbox domains in the DAG - number: 38 title: Cross-Mechanism Sandbox Coordination relationship: Devcontainer bind mounts are a key use case for cross-mechanism coordination - number: 39 title: Container and Execution Environment Resource Types relationship: Devcontainer-instance inherits from container-instance defined here - number: 42 title: Resource Type Inheritance relationship: Provides the inheritance mechanism that devcontainer-instance uses acceptance: votes_for: - voter: Jeffrey Phillips Freeman comment: Auto-detected devcontainers with lazy activation and precedence-based routing enable seamless container integration without disrupting existing workflows votes_against: [] abstentions: [] --- ## Context Modern software development increasingly relies on containerized development environments for reproducibility, isolation, and consistency. The [Development Containers Specification](https://containers.dev) (devcontainers) has emerged as a widely adopted standard for defining these environments via a `.devcontainer/devcontainer.json` configuration file. IDEs (VS Code, IntelliJ), cloud development environments (GitHub Codespaces, Gitpod), and CI systems recognize this standard. CleverAgents already models containers as first-class resources (ADR-039) and supports resource type inheritance (ADR-042). However, the system has no mechanism to: 1. **Auto-detect** devcontainer configurations when a project's git repository or directory contains a `.devcontainer/` folder. 2. **Associate containers with projects** through resource linking — whether the container is a devcontainer, an explicit container with a mounted directory, or a container that clones a remote repository. 3. **Route tool execution** into the appropriate container when a project has one or more container execution environments, with clear precedence rules when multiple containers and devcontainers coexist. Without these capabilities, users must manually configure container environments outside of CleverAgents and ensure tools execute in the right context — losing the system's ability to reason about execution environments, sandbox coordination, and tool dependency resolution. ## Decision Drivers - The devcontainer specification (`.devcontainer/devcontainer.json`) is widely adopted by IDEs and cloud environments, and projects containing it should be auto-detected without manual container configuration - Three distinct container-project association patterns exist (auto-detected devcontainer, explicit mount, remote clone) and all must be expressible through the resource linking model - Execution environment routing must resolve deterministically when multiple containers and devcontainers coexist, with clear precedence (plan > project > auto-detected > fallback > host) - Devcontainer builds can be slow; containers should not be built at discovery time but lazily activated on first plan execution, consistent with the system's lazy sandboxing philosophy - Monorepos with per-service devcontainer configurations require nearest-ancestor resolution to select the correct container for each file path ## Decision CleverAgents introduces: 1. **`devcontainer-instance`** — a built-in resource type that inherits from `container-instance` (using the resource type inheritance mechanism from ADR-042), representing a container defined by a `devcontainer.json` configuration. Auto-discovered when a `git-checkout` or `fs-directory` resource contains a `.devcontainer/` directory. 2. **Container-project association patterns** — three standard patterns for linking containers to projects via the existing resource linking mechanism, with mount and clone semantics expressed through `agents resource add` arguments. 3. **Execution environment routing** — a precedence-based system for determining where tools execute (host vs. container), configurable at project, plan, and resource scope with explicit override semantics. ## Design ### `devcontainer-instance` Resource Type `devcontainer-instance` inherits from `container-instance` and adds devcontainer-specific provisioning, auto-discovery, and lifecycle management. | Field | Value | |-------|-------| | **Inherits** | `container-instance` | | **Kind** | physical | | **Sandbox strategy** | `snapshot` (inherited) | | **User-addable** | yes | | **CLI arguments** | (inherited from `container-instance`) + `--config-path` (path to `.devcontainer/devcontainer.json` or `.devcontainer/` directory, optional), `--workspace-folder` (host folder to mount as workspace, optional) | | **Allowed parents** | `container-runtime` (inherited), `git-checkout` (0..1), `fs-directory` (0..1) | | **Allowed children** | (all inherited from `container-instance`: `container-mount`, `container-exec-env`, `container-port`) | | **Auto-discovery** | Discovered from `git-checkout` or `fs-directory` when `.devcontainer/devcontainer.json` exists in the filesystem tree. Also auto-discovers inherited children (mounts, exec-env, ports) on activation. | | **Capabilities** | read: true, write: true, sandbox: true, checkpoint: true (all inherited) | #### Properties All properties inherited from `container-instance` (`container_id`, `name`, `state`, `image_ref`, `engine_type`, etc.), plus: | Property | Type | Description | |----------|------|-------------| | `devcontainer_config` | object | Parsed contents of `devcontainer.json` | | `config_path` | path | Absolute path to the `devcontainer.json` file | | `workspace_folder` | path | Host folder mounted as the container workspace (from `devcontainer.json` `workspaceFolder` or CLI `--workspace-folder`) | | `workspace_mount` | string | The workspace mount specification (from `devcontainer.json` `workspaceMount`) | | `features` | list | Installed [devcontainer features](https://containers.dev/features) with version pins | | `post_create_command` | string/list | Command(s) to run after container creation | | `post_start_command` | string/list | Command(s) to run after container start | | `post_attach_command` | string/list | Command(s) to run after attaching to the container | | `remote_user` | string | The user to run commands as inside the container | | `container_env` | map | Environment variables set inside the container | | `provisioning_state` | enum | `discovered` \| `building` \| `active` \| `stopped` \| `failed` | #### Lifecycle: Lazy Activation Devcontainer resources follow a **lazy activation** model consistent with the system's lazy sandboxing philosophy: 1. **Discovery** (`provisioning_state: discovered`): When `git-checkout` or `fs-directory` auto-discovery finds `.devcontainer/devcontainer.json`, a `devcontainer-instance` resource is created. The `devcontainer.json` is parsed to populate `devcontainer_config`, `features`, `workspace_folder`, and other configuration properties. **No container is built or started.** No child resources (mounts, exec-env, ports) are created yet — they are pending lazy activation. 2. **Building** (`provisioning_state: building`): When a plan first needs to execute a tool inside the devcontainer, the system triggers the build. This uses the `devcontainer` CLI (if available) or falls back to direct Docker/Podman commands based on the devcontainer.json configuration. The image is built (from Dockerfile, image reference, or docker-compose), features are installed, and the container is created. 3. **Activation** (`provisioning_state: active`): Once the container is running, the inherited `container-instance` auto-discovery runs — discovering `container-mount` children (workspace mount + any additional mounts from devcontainer.json), `container-exec-env` (executables available inside the container), and `container-port` (forwarded ports). Post-create and post-start commands are executed. 4. **Deactivation** (`provisioning_state: stopped`): When the plan completes or the container is no longer needed, the container is stopped (not removed). Subsequent plans can reactivate it without rebuilding. 5. **Failure** (`provisioning_state: failed`): If the build or activation fails, the resource enters the `failed` state with diagnostic information. Plans that require this execution environment will fail with a descriptive error. #### Devcontainer.json Parsing The handler parses `devcontainer.json` to extract: | devcontainer.json field | Mapped to | |-------------------------|-----------| | `image` | `image_ref` property (inherited from container-instance) | | `build.dockerfile` / `dockerFile` | Build configuration for image creation | | `dockerComposeFile` | Compose-based provisioning (future: `compose-instance` subtype) | | `workspaceFolder` | `workspace_folder` property + `container-mount` child | | `workspaceMount` | `workspace_mount` property | | `features` | `features` property | | `forwardPorts` | `container-port` children | | `mounts` | Additional `container-mount` children | | `postCreateCommand` | `post_create_command` property | | `postStartCommand` | `post_start_command` property | | `postAttachCommand` | `post_attach_command` property | | `remoteUser` | `remote_user` property | | `containerEnv` | `container_env` property | | `remoteEnv` | Merged into `container_env` | #### Auto-Discovery from `git-checkout` and `fs-directory` The existing `GitCheckoutHandler` and `FilesystemHandler` auto-discovery is extended: 1. During filesystem scanning, if a `.devcontainer/` directory is found: a. Look for `devcontainer.json` inside it (required). b. If found, create a `devcontainer-instance` child resource with `provisioning_state: discovered`. c. The devcontainer's `workspace_folder` defaults to the `git-checkout` path (or `fs-directory` path). d. Record the `container-mount` relationship between the devcontainer and the parent resource's `fs-directory` child — but do not create the `container-mount` resource yet (pending activation). 2. Multiple `.devcontainer/` directories in nested subdirectories each produce separate `devcontainer-instance` resources. The devcontainer spec allows [multiple configurations](https://containers.dev/implementors/spec/#devcontainerjson) via named configurations in `.devcontainer//devcontainer.json`. 3. The devcontainer resource is marked `auto: true` in the DAG (same as other auto-discovered children). **Updated `git-checkout` auto-discovery result (when `.devcontainer/` is present):** ``` git-checkout (local/web-app) ├── git (repo metadata — branches, commits, trees, ...) ├── fs-directory (worktree root) │ ├── .devcontainer/ │ │ └── devcontainer.json │ ├── src/ │ ├── tests/ │ └── ... └── devcontainer-instance (provisioning_state: discovered) ├── [container-mount — pending activation] ├── [container-exec-env — pending activation] └── [container-port — pending activation] ``` ### Container-Project Association Patterns Three standard patterns for associating containers with projects, all using the existing resource linking mechanism: #### Pattern 1: Devcontainer Auto-Detection The simplest and most common pattern. No explicit container configuration needed. ```bash # 1. Register a git-checkout that contains .devcontainer/ agents resource add git-checkout local/web-app \ --path /home/user/projects/web-app --branch main # Output includes: devcontainer-instance discovered (from .devcontainer/devcontainer.json) # 2. Create project and link the resource agents project create --description "Web application" \ --resource local/web-app local/web-project # The devcontainer is automatically available as an execution environment # for tools operating on local/web-app within local/web-project. ``` #### Pattern 2: Explicit Container with Directory Mount For projects that use a container but do not have a `.devcontainer/` configuration. ```bash # 1. Register the directory/repo agents resource add git-checkout local/api-repo \ --path /home/user/projects/api --branch main # 2. Register a container-instance with a mount to the existing resource agents resource add container-instance local/api-container \ --image node:20-slim \ --mount local/api-repo:/workspace \ --port 3000:3000 # 3. Link both to the project agents project link-resource local/api-project local/api-repo agents project link-resource local/api-project local/api-container ``` The `--mount` argument on `agents resource add container-instance` accepts two formats: - **Resource reference**: `--mount local/api-repo:/workspace` — references a registered resource by name. Creates a DAG `container-mount` child that links to the referenced resource's `fs-directory`. When the resource is a `git-checkout`, the mount targets the worktree root's `fs-directory` child. - **Raw host path**: `--mount /home/user/projects/api:/workspace` — specifies a host path directly. If the path matches an already-registered resource's location, links to that resource. Otherwise, auto-creates an anonymous `fs-directory` resource for the host path and links to it. Both formats specify the container-internal mount target after the colon (`:`) separator. The `--mount` flag is repeatable for multiple mount points. #### Pattern 3: Container with Remote Repo Clone For cloud/CI scenarios where the repository is remote and should be cloned into the container. ```bash # 1. Register a remote git repo (no local checkout) agents resource add git local/upstream \ --url git@github.com:org/api-service.git # 2. Register a container that will clone the repo on activation agents resource add container-instance local/cloud-dev \ --image python:3.13-slim \ --clone-into local/upstream:/workspace \ --port 8000:8000 # 3. Link both to the project agents project link-resource local/cloud-project local/upstream agents project link-resource local/cloud-project local/cloud-dev ``` The `--clone-into` argument records that when the container is activated, the referenced `git` resource should be cloned into the container at the specified path. On activation: 1. The container is created from the specified image. 2. The `git` resource's repository is cloned into the container at the target path. 3. A `container-mount` child is created representing the cloned checkout. 4. The cloned checkout auto-discovers as a `git-checkout` child of the mount, creating virtual equivalence with the remote `git` resource's branches and commits. This pattern is particularly useful for: - CI/CD pipelines where the code is not checked out on the host - Cloud-based development environments - Ephemeral execution environments for untrusted code ### Execution Environment Routing When a project has one or more container execution environments (devcontainers, explicit containers, or both), the system must determine where each tool executes. This is governed by the **execution environment routing** model. #### Configuration Levels Execution environment preferences can be set at three levels: ##### Project-Level Set via `agents project context set` or at project creation time: ```bash agents project context set \ --execution-environment local/dev-container \ --execution-env-priority fallback \ local/my-project ``` Stored in the project configuration: ```yaml execution_environment: default: local/dev-container priority: fallback # fallback | override ``` ##### Plan-Level Set via `agents plan use` arguments, overriding the project-level setting for the duration of the plan: ```bash agents plan use \ --execution-environment local/staging-container \ --execution-env-priority override \ my-action local/my-project ``` ##### Resource-Scoped (Auto-Detected) When a resource (e.g., `git-checkout`) has an auto-detected `devcontainer-instance` child, that devcontainer automatically becomes the execution environment for tools operating on that resource. This is not explicitly configured — it is inferred from the resource DAG. #### Priority Semantics The `priority` field controls how an explicitly configured execution environment interacts with auto-detected devcontainers: - **`fallback`** (default): The configured execution environment is used only when no devcontainer is auto-detected for the current resource or its ancestors. If a resource has a `.devcontainer/`, the devcontainer wins. - **`override`**: The configured execution environment is always used, regardless of whether a devcontainer is present. This is for cases where the user explicitly wants to force a specific container, overriding any devcontainer configurations in the codebase. #### Precedence Chain The full precedence chain for execution environment resolution, from highest to lowest priority: | Priority | Source | When Used | |----------|--------|-----------| | 1 | Plan-level with `priority: override` | Always wins when set | | 2 | Project-level with `priority: override` | Wins if no plan-level override | | 3 | Resource-scoped auto-detected devcontainer (nearest ancestor) | Wins over fallback-priority settings | | 4 | Plan-level with `priority: fallback` | Used when no devcontainer detected for the current resource | | 5 | Project-level with `priority: fallback` | Used when no devcontainer detected and no plan-level fallback | | 6 | Host (no container) | Default when no execution environment is configured anywhere | #### Nearest-Ancestor Devcontainer Resolution When a resource hierarchy contains multiple `.devcontainer/` configurations at different levels, the system uses the **nearest ancestor** rule: 1. Starting from the file or directory being operated on by a tool, walk **up** the filesystem hierarchy toward the resource root. 2. At each level, check if a `.devcontainer/devcontainer.json` exists. 3. Use the **first** (nearest) devcontainer found. Example: A monorepo with per-service devcontainers: ``` monorepo/ ← git-checkout root ├── .devcontainer/ ← root devcontainer (Node.js) │ └── devcontainer.json ├── services/ │ ├── api/ │ │ ├── .devcontainer/ ← service-specific devcontainer (Python) │ │ │ └── devcontainer.json │ │ └── src/ │ └── web/ │ └── src/ └── packages/ ``` - A tool operating on `services/api/src/main.py` uses the **Python devcontainer** (nearest: `services/api/.devcontainer/`). - A tool operating on `services/web/src/index.ts` uses the **Node.js devcontainer** (nearest: root `.devcontainer/`). - A tool operating on `packages/shared/lib.ts` uses the **Node.js devcontainer** (nearest: root `.devcontainer/`). #### Tool Execution Environment Compatibility Tools declare their execution environment compatibility in their capability metadata: ```yaml capability: environment: required: container | host | any # Where the tool CAN run preferred: container | host # Where the tool PREFERS to run (optional) specific: local/dev-container # A specific container required (optional) ``` - `required: any` (default) — tool can run on host or in any container. - `required: container` — tool must run inside a container (e.g., tools that need specific container-only dependencies). - `required: host` — tool must run on the host (e.g., tools that need direct hardware access). - `preferred: container` — tool prefers container execution when available but falls back to host. - `specific: local/dev-container` — tool requires a specific named container. When the execution environment routing selects a container but a tool requires `host`, the tool runs on the host. When no container is available but a tool requires `container`, the plan reports an error for that tool invocation. ### Container-Instance CLI Argument Extensions The existing `container-instance` resource type gains new CLI arguments for mount and clone operations: | Argument | Type | Required | Description | |----------|------|----------|-------------| | `--mount` | string (repeatable) | No | Mount specification in format `:`. Creates a `container-mount` child resource linking the container to the mounted resource or host path. | | `--clone-into` | string (repeatable) | No | Clone specification in format `:`. On activation, clones the referenced `git` resource into the container at the specified path. | These arguments are **inherited** by all subtypes of `container-instance` (including `devcontainer-instance`) via the resource type inheritance mechanism (ADR-042). However, `devcontainer-instance` typically does not need `--mount` because the workspace mount is derived from `devcontainer.json`. ### Sandbox Coordination #### Devcontainer Sandbox Domain A `devcontainer-instance` creates a sandbox domain (inherited from `container-instance`). The sandbox strategy is `snapshot` — container commit/checkpoint for rollback. When a devcontainer bind-mounts the host workspace (the default devcontainer behavior), the mounted files exist in **two sandbox domains simultaneously**: 1. The `git-checkout` sandbox domain (using `git_worktree` strategy) 2. The `devcontainer-instance` sandbox domain (using `snapshot` strategy) This is the cross-mechanism coordination scenario defined in ADR-038. The coherence property for devcontainer workspace bind mounts is `transparent` — changes made inside the container are immediately visible on the host and vice versa, because they share the same underlying storage. For devcontainers that use volume mounts instead of bind mounts (configurable in `devcontainer.json` via `workspaceMount`), the coherence is `independent` — the container has its own copy of the workspace. #### Checkpoint Behavior During plan execution inside a devcontainer: 1. **File-level checkpoints** are handled by the `git_worktree` sandbox on the host side (since bind mounts are transparent). 2. **Container-level checkpoints** (environment state, installed packages, running processes) are handled by the `snapshot` sandbox on the container side. 3. **Rollback** restores both: git worktree reset for files, container snapshot restore for environment state. ## Constraints - Devcontainer auto-discovery only triggers when both `.devcontainer/` directory AND `devcontainer.json` file exist. A `.devcontainer/` directory without a `devcontainer.json` is ignored. - The `devcontainer` CLI is an optional dependency. When not installed, the handler falls back to direct Docker/Podman commands, which may not support all devcontainer features (e.g., features, lifecycle scripts). - Devcontainer build may be slow (pulling images, installing features). The lazy activation model ensures this cost is paid only when needed, but the first plan execution in a devcontainer will have higher latency. - `--clone-into` requires the referenced resource to be of type `git` or a subtype thereof. Non-git resources cannot be cloned. - `--mount` with a resource reference requires the resource to have a filesystem representation (a `git-checkout` with an `fs-directory` child, or a standalone `fs-directory`/`fs-mount`). - Execution environment precedence rules must be deterministic — when multiple devcontainers exist in the hierarchy, the nearest-ancestor rule provides a single unambiguous answer. - Container-project association is always through resource linking (`agents project link-resource`). There are no container-specific flags on `agents project create`. ## Consequences ### Positive - Devcontainer configurations are automatically detected and leveraged, giving agents access to reproducible, fully configured development environments without manual setup. - Three clear association patterns (auto-detect, explicit mount, clone-into) cover the full range of container usage scenarios from local development to cloud CI. - Execution environment routing with `fallback`/`override` priority gives users fine-grained control while preserving sensible defaults. - The nearest-ancestor devcontainer resolution supports monorepos with per-service container configurations. - Lazy activation avoids unnecessary container builds for plans that don't need container execution. - Resource type inheritance (ADR-042) ensures tools bound to `container-instance` work seamlessly with `devcontainer-instance`. ### Negative - Devcontainer.json parsing adds a dependency on understanding the devcontainer specification, which continues to evolve. - The execution environment precedence chain (6 levels) adds complexity to the system's routing logic. - Multi-devcontainer monorepos with nested configurations create complex resolution scenarios that may be surprising to users. - Container build failures during lazy activation cause plan execution failures at runtime rather than at registration time. ### Risks - The devcontainer specification may introduce breaking changes in future versions. Mitigated by parsing only well-established fields and gracefully ignoring unknown fields. - Container engine version mismatches (e.g., devcontainer.json targeting Docker features not available in Podman) may cause activation failures. Mitigated by descriptive error messages and engine compatibility checking during the build phase. - Users may be surprised by automatic devcontainer detection if they have `.devcontainer/` directories they do not intend to use with CleverAgents. Mitigated by the `priority: override` mechanism and by making devcontainer detection configurable (can be disabled via project context settings). ## Alternatives Considered **Devcontainer as a separate top-level resource type (not inheriting from container-instance)** — Would require duplicating all container-instance fields and capabilities. Tools bound to container-instance would not work with the devcontainer type without explicit support. Rejected because it violates DRY and breaks the polymorphism that type inheritance provides. **Container association via new CLI flags on `agents project create`** — Adding `--container` and `--mount-path` flags to project creation. Rejected because the existing resource-centric model (register resources independently, link to projects) is more flexible, avoids special-casing containers in the project creation flow, and is consistent with how all other resources are associated with projects. **Eager devcontainer activation (build on discovery)** — Building the container immediately when `.devcontainer/` is discovered during resource registration. Rejected because it adds significant latency to resource registration, builds containers for plans that may never need them, and is inconsistent with the system's lazy sandboxing philosophy. **Implicit execution environment routing (always prefer container)** — When a project has a container, always execute tools inside it. Rejected because projects may have multiple containers for different purposes (dev, test, deploy), and the user must be able to control which container is used and when. ## Compliance - **Devcontainer detection tests**: `git-checkout` and `fs-directory` auto-discovery correctly detect `.devcontainer/devcontainer.json` and create `devcontainer-instance` resources. - **Lazy activation tests**: Devcontainer resources remain in `discovered` state until a plan requires execution, then transition through `building` → `active`. - **Devcontainer.json parsing tests**: All documented devcontainer.json fields are correctly mapped to resource properties. - **Mount pattern tests**: `--mount` with resource references and raw paths correctly creates `container-mount` children with appropriate DAG links. - **Clone pattern tests**: `--clone-into` correctly records the clone specification and triggers cloning on activation. - **Execution environment routing tests**: The full 6-level precedence chain resolves correctly in all combinations of project/plan/resource settings. - **Nearest-ancestor tests**: Multi-devcontainer hierarchies resolve to the correct nearest devcontainer for each file path. - **Priority override tests**: `priority: override` correctly overrides auto-detected devcontainers; `priority: fallback` correctly defers to them. - **Sandbox coordination tests**: Bind-mounted devcontainer files have `transparent` coherence; volume-mounted files have `independent` coherence. - **Type inheritance tests**: `devcontainer-instance` is correctly recognized as a subtype of `container-instance` by tool bindings, auto-discovery, and DAG queries.