---
adr_number: 37
title: Tool Reachability and Access Projection
status_history:
- - '2026-02-21'
  - Proposed
  - Jeffrey Phillips Freeman
- - '2026-02-21'
  - Accepted
  - Jeffrey Phillips Freeman
tier: 3
authors:
- Jeffrey Phillips Freeman
superseded_by: null
related_adrs:
- number: 8
  title: Resource System
  relationship: Resource DAG provides the containment and equivalence structure for reachability
- number: 11
  title: Tool System
  relationship: Tools declare resource slots; this ADR extends binding resolution with transitive reachability
- number: 36
  title: Resource DAG Operational Semantics
  relationship: Defines purposes 1–3 (reachability, equivalence, routing) that this ADR formalizes
- number: 38
  title: Cross-Mechanism Sandbox Coordination
  relationship: Routing decisions must account for cross-mechanism coherence and sandbox coordination
- number: 39
  title: Container and Execution Environment Resource Types
  relationship: Container resources create alternative reachability paths through mount hierarchies
- number: 40
  title: LSP Resource Types
  relationship: LSP resources are the primary use case for semantically-rich read routing
acceptance:
  votes_for:
  - voter: Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com>
    comment: Transitive reachability with access projection and read/write routing is the missing link between DAG structure and tool execution
  votes_against: []
  abstentions: []
---
## Context

ADR-011 defines how tools declare resource slots and how the `BindingResolutionService` resolves slots to concrete resources. The specification (lines 20113–20321) details the three binding modes (contextual, static, parameter) and shows resource discovery queries like "What tools can modify this resource?" However, the current model only answers **direct binding** questions — whether a tool's slot type matches a specific resource's type. It does not formalize:

1. **Transitive reachability**: A tool bound to a `git-checkout` can transitively reach every file within that checkout, but this reach is implicit in the tool's implementation, not explicit in the DAG.
2. **Inverse reachability**: Given a specific file deep in the DAG, what tools can reach it? This requires walking up the containment hierarchy, not just checking direct type matches.
3. **Cross-equivalence reachability**: A file accessible through both a git checkout and a Docker container mount might be reachable through tools bound to either ancestor chain.
4. **Access projection**: When a tool is bound to an ancestor resource and needs to access a descendant, how is the descendant identified within the ancestor's access space? (e.g., relative path for filesystem, document URI for LSP, SQL query for database).
5. **Read/write routing**: When a virtual resource has multiple physical manifestations, which tool should the system prefer for reads (semantic richness) versus writes (sandbox tracking)?

## Decision Drivers

- Current binding resolution only answers direct type-match questions; a tool bound to a `git-checkout` implicitly reaches all contained files but this reach is not formalized in the DAG
- Inverse reachability ("what tools can reach this specific file?") requires walking up the containment hierarchy, not just checking direct type matches
- A virtual resource with multiple physical manifestations (git checkout, container mount, LSP document) may be reachable through tools bound to any ancestor chain
- Access projection must translate between namespaces (filesystem paths, LSP document URIs, container-internal paths) when a tool accesses a descendant through an ancestor binding
- Read and write operations benefit from different physical paths — semantic reads through LSP, sandbox-tracked writes through git — requiring explicit routing by richness and sandbox tracking capability

## Decision

CleverAgents formalizes **tool reachability** as a transitive property through the resource DAG's `contains` edges, adds an **access projection** method to the `ResourceHandler` protocol, and defines a **read/write routing** algorithm that selects optimal tools and access paths based on virtual resource equivalence classes.

## Design

### Tool Reachability

#### Forward Reachability

**Definition**: The forward reachability set of a tool `T` bound to resource `R` is:

```
forward_reach(T, R) = {R} ∪ {d : d is a descendant of R via 'contains' edges}
```

Every resource in the forward reachability set can be accessed by tool `T` through resource `R`, subject to:
- The tool's access mode (read_only, read_write, write_only) constraining what operations are allowed.
- The descendant resource's effective writability (see ADR-036: Access Control Propagation).
- The access projection being defined for the (R.type, d.type) pair.

**Example**: A `write_file` tool bound to `git-checkout` at `/repo` has forward reachability over:
```
git-checkout "/repo"
  └── fs-directory "/repo" (worktree root)
       ├── fs-directory "/repo/src"
       │    ├── fs-file "/repo/src/main.py"     ← reachable
       │    └── fs-file "/repo/src/utils.py"     ← reachable
       └── fs-file "/repo/README.md"             ← reachable
```

#### Inverse Reachability

**Definition**: The inverse reachability set of a resource `r` is the set of all tools that can transitively reach `r`:

```
inverse_reach(r) = {(T, R, path) : T is a tool with resource slot bound to R,
                                    and r ∈ forward_reach(T, R),
                                    and path = containment_path(R, r)}
```

Each entry in the inverse reachability set is a triple of (tool, binding resource, containment path from binding resource to target).

