GitCheckoutHandler.discover_children() and FsDirectoryHandler.discover_children() now call discover_devcontainers() after scanning for fs-directory children. Any .devcontainer/devcontainer.json or root-level .devcontainer.json found at the resource location is registered as a devcontainer-instance child resource with provisioning_state: discovered. Named configurations are also discovered and carry the configuration name in the config_name property. This wires the previously-isolated discover_devcontainers() function into the production code path, enabling the spec's zero-configuration devcontainer experience. ISSUES CLOSED: #4740
13 KiB
Devcontainer Resources
Devcontainer resources integrate the Development Containers Specification into the CleverAgents resource model. Two resource types and an auto-discovery mechanism allow CleverAgents to detect, register, and use devcontainer environments automatically.
Resource Types
devcontainer-instance
A container execution environment defined by a
.devcontainer/devcontainer.json configuration file. Inherits from
container-instance (see ADR-042 for resource type inheritance).
| Property | Value |
|---|---|
| Kind | physical |
| Sandbox strategy | none (container provides isolation) |
| User-addable | yes |
| Parent types | git-checkout, fs-directory |
| Child types | devcontainer-file |
| Handler | DevcontainerHandler |
CLI arguments:
--path(path, required): Path to the directory containing.devcontainer/devcontainer.json.
devcontainer-file
A single-file resource pointing to the devcontainer.json
configuration file itself. Created automatically as a child of
devcontainer-instance during auto-discovery.
| Property | Value |
|---|---|
| Kind | physical |
| Sandbox strategy | copy_on_write |
| User-addable | no |
| Parent types | devcontainer-instance |
| Child types | (none) |
| Handler | DevcontainerHandler |
container-instance
The base container resource type from which devcontainer-instance
inherits. Represents a generic container execution environment.
| Property | Value |
|---|---|
| Kind | physical |
| Sandbox strategy | none (container provides isolation) |
| User-addable | yes |
| Parent types | git-checkout, fs-directory |
| Child types | (none) |
| Handler | DevcontainerHandler |
Auto-Discovery
Auto-discovery is wired into GitCheckoutHandler.discover_children() and
FsDirectoryHandler.discover_children() (issue #4740). When either handler
scans a resource location, it calls discover_devcontainers() after
discovering fs-directory children.
Devcontainer configurations are detected at the following locations (relative to the resource root):
.devcontainer/devcontainer.json.devcontainer.json(root-level).devcontainer/<name>/devcontainer.json(named configurations)
Discovery Process
- Trigger:
discover_children()is called on agit-checkoutorfs-directoryresource. - Scan:
discover_devcontainers()checks for configuration files at the well-known paths listed above. - Validate: Each discovered file is parsed as JSON. Invalid files are skipped with a warning.
- Register: For each valid configuration:
- A
devcontainer-instancechild resource is created under the parent resource withprovisioning_state: discovered. - Named configurations carry the subdirectory name as
config_name.
- A
Lazy Activation
Devcontainers use lazy activation: the container is only built and started when first needed by a plan execution. The discovery phase only registers the resource metadata.
Lifecycle Management
Devcontainer-instance resources support a full lifecycle state machine with six states and validated transitions.
State Diagram
detected ──► building ──► running ──► stopping ──► stopped
│ │
▼ │
failed ◄───────────────────────────────┘
│ ▲
└──► building │
(retry) (rebuild)
│
stopped ────────┘
States
| State | Description |
|---|---|
detected |
Container not yet started (default for new) |
building |
Container is being activated (devcontainer up) |
running |
Container is running and healthy |
stopping |
Container is being stopped |
stopped |
Container has been stopped cleanly |
failed |
Container is in an error state |
Valid Transitions
| From | To | Trigger |
|---|---|---|
| detected | building | lazy activation |
| building | running | devcontainer up OK |
| building | failed | devcontainer up fail |
| running | stopping | stop command |
| stopping | stopped | container stopped |
| stopping | failed | stop failed |
| stopped | building | rebuild |
| failed | building | retry |
Lazy Activation Flow
When a tool targets a devcontainer-instance that is in detected
state:
- Transition to
building. - Run
devcontainer up --workspace-folder <location>. - Parse JSON output for
containerIdandremoteWorkspaceFolder. - On success: transition to
running. - On failure: transition to
failed.
Health Checking
Implementation-ahead-of-spec (SPEC-3): Health checking and session-scoped cleanup are implemented but not yet formally specified in
docs/specification.mdfor container resources (the spec only defines health checking for LSP servers). These features fulfil Issue #514 acceptance criteria and will be added to the specification in a follow-up PR.
Active containers receive periodic health probes. Health checking
starts automatically when a container transitions to running state
via activate_container.
- Command:
devcontainer exec ... echo ping - Default interval: 30 seconds (configurable via
health_check_intervalon the lifecycle tracker). - On failure: transitions to
failedviastopping. - Health checks run as background daemon threads.
- Health checks are stopped automatically before container stop.
Session/Plan Cleanup
When a session closes or a plan completes/fails/is-cancelled, active
devcontainer-instances scoped to that session or plan are stopped
automatically. Each ContainerLifecycleTracker carries an optional
session_id field, set during activation, used to scope cleanup.
Cleanup is wired into the following hooks:
A2aLocalFacade._handle_session_close— session closePlanLifecycleService.complete_apply— plan successPlanLifecycleService.fail_apply— plan apply failurePlanLifecycleService.fail_execute— plan execute failurePlanLifecycleService.cancel_plan— plan cancellation
CleanupService.stop_active_devcontainers(session_id="...")
CLI Usage
# Manually add a devcontainer instance
agents resource add devcontainer-instance local/my-dc --path /home/user/project
# Add a container instance
agents resource add container-instance local/my-ctr --image ubuntu:latest
# List all devcontainer resources
agents resource list --type devcontainer-instance
# Show devcontainer details
agents resource show local/my-dc
# View resource tree (shows devcontainer children)
agents resource tree local/my-repo
# Inspect resource with tree view
agents resource inspect local/my-repo --tree
# Stop an active devcontainer
agents resource stop local/my-dc
# Stop without confirmation prompt (for scripts)
agents resource stop local/my-dc --yes
# Rebuild a stopped or errored devcontainer
agents resource rebuild local/my-dc --yes
Stop Command
agents resource stop <devcontainer-name> [--yes|-y]
Transitions an active container through running → stopping → stopped.
Only devcontainer-instance resources may be stopped (F19 fix:
container-instance was removed because it has no lifecycle tracker
wiring). Prompts for confirmation unless --yes (-y) is passed.
If the container is already in a terminal state (stopped or
failed), the stop is a no-op and returns immediately.
Rebuild Command
agents resource rebuild <devcontainer-name> [--yes|-y]
Transitions a stopped or errored container through
stopped/failed → building → running. Runs devcontainer up to
rebuild the container. Only devcontainer-instance resources may
be rebuilt (not generic container-instance). Prompts for
confirmation unless --yes (-y) is passed.
Known Limitations
| Limitation | Detail | Planned Fix |
|---|---|---|
| In-memory lifecycle state (F20) | Lifecycle state is tracked in an in-memory registry (_lifecycle_registry dict), not persisted on the resource model or database. State is lost on process restart. CLI stop/rebuild in a new process cannot see trackers from a previous process. |
M7+: Persist activation_state on the resource model per the spec field definition, hydrating trackers on CLI operations. |
| Registry eviction | Terminal-state trackers (stopped/failed) are evicted when count exceeds 200 (_MAX_TERMINAL_TRACKERS). Eviction runs automatically after bulk cleanup via stop_all_active_containers. Long-running processes with many container cycles may lose historical tracker data. |
Acceptable for MVP; persisted state in M7+ eliminates this. |
| Sandbox strategy (F22/F25) | Specification uses container_snapshot; SandboxFactory does not yet implement snapshot. Handler now uses SandboxStrategy.NONE — the container itself provides isolation. |
Implement container_snapshot in SandboxFactory and switch handler back to it. |
Hardcoded docker stop (F21) |
stop_container() calls docker stop directly. Devcontainer CLI can target Podman or other engines; the devcontainer CLI does not yet offer a stop subcommand. |
Detect the container engine from resource properties or devcontainer CLI config and dispatch to the appropriate stop command. |
| Auto-discovery wired (F23 resolved) | discover_devcontainers() is now called from GitCheckoutHandler.discover_children() and FsDirectoryHandler.discover_children(). Devcontainer configs are auto-detected when discover_children() is invoked. |
Completed in issue #4740. |
| Container execution stubbed (F24) | ToolRunner returns an error for ExecutionEnvironment.CONTAINER, so lazy activation cannot trigger via tool use in production. Lazy activation currently works through DevcontainerHandler.resolve() on plan sandbox resolution. |
Full container execution support is scoped for a follow-up PR (#616). |
DevcontainerHandler Protocol Methods
The following protocol methods were completed in v3.7.0 (issue #1242) as
part of the ResourceHandler Protocol Completion epic (#825). All methods
raise ValueError if the resource has no location.
| Method | Behaviour |
|---|---|
delete(*, resource, path="") |
Runs devcontainer exec rm -rf <path> inside the container. Returns DeleteResult(success=False) for missing or stopped containers rather than raising. Empty path targets the workspace root. |
list_children(*, resource) |
Runs devcontainer exec ls -1 to enumerate workspace entries. Returns an empty list on container failure. |
diff(*, resource, other_location) |
Compares content hashes between the devcontainer workspace and another filesystem location. Uses EMPTY_CONTENT_HASH sentinel to treat both-absent as no-change. Returns DiffResult. |
create_sandbox(*, resource) |
Delegates to BaseResourceHandler.create_sandbox with lazy activation (same DETECTED/STOPPED/FAILED guard as resolve()). |
Error Handling
All four methods follow the same pattern:
- If the resource has no location → raise
ValueError. - If the container is missing, stopped, or the subprocess fails → return a failure result (not raise an exception).
This ensures that tool execution against a stopped devcontainer produces a clear failure message rather than an unhandled exception.
Architecture
The devcontainer integration follows ADR-043 and builds on the resource type inheritance model defined in ADR-042.
Handler
DevcontainerHandler extends BaseResourceHandler and uses the
none sandbox strategy (the container itself provides isolation).
For devcontainer-instance resources, the handler overrides
resolve() to trigger lazy activation via activate_container()
when the resource is in a non-running state. It handles both
devcontainer-instance and devcontainer-file resource types.
Discovery Module
The cleveragents.resource.handlers.discovery module provides:
discover_devcontainers(location, type): Scans a path for devcontainer configs.is_trigger_type(type): Checks if a resource type triggers auto-discovery.DevcontainerDiscoveryResult: Data class holding discovery results.