Skip to content

feat(vended_tools): add edit tool, deprecate file_editor - #5317

Open
Unshure wants to merge 1 commit into
strands-agents:mainfrom
Unshure:main
Open

Unshure wants to merge 1 commit into
strands-agents:mainfrom
Unshure:main

Conversation

@Unshure

@Unshure Unshure commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Human overview

Implement an edit tool, and deprecate the file_editor tool. This better aligns with the teams work done in the strands harness and vends the same tools it uses.

Summary

Shift direction on vended file-editing tools. Instead of growing file_editor into a reconciled multi-command tool (view/create/str_replace/insert/find_line/undo_edit), introduce three focused tools — read, write, and edit — that match the harness's existing strands_harness.tools shape and let file_editor be retired. This PR lands the first of the three, edit.

Tracks issue #3235 ("Vended tools: edit tool"). Companion issues #3236 (read) and #3237 (write) land in follow-up PRs off agent-tasks/3236-3237-read-write-baseline.

What's in this PR

  • New strands.vended_tools.edit — bespoke edit tool: replaces an exact, unique old_str with new_str (errors on 0 or >1 occurrences). Signature matches the harness edit tool byte-for-byte so a harness migration is a drop-in swap.
    • Sandbox-routed via the same construction-time bind / call-time resolve pattern as make_shell and make_file_editor: pass a sandbox= to bind it (as the Docker/SSH sandboxes do), otherwise it reads from tool_context.agent.sandbox.
    • Lexical path validation (absolute paths only, .. rejected). Will delegate to the sandbox once Sandbox: own path validation and containment (root/jail) #5249 lands.
  • file_editor marked deprecated — docstring note + warnings.warn(DeprecationWarning) in make_file_editor. The module stays importable and functional; nothing else changes. Will be removed once read and write land.
  • DockerSandbox.get_tools() / SshSandbox.get_tools() — unchanged in this PR: still vend sandbox_file_editor + sandbox_shell. The sandbox-internal make_file_editor(...) call is wrapped in warnings.catch_warnings() so constructing a sandbox does not emit the deprecation; a user-facing make_file_editor() call still warns.
  • Sandbox hardening tests — the two test_backslash_in_filename_is_literal tests document a sandbox-level contract (POSIX \ is a legal filename character). Kept because they're orthogonal to the tool above the sandbox.
  • Docs — new ### Edit section in site/.../vended-tools.mdx; ### File editor marked deprecated; sourceLinks adds edit/edit.py.

What's intentionally not in this PR

  • read and write vended tools — tracked separately in Vended tools: read tool (py + ts) #3236 / Vended tools: write tool (py + ts) #3237.
  • Full file-editor reconciliation (find_line, undo_edit, media view, root confinement, LRU undo state, oversize caps, replace_all, binary-document handling). The baseline for that work is preserved on Unshure/harness-sdk @ agent-tasks/3236-3237-read-write-baseline (at commit 527af4bd5) for the read-tool port, but it does not ship through this PR.
  • TypeScript parity. The strands-ts/src/vended-tools/file-editor pivot (and the new edit tool in TS) ships in a follow-up.
  • Removal of file_editor. It stays importable with a deprecation warning until read and write are vended.

Test plan

  • tests/strands/vended_tools/test_edit.py — 18 tests covering both commands, path validation, sandbox-binding behavior, and ambiguous/missing-match errors.
  • tests/strands/vended_tools/test_file_editor.py::TestFileEditorDeprecation — regression tests covering: import strands.vended_tools is quiet; make_file_editor() warns; DockerSandbox.get_tools() / SshSandbox.get_tools() are quiet.
  • Pre-existing test_file_editor.py suite still passes (deprecation-only change to the module).
  • ruff check + ruff format --check + mypy ./src ./tests_typing all green.
  • 211 tests pass locally across edit, file_editor, sandbox, agent-sandbox suites.

@Unshure
Unshure marked this pull request as ready for review October 21, 2026 18:32
@Unshure
Unshure requested a review from a team as a code owner October 21, 2026 18:32
@Unshure
Unshure requested a review from arielnabavian October 21, 2026 18:32
@github-actions github-actions Bot added area-tool Tool behavior/api enhancement New feature or request complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting size/l strands-running labels Oct 21, 2026
@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Reconciliation of two previously-approved PRs (find_line + undo_edit, media/document view, configurable size caps, confinement root). Code quality is high — the implementation carries clear design-rationale comments and the test suite is thorough. I ran the full test_file_editor.py suite locally: 121 passed. One pre-merge gate needs attention.