**Algorithm**:
1. Walk up `contains` edges from `r` to collect all ancestors: `ancestors(r) = {r, parent(r), parent(parent(r)), ...}`.
2. For each ancestor `a`, find all tools with resource slots whose type is compatible with `a.resource_type`.
3. Filter by access mode compatibility (e.g., if querying for write-reachability, only include tools with `read_write` or `write_only` access).
4. Return the set of (tool, ancestor, path_from_ancestor_to_r) triples.

**Complexity**: O(depth × |tools|) where depth is the DAG depth from `r` to root. In practice, depth is 3–6 and tool count is bounded, making this efficient.

#### Cross-Equivalence Reachability

When a resource `r` has a virtual parent (equivalence link), the system can also find tools that reach equivalent physical manifestations of the same virtual resource:

```
cross_equiv_reach(r) = inverse_reach(r)
                       ∪ {(T, R, path) : for each sibling s of r under the same virtual parent,
                                          (T, R, path) ∈ inverse_reach(s)}
```

This answers: "What tools can reach the same logical resource through any physical path?"

**Example**: File `src/main.py` exists as:
- `fs-file` under `git-checkout` (reachable by `write_file`, `edit_file`)
- `lsp-document` under `lsp-workspace` (reachable by hypothetical `lsp_hover`, `lsp_goto_def` tools)
- `fs-file` under `container-mount` (reachable by `docker_exec` tools)

Cross-equivalence reachability for any of these physical manifestations returns all tools from all three paths.

### Access Projection

When a tool is bound to resource `R` and needs to access a descendant resource `d`, the **access projection** computes how `d` is identified within `R`'s access space.

#### The `project_access` Handler Method

Every resource type handler implements:

```python
class ResourceHandler(Protocol):
    # ... existing methods ...

    def project_access(
        self,
        binding_resource: Resource,
        target_resource: Resource,
        containment_path: list[Resource],
        sandbox: Sandbox | None,
    ) -> AccessProjection | None:
        """Compute how to reach target_resource from binding_resource.

        Args:
            binding_resource: The resource that the tool is bound to.
            target_resource: The descendant resource to be accessed.
            containment_path: The path of resources from binding_resource
                              to target_resource through 'contains' edges.
            sandbox: The active sandbox, if any. Used for path rewriting.

        Returns:
            An AccessProjection if the target is reachable through this
            handler, or None if the handler cannot project access to
            the target type.
        """
        ...
```

#### AccessProjection

```python
@dataclass(frozen=True)
class AccessProjection:
    """How to reach a target resource from a bound resource."""

    # The access path in the binding resource's namespace
    access_path: str

    # The protocol/mechanism for access
    protocol: str

    # Whether this projection crosses a sandbox boundary
    # (i.e., the access path goes through a different sandbox
    # domain than the binding resource's domain)
    crosses_sandbox: bool

    # The sandbox boundary resource_id that the target
    # resides in (may differ from binding resource's boundary)
    target_sandbox_boundary_id: str | None

    # Access capabilities through this projection
    readable: bool
    writable: bool

    # Richness score for read routing (higher = more information)
    # Filesystem: 1, LSP: 10, semantic index: 5, etc.
    read_richness: int
```

#### Built-in Projection Examples

**GitCheckoutHandler → fs-file**:
```python
def project_access(self, binding, target, path, sandbox):
    # Compute relative path from checkout root to target file
    rel_path = os.path.relpath(target.location, binding.location)
    sandbox_path = sandbox.get_path() if sandbox else binding.location
    return AccessProjection(
        access_path=os.path.join(sandbox_path, rel_path),
        protocol="filesystem",
        crosses_sandbox=False,  # same sandbox domain
        target_sandbox_boundary_id=binding.resource_id,
        readable=True,
        writable=True,
        read_richness=1,  # raw file content only
    )
```

**LSPWorkspaceHandler → lsp-document**:
```python
def project_access(self, binding, target, path, sandbox):
    return AccessProjection(
        access_path=target.properties["document_uri"],
        protocol="lsp-textdocument",
        crosses_sandbox=True,  # LSP reads from its own buffer
        target_sandbox_boundary_id=None,  # no sandbox for LSP
        readable=True,
        writable=True,  # via workspace/applyEdit
        read_richness=10,  # semantic info: types, refs, diagnostics
    )
```

**ContainerInstanceHandler → fs-file (via container-mount)**:
```python
def project_access(self, binding, target, path, sandbox):
    mount = find_mount_in_path(path)  # find container-mount in containment path
    container_path = remap_to_container_path(target.location, mount)
    return AccessProjection(
        access_path=container_path,
        protocol="container-exec",
        crosses_sandbox=False,  # within container's sandbox domain
        target_sandbox_boundary_id=binding.resource_id,
        readable=True,
        writable=not mount.properties.get("read_only", False),
        read_richness=1,  # same as filesystem
    )
```

### Read/Write Routing

When the system needs to access a virtual resource (or any resource with multiple physical manifestations), it must choose which physical path and tool to use.

#### Canonical Write Target

Each virtual resource designates one physical manifestation as the **canonical write target** — the manifestation through which writes should be routed to ensure sandbox and checkpoint tracking.

