29 KiB
adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
| adr_number | title | status_history | tier | authors | superseded_by | related_adrs | acceptance | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 43 | Devcontainer Integration and Container-Project Association |
|
4 |
|
null |
|
|
Context
Modern software development increasingly relies on containerized development environments for reproducibility, isolation, and consistency. The Development Containers Specification (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:
- Auto-detect devcontainer configurations when a project's git repository or directory contains a
.devcontainer/folder. - 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.
- 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:
-
devcontainer-instance— a built-in resource type that inherits fromcontainer-instance(using the resource type inheritance mechanism from ADR-042), representing a container defined by adevcontainer.jsonconfiguration. Auto-discovered when agit-checkoutorfs-directoryresource contains a.devcontainer/directory. -
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 addarguments. -
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 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:
-
Discovery (
provisioning_state: discovered): Whengit-checkoutorfs-directoryauto-discovery finds.devcontainer/devcontainer.json, adevcontainer-instanceresource is created. Thedevcontainer.jsonis parsed to populatedevcontainer_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. -
Building (
provisioning_state: building): When a plan first needs to execute a tool inside the devcontainer, the system triggers the build. This uses thedevcontainerCLI (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. -
Activation (
provisioning_state: active): Once the container is running, the inheritedcontainer-instanceauto-discovery runs — discoveringcontainer-mountchildren (workspace mount + any additional mounts from devcontainer.json),container-exec-env(executables available inside the container), andcontainer-port(forwarded ports). Post-create and post-start commands are executed. -
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. -
Failure (
provisioning_state: failed): If the build or activation fails, the resource enters thefailedstate 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:
-
During filesystem scanning, if a
.devcontainer/directory is found: a. Look fordevcontainer.jsoninside it (required). b. If found, create adevcontainer-instancechild resource withprovisioning_state: discovered. c. The devcontainer'sworkspace_folderdefaults to thegit-checkoutpath (orfs-directorypath). d. Record thecontainer-mountrelationship between the devcontainer and the parent resource'sfs-directorychild — but do not create thecontainer-mountresource yet (pending activation). -
Multiple
.devcontainer/directories in nested subdirectories each produce separatedevcontainer-instanceresources. The devcontainer spec allows multiple configurations via named configurations in.devcontainer/<name>/devcontainer.json. -
The devcontainer resource is marked
auto: truein 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.
# 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.
# 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 DAGcontainer-mountchild that links to the referenced resource'sfs-directory. When the resource is agit-checkout, the mount targets the worktree root'sfs-directorychild. -
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 anonymousfs-directoryresource 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.
# 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:
- The container is created from the specified image.
- The
gitresource's repository is cloned into the container at the target path. - A
container-mountchild is created representing the cloned checkout. - The cloned checkout auto-discovers as a
git-checkoutchild of the mount, creating virtual equivalence with the remotegitresource'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:
agents project context set \
--execution-environment local/dev-container \
--execution-env-priority fallback \
local/my-project
Stored in the project configuration:
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:
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:
- Starting from the file or directory being operated on by a tool, walk up the filesystem hierarchy toward the resource root.
- At each level, check if a
.devcontainer/devcontainer.jsonexists. - 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.pyuses the Python devcontainer (nearest:services/api/.devcontainer/). - A tool operating on
services/web/src/index.tsuses the Node.js devcontainer (nearest: root.devcontainer/). - A tool operating on
packages/shared/lib.tsuses the Node.js devcontainer (nearest: root.devcontainer/).
Tool Execution Environment Compatibility
Tools declare their execution environment compatibility in their capability metadata:
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 <resource-or-path>:<container-path>. 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 <git-resource>:<container-path>. 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:
- The
git-checkoutsandbox domain (usinggit_worktreestrategy) - The
devcontainer-instancesandbox domain (usingsnapshotstrategy)
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:
- File-level checkpoints are handled by the
git_worktreesandbox on the host side (since bind mounts are transparent). - Container-level checkpoints (environment state, installed packages, running processes) are handled by the
snapshotsandbox on the container side. - Rollback restores both: git worktree reset for files, container snapshot restore for environment state.
Constraints
- Devcontainer auto-discovery only triggers when both
.devcontainer/directory ANDdevcontainer.jsonfile exist. A.devcontainer/directory without adevcontainer.jsonis ignored. - The
devcontainerCLI 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-intorequires the referenced resource to be of typegitor a subtype thereof. Non-git resources cannot be cloned.--mountwith a resource reference requires the resource to have a filesystem representation (agit-checkoutwith anfs-directorychild, or a standalonefs-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 onagents 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/overridepriority 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-instancework seamlessly withdevcontainer-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 thepriority: overridemechanism 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-checkoutandfs-directoryauto-discovery correctly detect.devcontainer/devcontainer.jsonand createdevcontainer-instanceresources. - Lazy activation tests: Devcontainer resources remain in
discoveredstate until a plan requires execution, then transition throughbuilding→active. - Devcontainer.json parsing tests: All documented devcontainer.json fields are correctly mapped to resource properties.
- Mount pattern tests:
--mountwith resource references and raw paths correctly createscontainer-mountchildren with appropriate DAG links. - Clone pattern tests:
--clone-intocorrectly 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: overridecorrectly overrides auto-detected devcontainers;priority: fallbackcorrectly defers to them. - Sandbox coordination tests: Bind-mounted devcontainer files have
transparentcoherence; volume-mounted files haveindependentcoherence. - Type inheritance tests:
devcontainer-instanceis correctly recognized as a subtype ofcontainer-instanceby tool bindings, auto-discovery, and DAG queries.