Review Categories
  • Formatting (must fix before merge): file_editor.py is not ruff-formatted; the CI ruff format --check gate will fail. One-command fix (hatch fmt --formatter) — see inline comment.
  • Public API surface: DEFAULT_UNDO_STATE_KEY is public-named but not in __all__; minor consistency nit.
  • Tests: Comprehensive — covers LRU eviction, per-agent scoping, state roundtrip persistence, oversize preflight, fuzzy find, concurrency serialization, and transient-write-failure retry paths.
  • Design: Undo-via-agent-state, lexical path confinement, and media/document handling are well-reasoned and documented.

Nicely done — the inline design comments and edge-case test coverage make this easy to review.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Documentation Preview Ready

Your documentation preview has been successfully deployed!

Changed pages:

Updated at: 2026-10-26T20:45:59.801Z

Comment thread strands-py/src/strands/sandbox/docker.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Re-reviewed at 527af4b. This commit restores the sandbox_file_editor description in docker.py/ssh.py (now built from _default_description(media=True, ...) + a container/host suffix). The change is lint- and format-clean, and the get_tools tests pass.

New observations on this commit
  • Missing regression test: The description bug this commit fixes isn't covered by any test — the shell tool has *_description_names_container/host tests but the file_editor tool doesn't. See inline suggestion.
  • Cross-module private imports: docker.py/ssh.py reach into file_editor's _default_description / _DEFAULT_BINARY_DOCUMENT_FORMATS; a rename there (exactly what triggered this fix) breaks them silently. Consider promoting to public API. See inline suggestion.
Still unresolved from the previous review
  • Formatting (blocks CI): file_editor.py is still not ruff-formatted — ruff format --check fails (the while condition, the undo_history dict comprehension, and trailing EOF blank lines). Run hatch fmt --formatter.
  • DEFAULT_UNDO_STATE_KEY is still public-named but absent from __all__.

Comment thread strands-py/src/strands/vended_tools/edit/edit.py
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Request Changes

Re-reviewed at 1803c86 (feat(vended_tools)!: add edit tool, deprecate file_editor). The new edit tool is clean and well-tested (54 tests pass locally), and this commit resolves all three items from my earlier reviews: file_editor.py is now ruff-formatted, the sandbox providers use the public DEFAULT_EDIT_DESCRIPTION instead of private internals, and the sandbox_edit description now has regression tests (test_get_tools_edit_description_names_container/host).

One import-time issue should be fixed before merge.

Findings
  • Import-time deprecation warning (important): file_editor = make_file_editor() runs at module load and make_file_editor warns unconditionally, so import strands.vended_tools emits the file_editor DeprecationWarning for all users — even those only using edit/shell. The repo already solves this for bash via a lazy __getattr__; apply the same pattern. See inline comment.
  • Doc wording (suggestion): edit.py describes read/write as existing companions, but they're forthcoming. See inline comment.

The edit API itself (two focused commands, unique-match enforcement, 0-indexed insert semantics, lexical path validation) is a clear, well-scoped improvement over the monolithic file_editor.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Request Changes

Re-reviewed at 8a26477. The only change since my last pass (1803c86) is the revert of drive-by reformatting in test_file_editor.py — and that revert introduced one CI-blocking issue.

Findings
  • Format gate fails (important, new): ruff format --check . reports 1 file would be reformatted (test_file_editor.py). The revert left several make_file_editor(...) / editor(...) calls in an expanded multi-line form that ruff collapses under line-length = 120, plus a stray blank line before class TestUndo. Running ruff format on the file fixes it. See inline comment.
  • Still open (from prior review): the module-level file_editor = make_file_editor() makes import strands.vended_tools emit the DeprecationWarning for all users (even those only using edit/shell). I see the docs now intentionally document "Importing or constructing it emits a DeprecationWarning," so this is partly by design — but the collateral of warning on any package import still merits a lazy __getattr__ (matching the bash pattern). Non-blocking.
  • Still open (nit): edit.py's docstring says the tool "Pairs with the companion read and write vended tools," while the shipped docs correctly call them "forthcoming." Align the wording.

