Skip to content

feat(vended-tools): add read tool in Python - #5548

Open
Unshure wants to merge 4 commits into
strands-agents:mainfrom
Unshure:issue-3236
Open

Unshure wants to merge 4 commits into
strands-agents:mainfrom
Unshure:issue-3236

Conversation

@Unshure

@Unshure Unshure commented Oct 26, 2026

Copy link
Copy Markdown
Member

Human Overview

Description

Adds a sandbox-routed read vended tool to the Python SDK, ported from the harness's strands_harness.tools.file_tools.read. The tool returns cat -n-style numbered lines for text (with offset/limit paging so a large file can be read in pages), returns image and binary-document content blocks when the model supports media, and falls back to a text placeholder when media=False. Routing goes through tool_context.agent.sandbox, with optional pre-binding via make_read(sandbox=...) to match the file_editor factory pattern.

Non-absolute paths and .. segments are rejected up front; sandbox-level containment can take over once #5249 lands.

Related Issues

Part of #3236. TypeScript port is deferred to a follow-up.

Documentation PR

Included in this PR under site/: a new ### Read section in vended-tools.mdx and a read.yaml catalog entry.

Type of Change

New feature

Testing

  • 23 new unit tests in strands-py/tests/strands/vended_tools/test_read.py cover text paging, image sniffing and rejection, document rendering, media-off placeholders, path validation, and the agent-sandbox fallback. Exercised against the real NotASandboxLocalEnvironment on the host filesystem.
  • hatch check code passes on the new files.
  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Port the harness ``read`` tool into ``strands.vended_tools`` as a sandbox-routed
``read``/``make_read`` pair. Text files return ``cat -n`` numbered lines with
``offset``/``limit`` paging; images and binary documents return media content
blocks when ``media=True``, text placeholders otherwise. Validates absolute
paths and rejects ``..`` traversal.

Refs strands-agents#3236.
@github-actions github-actions Bot added the area-catalog Integrations catalog: entries under site/src/content/catalog, submission process, catalog site pages label Oct 26, 2026
@github-actions github-actions Bot added enhancement New feature or request python Pull requests that update python code area-tool Tool behavior/api area-server Related to strands Server, sandbox, runtime container complexity/medium Touched functions have moderate cognitive complexity (11-25) size/m labels Oct 26, 2026
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Vended tools don't need their own catalog entries.
@github-actions github-actions Bot removed the area-catalog Integrations catalog: entries under site/src/content/catalog, submission process, catalog site pages label Oct 26, 2026
Share the absolute-path / traversal check between read (this PR) and the
upcoming write / edit tools so the three cannot drift on validation.
Picks up the _paths module from strands-agents#5549.
@Unshure
Unshure marked this pull request as ready for review October 26, 2026 21:14
@Unshure
Unshure requested a review from a team as a code owner October 26, 2026 21:14
@Unshure
Unshure requested a review from pgrayy October 26, 2026 21:14
count = limit if limit is not None else _READ_DEFAULT_LIMIT
window = lines[start : start + count]
if not window:
return f"[File has {len(lines)} lines; offset {offset} is past the end.]"

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.

Issue: offset/limit have surprising behavior on non-positive inputs.

  • offset=0 silently acts like offset=1 (because 0 is falsy in (offset - 1) if offset else 0), even though the arg is documented as 1-indexed.
  • A negative offset collapses to start=0 via max(0, ...), so e.g. offset=-5 is silently equivalent to offset=1.
  • limit=0 returns the message "[File has N lines; offset None is past the end.]" even when offset is None and offset is not actually past the end — the diagnostic is misleading and includes a literal None.

Suggestion: Validate up front — reject offset < 1 and limit < 1 with a clear ValueError (or document a specific semantic). This matches file_editor's view-range validation and keeps the model from quietly getting the wrong window.

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.

Addressed in 96fa385 — non-positive offset/limit are now rejected with explicit ValueErrors (offset must be a positive 1-indexed line number / limit must be a positive line count), matching the spirit of file_editor's view-range validation. Verified by the new parametrized tests. ✅

return read_tool


read = make_read()

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.

Issue: make_file_editor is auto-vended by the built-in Docker and SSH sandboxes via get_tools() (as sandbox_file_editor), but no analogous sandbox_read is registered. A user who swaps in a Docker/SSH sandbox and imports read from strands.vended_tools gets a read bound to the agent's sandbox at call time (so it works), but a user following the Docker/SSH pattern of relying on get_tools() only sees a file editor, not a read tool.

Suggestion: Either (a) wire make_read(sandbox=self, name="sandbox_read", description=...) into docker.py/ssh.py's get_tools() for parity, or (b) call out explicitly in the module docstring / docs that read is NOT auto-vended by sandboxes and must be registered by the user.

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.

