Files
cleveragents-core/docs/adr/ADR-043-devcontainer-integration.md
freemo c2db74ba81
CI / lint (push) Successful in 15s
CI / build (push) Successful in 16s
CI / quality (push) Successful in 20s
CI / security (push) Successful in 35s
CI / typecheck (push) Successful in 42s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m38s
CI / integration_tests (push) Successful in 3m11s
CI / docker (push) Successful in 39s
CI / coverage (push) Successful in 4m58s
CI / benchmark-publish (push) Successful in 17m32s
Docs: Restyled ADR pages
2026-03-10 12:38:35 -04:00

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
2026-03-01
Proposed
Jeffrey Phillips Freeman
2026-03-01
Accepted
Jeffrey Phillips Freeman
4
Jeffrey Phillips Freeman
null
number title relationship
8 Resource System Devcontainer-instance extends the built-in resource type registry
number title relationship
9 Project Model Execution environment routing adds configuration to the project model
number title relationship
15 Sandbox and Checkpoint Devcontainer sandbox coordination builds on the sandbox model
number title relationship
36 Resource DAG Operational Semantics Devcontainer instances create new sandbox domains in the DAG
number title relationship
38 Cross-Mechanism Sandbox Coordination Devcontainer bind mounts are a key use case for cross-mechanism coordination
number title relationship
39 Container and Execution Environment Resource Types Devcontainer-instance inherits from container-instance defined here
number title relationship
42 Resource Type Inheritance Provides the inheritance mechanism that devcontainer-instance uses
votes_for votes_against abstentions
voter comment
Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com> Auto-detected devcontainers with lazy activation and precedence-based routing enable seamless container integration without disrupting existing workflows

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:

  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 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 via named configurations in .devcontainer/<name>/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.

# 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 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.

# 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:

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:

  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:

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:

  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 buildingactive.
  • 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.