**Selection rules** (in priority order):
1. If exactly one physical manifestation is sandboxable, it is the canonical write target.
2. If multiple are sandboxable, prefer the one with the strongest sandbox strategy: `git_worktree` > `snapshot` > `copy_on_write` > `transaction_rollback` > `none`.
3. If still ambiguous, prefer the manifestation linked to the plan's primary project.
4. The user can override canonical write target via resource configuration.

The canonical write target is stored on the virtual resource as metadata: `canonical_write_resource_id`.

#### Read Source Ranking

When routing a read, physical manifestations are ranked by **read richness** (the `read_richness` score from `AccessProjection`):

| Source | read_richness | Provides |
|--------|--------------|----------|
| `lsp-document` | 10 | Type info, symbol table, diagnostics, go-to-def, references, completions |
| Semantic index (future) | 5 | Pre-computed symbol index, dependency graph |
| `fs-file` via git-checkout | 1 | Raw file content, git history |
| `fs-file` via container-mount | 1 | Raw file content |
| `git-tree-entry` | 1 | File content at specific commit |

The system selects the highest-richness source that is:
1. Available (the source resource exists and its handler is accessible).
2. Current (not stale — coherence checks pass, or staleness is acceptable for the query).
3. Compatible with the query type (some queries require semantic info, others just need raw content).

#### Routing Algorithm

```
route_access(virtual_resource, access_mode, query_type):
    manifestations = physical_children(virtual_resource)

    if access_mode == WRITE:
        target = canonical_write_target(virtual_resource)
        tools = inverse_reach(target) filtered by writes=True
        return best_tool(tools), project_access(tool.binding, target)

    if access_mode == READ:
        ranked = sort(manifestations, key=lambda m: best_projection(m).read_richness, reverse=True)
        for m in ranked:
            if is_available(m) and is_current_enough(m, query_type):
                tools = inverse_reach(m) filtered by read_only compatible
                if tools:
                    return best_tool(tools), project_access(tool.binding, m)
        # Fall back to any available manifestation
        return fallback_read(manifestations)
```

### Integration with Existing Binding Resolution

The access projection and routing system extends, not replaces, the existing `BindingResolutionService`. The flow becomes:

1. **Binding resolution** (existing): Tool slots are resolved to concrete resources via contextual/static/parameter binding.
2. **Forward reachability** (new): Once bound, the system computes the tool's transitive reachability set through the DAG.
3. **Access projection** (new): When the tool accesses a specific descendant, the handler computes the access projection.
4. **Read/write routing** (new): When the planner selects which tool to use for a specific virtual resource, the routing algorithm selects the optimal physical path.

Steps 1 is performed at tool activation time. Steps 2–4 are performed at execution time.

## Constraints

- Access projection must be O(1) per handler invocation (no recursive DAG traversal within the projection itself — the containment path is pre-computed and passed in).
- Read/write routing must not introduce latency-sensitive decisions into the execution hot path. Routing can be pre-computed during the Strategize phase for known resource access patterns.
- Cross-equivalence reachability may return large result sets for virtual resources with many physical manifestations. Results should be lazily computed and filtered early.
- The `read_richness` score is a heuristic. Tool authors can override it for custom resource types.

## Consequences

### Positive
- The system can answer "what tools can reach this file?" without the caller knowing the file's position in the DAG hierarchy.
- Read/write routing through different physical paths enables using the best tool for each access pattern (semantic reads through LSP, sandbox-tracked writes through git).
- Access projection formalizes the implicit knowledge that tools currently carry about how to reach descendant resources.

### Negative
- The `project_access` method adds a new responsibility to every resource handler. Existing handlers must be updated.
- Routing introduces decision-making that could produce suboptimal choices if `read_richness` scores are miscalibrated.
- Cross-equivalence reachability creates coupling between otherwise independent physical resource hierarchies.

### Risks
- The routing algorithm depends on LSP servers being available and current. If the LSP server is down, the system falls back to raw filesystem reads, but the fallback must be seamless.
- Access projection assumes that containment paths are stable during execution. If the DAG is modified during execution (e.g., auto-discovery adds new resources), projections may become stale.

## Alternatives Considered

**Flat tool-resource matching (status quo)** — Tools are matched to resources only by direct type compatibility. Simple but cannot support transitive reachability or semantic read routing.

**Resource URL schemes for access projection** — Instead of handler-based projection, use URL schemes (e.g., `file://`, `lsp://`, `docker://`) to encode access paths. This is more declarative but less flexible — URL schemes cannot encode sandbox-specific path rewriting or cross-boundary awareness.

## Compliance

- **Forward reachability tests**: Verify that a tool bound to a `git-checkout` correctly reaches all descendant `fs-file` resources.
- **Inverse reachability tests**: Verify that querying inverse reach for a deeply nested file returns tools bound to all ancestors.
- **Cross-equivalence tests**: Verify that querying reachability for one physical manifestation returns tools from equivalent manifestations.
- **Access projection tests**: Each built-in handler's `project_access` method is tested for correct path computation, sandbox path rewriting, and protocol assignment.
- **Routing tests**: Read routing prefers LSP sources over filesystem sources. Write routing uses the canonical write target.