Only the format-check failure blocks merge; the rest is unchanged from my prior review and the new edit tool remains clean and well-tested.

@github-actions github-actions Bot added the complexity/low Touched functions have low cognitive complexity (<=10) label Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Re-reviewed at a0c90ad (branch rebased/squashed again). Content is unchanged from my previous pass, gates remain green (ruff format --check . 783 files + ruff check . both pass), and both prior items are still open:

  • Important: import strands.vended_tools still emits a DeprecationWarning — __init__.py:75 eagerly imports file_editor, whose module-level file_editor = make_file_editor() (file_editor.py:111) warns at import. Re-confirmed it raises under -W error. Lazy-loading via the existing __getattr__ would confine the warning to actual file_editor consumers.
  • Nit: edit/edit.py:7 docstring still says it "Pairs with the companion read/write vended tools" (present tense); docs/commit describe them as forthcoming (Vended tools: read tool (py + ts) #3236/Vended tools: write tool (py + ts) #3237).

No new issues introduced by the rebase.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve (one optional nit remaining)

Re-reviewed at 3e9d012. The important finding from prior rounds is resolved: file_editor/make_file_editor are now lazy-loaded through the package __getattr__, and the DeprecationWarning was moved out of make_file_editor. Verified behavior:

  • python -W error::DeprecationWarning -c "import strands.vended_tools" → imports cleanly, no warning for consumers of edit/shell/etc.
  • Accessing strands.vended_tools.file_editor → still emits the DeprecationWarning as intended.
  • Gates green: ruff format --check . (783 files) + ruff check . both pass; test_edit.py + test_file_editor.py → 65 passed.

Only remaining item is the earlier nit (non-blocking): edit/edit.py:7 docstring still says it "Pairs with the companion read/write vended tools" (present tense), while those tools are still forthcoming (#3236/#3237). Fine to address here or in a follow-up.

Nice iteration on the lazy-loading fix — the deprecation is now correctly scoped to actual file_editor users.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve (suggestions, non-blocking)

Re-reviewed at 3f7e3bd. The import-time deprecation concern remains resolved — the author took a different-but-valid approach this round: make_file_editor() now carries the warning, the module-level default instance is built under warnings.catch_warnings()/simplefilter("ignore", ...), and both DockerSandbox.get_tools/SshSandbox.get_tools suppress it for their internal construction with a clear rationale comment. Verified:

  • python -W error::DeprecationWarning -c "import strands.vended_tools" → silent ✅
  • make_file_editor() (user-facing) → warns ✅
  • Gates green: ruff format --check . (783) + ruff check . pass; test_edit + test_file_editor + tests/strands/sandbox → 199 passed, 6 skipped.

Two non-blocking items:

Suggestion (testing): There's no regression test locking in this deprecation contract (silent import, silent sandbox get_tools, but warning on a user make_file_editor() call). Given this exact behavior has changed across several revisions, a small test with pytest.warns(DeprecationWarning) for the direct call plus warnings.catch_warnings(record=True) asserting zero warnings on import / get_tools() would prevent future regressions.

Minor (behavior tradeoff): Using the default file_editor instance — the pattern shown in file_editor/__init__.py's own docstring (from strands.vended_tools import file_editor) — does not emit a warning on use; only make_file_editor() does. Users following the documented deprecated path won't see the signal. This is an acceptable tradeoff (tool instances can't easily warn-on-invoke), just flagging the gap.

Nit (still open): edit/edit.py:7 docstring still says it "Pairs with the companion read/write vended tools" (present tense) while those are forthcoming (#3236/#3237).

Solid handling of the sandbox suppression with explanatory comments.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve

Re-reviewed at 16ad38f. The testing suggestion from the prior round is resolved — the new TestFileEditorDeprecation class locks in the full deprecation contract I was concerned about:

  • test_import_vended_tools_does_not_warn (subprocess with -W error::DeprecationWarning)
  • test_make_file_editor_call_fires_deprecation_warning (pytest.deprecated_call)
  • test_docker_sandbox_get_tools_does_not_warn / test_ssh_sandbox_get_tools_does_not_warn

That's exactly the regression protection needed given how many times this behavior flip-flopped. Gates green: ruff format --check . (783) + ruff check . pass; test_edit + test_file_editor + tests/strands/sandbox → 199 passed, 6 skipped.

Nit (still open, non-blocking): read/write tools don't exist yet (only edit/file_editor dirs present), yet edit/edit.py:7 says it "Pairs with the companion read and write vended tools" and the deprecation warning now reads "paired with the read and write tools" (the "forthcoming" qualifier was dropped). Both now describe not-yet-shipped tools as present. Minor wording only.

Nice addition of the deprecation regression tests — happy to approve.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Request Changes

Re-reviewed at 867693b. This revision regressed the previously-resolved deprecation-noise fix.

What changed
  • The warnings.catch_warnings() suppression wrapping the internal make_file_editor(...) calls in DockerSandbox.get_tools (docker.py:107) and SshSandbox.get_tools (ssh.py:189) was removed.
  • The two guarding tests (test_docker_sandbox_get_tools_does_not_warn / test_ssh_sandbox_get_tools_does_not_warn) were removed from TestFileEditorDeprecation (test count 199 → 197), so CI no longer catches this.

Net effect: any user who vends a sandbox's default tools now receives a file_editor is deprecated DeprecationWarning they neither caused nor can act on (read/write aren't shipped). Verified live against DockerSandbox.get_tools().

Fix: restore the suppression in both get_tools methods and re-add the two regression tests. Remaining gates are otherwise green (ruff format/ruff check pass; 197 passed, 6 skipped).

Minor (unchanged from prior round): edit/edit.py:8 and the deprecation warning text describe read/write as present companions though they don't exist yet.

Introduce `strands.vended_tools.edit`, a focused edit tool that replaces
an exact, unique string in a text file. Signature matches the harness
`edit` tool byte-for-byte so a harness migration is a drop-in swap:
`edit(path, old_str, new_str, tool_context) -> "Edited {path}."`.

Sandbox-routed via the same construction-time bind / call-time resolve
pattern as the existing shell and file_editor tools; `make_edit` lets
custom sandbox implementations mount it with a bound sandbox.

Mark `file_editor` and `make_file_editor` deprecated. The warning fires
when a consumer calls `make_file_editor` directly. The two internal
callers — the module-level default `file_editor` instance and
`DockerSandbox` / `SshSandbox` `get_tools` (which continues to vend
`sandbox_file_editor` until the companion `read`/`write` tools land) —
suppress the warning via `warnings.catch_warnings`, so
`import strands.vended_tools` and `DockerSandbox(...).get_tools()` do
not emit the deprecation warning. (A pure lazy `__getattr__` lazy-load
in the parent package was explored but is unreliable here: `file_editor/`
is a subpackage name, so any `from strands.vended_tools.file_editor import X`
auto-binds `vended_tools.file_editor` to the subpackage and bypasses
`__getattr__`.)

Path validation accepts either a POSIX or a Windows absolute path (the
path belongs to the sandbox, not the host) and rejects `..` on either
separator, matching the harness `edit` tool.

Also add two `test_backslash_in_filename_is_literal` tests covering the
POSIX sandbox contract that `\\` in a filename is a literal character,
not a separator — orthogonal to any tool above the sandbox.
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Request Changes

Re-reviewed at b32fbe9. The Critical regression flagged on the previous revision is still not fixed.

Status
  • Spurious deprecation warning (unresolved): DockerSandbox.get_tools (docker.py:107) and SshSandbox.get_tools (ssh.py:189) still call make_file_editor(...) without the warnings.catch_warnings() suppression. Verified live on this commit — both emit a file_editor is deprecated DeprecationWarning to users who never used file_editor and can't migrate (read/write not shipped). The test run itself emits 56 warnings from this path.
  • No regression guard: still no test asserting get_tools() is warning-free, so CI won't catch this.
  • Minor (unchanged): edit/edit.py:8 and the warning text describe read/write as present companions though they don't exist yet.

Gates otherwise green: ruff format/ruff check pass; 101 tests pass in the affected suites.

Fix: restore the suppression in both get_tools methods and add a no-warn regression test.

occurrences = content.count(old_str)
if occurrences == 0:
raise ValueError(f"old_str did not appear verbatim in {path}.")
if occurrences > 1:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think requiring the target string to appear exactly once is too stringent for ordinary file edits. Why don't we add replace_all as a tool parameter like what we did for the reconciled file_editor?

await edit(path="\\\\server\\share\\file.txt", tool_context=ctx, old_str="a", new_str="b")


class TestFileEditorDeprecation:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test class belongs in test_file_editor rather than test_edit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will move it

Returns:
A decorated tool that performs file operations through the sandbox.
"""
warnings.warn(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docker and SSH sandbox users now get a deprecation warning they cannot act on, since they still instantiate file_editor tools. We should file an issue where the sandboxes use the new tools after they are all merged in. Until then, consider suppressing the sandbox warnings with with warnings.catch_warnings().

Also, this line in the PR description is inaccurate:

DockerSandbox.get_tools() / SshSandbox.get_tools() — now vend sandbox_edit + sandbox_shell instead of sandbox_file_editor + sandbox_shell

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was going to try and merge all of the tools before the next release to avoid this issue, but I think you are right and its safer to just catch the warning.

Returns:
A decorated tool that performs file operations through the sandbox.
"""
warnings.warn(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other deprecations use logger.warning("tool_name=<...> | %s", ...) instead of a bare warnings.warn. Consider switching to that style for consistency.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have guidance where we use warnings.warn for deprecation like messages: #5317

class TestSandboxBinding:
@pytest.mark.asyncio
async def test_bound_sandbox_overrides_agent_sandbox(self, tmp_path):
bound = NotASandboxLocalEnvironment()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't test the override because the test agent already defaults to a NotASandboxLocalEnvironment. Perhaps we can make the agent's sandbox raise in this test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

new_str: str,
tool_context: ToolContext,
) -> str:
"""Replace an exact string in a file. ``old_str`` must appear exactly once.

@liramon2 liramon2 Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the insert operation promised in the PR description. Is that intended?

@@ -0,0 +1,101 @@
"""Sandbox-routed edit tool.

Replaces an exact, unique string in a text file through a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that we're dropping the file size cap in this tool. Are we doing the same in the write tool? I ask because the model can get around a possible size check in the write tool by using the edit tool to replace a string with a bigger string.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, ill make sure these are consistent

if occurrences == 0:
raise ValueError(f"old_str did not appear verbatim in {path}.")
if occurrences > 1:
raise ValueError(f"old_str appears {occurrences} times in {path}; make it unique.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no check for an empty old_str, so this produces a misleading error message: "appears N times; make it unique"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will add an empty check

A decorated tool that performs file operations through the sandbox.
"""
warnings.warn(
"file_editor is deprecated and will be removed in a future release. "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_file_editor now emits many DeprecationWarnings. Should we mark the module with filterwarnings?

raise ValueError(f"old_str did not appear verbatim in {path}.")
if occurrences > 1:
raise ValueError(f"old_str appears {occurrences} times in {path}; make it unique.")
await active.write_text(path, content.replace(old_str, new_str, 1))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that we have a race condition where 2 concurrent edits can drop the first. The reordering trick we used in the reconciled file_editor doesn't work here because we have 2 separate awaits. Fixing this would require a lock shared between file tools, since the write tool can presumably overwrite edits.

@@ -0,0 +1,101 @@
"""Sandbox-routed edit tool.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we're splitting edit, read, and write into their own modules. Is that for more intuitive imports and organization? Noting that this makes it harder to share file-related helpers and deviates from harness.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I was working on write and read, I noticed the same thing and moved path validation to a common utility function. Why do separate modules make it harder to share file-related helpers?

@liramon2 liramon2 Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sharing file-related helpers across separate modules requires us to write a vended tool helper file, which is the first of its kind. It's not hard to implement, but it introduces a new pattern.

This branch was successfully deployed

1 active deployment
auto-approve — b32fbe9d Deployed Oct 26, 2026 by Unshure via Build and Deploy Preview #3126
manual-approval — c9bfcfaa Deployed Oct 21, 2026 by Unshure via Build and Deploy Preview #2982
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-tool Tool behavior/api complexity/low Touched functions have low cognitive complexity (<=10) enhancement New feature or request size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants