fix: strengthen immutability for BaseTool schema and ToolInvoker boundaries#117
fix: strengthen immutability for BaseTool schema and ToolInvoker boundaries#117
Conversation
#113) Deep-copy at system boundaries for Pydantic frozen models; MappingProxyType for non-Pydantic internal collections (BaseTool). - ToolInvoker: deepcopy arguments at tool execution boundary - BaseTool: MappingProxyType at construction, shallow dict on access - Update docstrings (ToolDefinition, ToolCall, ToolExecutionResult) - Update DESIGN_SPEC.md §11.1.1 and §15.5 - Update CLAUDE.md immutability convention - Add boundary isolation and read-only tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…daries - Deep-copy parameters_schema at construction before MappingProxyType wrapping, fully isolating nested structures from caller mutation - Return deep copy from parameters_schema property to prevent cross- caller nested mutation - Separate deepcopy of tool arguments into its own try-block in ToolInvoker._execute_tool for accurate error attribution - Add _MutatingTool.parameters_schema so boundary isolation tests exercise the validation path - Add nested mutation isolation tests for both construction and property access - Remove stale (M3) tags from implemented engine files in DESIGN_SPEC - Update immutability convention docs in CLAUDE.md and DESIGN_SPEC.md - Fix misleading "accepts any arguments" docstring wording - Clarify ToolExecutionResult.metadata boundary-copy scope Pre-reviewed by 10 agents, 11 findings addressed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughDeep-copy-at-boundary and MappingProxyType-based immutability fixes: BaseTool now deep-copies and stores a MappingProxyType-wrapped parameters schema; ToolInvoker deep-copies tool call arguments before execution and reports deepcopy failures; docs and tests updated to reflect and verify these behaviors. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client/Caller
participant Invoker as ToolInvoker
participant Tool as BaseTool
participant Executor as Tool.execute()
Client->>Invoker: invoke_all(tool_calls)
Invoker->>Invoker: safe_args = deepcopy(tool_call.arguments)
rect rgba(76, 175, 80, 0.5)
Note over Invoker,Executor: Boundary isolation — tool receives safe_args copy
end
Invoker->>Tool: execute(safe_args)
Tool->>Executor: implementation runs with safe_args
Executor-->>Tool: ToolResult
Tool-->>Invoker: ToolResult
Invoker-->>Client: aggregated results
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the immutability guarantees within the system, particularly for Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This is a high-quality pull request that effectively strengthens the immutability guarantees around tool schemas and execution boundaries. The use of copy.deepcopy and MappingProxyType at construction in BaseTool is a solid improvement. Separating the argument-copying logic in ToolInvoker with its own error handling significantly improves error attribution and robustness. The accompanying test additions are thorough and correctly validate the new behavior. The documentation updates are also clear and consistent with the changes. I have a couple of minor suggestions for improving code conciseness and documentation consistency.
src/ai_company/engine/prompt.py
Outdated
| ) | ||
| metadata: dict[str, str] = Field( | ||
| description="Agent identity metadata (treat as read-only)", | ||
| description="Agent identity metadata", |
There was a problem hiding this comment.
This PR does a great job of strengthening immutability and updating docstrings to reflect that. However, simply removing the (treat as read-only) note here reduces clarity about the shallow-freezing nature of Pydantic's frozen=True on collection fields. For consistency with the detailed notes added in other models (like ToolDefinition and ToolCall), consider providing a more explicit description to guide developers.
| description="Agent identity metadata", | |
| description="Agent identity metadata. This dict is shallowly frozen; do not mutate in place.", |
| if self._parameters_schema is None: | ||
| return None | ||
| return copy.deepcopy(dict(self._parameters_schema)) |
There was a problem hiding this comment.
Greptile SummaryThis PR strengthens immutability guarantees for Core implementation: Correct and sound.
Findings:
Confidence Score: 4/5
Last reviewed commit: db36d19 |
There was a problem hiding this comment.
Pull request overview
Strengthens immutability guarantees around tool parameter schemas (BaseTool) and tool execution boundaries (ToolInvoker) to prevent nested structure mutation from leaking across callers, while updating tests and documentation to match the new conventions.
Changes:
- Deep-copies tool call arguments at the
ToolInvokerexecution boundary and separates deepcopy error handling from tool execution errors. - Freezes
BaseToolparameter schemas usingdeepcopy+MappingProxyType, and returns deep copies from the schema property. - Adds/updates unit tests for nested mutation isolation and updates documentation to reflect the immutability strategy.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/tools/test_invoker.py | Adds boundary-isolation tests ensuring tool argument mutation doesn’t leak back into ToolCall.arguments. |
| tests/unit/tools/test_base.py | Expands schema immutability tests (construction isolation, nested isolation, read-only internal). |
| tests/unit/tools/conftest.py | Introduces a mutating test tool and wires it into the extended invoker fixture. |
| src/ai_company/tools/invoker.py | Deep-copies arguments before execute() and improves error attribution for deepcopy failures. |
| src/ai_company/tools/base.py | Freezes parameters_schema via MappingProxyType and returns deep copies on access; adjusts docstrings. |
| src/ai_company/providers/models.py | Clarifies shallow immutability notes for ToolDefinition.parameters_schema and ToolCall.arguments. |
| src/ai_company/engine/prompt.py | Updates field description text for SystemPrompt.metadata. |
| DESIGN_SPEC.md | Updates immutability convention documentation and removes stale engine milestone tags. |
| CLAUDE.md | Updates the documented immutability strategy for non-Pydantic vs Pydantic-frozen models. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| logger.exception( | ||
| TOOL_INVOKE_PARAMETER_ERROR, | ||
| tool_call_id=tool_call.id, | ||
| tool_name=tool_call.name, | ||
| error=f"Failed to deep-copy arguments: {error_msg}", | ||
| ) |
There was a problem hiding this comment.
TOOL_INVOKE_PARAMETER_ERROR is now used for two distinct failure modes: JSON Schema validation failures (_param_error_result, logged as warning) and argument deepcopy failures here (logged as exception). This will make observability/alerting ambiguous. Consider introducing a dedicated event constant for argument copy failures (or reusing a more appropriate existing event) so dashboards can distinguish validation errors from internal boundary-copy failures.
src/ai_company/tools/base.py
Outdated
| ``frozen=True``. This model is not forwarded to tool | ||
| implementations or LLM providers, so no additional boundary | ||
| copy is needed at this layer. |
There was a problem hiding this comment.
The ToolExecutionResult doc note says the model is "not forwarded to tool implementations", but tool implementations explicitly construct and return ToolExecutionResult. Consider rewording to clarify that metadata is not forwarded to LLM providers / external boundaries (and that the invoker converts this to ToolResult), rather than implying tools never see this model.
| ``frozen=True``. This model is not forwarded to tool | |
| implementations or LLM providers, so no additional boundary | |
| copy is needed at this layer. | |
| ``frozen=True``. Tool implementations construct and return this | |
| model, but it is not forwarded to LLM providers or other external | |
| boundaries. Instead, the invoker converts it into a provider-facing | |
| ``ToolResult``, so no additional boundary copy is needed at this | |
| layer. |
CLAUDE.md
Outdated
| - **Type hints**: all public functions, mypy strict mode | ||
| - **Docstrings**: Google style, required on public classes/functions (enforced by ruff D rules) | ||
| - **Immutability**: create new objects, never mutate existing ones. For `dict`/`list` fields in frozen Pydantic models, use `MappingProxyType` wrapping at construction (not `deepcopy` on access). Deep-copy only at system boundaries (e.g. passing data to `tool.execute()`, serializing for persistence). | ||
| - **Immutability**: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction + `MappingProxyType` wrapping for read-only enforcement. For `dict`/`list` fields in frozen Pydantic models, rely on `frozen=True` for field reassignment prevention and `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation). Deep-copy only at system boundaries (e.g. passing data to `tool.execute()`, serializing for persistence). |
There was a problem hiding this comment.
This immutability bullet is a bit self-contradictory: it recommends copy.deepcopy() at construction for non-Pydantic collections, then ends with "Deep-copy only at system boundaries". Consider rephrasing the last sentence to explicitly scope it to Pydantic frozen models (or remove it) so readers don’t infer that construction-time deep-copies are discouraged.
| - **Immutability**: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction + `MappingProxyType` wrapping for read-only enforcement. For `dict`/`list` fields in frozen Pydantic models, rely on `frozen=True` for field reassignment prevention and `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation). Deep-copy only at system boundaries (e.g. passing data to `tool.execute()`, serializing for persistence). | |
| - **Immutability**: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction + `MappingProxyType` wrapping for read-only enforcement. For `dict`/`list` fields in frozen Pydantic models, rely on `frozen=True` for field reassignment prevention and deep-copy those fields only at system boundaries (e.g. passing data to `tool.execute()`, LLM provider serialization, inter-agent delegation, serializing for persistence). |
DESIGN_SPEC.md
Outdated
| | Convention | Status | Decision | Rationale | | ||
| |------------|--------|----------|-----------| | ||
| | **Immutability strategy** | Adopted | `MappingProxyType` at construction for dict fields in registries and collections; `frozen=True` on all config/identity models | MappingProxyType is O(1) and prevents accidental mutation. Pydantic `frozen=True` is confirmed shallow (pydantic#7784). | | ||
| | **Immutability strategy** | Adopted | `copy.deepcopy()` at construction + `MappingProxyType` wrapping for non-Pydantic internal collections (registries, `BaseTool`). For Pydantic frozen models: `frozen=True` prevents field reassignment; `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization) prevents nested mutation. No MappingProxyType inside Pydantic models (serialization friction). | Deep-copy at construction fully isolates nested structures; `MappingProxyType` enforces read-only access. Boundary-copy for Pydantic models is simple, centralized, and Pydantic-native. PEP 814 `frozendict` (Python 3.15) will provide zero-friction field-level immutability when available. | |
There was a problem hiding this comment.
The rationale references "PEP 814 frozendict (Python 3.15)", but PEP 814 doesn’t appear elsewhere in the repo and is likely an incorrect/placeholder citation. Consider replacing this with a verified reference (or wording it generically as a potential future frozendict in CPython) to avoid publishing misleading spec guidance.
| | **Immutability strategy** | Adopted | `copy.deepcopy()` at construction + `MappingProxyType` wrapping for non-Pydantic internal collections (registries, `BaseTool`). For Pydantic frozen models: `frozen=True` prevents field reassignment; `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization) prevents nested mutation. No MappingProxyType inside Pydantic models (serialization friction). | Deep-copy at construction fully isolates nested structures; `MappingProxyType` enforces read-only access. Boundary-copy for Pydantic models is simple, centralized, and Pydantic-native. PEP 814 `frozendict` (Python 3.15) will provide zero-friction field-level immutability when available. | | |
| | **Immutability strategy** | Adopted | `copy.deepcopy()` at construction + `MappingProxyType` wrapping for non-Pydantic internal collections (registries, `BaseTool`). For Pydantic frozen models: `frozen=True` prevents field reassignment; `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization) prevents nested mutation. No MappingProxyType inside Pydantic models (serialization friction). | Deep-copy at construction fully isolates nested structures; `MappingProxyType` enforces read-only access. Boundary-copy for Pydantic models is simple, centralized, and Pydantic-native. If CPython eventually exposes a standardized immutable mapping type (for example, a built-in frozendict), it can be adopted to provide zero-friction field-level immutability when available. | |
…and Copilot - Add TOOL_INVOKE_DEEPCOPY_ERROR event constant for accurate log attribution - Fix invoker module docstring contradicting re-raise behavior - Update _execute_tool docstring for two-phase deepcopy+execute - Fix ToolExecutionResult Note about tool implementation usage - Fix ToolDefinition Note to reflect centralized deepcopy responsibility - Fix CLAUDE.md immutability bullet self-contradiction - Replace unverified PEP 814 reference in DESIGN_SPEC.md - Add shallow-freeze note to SystemPrompt.metadata description - Add inline comment about MappingProxyType deepcopy limitation - Fix test arguments to trigger _MutatingTool nested mutation path - Add tests for deepcopy failure exception and RecursionError paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| # dict() needed: MappingProxyType cannot be deep-copied directly | ||
| return copy.deepcopy(dict(self._parameters_schema)) |
There was a problem hiding this comment.
Misleading comment — MappingProxyType can be deep-copied
The inline comment # dict() needed: MappingProxyType cannot be deep-copied directly is inaccurate. In Python 3, copy.deepcopy(MappingProxyType(…)) is well-defined and returns a new MappingProxyType wrapping a deep-copied dict. The real reason dict() is needed here is to satisfy the dict[str, Any] return-type annotation — a deepcopy of a MappingProxyType would return another MappingProxyType, which mismatches the declared return type.
| # dict() needed: MappingProxyType cannot be deep-copied directly | |
| return copy.deepcopy(dict(self._parameters_schema)) | |
| # Convert to dict first: deepcopy of MappingProxyType returns a | |
| # MappingProxyType, but the public API declares dict[str, Any]. | |
| return copy.deepcopy(dict(self._parameters_schema)) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/tools/base.py
Line: 115-116
Comment:
**Misleading comment — `MappingProxyType` *can* be deep-copied**
The inline comment `# dict() needed: MappingProxyType cannot be deep-copied directly` is inaccurate. In Python 3, `copy.deepcopy(MappingProxyType(…))` is well-defined and returns a new `MappingProxyType` wrapping a deep-copied dict. The real reason `dict()` is needed here is to satisfy the `dict[str, Any]` return-type annotation — a `deepcopy` of a `MappingProxyType` would return another `MappingProxyType`, which mismatches the declared return type.
```suggestion
# Convert to dict first: deepcopy of MappingProxyType returns a
# MappingProxyType, but the public API declares dict[str, Any].
return copy.deepcopy(dict(self._parameters_schema))
```
How can I resolve this? If you propose a fix, please make it concise.| async def test_deepcopy_failure_returns_error_result( | ||
| self, | ||
| extended_invoker: ToolInvoker, | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| """When deepcopy of arguments fails, a ToolResult error is returned.""" | ||
| import copy as _copy_mod | ||
|
|
||
| real_deepcopy = _copy_mod.deepcopy | ||
| call_count = 0 | ||
|
|
||
| def _fail_on_execute(obj: object, memo: object = None) -> object: | ||
| nonlocal call_count | ||
| call_count += 1 | ||
| # First deepcopy call is in _validate_params via | ||
| # parameters_schema; let it pass. Fail on the second | ||
| # call in _execute_tool. | ||
| if call_count > 1: | ||
| msg = "cannot copy" | ||
| raise TypeError(msg) | ||
| return real_deepcopy(obj, memo) # type: ignore[arg-type] | ||
|
|
||
| call = ToolCall(id="c_dc", name="mutating", arguments={"key": "val"}) | ||
| monkeypatch.setattr( | ||
| "ai_company.tools.invoker.copy.deepcopy", | ||
| _fail_on_execute, | ||
| ) | ||
| result = await extended_invoker.invoke(call) | ||
| assert result.is_error is True | ||
| assert "safely copied" in result.content | ||
| assert result.tool_call_id == "c_dc" | ||
|
|
||
| async def test_recursion_error_during_deepcopy_propagates( | ||
| self, | ||
| extended_invoker: ToolInvoker, | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| """RecursionError during deepcopy is re-raised, not swallowed.""" | ||
| import copy as _copy_mod | ||
|
|
||
| real_deepcopy = _copy_mod.deepcopy | ||
| call_count = 0 | ||
|
|
||
| def _fail_on_execute(obj: object, memo: object = None) -> object: | ||
| nonlocal call_count | ||
| call_count += 1 | ||
| if call_count > 1: | ||
| msg = "maximum recursion depth exceeded" | ||
| raise RecursionError(msg) | ||
| return real_deepcopy(obj, memo) # type: ignore[arg-type] | ||
|
|
||
| call = ToolCall(id="c_rec", name="mutating", arguments={"key": "val"}) | ||
| monkeypatch.setattr( | ||
| "ai_company.tools.invoker.copy.deepcopy", | ||
| _fail_on_execute, | ||
| ) | ||
| with pytest.raises(RecursionError, match="maximum recursion depth"): | ||
| await extended_invoker.invoke(call) |
There was a problem hiding this comment.
Fragile call_count coupling to implementation internals
Both test_deepcopy_failure_returns_error_result (line 375: if call_count > 1) and test_recursion_error_during_deepcopy_propagates (line 404: if call_count > 1) rely on the assumption that exactly one copy.deepcopy call is made before _execute_tool's deepcopy — specifically the call inside BaseTool.parameters_schema triggered by _validate_params.
Because monkeypatch.setattr("ai_company.tools.invoker.copy.deepcopy", …) patches the copy module globally (it's a singleton), every copy.deepcopy call anywhere — including the one inside base.py — is intercepted. This means the counter is tightly coupled to the current code path: if _validate_params ever calls parameters_schema twice, if a new early-exit is added, or if the _MutatingTool fixture's schema is removed, the counter offset shifts and the test silently exercises the wrong failure mode (or never triggers the failure at all).
A more robust approach is to patch deepcopy only for the arguments object (e.g., by checking isinstance(obj, dict) and …) or to separate the two patched scopes — patch base.copy.deepcopy to a pass-through and patch invoker.copy.deepcopy to unconditionally fail, removing the counter entirely:
monkeypatch.setattr("ai_company.tools.base.copy.deepcopy", real_deepcopy)
monkeypatch.setattr("ai_company.tools.invoker.copy.deepcopy", lambda obj, memo=None: (_ for _ in ()).throw(TypeError("cannot copy")))Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/tools/test_invoker.py
Line: 358-415
Comment:
**Fragile `call_count` coupling to implementation internals**
Both `test_deepcopy_failure_returns_error_result` (line 375: `if call_count > 1`) and `test_recursion_error_during_deepcopy_propagates` (line 404: `if call_count > 1`) rely on the assumption that exactly **one** `copy.deepcopy` call is made before `_execute_tool`'s deepcopy — specifically the call inside `BaseTool.parameters_schema` triggered by `_validate_params`.
Because `monkeypatch.setattr("ai_company.tools.invoker.copy.deepcopy", …)` patches the `copy` module globally (it's a singleton), every `copy.deepcopy` call anywhere — including the one inside `base.py` — is intercepted. This means the counter is tightly coupled to the current code path: if `_validate_params` ever calls `parameters_schema` twice, if a new early-exit is added, or if the `_MutatingTool` fixture's schema is removed, the counter offset shifts and the test silently exercises the wrong failure mode (or never triggers the failure at all).
A more robust approach is to patch deepcopy *only for the arguments object* (e.g., by checking `isinstance(obj, dict) and …`) or to separate the two patched scopes — patch `base.copy.deepcopy` to a pass-through and patch `invoker.copy.deepcopy` to unconditionally fail, removing the counter entirely:
```python
monkeypatch.setattr("ai_company.tools.base.copy.deepcopy", real_deepcopy)
monkeypatch.setattr("ai_company.tools.invoker.copy.deepcopy", lambda obj, memo=None: (_ for _ in ()).throw(TypeError("cannot copy")))
```
How can I resolve this? If you propose a fix, please make it concise.| def test_schema_internal_is_read_only(self) -> None: | ||
| schema = {"type": "object", "properties": {"x": {"type": "string"}}} | ||
| tool = _ConcreteTool(name="t", parameters_schema=schema) | ||
| with pytest.raises(TypeError): | ||
| tool._parameters_schema["injected"] = True # type: ignore[index] |
There was a problem hiding this comment.
test_schema_internal_is_read_only only covers top-level mutation
The test correctly asserts that tool._parameters_schema["injected"] = True raises TypeError (top-level MappingProxyType enforcement). However, it does not cover the nested mutability gap: since _parameters_schema["properties"] is a plain dict, the following silently succeeds and corrupts internal state:
tool._parameters_schema["properties"]["x"]["type"] = "integer" # No TypeError raised!Given the PR's stated goal of strengthening immutability and the explicit test_schema_nested_isolated_on_construction / test_schema_property_nested_mutation_isolated additions, adding a clarifying comment would make the documented guarantees clear and prevent future confusion:
def test_schema_internal_is_read_only(self) -> None:
schema = {"type": "object", "properties": {"x": {"type": "string"}}}
tool = _ConcreteTool(name="t", parameters_schema=schema)
with pytest.raises(TypeError):
tool._parameters_schema["injected"] = True # type: ignore[index]
# Note: MappingProxyType provides only top-level protection. Nested dicts
# in _parameters_schema remain mutable, but direct access is discouraged
# since it's a private attribute. Public isolation is guaranteed by the
# parameters_schema property, which returns a deep copy.Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/tools/test_base.py
Line: 119-123
Comment:
**`test_schema_internal_is_read_only` only covers top-level mutation**
The test correctly asserts that `tool._parameters_schema["injected"] = True` raises `TypeError` (top-level `MappingProxyType` enforcement). However, it does not cover the nested mutability gap: since `_parameters_schema["properties"]` is a plain `dict`, the following silently succeeds and corrupts internal state:
```python
tool._parameters_schema["properties"]["x"]["type"] = "integer" # No TypeError raised!
```
Given the PR's stated goal of strengthening immutability and the explicit `test_schema_nested_isolated_on_construction` / `test_schema_property_nested_mutation_isolated` additions, adding a clarifying comment would make the documented guarantees clear and prevent future confusion:
```python
def test_schema_internal_is_read_only(self) -> None:
schema = {"type": "object", "properties": {"x": {"type": "string"}}}
tool = _ConcreteTool(name="t", parameters_schema=schema)
with pytest.raises(TypeError):
tool._parameters_schema["injected"] = True # type: ignore[index]
# Note: MappingProxyType provides only top-level protection. Nested dicts
# in _parameters_schema remain mutable, but direct access is discouraged
# since it's a private attribute. Public isolation is guaranteed by the
# parameters_schema property, which returns a deep copy.
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
🤖 I have created a release *beep* *boop* --- ## [0.1.1](ai-company-v0.1.0...ai-company-v0.1.1) (2026-03-10) ### Features * add autonomy levels and approval timeout policies ([#42](#42), [#126](#126)) ([#197](#197)) ([eecc25a](eecc25a)) * add CFO cost optimization service with anomaly detection, reports, and approval decisions ([#186](#186)) ([a7fa00b](a7fa00b)) * add code quality toolchain (ruff, mypy, pre-commit, dependabot) ([#63](#63)) ([36681a8](36681a8)) * add configurable cost tiers and subscription/quota-aware tracking ([#67](#67)) ([#185](#185)) ([9baedfa](9baedfa)) * add container packaging, Docker Compose, and CI pipeline ([#269](#269)) ([435bdfe](435bdfe)), closes [#267](#267) * add coordination error taxonomy classification pipeline ([#146](#146)) ([#181](#181)) ([70c7480](70c7480)) * add cost-optimized, hierarchical, and auction assignment strategies ([#175](#175)) ([ce924fa](ce924fa)), closes [#173](#173) * add design specification, license, and project setup ([8669a09](8669a09)) * add env var substitution and config file auto-discovery ([#77](#77)) ([7f53832](7f53832)) * add FastestStrategy routing + vendor-agnostic cleanup ([#140](#140)) ([09619cb](09619cb)), closes [#139](#139) * add HR engine and performance tracking ([#45](#45), [#47](#47)) ([#193](#193)) ([2d091ea](2d091ea)) * add issue auto-search and resolution verification to PR review skill ([#119](#119)) ([deecc39](deecc39)) * add memory retrieval, ranking, and context injection pipeline ([#41](#41)) ([873b0aa](873b0aa)) * add pluggable MemoryBackend protocol with models, config, and events ([#180](#180)) ([46cfdd4](46cfdd4)) * add pluggable MemoryBackend protocol with models, config, and events ([#32](#32)) ([46cfdd4](46cfdd4)) * add pluggable PersistenceBackend protocol with SQLite implementation ([#36](#36)) ([f753779](f753779)) * add progressive trust and promotion/demotion subsystems ([#43](#43), [#49](#49)) ([3a87c08](3a87c08)) * add retry handler, rate limiter, and provider resilience ([#100](#100)) ([b890545](b890545)) * add SecOps security agent with rule engine, audit log, and ToolInvoker integration ([#40](#40)) ([83b7b6c](83b7b6c)) * add shared org memory and memory consolidation/archival ([#125](#125), [#48](#48)) ([4a0832b](4a0832b)) * design unified provider interface ([#86](#86)) ([3e23d64](3e23d64)) * expand template presets, rosters, and add inheritance ([#80](#80), [#81](#81), [#84](#84)) ([15a9134](15a9134)) * implement agent runtime state vs immutable config split ([#115](#115)) ([4cb1ca5](4cb1ca5)) * implement AgentEngine core orchestrator ([#11](#11)) ([#143](#143)) ([f2eb73a](f2eb73a)) * implement basic tool system (registry, invocation, results) ([#15](#15)) ([c51068b](c51068b)) * implement built-in file system tools ([#18](#18)) ([325ef98](325ef98)) * implement communication foundation — message bus, dispatcher, and messenger ([#157](#157)) ([8e71bfd](8e71bfd)) * implement company template system with 7 built-in presets ([#85](#85)) ([cbf1496](cbf1496)) * implement conflict resolution protocol ([#122](#122)) ([#166](#166)) ([e03f9f2](e03f9f2)) * implement core entity and role system models ([#69](#69)) ([acf9801](acf9801)) * implement crash recovery with fail-and-reassign strategy ([#149](#149)) ([e6e91ed](e6e91ed)) * implement engine extensions — Plan-and-Execute loop and call categorization ([#134](#134), [#135](#135)) ([#159](#159)) ([9b2699f](9b2699f)) * implement enterprise logging system with structlog ([#73](#73)) ([2f787e5](2f787e5)) * implement graceful shutdown with cooperative timeout strategy ([#130](#130)) ([6592515](6592515)) * implement hierarchical delegation and loop prevention ([#12](#12), [#17](#17)) ([6be60b6](6be60b6)) * implement LiteLLM driver and provider registry ([#88](#88)) ([ae3f18b](ae3f18b)), closes [#4](#4) * implement LLM decomposition strategy and workspace isolation ([#174](#174)) ([aa0eefe](aa0eefe)) * implement meeting protocol system ([#123](#123)) ([ee7caca](ee7caca)) * implement message and communication domain models ([#74](#74)) ([560a5d2](560a5d2)) * implement model routing engine ([#99](#99)) ([d3c250b](d3c250b)) * implement parallel agent execution ([#22](#22)) ([#161](#161)) ([65940b3](65940b3)) * implement per-call cost tracking service ([#7](#7)) ([#102](#102)) ([c4f1f1c](c4f1f1c)) * implement personality injection and system prompt construction ([#105](#105)) ([934dd85](934dd85)) * implement single-task execution lifecycle ([#21](#21)) ([#144](#144)) ([c7e64e4](c7e64e4)) * implement subprocess sandbox for tool execution isolation ([#131](#131)) ([#153](#153)) ([3c8394e](3c8394e)) * implement task assignment subsystem with pluggable strategies ([#172](#172)) ([c7f1b26](c7f1b26)), closes [#26](#26) [#30](#30) * implement task decomposition and routing engine ([#14](#14)) ([9c7fb52](9c7fb52)) * implement Task, Project, Artifact, Budget, and Cost domain models ([#71](#71)) ([81eabf1](81eabf1)) * implement tool permission checking ([#16](#16)) ([833c190](833c190)) * implement YAML config loader with Pydantic validation ([#59](#59)) ([ff3a2ba](ff3a2ba)) * implement YAML config loader with Pydantic validation ([#75](#75)) ([ff3a2ba](ff3a2ba)) * initialize project with uv, hatchling, and src layout ([39005f9](39005f9)) * initialize project with uv, hatchling, and src layout ([#62](#62)) ([39005f9](39005f9)) * Litestar REST API, WebSocket feed, and approval queue (M6) ([#189](#189)) ([29fcd08](29fcd08)) * make TokenUsage.total_tokens a computed field ([#118](#118)) ([c0bab18](c0bab18)), closes [#109](#109) * parallel tool execution in ToolInvoker.invoke_all ([#137](#137)) ([58517ee](58517ee)) * testing framework, CI pipeline, and M0 gap fixes ([#64](#64)) ([f581749](f581749)) * wire all modules into observability system ([#97](#97)) ([f7a0617](f7a0617)) ### Bug Fixes * address Greptile post-merge review findings from PRs [#170](https://github.com/Aureliolo/ai-company/issues/170)-[#175](https://github.com/Aureliolo/ai-company/issues/175) ([#176](#176)) ([c5ca929](c5ca929)) * address post-merge review feedback from PRs [#164](https://github.com/Aureliolo/ai-company/issues/164)-[#167](https://github.com/Aureliolo/ai-company/issues/167) ([#170](#170)) ([3bf897a](3bf897a)), closes [#169](#169) * enforce strict mypy on test files ([#89](#89)) ([aeeff8c](aeeff8c)) * harden Docker sandbox, MCP bridge, and code runner ([#50](#50), [#53](#53)) ([d5e1b6e](d5e1b6e)) * harden git tools security + code quality improvements ([#150](#150)) ([000a325](000a325)) * harden subprocess cleanup, env filtering, and shutdown resilience ([#155](#155)) ([d1fe1fb](d1fe1fb)) * incorporate post-merge feedback + pre-PR review fixes ([#164](#164)) ([c02832a](c02832a)) * pre-PR review fixes for post-merge findings ([#183](#183)) ([26b3108](26b3108)) * strengthen immutability for BaseTool schema and ToolInvoker boundaries ([#117](#117)) ([7e5e861](7e5e861)) ### Performance * harden non-inferable principle implementation ([#195](#195)) ([02b5f4e](02b5f4e)), closes [#188](#188) ### Refactoring * adopt NotBlankStr across all models ([#108](#108)) ([#120](#120)) ([ef89b90](ef89b90)) * extract _SpendingTotals base class from spending summary models ([#111](#111)) ([2f39c1b](2f39c1b)) * harden BudgetEnforcer with error handling, validation extraction, and review fixes ([#182](#182)) ([c107bf9](c107bf9)) * harden personality profiles, department validation, and template rendering ([#158](#158)) ([10b2299](10b2299)) * pre-PR review improvements for ExecutionLoop + ReAct loop ([#124](#124)) ([8dfb3c0](8dfb3c0)) * split events.py into per-domain event modules ([#136](#136)) ([e9cba89](e9cba89)) ### Documentation * add ADR-001 memory layer evaluation and selection ([#178](#178)) ([db3026f](db3026f)), closes [#39](#39) * add agent scaling research findings to DESIGN_SPEC ([#145](#145)) ([57e487b](57e487b)) * add CLAUDE.md, contributing guide, and dev documentation ([#65](#65)) ([55c1025](55c1025)), closes [#54](#54) * add crash recovery, sandboxing, analytics, and testing decisions ([#127](#127)) ([5c11595](5c11595)) * address external review feedback with MVP scope and new protocols ([#128](#128)) ([3b30b9a](3b30b9a)) * expand design spec with pluggable strategy protocols ([#121](#121)) ([6832db6](6832db6)) * finalize 23 design decisions (ADR-002) ([#190](#190)) ([8c39742](8c39742)) * update project docs for M2.5 conventions and add docs-consistency review agent ([#114](#114)) ([99766ee](99766ee)) ### Tests * add e2e single agent integration tests ([#24](#24)) ([#156](#156)) ([f566fb4](f566fb4)) * add provider adapter integration tests ([#90](#90)) ([40a61f4](40a61f4)) ### CI/CD * add Release Please for automated versioning and GitHub Releases ([#278](#278)) ([a488758](a488758)) * bump actions/checkout from 4 to 6 ([#95](#95)) ([1897247](1897247)) * bump actions/upload-artifact from 4 to 7 ([#94](#94)) ([27b1517](27b1517)) * harden CI/CD pipeline ([#92](#92)) ([ce4693c](ce4693c)) * split vulnerability scans into critical-fail and high-warn tiers ([#277](#277)) ([aba48af](aba48af)) ### Maintenance * add /worktree skill for parallel worktree management ([#171](#171)) ([951e337](951e337)) * add design spec context loading to research-link skill ([8ef9685](8ef9685)) * add post-merge-cleanup skill ([#70](#70)) ([f913705](f913705)) * add pre-pr-review skill and update CLAUDE.md ([#103](#103)) ([92e9023](92e9023)) * add research-link skill and rename skill files to SKILL.md ([#101](#101)) ([651c577](651c577)) * bump aiosqlite from 0.21.0 to 0.22.1 ([#191](#191)) ([3274a86](3274a86)) * bump pyyaml from 6.0.2 to 6.0.3 in the minor-and-patch group ([#96](#96)) ([0338d0c](0338d0c)) * bump ruff from 0.15.4 to 0.15.5 ([a49ee46](a49ee46)) * fix M0 audit items ([#66](#66)) ([c7724b5](c7724b5)) * pin setup-uv action to full SHA ([#281](#281)) ([4448002](4448002)) * post-audit cleanup — PEP 758, loggers, bug fixes, refactoring, tests, hookify rules ([#148](#148)) ([c57a6a9](c57a6a9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
🤖 I have created a release *beep* *boop* --- ## [0.1.0](v0.0.0...v0.1.0) (2026-03-11) ### Features * add autonomy levels and approval timeout policies ([#42](#42), [#126](#126)) ([#197](#197)) ([eecc25a](eecc25a)) * add CFO cost optimization service with anomaly detection, reports, and approval decisions ([#186](#186)) ([a7fa00b](a7fa00b)) * add code quality toolchain (ruff, mypy, pre-commit, dependabot) ([#63](#63)) ([36681a8](36681a8)) * add configurable cost tiers and subscription/quota-aware tracking ([#67](#67)) ([#185](#185)) ([9baedfa](9baedfa)) * add container packaging, Docker Compose, and CI pipeline ([#269](#269)) ([435bdfe](435bdfe)), closes [#267](#267) * add coordination error taxonomy classification pipeline ([#146](#146)) ([#181](#181)) ([70c7480](70c7480)) * add cost-optimized, hierarchical, and auction assignment strategies ([#175](#175)) ([ce924fa](ce924fa)), closes [#173](#173) * add design specification, license, and project setup ([8669a09](8669a09)) * add env var substitution and config file auto-discovery ([#77](#77)) ([7f53832](7f53832)) * add FastestStrategy routing + vendor-agnostic cleanup ([#140](#140)) ([09619cb](09619cb)), closes [#139](#139) * add HR engine and performance tracking ([#45](#45), [#47](#47)) ([#193](#193)) ([2d091ea](2d091ea)) * add issue auto-search and resolution verification to PR review skill ([#119](#119)) ([deecc39](deecc39)) * add mandatory JWT + API key authentication ([#256](#256)) ([c279cfe](c279cfe)) * add memory retrieval, ranking, and context injection pipeline ([#41](#41)) ([873b0aa](873b0aa)) * add pluggable MemoryBackend protocol with models, config, and events ([#180](#180)) ([46cfdd4](46cfdd4)) * add pluggable MemoryBackend protocol with models, config, and events ([#32](#32)) ([46cfdd4](46cfdd4)) * add pluggable output scan response policies ([#263](#263)) ([b9907e8](b9907e8)) * add pluggable PersistenceBackend protocol with SQLite implementation ([#36](#36)) ([f753779](f753779)) * add progressive trust and promotion/demotion subsystems ([#43](#43), [#49](#49)) ([3a87c08](3a87c08)) * add retry handler, rate limiter, and provider resilience ([#100](#100)) ([b890545](b890545)) * add SecOps security agent with rule engine, audit log, and ToolInvoker integration ([#40](#40)) ([83b7b6c](83b7b6c)) * add shared org memory and memory consolidation/archival ([#125](#125), [#48](#48)) ([4a0832b](4a0832b)) * design unified provider interface ([#86](#86)) ([3e23d64](3e23d64)) * expand template presets, rosters, and add inheritance ([#80](#80), [#81](#81), [#84](#84)) ([15a9134](15a9134)) * implement agent runtime state vs immutable config split ([#115](#115)) ([4cb1ca5](4cb1ca5)) * implement AgentEngine core orchestrator ([#11](#11)) ([#143](#143)) ([f2eb73a](f2eb73a)) * implement AuditRepository for security audit log persistence ([#279](#279)) ([94bc29f](94bc29f)) * implement basic tool system (registry, invocation, results) ([#15](#15)) ([c51068b](c51068b)) * implement built-in file system tools ([#18](#18)) ([325ef98](325ef98)) * implement communication foundation — message bus, dispatcher, and messenger ([#157](#157)) ([8e71bfd](8e71bfd)) * implement company template system with 7 built-in presets ([#85](#85)) ([cbf1496](cbf1496)) * implement conflict resolution protocol ([#122](#122)) ([#166](#166)) ([e03f9f2](e03f9f2)) * implement core entity and role system models ([#69](#69)) ([acf9801](acf9801)) * implement crash recovery with fail-and-reassign strategy ([#149](#149)) ([e6e91ed](e6e91ed)) * implement engine extensions — Plan-and-Execute loop and call categorization ([#134](#134), [#135](#135)) ([#159](#159)) ([9b2699f](9b2699f)) * implement enterprise logging system with structlog ([#73](#73)) ([2f787e5](2f787e5)) * implement graceful shutdown with cooperative timeout strategy ([#130](#130)) ([6592515](6592515)) * implement hierarchical delegation and loop prevention ([#12](#12), [#17](#17)) ([6be60b6](6be60b6)) * implement LiteLLM driver and provider registry ([#88](#88)) ([ae3f18b](ae3f18b)), closes [#4](#4) * implement LLM decomposition strategy and workspace isolation ([#174](#174)) ([aa0eefe](aa0eefe)) * implement meeting protocol system ([#123](#123)) ([ee7caca](ee7caca)) * implement message and communication domain models ([#74](#74)) ([560a5d2](560a5d2)) * implement model routing engine ([#99](#99)) ([d3c250b](d3c250b)) * implement parallel agent execution ([#22](#22)) ([#161](#161)) ([65940b3](65940b3)) * implement per-call cost tracking service ([#7](#7)) ([#102](#102)) ([c4f1f1c](c4f1f1c)) * implement personality injection and system prompt construction ([#105](#105)) ([934dd85](934dd85)) * implement single-task execution lifecycle ([#21](#21)) ([#144](#144)) ([c7e64e4](c7e64e4)) * implement subprocess sandbox for tool execution isolation ([#131](#131)) ([#153](#153)) ([3c8394e](3c8394e)) * implement task assignment subsystem with pluggable strategies ([#172](#172)) ([c7f1b26](c7f1b26)), closes [#26](#26) [#30](#30) * implement task decomposition and routing engine ([#14](#14)) ([9c7fb52](9c7fb52)) * implement Task, Project, Artifact, Budget, and Cost domain models ([#71](#71)) ([81eabf1](81eabf1)) * implement tool permission checking ([#16](#16)) ([833c190](833c190)) * implement YAML config loader with Pydantic validation ([#59](#59)) ([ff3a2ba](ff3a2ba)) * implement YAML config loader with Pydantic validation ([#75](#75)) ([ff3a2ba](ff3a2ba)) * initialize project with uv, hatchling, and src layout ([39005f9](39005f9)) * initialize project with uv, hatchling, and src layout ([#62](#62)) ([39005f9](39005f9)) * Litestar REST API, WebSocket feed, and approval queue (M6) ([#189](#189)) ([29fcd08](29fcd08)) * make TokenUsage.total_tokens a computed field ([#118](#118)) ([c0bab18](c0bab18)), closes [#109](#109) * parallel tool execution in ToolInvoker.invoke_all ([#137](#137)) ([58517ee](58517ee)) * testing framework, CI pipeline, and M0 gap fixes ([#64](#64)) ([f581749](f581749)) * wire all modules into observability system ([#97](#97)) ([f7a0617](f7a0617)) ### Bug Fixes * address Greptile post-merge review findings from PRs [#170](https://github.com/Aureliolo/ai-company/issues/170)-[#175](https://github.com/Aureliolo/ai-company/issues/175) ([#176](#176)) ([c5ca929](c5ca929)) * address post-merge review feedback from PRs [#164](https://github.com/Aureliolo/ai-company/issues/164)-[#167](https://github.com/Aureliolo/ai-company/issues/167) ([#170](#170)) ([3bf897a](3bf897a)), closes [#169](#169) * enforce strict mypy on test files ([#89](#89)) ([aeeff8c](aeeff8c)) * harden Docker sandbox, MCP bridge, and code runner ([#50](#50), [#53](#53)) ([d5e1b6e](d5e1b6e)) * harden git tools security + code quality improvements ([#150](#150)) ([000a325](000a325)) * harden subprocess cleanup, env filtering, and shutdown resilience ([#155](#155)) ([d1fe1fb](d1fe1fb)) * incorporate post-merge feedback + pre-PR review fixes ([#164](#164)) ([c02832a](c02832a)) * pre-PR review fixes for post-merge findings ([#183](#183)) ([26b3108](26b3108)) * resolve circular imports, bump litellm, fix release tag format ([#286](#286)) ([a6659b5](a6659b5)) * strengthen immutability for BaseTool schema and ToolInvoker boundaries ([#117](#117)) ([7e5e861](7e5e861)) ### Performance * harden non-inferable principle implementation ([#195](#195)) ([02b5f4e](02b5f4e)), closes [#188](#188) ### Refactoring * adopt NotBlankStr across all models ([#108](#108)) ([#120](#120)) ([ef89b90](ef89b90)) * extract _SpendingTotals base class from spending summary models ([#111](#111)) ([2f39c1b](2f39c1b)) * harden BudgetEnforcer with error handling, validation extraction, and review fixes ([#182](#182)) ([c107bf9](c107bf9)) * harden personality profiles, department validation, and template rendering ([#158](#158)) ([10b2299](10b2299)) * pre-PR review improvements for ExecutionLoop + ReAct loop ([#124](#124)) ([8dfb3c0](8dfb3c0)) * split events.py into per-domain event modules ([#136](#136)) ([e9cba89](e9cba89)) ### Documentation * add ADR-001 memory layer evaluation and selection ([#178](#178)) ([db3026f](db3026f)), closes [#39](#39) * add agent scaling research findings to DESIGN_SPEC ([#145](#145)) ([57e487b](57e487b)) * add CLAUDE.md, contributing guide, and dev documentation ([#65](#65)) ([55c1025](55c1025)), closes [#54](#54) * add crash recovery, sandboxing, analytics, and testing decisions ([#127](#127)) ([5c11595](5c11595)) * address external review feedback with MVP scope and new protocols ([#128](#128)) ([3b30b9a](3b30b9a)) * expand design spec with pluggable strategy protocols ([#121](#121)) ([6832db6](6832db6)) * finalize 23 design decisions (ADR-002) ([#190](#190)) ([8c39742](8c39742)) * update project docs for M2.5 conventions and add docs-consistency review agent ([#114](#114)) ([99766ee](99766ee)) ### Tests * add e2e single agent integration tests ([#24](#24)) ([#156](#156)) ([f566fb4](f566fb4)) * add provider adapter integration tests ([#90](#90)) ([40a61f4](40a61f4)) ### CI/CD * add Release Please for automated versioning and GitHub Releases ([#278](#278)) ([a488758](a488758)) * bump actions/checkout from 4 to 6 ([#95](#95)) ([1897247](1897247)) * bump actions/upload-artifact from 4 to 7 ([#94](#94)) ([27b1517](27b1517)) * bump anchore/scan-action from 6.5.1 to 7.3.2 ([#271](#271)) ([80a1c15](80a1c15)) * bump docker/build-push-action from 6.19.2 to 7.0.0 ([#273](#273)) ([dd0219e](dd0219e)) * bump docker/login-action from 3.7.0 to 4.0.0 ([#272](#272)) ([33d6238](33d6238)) * bump docker/metadata-action from 5.10.0 to 6.0.0 ([#270](#270)) ([baee04e](baee04e)) * bump docker/setup-buildx-action from 3.12.0 to 4.0.0 ([#274](#274)) ([5fc06f7](5fc06f7)) * bump sigstore/cosign-installer from 3.9.1 to 4.1.0 ([#275](#275)) ([29dd16c](29dd16c)) * harden CI/CD pipeline ([#92](#92)) ([ce4693c](ce4693c)) * split vulnerability scans into critical-fail and high-warn tiers ([#277](#277)) ([aba48af](aba48af)) ### Maintenance * add /worktree skill for parallel worktree management ([#171](#171)) ([951e337](951e337)) * add design spec context loading to research-link skill ([8ef9685](8ef9685)) * add post-merge-cleanup skill ([#70](#70)) ([f913705](f913705)) * add pre-pr-review skill and update CLAUDE.md ([#103](#103)) ([92e9023](92e9023)) * add research-link skill and rename skill files to SKILL.md ([#101](#101)) ([651c577](651c577)) * bump aiosqlite from 0.21.0 to 0.22.1 ([#191](#191)) ([3274a86](3274a86)) * bump pyyaml from 6.0.2 to 6.0.3 in the minor-and-patch group ([#96](#96)) ([0338d0c](0338d0c)) * bump ruff from 0.15.4 to 0.15.5 ([a49ee46](a49ee46)) * fix M0 audit items ([#66](#66)) ([c7724b5](c7724b5)) * **main:** release ai-company 0.1.1 ([#282](#282)) ([2f4703d](2f4703d)) * pin setup-uv action to full SHA ([#281](#281)) ([4448002](4448002)) * post-audit cleanup — PEP 758, loggers, bug fixes, refactoring, tests, hookify rules ([#148](#148)) ([c57a6a9](c57a6a9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Signed-off-by: Aurelio <19254254+Aureliolo@users.noreply.github.com>
Closes #113
Summary
BaseTool._parameters_schemanow usescopy.deepcopy()beforeMappingProxyTypewrapping, fully isolating nested structures from caller mutationparameters_schemaproperty returns a deep copy to prevent cross-caller nested mutationToolInvoker._execute_toolseparates argument deep-copy into its own try-block for accurate error attribution (previously misattributed as tool execution failure)parameters_schemato_MutatingToolso boundary isolation tests exercise the validation path(M3)tags from 5 implemented engine files inDESIGN_SPEC.md; updated immutability convention docs inCLAUDE.mdandDESIGN_SPEC.mdToolExecutionResult.metadataboundary-copy scopeTest plan
Review coverage
Pre-reviewed by 10 agents: code-reviewer, python-reviewer, pr-test-analyzer, silent-failure-hunter, comment-analyzer, type-design-analyzer, logging-audit, resilience-audit, security-reviewer, docs-consistency. 11 findings addressed out of 12 total (1 reverted: async generator incompatible with
tuple()).