Addressed in 96fa385 — make_read(sandbox=self, name="sandbox_read", description=...) is now wired into both docker.py and ssh.py get_tools() alongside sandbox_file_editor, so the Docker/SSH auto-vending path now exposes a read tool at parity. ✅

("name", "data", "expected"),
[
("a.gif", b"GIF89a fake", "gif"),
("a.gif", b"GIF87a fake", "gif"),

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.

Issue: Per-field assertions here (and in test_pdf_returns_document_content) can silently miss regressions in sibling fields of the returned ToolResult.

Suggestion: Assert the whole shape at once, e.g.:

assert result == {
    "toolUseId": "test-id",
    "status": "success",
    "content": [{"image": {"format": "png", "source": {"bytes": b"\x89PNG\r\n\x1a\nfake"}}}],
}

The strands-py AGENTS guide (docs/TESTING.md) prefers whole-object equality for exactly this reason.

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.

Addressed in 96fa385 — test_png_returns_image_content and test_pdf_returns_document_content now assert the full ToolResult shape with a single equality, so a regression in any sibling field (toolUseId, status, extra content blocks) is caught. ✅


Raises:
ValueError: If the path is not absolute, or contains a ``..`` segment.
"""

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.

Issue: The commit title (refactor: extract shared validate_absolute_path) suggests this is a shared helper, but file_editor.py still carries its own _validate_path with different semantics (POSIX-only, plus an abspath(...) suggestion in the error, and a different .. error message with no trailing period). The two validators have drifted — a traversal rejected by one is rejected with a different error by the other.

Suggestion: Either (a) collapse file_editor._validate_path onto this shared helper (adjusting the error message or adding an optional suggest_abspath flag if the hint is worth preserving), or (b) rename this helper to something more scoped (e.g. validate_sandbox_path) and document in a comment why file_editor keeps a separate variant. As-is, future readers will have to re-derive which to use.

@Unshure Unshure left a comment

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.

Nice port — the structure matches the existing make_file_editor factory cleanly and the tests exercise the real sandbox seam rather than mocking it, which I appreciate. The byte-sniffing-before-trusting-the-extension logic (with the comment about poisoning history) is a strong defensive choice.

A few things to look at before merging:

Themes
  • Input validation on offset/limit is too lenient — non-positive values produce silently-wrong windows or a diagnostic that includes a literal None.
  • Parity gap with file_editor: Docker/SSH sandboxes auto-vend sandbox_file_editor via get_tools() but don't vend an analogous sandbox_read; worth either wiring up or documenting.
  • The extracted validate_absolute_path is only used by the new tool; file_editor._validate_path still exists with different semantics, which the commit title suggests was not the intent.
  • A couple of tool-result assertions in the tests would catch more regressions as whole-object equality checks.

],
}

content = await active.read_text(path)

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.

Issue: Unlike file_editor (which caps reads at 1MB via _assert_within_size_limit), read has no size bound. For text it reads the whole file into memory and numbers every line before slicing to the offset/limit window; for images/documents (lines 97, 113) it loads the full bytes and ships them to the model. A multi-hundred-MB file could exhaust memory or blow past provider payload limits.

Suggestion: Mirror file_editor's size guard. For text, consider reading only the requested window where the sandbox seam allows, or at least assert a max size. For media, cap the byte size before building the content block and return a text placeholder/error when exceeded.

import re


def validate_absolute_path(path: str) -> None:

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.

Issue: This module's docstring says validation is "shared across sandbox-routed file tools," and the head commit is titled extract shared validate_absolute_path — but file_editor._validate_path still carries its own duplicate copy of the absolute/.. checks and was not migrated to this helper. The result is two validators to keep in sync, which is the opposite of what the extraction intended.

Suggestion: Migrate file_editor to call validate_absolute_path (reconciling the message/behavior differences noted below), or if the duplication is deliberate for now, say so in the PR description and drop the "shared" framing.

"toolUseId": tool_context.tool_use["toolUseId"],
"status": "success",
"content": [
{"document": {"format": extension, "name": _document_name(path), "source": {"bytes": data}}}

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.

Issue: Images are signature-sniffed (with a comment noting that a mislabeled success image block "poisons history"), but documents are trusted purely by extension — a text or arbitrary file named report.pdf is sent as a success document block with no content check. If a malformed document block poisons history the same way a bad image does, this path has the exact risk the image path guards against.

Suggestion: Either sniff document magic bytes (e.g. %PDF, OLE/ZIP headers) before emitting the block, or document why documents don't need the same guard that images do.


lines = content.split("\n")
start = max(0, (offset - 1) if offset else 0)
count = limit if limit is not None else _READ_DEFAULT_LIMIT

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.

Issue: count is used directly in the slice without guarding non-positive values. limit=0 yields an empty window and falls into the "past the end" branch, producing a misleading message like offset None is past the end. even when offset was never supplied. A negative limit slices unexpectedly (lines[start:start+count] with count<0).

Suggestion: Guard limit/offset to be positive (reject or clamp) before slicing, and keep the "past the end" message reserved for a genuine offset-beyond-EOF case.

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.

Resolved in 96fa385 — offset < 1 and limit < 1 now raise a clear ValueError up front, so the empty-window branch is only reached on a genuine offset-past-EOF (no more literal None in the message). New parametrized tests (test_non_positive_offset_rejected, test_non_positive_limit_rejected) cover 0/-1/-100. ✅


Raises:
ValueError: If the path is not absolute, or contains a ``..`` segment.
"""

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.

Issue: The shared validator accepts Windows-absolute paths (ntpath.isabs, drive letters, UNC shares), but file_editor's own check is POSIX-only with the comment "Absolute means POSIX-absolute (leading /), matching the sandbox path model." So read will accept C:\foo as valid, then hand it to a sandbox whose path model is POSIX — a validation pass that fails confusingly downstream. The read tests even declare "POSIX path semantics assumed," yet test_paths.py asserts Windows paths are accepted.

Suggestion: Decide which path model is canonical for sandbox-routed tools and make both tools agree. If the sandbox is POSIX, keep the validator POSIX-only; if Windows paths are genuinely supported, update file_editor and document it.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Clean, well-documented port that closely follows the established make_file_editor factory pattern, with thorough tests covering paging, signature sniffing, media-off placeholders, and the agent-sandbox fallback. The main themes below are worth resolving before merge.

Review Categories
  • Resource safety: read has no size cap, while file_editor bounds reads at 1MB — large text/media files are loaded fully into memory and forwarded to the model.
  • Refactor completeness: the extracted validate_absolute_path is only consumed by read; file_editor still duplicates the same logic, and the two validators disagree on Windows path handling. Reconciling them would deliver the refactor's intent.
  • Content trust: images are magic-byte sniffed to avoid poisoning history, but documents are trusted by extension alone — worth either sniffing or documenting the asymmetry.
  • Input edge cases: non-positive offset/limit slice unexpectedly and can emit a misleading "past the end" message.
  • API process: this adds a new public tool (read/make_read) but has no api/needs-review label — confirm whether a designated API review is required (the PR description does include the API docs, which is great).

Nice work on the cat -n paging and the clear rationale comments around the media/signature handling.

- Reject non-positive offset/limit up front so offset=0/-N and limit=0/-N
  no longer silently collapse or emit a misleading "past the end" message.
- Register sandbox_read in Docker and SSH sandboxes' get_tools() alongside
  sandbox_file_editor, matching the auto-vending pattern.
- Tighten image/document tests to whole-object equality.
@Unshure
Unshure deployed to auto-approve October 27, 2026 13:49 — with GitHub Actions Active
@github-actions github-actions Bot added strands-running complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting and removed complexity/medium Touched functions have moderate cognitive complexity (11-25) labels Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Documentation Preview Ready

Your documentation preview has been successfully deployed!

Changed pages:

Updated at: 2026-10-27T13:52:43.343Z

@github-actions

Copy link
Copy Markdown
Contributor

Re-review of 96fa385 — thanks for the quick turnaround.

Verified fixed (ran test_read.py + test_paths.py locally, 36 passed):

  • ✅ offset/limit now rejected when non-positive, with tests
  • ✅ sandbox_read auto-vended in Docker + SSH get_tools()
  • ✅ image/document tests tightened to whole-object equality

Still open from the prior round (not touched in this commit):

  • No size cap on reads — file_editor bounds at 1MB, read still loads whole files/media into memory unbounded (thread).
  • validate_absolute_path duplication — file_editor._validate_path still carries its own POSIX-only copy; the "shared" helper has only one consumer and the two have drifted (also flagged by @Unshure).
  • Windows-path divergence — the shared validator accepts C:\…/UNC while file_editor is POSIX-only; decide the canonical model.
  • Documents trusted by extension — images are signature-sniffed but documents aren't.

None are blockers I'd insist on, but the size cap and the validator duplication/divergence are worth resolving before merge for consistency with file_editor.

This branch was successfully deployed

1 active deployment
auto-approve — 96fa385d Deployed Oct 27, 2026 by Unshure via Build and Deploy Preview #3145
manual-approval — edb6673f Deployed Oct 26, 2026 by Unshure via Build and Deploy Preview #3134
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-server Related to strands Server, sandbox, runtime container area-tool Tool behavior/api complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting enhancement New feature or request python Pull requests that update python code size/m

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant