---
adr_number: 42
title: Resource Type Inheritance
status_history:
- - '2026-03-01'
  - Proposed
  - Jeffrey Phillips Freeman
- - '2026-03-01'
  - Accepted
  - Jeffrey Phillips Freeman
tier: 3
authors:
- Jeffrey Phillips Freeman
superseded_by: null
related_adrs:
- number: 8
  title: Resource System
  relationship: Extends the resource type registry with inheritance semantics
- number: 11
  title: Tool System
  relationship: Tool resource bindings gain polymorphic subtype matching
- number: 36
  title: Resource DAG Operational Semantics
  relationship: DAG queries gain polymorphic type filtering
- number: 37
  title: Tool Reachability and Access Projection
  relationship: Reachability computation considers subtype relationships
- number: 39
  title: Container and Execution Environment Resource Types
  relationship: Container types are the primary use case for type inheritance
- number: 43
  title: Devcontainer Integration and Container-Project Association
  relationship: First concrete subtype (`devcontainer-instance`) built on this mechanism
acceptance:
  votes_for:
  - voter: Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com>
    comment: Single-inheritance with polymorphic tool binding enables extensible container specialization without duplicating type definitions
  votes_against: []
  abstentions: []
---
## Context

CleverAgents defines resource types as flat, independent definitions — each type declares its own properties, capabilities, child types, sandbox strategy, and handler from scratch. This works well for types that are fundamentally different (e.g., `git-checkout` vs. `fs-directory` vs. `container-instance`), but creates significant problems when types share a core identity with specialized behavior.

