# ADR-004: Tool Shared State Ownership Contract

**Status:** Accepted

**Date:** 2026-03-22

**Issue:** [#4057](https://github.com/construct/construct/issues/4057)

## Context

Construct tools execute in a multi-client environment where a single daemon
process serves requests from multiple connected clients simultaneously. Several
tools already maintain long-lived shared state:

- **`DelegateParentToolsHandle`** (`src/tools/mod.rs`):
  `Arc<RwLock<Vec<Arc<dyn Tool>>>>` — holds parent tools for delegate agents
  with no per-client isolation.
- **`ChannelMapHandle`** (`src/tools/reaction.rs`):
  `Arc<RwLock<HashMap<String, Arc<dyn Channel>>>>` — global channel map shared
  across all clients.
- **`CanvasStore`** (`src/tools/canvas.rs`):
  `Arc<RwLock<HashMap<String, CanvasEntry>>>` — canvas IDs are plain strings
  with no client namespace.

These patterns emerged organically. As the tool surface grows and more clients
connect concurrently, we need a clear contract governing ownership, identity,
isolation, lifecycle, and reload behavior for tool-held shared state. Without
this contract, new tools risk introducing data leaks between clients, stale
state after config reloads, or inconsistent initialization timing.

Additional context:

- The tool registry is immutable after startup, built once in
  `all_tools_with_runtime()`.
- Client identity is currently derived from IP address only
  (`src/gateway/mod.rs`), which is insufficient for reliable namespacing.
- `SecurityPolicy` is scoped per agent, not per client.
- `WorkspaceManager` provides some isolation but workspace switching is global.

## Decision

### 1. Ownership: May tools own long-lived shared state?

**Yes.** Tools MAY own long-lived shared state, provided they follow the
established **handle pattern**: wrap the state in `Arc<RwLock<T>>` (or
`Arc<parking_lot::RwLock<T>>`) and expose a cloneable handle type.

This pattern is already proven by three independent implementations:

| Handle | Location | Inner type |
|--------|----------|-----------|
| `DelegateParentToolsHandle` | `src/tools/mod.rs` | `Vec<Arc<dyn Tool>>` |
| `ChannelMapHandle` | `src/tools/reaction.rs` | `HashMap<String, Arc<dyn Channel>>` |
| `CanvasStore` | `src/tools/canvas.rs` | `HashMap<String, CanvasEntry>` |

Tools that need shared state MUST:

- Define a named handle type alias (e.g., `pub type FooHandle = Arc<RwLock<T>>`).
- Accept the handle at construction time rather than creating global state.
- Document the concurrency contract in the handle type's doc comment.

Tools MUST NOT use static mutable state (`lazy_static!`, `OnceCell` with
interior mutability) for per-request or per-client data.

### 2. Identity assignment: Who constructs identity keys?

**The daemon SHOULD provide identity.** Tools MUST NOT construct their own
client identity keys.

A new `ClientId` type should be introduced (opaque, `Clone + Eq + Hash + Send + Sync`)
that the daemon assigns at connection time. This replaces the current approach
of using raw IP addresses (`src/gateway/mod.rs:259-306`), which breaks when
multiple clients share a NAT address or when proxied connections arrive.

`ClientId` is passed to tools that require per-client state namespacing as part
of the tool execution context. Tools that do not need per-client isolation
(e.g., the immutable tool registry) may ignore it.

The `ClientId` contract:

- Generated by the gateway layer at connection establishment.
- Opaque to tools — tools must not parse or derive meaning from the value.
- Stable for the lifetime of a single client session.
- Passed through the execution context, not stored globally.

### 3. Lifecycle: When may tools run startup-style validation?

**Validation runs once at first registration, and again when config changes
are detected.**

The lifecycle phases are:

1. **Construction** — tool is instantiated with handles and config. No I/O or
   validation occurs here.
2. **Registration** — tool is registered in the tool registry via
   `all_tools_with_runtime()`. At this point the tool MAY perform one-time
   startup validation (e.g., checking that required credentials exist, verifying
   external service connectivity).
3. **Execution** — tool handles individual requests. No re-validation unless
   the config-change signal fires (see Reload Semantics below).
4. **Shutdown** — daemon is stopping. Tools with open resources SHOULD clean up
   gracefully via `Drop` or an explicit shutdown method.

Tools MUST NOT perform blocking validation during execution-phase calls.
Validation results SHOULD be cached in the tool's handle state and checked
via a fast path during execution.

### 4. Isolation: What must be isolated per client?

State falls into two categories with different isolation requirements:

**MUST be isolated per client:**

- Security-sensitive state: credentials, API keys, quotas, rate-limit counters,
  per-client authorization decisions.
- User-specific session data: conversation context, user preferences,
  workspace-scoped file paths.

Isolation mechanism: tools holding per-client state MUST key their internal
maps by `ClientId`. The handle pattern naturally supports this by using
`HashMap<ClientId, T>` inside the `RwLock`.

**MAY be shared across clients (with namespace prefixing):**

- Broadcast/display state: canvas frames (`CanvasStore`), notification channels
  (`ChannelMapHandle`).
- Read-only reference data: tool registry, static configuration, model
  metadata.

When shared state uses string keys (e.g., canvas IDs, channel names), tools
SHOULD support optional namespace prefixing (e.g., `{client_id}:{canvas_name}`)
to allow per-client isolation when needed without mandating it for broadcast
use cases.

Tools MUST NOT store per-client secrets in shared (non-isolated) state
structures.

### 5. Reload semantics: What invalidates prior shared state on config change?

**Config changes detected via hash comparison MUST invalidate cached
validation state.**

The reload contract:

- The daemon computes a hash of the tool-relevant config section at startup and
  after each config reload event.
- When the hash changes, the daemon signals affected tools to re-run their
  registration-phase validation.
- Tools MUST treat their cached validation result as stale when signaled and
  re-validate before the next execution.

Specific invalidation rules:

| Config change | Invalidation scope |
|--------------|-------------------|
| Credential/secret rotation | Per-tool validation cache; per-client credential state |
| Tool enable/disable | Full tool registry rebuild via `all_tools_with_runtime()` |
| Security policy change | `SecurityPolicy` re-derivation; per-agent policy state |
| Workspace directory change | `WorkspaceManager` state; file-path-dependent tool state |
| Provider config change | Provider-dependent tools re-validate connectivity |

Tools MAY retain non-security shared state (e.g., canvas content, channel
subscriptions) across config reloads unless the reload explicitly affects that
state's validity.

## Consequences

### Positive

- **Consistency:** All new tools follow the same handle pattern, making shared
  state discoverable and auditable.
- **Safety:** Per-client isolation of security-sensitive state prevents data
  leaks in multi-tenant scenarios.
- **Clarity:** Explicit lifecycle phases eliminate ambiguity about when
  validation runs.
- **Evolvability:** The `ClientId` abstraction decouples tools from transport
  details, supporting future identity mechanisms (tokens, certificates).

### Negative

- **Migration cost:** Existing tools (`CanvasStore`, `ReactionTool`) may need
  refactoring to accept `ClientId` and namespace their state.
- **Complexity:** Tools that were simple singletons now need to consider
  multi-client semantics even if they currently have one client.
- **Performance:** Per-client keying adds a hash lookup on each access, though
  this is negligible compared to I/O costs.

### Neutral

- The tool registry remains immutable after startup; this ADR does not change
  that invariant.
- `SecurityPolicy` remains per-agent; this ADR documents that client isolation
  is orthogonal to agent-level policy.

## References

- `src/tools/mod.rs` — `DelegateParentToolsHandle`, `all_tools_with_runtime()`
- `src/tools/reaction.rs` — `ChannelMapHandle`, `ReactionTool`
- `src/tools/canvas.rs` — `CanvasStore`, `CanvasEntry`
- `src/tools/traits.rs` — `Tool` trait
- `src/gateway/mod.rs` — client IP extraction (`forwarded_client_ip`, `resolve_client_ip`)
- `src/security/` — `SecurityPolicy`