The container resource type hierarchy (ADR-039) introduced seven container types that are engine-agnostic (Docker, Podman, containerd, etc.). However, modern development workflows require further specialization: devcontainer-based environments (defined by the [Development Containers Specification](https://containers.dev)), docker-compose service instances, Kubernetes pod instances, and other container provisioning models all share the fundamental characteristics of a `container-instance` — they are running containers with mounts, ports, execution environments, and snapshot-based sandboxing — but each adds provisioning-specific configuration, auto-discovery rules, and lifecycle behavior.

Without a type inheritance mechanism, each specialization would need to duplicate every field from `container-instance` in its own type definition, tools bound to `container-instance` would not automatically work with specialized types, and the auto-discovery system would need explicit enumeration of every possible container subtype rather than polymorphic matching.

The same pattern appears in other parts of the type hierarchy: database types sharing common query interfaces, cloud resource types sharing API access patterns, and document types sharing structural properties. A general-purpose inheritance mechanism addresses all of these cases.

## Decision Drivers

- Specialized container types (devcontainer, docker-compose, kubernetes pod) share the core identity of `container-instance` but each adds provisioning-specific configuration, discovery, and lifecycle behavior
- Without inheritance, each specialization must duplicate every field from its logical parent, and tools bound to the parent type cannot work with specialized types
- Tool bindings, auto-discovery child type matching, and DAG queries must be polymorphic — referencing a parent type must automatically include all subtypes
- The pattern applies beyond containers: databases, cloud resources, and document types all exhibit shared-base-with-specialization relationships
- Inheritance must be simple (single-parent) and bounded in depth to prevent pathological hierarchies and ensure resolution performance

## Decision

CleverAgents introduces **single-inheritance resource type specialization** via an `inherits` field on resource type definitions. A subtype inherits all properties, capabilities, child type constraints, sandbox strategy, and handler behavior from its parent type, and can selectively override or extend any inherited field. Tool bindings, auto-discovery child type matching, and DAG queries that reference a parent type automatically include all subtypes — this is the core polymorphism guarantee.

## Design

### The `inherits` Field

Resource type YAML definitions gain an optional `inherits` field specifying the parent type name:

```yaml
name: devcontainer-instance
inherits: container-instance
description: "A container provisioned from a devcontainer.json configuration"

# Only fields that differ from or extend container-instance need to be declared.
# All other fields (capabilities, sandbox_strategy, child_types, etc.) are inherited.

cli_args:
  # Inherited args from container-instance are available.
  # Additional args specific to devcontainer:
  - name: config-path
    type: path
    required: false
    description: "Path to .devcontainer/devcontainer.json or .devcontainer/ directory"
  - name: workspace-folder
    type: path
    required: false
    description: "Host folder to mount as the container workspace"

handler:
  class: DevcontainerInstanceHandler
  module: cleveragents.resources.handlers.devcontainer
```

### Inheritance Semantics

#### Field Resolution

When the system resolves a field for a resource type, it walks the inheritance chain from the most specific type to the root:

1. If the subtype **declares** the field, use the subtype's value (override).
2. If the subtype **omits** the field, inherit from the parent.
3. Walk continues up the chain until a value is found or the root type is reached.

This applies to all top-level fields: `description`, `physical`, `sandbox_strategy`, `capabilities`, `handler`, and scalar configuration values.

#### Collection Field Merging

Fields that are collections (`cli_args`, `child_types`, `parent_types`, properties) use **additive merging** by default:

- **`cli_args`**: The subtype's CLI arguments are appended to the parent's arguments. If a subtype declares an argument with the same `name` as a parent argument, the subtype's definition overrides the parent's (same-name replacement).
- **`child_types`**: The subtype's allowed child types are merged with the parent's. The subtype can add new child types or override `auto_discover`/`max_count` for inherited child types by redeclaring them.
- **`parent_types`**: The subtype inherits all allowed parent types from the parent and can add additional ones. The parent type itself is implicitly an allowed parent.
- **Properties**: The subtype inherits all parent properties and can add or override individual properties.

A subtype can explicitly **replace** (rather than merge) a collection field by declaring `<field>_replace: true`:

```yaml
# Replace parent's child_types entirely rather than merging
child_types_replace: true
child_types:
  - type: custom-mount
    auto_discover: true
```

#### Handler Inheritance

The subtype's handler class should extend the parent's handler class in the Python implementation. The resource type system does not enforce this at the YAML level (it only knows class names), but the handler contract assumes Liskov substitutability — a subtype handler must honor all parent handler behaviors while extending them.

```python
class DevcontainerInstanceHandler(ContainerInstanceHandler):
    """Extends container-instance with devcontainer-specific provisioning."""

    def discover_children(self, resource: ResourceRecord) -> list[ResourceRecord]:
        # Call parent discovery (mounts, exec-env, ports)
        children = super().discover_children(resource)
        # Add devcontainer-specific discovery (features, workspace mount)
        children.extend(self._discover_devcontainer_features(resource))
        return children

    def activate(self, resource: ResourceRecord) -> None:
        """Build the devcontainer from devcontainer.json on first access."""
        self._build_devcontainer(resource)
        super().activate(resource)
```

### Polymorphism Guarantees

#### Tool Binding Polymorphism

A tool with a resource slot typed to `container-instance` accepts any resource whose type is `container-instance` **or any type that inherits from it** (directly or transitively). The tool does not need to know about subtypes — it operates on the parent type's interface.

```yaml
# This tool works with container-instance AND devcontainer-instance
# AND compose-instance AND any future subtype.
tool:
  name: local/container-shell
  resource_bindings:
    - slot: target
      type: container-instance    # Matches all subtypes
      access: read_write
```

Resolution: When binding a resource to a tool slot, the system checks `resource.type == slot.type OR resource.type.inherits_from(slot.type)`.

#### Auto-Discovery Child Type Polymorphism

When a parent resource type declares a child type, subtypes of that child type also satisfy the constraint:

```yaml
# container-runtime allows container-instance children
child_types:
  - type: container-instance
    auto_discover: true

# devcontainer-instance (inherits container-instance) is also
# a valid child of container-runtime — no configuration change needed.
```

#### DAG Query Polymorphism

Queries against the resource DAG that filter by type automatically include subtypes:

- `find_resources(type="container-instance")` returns container-instance resources AND devcontainer-instance resources AND any other subtypes.
- `find_resources(type="container-instance", exact=True)` returns only exact `container-instance` resources (opt-in exact matching).

### Inheritance Chain Rules

1. **Single inheritance only.** A type may declare at most one `inherits` value. Diamond inheritance (multiple parents) is not supported.
2. **Depth limit.** The inheritance chain is limited to 5 levels to prevent pathological hierarchies and ensure resolution performance. Built-in types occupy at most 2 levels (e.g., `container-instance` → `devcontainer-instance`).
3. **No circular inheritance.** The system validates at registration time that the `inherits` chain is acyclic.
4. **Built-in types as parents.** Custom types (namespaced) may inherit from built-in types (unnamespaced). Built-in types may inherit from other built-in types. Built-in types may **not** inherit from custom types.
5. **Stable parent contract.** Removing or renaming a parent type is a breaking change that invalidates all subtypes. The system prevents removal of a type that has registered subtypes.

### Registration and Validation

When a resource type with `inherits` is registered:

1. The parent type must already be registered.
2. The system resolves the full inheritance chain and validates no cycles exist.
3. Merged fields are computed and cached for runtime performance.
4. Any `cli_args` name collisions between parent and subtype are resolved (subtype wins).
5. The handler class is validated as loadable (but Liskov substitutability is not checked at registration time — it is a development-time contract).

### Impact on Existing Resource Types

No existing resource types gain `inherits` fields in this ADR. The mechanism is introduced to enable ADR-043 (Devcontainer Integration) and future specialization patterns. The existing container types (`container-instance`, `container-mount`, etc.) remain root types with no parent.

Potential future applications of type inheritance include:

| Subtype | Parent | Purpose |
|---------|--------|---------|
| `devcontainer-instance` | `container-instance` | Devcontainer.json-provisioned containers (ADR-043) |
| `compose-instance` | `container-instance` | Docker Compose service instances |
| `kubernetes-pod` | `container-instance` | Kubernetes pod instances |
| `postgresql-database` | (custom `database`) | PostgreSQL-specific database type |

## Constraints

- A resource type definition must have at most one `inherits` value (single inheritance).
- The inheritance chain must not exceed 5 levels.
- The inheritance chain must be acyclic — validated at registration time.
- Built-in types must not inherit from custom (namespaced) types.
- Removing a parent type is prohibited while subtypes exist in the registry.
- Subtype handlers must honor the parent handler's behavioral contract (Liskov substitutability). This is a development-time responsibility enforced by integration tests, not by the type system.

## Consequences

### Positive

- Specialized resource types (devcontainer, compose, kubernetes) share a common base without field duplication.
- Tools bound to parent types automatically work with all subtypes — no tool changes needed when new subtypes are introduced.
- Auto-discovery and DAG queries are polymorphic by default, reducing configuration burden.
- The pattern is general-purpose and applicable beyond containers (databases, cloud resources, documents).

### Negative

- Adds complexity to the resource type resolution system (field merging, chain walking, polymorphic matching).
- Collection field merging rules (additive vs. replace) add cognitive overhead for type authors.
- Handler inheritance assumes Python class hierarchy alignment, creating a coupling between YAML definitions and Python implementation structure.

### Risks

- Deep inheritance chains (approaching the 5-level limit) may make field resolution confusing. Mitigated by the depth limit and the expectation that most hierarchies are 2 levels.
- Polymorphic tool binding could cause unexpected matches if a subtype inherits a parent type that the tool author did not intend to cover. Mitigated by `exact=True` matching where precision is needed.
- Handler Liskov substitutability violations may cause runtime errors. Mitigated by integration test requirements.

## Alternatives Considered

**Discriminated variant pattern** — Add a `variant` or `source` field to `container-instance` instead of creating subtypes. Different handler classes would be dispatched based on the variant value. This is simpler but less extensible: every new variant modifies the base type definition, tools cannot distinguish between variants, and custom types cannot participate in the hierarchy. Rejected because it fails the open/closed principle — the base type must be modified for every extension.

**Composition over inheritance** — Attach specialized behavior via linked "capability" resources rather than type inheritance. For example, a `devcontainer-config` resource would be linked to a plain `container-instance`. This avoids type hierarchy complexity but breaks the polymorphism guarantee (tools bound to `container-instance` would not know about the devcontainer capabilities) and makes auto-discovery more complex (the system would need to discover both the container and its configuration resource separately).

## Compliance

- **Schema validation tests**: Resource type YAML with `inherits` fields parse correctly, with valid and invalid chains.
- **Field resolution tests**: Subtype field resolution follows the documented precedence (subtype > parent) for scalar and collection fields.
- **Collection merging tests**: `cli_args`, `child_types`, and `parent_types` merge correctly with additive and replace modes.
- **Polymorphism tests**: Tool binding, auto-discovery child matching, and DAG queries correctly include subtypes.
- **Cycle detection tests**: Circular `inherits` chains are rejected at registration time.
- **Depth limit tests**: Chains exceeding 5 levels are rejected.
- **Parent removal tests**: Attempting to remove a type with registered subtypes fails with a descriptive error.
