feat(tools): make the file tools' backing store pluggable - #6709
feat(tools): make the file tools' backing store pluggable#6709joaomdmoura wants to merge 8 commits into
Conversation
📝 WalkthroughWalkthroughChangesThe pull request adds a pluggable File storage seam
Sequence Diagram(s)sequenceDiagram
participant FileWriterTool
participant FileStore
participant FileReadTool
FileWriterTool->>FileStore: resolve and resolve_within target
FileWriterTool->>FileStore: ensure_parent and write_text
FileReadTool->>FileStore: normalize and resolve path
FileReadTool->>FileStore: open_text resolved file
FileStore-->>FileReadTool: text content
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Pull request overview
This PR introduces a pluggable FileStore seam so FileReadTool / FileWriterTool can route all path resolution and I/O through a durable backing store (while defaulting to the current local-filesystem behavior via LocalFileStore).
Changes:
- Added a
FileStoreprotocol plus aLocalFileStoreimplementation and a process-wide registry (register_file_store_factory/resolve_file_store). - Updated
FileReadToolandFileWriterToolto bind a store once per tool instance and perform all resolve/read/write operations through it. - Added dict-backed tests proving file tools do not bypass the store seam and verifying sandbox/containment and fallback behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/crewai-tools/tests/file_storage/test_file_store_seam.py | Adds seam-focused tests using an in-memory store to prove all I/O routes through FileStore. |
| lib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.py | Refactors writer to resolve paths and write via a bound FileStore instance. |
| lib/crewai-tools/src/crewai_tools/tools/file_read_tool/file_read_tool.py | Refactors reader to resolve/pin paths and read via a bound FileStore instance. |
| lib/crewai-tools/src/crewai_tools/file_storage/registry.py | Adds the global factory registry with safe fallbacks to the local store. |
| lib/crewai-tools/src/crewai_tools/file_storage/local.py | Implements LocalFileStore preserving existing sandboxed filesystem behavior. |
| lib/crewai-tools/src/crewai_tools/file_storage/base.py | Defines the FileStore protocol and FileStoreError. |
| lib/crewai-tools/src/crewai_tools/file_storage/init.py | Exposes the new storage seam API surface from the package. |
Comments suppressed due to low confidence (1)
lib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.py:139
store.resolve_within(...)is only caught asValueError. If a customFileStoreraisesOSError/FileStoreError(e.g. invalid key syntax, backend failure),_runwill raise instead of returning a structured error message.
# Then keep filename inside that directory.
try:
resolved_filepath = store.resolve_within(resolved_directory, filename)
except ValueError as e:
return f"Error: Invalid file path — {e!s}"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@lib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.py`:
- Around line 121-139: Handle FileStoreError at every identified file-store
boundary, returning the same sanitized file-tool error format instead of
allowing it to escape. In file_writer_tool.py ranges 121-139 and 144-158, add
handling around store.resolve, store.resolve_within, store.ensure_parent, and
unguarded store.exists; in file_read_tool.py range 166-178, cover _resolve_path
and the read I/O boundary. Preserve existing handling for other exception types
and use each error’s details in the returned message.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: fb209830-11e6-43ab-a681-8b0101074b91
📒 Files selected for processing (7)
lib/crewai-tools/src/crewai_tools/file_storage/__init__.pylib/crewai-tools/src/crewai_tools/file_storage/base.pylib/crewai-tools/src/crewai_tools/file_storage/local.pylib/crewai-tools/src/crewai_tools/file_storage/registry.pylib/crewai-tools/src/crewai_tools/tools/file_read_tool/file_read_tool.pylib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.pylib/crewai-tools/tests/file_storage/test_file_store_seam.py
0609959 to
a2f6bd0
Compare
7519268 to
e6e9554
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (3)
lib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.py:126
_run()only catches(FileStoreError, OSError). If a non-local store (or its dependencies) raises a different exception type duringresolve()/display()/exists()/ensure_parent()(e.g.TimeoutError,RuntimeError), it will still propagate and can abort the agent step instead of returning an agent-visible error string as intended for store failures.
try:
return self._write(filename, content, directory, overwrite)
except (FileStoreError, OSError) as e:
# A store can fail for reasons the local filesystem never had: an
# unreachable endpoint, a size ceiling. Every other exit from this
# tool is an agent-visible string, and raising here would kill the
# agent's step rather than let it react, so this one is too. It
# also covers the calls with no handler of their own — exists() and
# the store's own path labelling — which is why it carries no path.
return (
f"An error occurred while writing to the file: the "
f"{self._store.label} store failed. {format_error_for_display(e)}"
)
lib/crewai-tools/src/crewai_tools/tools/file_read_tool/file_read_tool.py:198
_run()only catches(FileStoreError, OSError). If a non-local store raises another exception type during path resolution or labelling (e.g.TimeoutError,RuntimeError), it can still propagate and abort the agent step rather than returning an agent-visible error string.
try:
return self._read(file_path, start_line, line_count)
except (FileStoreError, OSError) as e:
# A store can fail for reasons the local filesystem never had: an
# unreachable endpoint, a rejected request. Every other exit from
# this tool is an agent-visible string, and raising here would kill
# the agent's step rather than let it react, so this one is too.
# Path resolution and the store's own path labelling both live
# under here, which is why the message carries no path.
return (
f"Error: the {self._store.label} store failed. "
f"{format_error_for_display(e)}"
)
lib/crewai-tools/src/crewai_tools/tools/file_read_tool/file_read_tool.py:208
line_count = line_count or Nonetreatsline_count=0as “read the entire file” (because it becomesNone). If callers/LLMs pass 0, the tool will unexpectedly read everything instead of returning an empty result.
start_line = start_line or 1
line_count = line_count or None
FileReadTool and FileWriterTool assume a durable local disk. That holds on a developer's machine and breaks in any deployment environment where the runtime is ephemeral: whatever an agent writes is discarded when the run ends, and a later run cannot read it back. A crew that generates a report in one task and reads it in the next passes locally and fails there. This adds the seam needed to point those tools at durable storage instead. Both now route every path resolution and every read/write through a FileStore, defaulting to LocalFileStore — the current filesystem behavior, moved rather than rewritten. A deployment registers a different store through register_file_store_factory and the tools pick it up. The store owns its own containment, because the tools call nothing else before doing I/O. For the local store that stays validate_file_path plus the is_relative_to check; another store enforces whatever its own namespace requires, which may be prefix-based rather than realpath-based. resolve() and normalize() are separate so the reader can still pin its declared file for identity without a containment check, and base_dir is anchored through the store so both tools derive the same sandbox root from the same input. open_text() returns a handle rather than a string, which keeps the local store lazy: reading a small window out of a huge file does not pull the whole thing into memory. A store that must fetch eagerly can wrap its payload in StringIO. Behaviour is unchanged: every pre-existing file tool test passes untouched. The new suite stands in a store backed by a dict with no filesystem at all, which is what proves the seam is real — a tool that reached past it to open() or os.path would fail those assertions. It also covers the fallbacks that keep this safe to ship before any integration exists: a factory returning None, and a factory that raises, both leave the local filesystem in place rather than breaking file I/O. No new dependencies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…failures
Two review findings, both only reachable once a non-local store is
registered — which is why the local-filesystem tests could not see them.
The reader bound `_store` in `__init__` only. `BaseTool._resolve_tool_dict`
rebuilds a serialized tool with `model_validate`, which skips `__init__`
entirely, so a reconstructed reader came back with `_store` still None and
raised AttributeError on the first read — where before this branch it would
have called `validate_file_path`/`open` directly and worked. Binding moved
to `model_post_init`, which pydantic runs on both paths, and the declared
path, its label and the generated description are derived there too so a
rebuilt reader is indistinguishable from a fresh one. The writer already
did this correctly.
Every store call was guarded for `ValueError` alone, but the protocol
explicitly sanctions `FileStoreError` for failures the local filesystem
cannot have. Such a failure escaped `_run` and aborted the agent's step
instead of returning the error string the tools otherwise always return.
Both tools now wrap the whole operation, which also covers `exists()` and
the store's own `display()` — neither of which had any handler. The
established messages stay on their specific boundaries.
A bare `OSError("...")` still degrades to its type, because
`format_error_for_display` only passes `strerror` through: an OS-populated
OSError renders its absolute filename into `str()`, and #6692 deliberately
closed that. Stores wanting a legible message should raise `FileStoreError`.
Left that helper alone rather than widen a redaction from here.
12 new tests: reconstruction through both `model_validate` and a full
`model_dump` round-trip, a store failing at each of resolve/resolve_within/
display/exists/ensure_parent/open_text/write_text, and the redaction holding
on the new path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
Bugbot flagged that `model_post_init` re-derives `_declared_realpath` from the serialized `file_path`, so a rebuild in a different working directory can repoint the declared default. The mechanism is real. Traced it to exactly one case of three: absolute file_path -> survives a rebuild anywhere relative file_path + base_dir -> survives; base_dir is anchored already relative file_path, no base_dir -> re-anchors to the rebuilding cwd Keeping the re-anchor, deliberately. A bare relative path names nothing absolute to preserve, and the alternative is pinning a directory that, for a rebuild in a fresh container, no longer exists — reading a stale absolute path would be the worse failure. It is also not a regression in any case: before this branch a rebuilt reader lost the declared file outright and answered "No file path provided". Rewriting `file_path` to its resolved form at construction would close it, but `tests/agents/test_agent.py:2311` pins that the authored string survives, so that is a public-contract change rather than a fix. A serialized pin field would too, at the cost of a schema change — noted on the thread for whoever reviews, not taken unilaterally. So: all three cases now have a test, and the class docstring says which is which, so the behavior is a decision rather than something a reader has to infer. 38 tests in the seam suite, 338 across the file tools and crewai's tool suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
0e7f676 to
1c252e2
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (3)
lib/crewai-tools/src/crewai_tools/file_storage/base.py:68
- Same as
resolve():resolve_within()ValueError messages can be surfaced to the agent byFileWriterTool, so the protocol should require these messages to be safe for agent-visible output (no absolute/store-internal prefixes).
Raises:
ValueError: If *filename* escapes *directory*, or names the
directory itself.
"""
lib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.py:162
resolve_within()ValueError text is returned verbatim to the agent. For non-local stores this message is untrusted and can accidentally include absolute/store-internal path prefixes (similar to whyOSErroris redacted), and it also makes the user-facing error wording depend on store implementation details. Prefer a stable, non-leaky message here and keep details for errors that are explicitly sanitized (e.g.,OSErrorviaformat_error_for_display) or surfaced viaFileStoreError.
try:
resolved_filepath = store.resolve_within(resolved_directory, filename)
except ValueError as e:
return f"Error: Invalid file path — {e!s}"
lib/crewai-tools/src/crewai_tools/file_storage/base.py:47
- The tools surface
ValueErrormessages fromresolve()directly (e.g., viaformat_sandbox_error(str(error))). To prevent accidental leakage of absolute/store-internal prefixes from third-party stores, the protocol should explicitly require thatValueErrormessages are safe for agent-visible output.
This issue also appears on line 65 of the same file.
Raises:
ValueError: If the path falls outside what the store allows.
"""
…y can be Copilot flagged that `model_post_init` calls `store.normalize` / `store.display` unguarded, so a store raising there stops a serialized crew from loading — against this PR's own claim that a broken integration degrades rather than taking file I/O down. It suggested falling back to the local store. Not doing that: silently redirecting a crew configured for durable storage onto a disk that will be discarded trades a loud failure for quiet data loss, which is the exact bug this PR exists to fix. Three changes instead. The protocol now *states* the invariant it only implied. `normalize` and `display` must be pure string computation — no I/O, no raising — because the tools call them while a tool is being constructed. Both real stores already comply (`CdoFileStore` does `posixpath` arithmetic and never touches its client); this makes it a contract a new store is held to rather than a coincidence. The reader's declared-file derivation is now guarded, because a crew that cannot load over a *convenience default filename* is indefensible. It comes back without a default file and logs; any real problem resurfaces on the first read, where `_run` already reports it. `base_dir` anchoring stays unguarded, deliberately, and now says why: it is a containment guarantee, not a convenience. Leaving the root relative because a store hiccuped would let a later chdir move the sandbox — handing back a weaker sandbox than the caller asked for, silently. That failure should surface. 104 tests across the file tools. Two new, one per half of the asymmetry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (3)
lib/crewai-tools/src/crewai_tools/file_storage/base.py:63
- The FileStore protocol doc for normalize() says it must do “no I/O … and no raising”, but the default LocalFileStore.normalize() uses os.path.realpath()/os.getcwd() (filesystem-dependent) and can raise for invalid paths (e.g., embedded NUL). This makes the protocol contract inaccurate and could mislead third-party store implementers.
Must be a pure computation on the string: no I/O, no network, and no
raising. The tools call it while a tool is being constructed —
including when pydantic rebuilds a serialized crew — so a store that
reaches its backing service here turns a transient outage into a crew
that cannot load at all. Defer anything that can fail to
lib/crewai-tools/src/crewai_tools/file_storage/registry.py:74
- resolve_file_store() assumes the factory returns a valid FileStore. If an integration returns the wrong object type (a realistic “broken integration” case), the tools will hit AttributeError later (not caught by the current (FileStoreError, OSError) wrappers) and the agent step can crash. Consider validating the returned object against the runtime-checkable FileStore protocol and falling back to the local store with a warning.
if store is None:
return _local
return store
lib/crewai-tools/src/crewai_tools/file_storage/base.py:85
- Similarly, the FileStore.display() doc says it must be “pure” for the same reason as normalize(), but the local implementation delegates to format_path_for_display(), which calls os.getcwd()/os.path.realpath(). The guarantee that matters here is “no backend calls” and “no leaking absolute prefixes”, not “no I/O”.
Must not leak absolute directory prefixes; the tools put the result
straight into agent-visible output. Pure and non-raising for the same
reason as :meth:`normalize` — the reader labels its declared file at
construction time.
"""
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 953e1b9. Configure here.
On the two red checks — both pre-existing, neither from this PRFlagging this so the ❌ doesn't stall review. Every change in this PR is under
Each one passes locally on this branch and on a clean
The checks that do cover this PR's code — |
Bugbot caught a hole in the previous commit's own fix. Guarding the declared -file derivation clears `_declared_realpath` and `_declared_label`, but `description` is a serialized *field* — on a `model_validate` rebuild it arrives already saying "The default file is notes.txt, which is read when 'file_path' is omitted". So a rebuilt reader that lost its pin still advertised a default it could not read. The LLM would take the tool at its word, call it with no arguments, and get "No file path provided" — the tool lying about its own contract, which is worse than the missing default it was meant to degrade to. The failure path now restores the class-default description alongside clearing the pin, so what the tool says and what it does stay matched. 105 tests across the file tools; the new one asserts a real dumped description advertises the default, and that after a rebuild losing the pin it does not, matches the class default, and reports "No file path provided" on a bare call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/crewai-tools/src/crewai_tools/tools/file_read_tool/file_read_tool.py:166
- In
model_post_init, the declared-file derivation is meant to be a convenience that degrades rather than preventing a crew/tool from loading. Catching only(FileStoreError, OSError)still allows other store/implementation failures (e.g.,ValueError,TypeError,RuntimeError) fromnormalize()/display()to propagate during construction /model_validate, which can still break reconstruction despite the intended safety behavior.
Consider broadening this handler to except Exception so any unexpected store failure while deriving the default file results in “no default file” + warning, instead of an exception at load time.
try:
self._declared_realpath = store.normalize(self.file_path, self.base_dir)
self._declared_label = store.display(
self._declared_realpath, self.base_dir
)
except (FileStoreError, OSError):

Why
FileReadToolandFileWriterTooldo their I/O directly against the local disk. That is correct on a developer's machine, and it is correct within a single run anywhere: the tasks of one execution share a working directory, so a report written by one task is readable by the next.What it does not survive is the end of a run. Where the runtime is provisioned per execution, the working directory goes away with it, and nothing a crew wrote is reachable afterwards — not by the next run, not by an operator wanting the artifact. A crew that accumulates a dataset across runs, or writes an output meant to be collected later, has no way to do it with these tools today. There is no seam to point them anywhere else, so the only workaround is to stop using them.
This adds that seam. It changes no current behavior.
What this adds
A
FileStoreseam. Both tools now do every path resolution and every read/write through a store, defaulting toLocalFileStore— today's filesystem behavior, moved rather than rewritten. A deployment swaps in a different store withregister_file_store_factory.Design notes worth reviewing:
resolve()/resolve_within()must reject anything out of bounds. Locally that staysvalidate_file_pathplus theis_relative_tocheck; another store enforces whatever its own namespace requires, which may be prefix-based rather thanrealpath-based.resolve()andnormalize()are separate. The reader pins the file declared at construction so a laterchdircan't repoint it — that needs canonicalization without a containment check, whichnormalize()provides.open_text()returns a handle, not a string. That keeps the local store lazy, sostart_line/line_countstill reads a window out of a huge file without pulling it into memory. Stores that must fetch eagerly wrap the payload inStringIO.model_post_init, not__init__.BaseTool._resolve_tool_dictrebuilds a serialized tool withmodel_validate, which skips__init__. Anything derived only there comes back missing — see below.Safety
Three fallbacks keep this inert until a store is registered, and keep a broken integration from taking file I/O down:
None(its backing service isn't configured) → local filesystem.Review round
Two defects the bots found, both only reachable once a non-local store is registered — which is exactly why the local-filesystem tests could not see them:
Reader lost its store on reconstruction (Cursor, High).
_storewas bound in__init__only, andmodel_validateskips__init__, so a rebuilt reader came back with_storeasNoneand raisedAttributeErroron the first read — a regression, since before this branch it calledvalidate_file_path/opendirectly and worked. Binding moved tomodel_post_init, along with the declared path, its label and the generated description, so a reconstructed reader is indistinguishable from a fresh one. The writer already did this correctly; the two now match.Store failures escaped
_run(Copilot ×2, CodeRabbit). Every boundary caughtValueErroralone, while the protocol explicitly sanctionsFileStoreErrorfor failures the local filesystem cannot have. Both tools now wrap the whole operation, which also coversexists()and the store's owndisplay()— neither of which had a handler at all. The established messages stay on their specific boundaries, so nothing an existing test asserts has moved.The declared-path pin across a rebuild (Cursor, on the fix above). Moving derivation into
model_post_initmeant_declared_realpathis re-derived from the serializedfile_path, and Bugbot spotted that this can repoint the declared default. Traced to exactly one case of three: an absolutefile_path, or a relative one withbase_dirset, names the same file after a rebuild; a bare relativefile_pathre-anchors to the rebuilding process's cwd. I kept the re-anchor deliberately — a bare relative path names nothing absolute to preserve, and pinning a directory that no longer exists in a fresh container is the worse failure — and it is not a regression in any case, since before this branch a rebuilt reader lost the declared file outright. All three cases are now tests and the class docstring says which is which. Two stronger fixes exist (normalizefile_pathin place; carry the pin in a serialized field); both are contract or schema changes, so they're written up on the thread rather than taken unilaterally. Decided: keeping the re-anchor; the two stronger fixes were a public-contract change and a schema change respectively, neither worth the one case they close. Thread resolved.Construction-time store failures (Copilot ×2, on the fix above).
model_post_initruns duringmodel_validatetoo, so a store raising while normalizing could stop a serialized crew from loading. Copilot suggested falling back to the local store; I didn't, because for this feature that swaps a loud transient failure for silent permanent data loss — a crew configured for durable storage would quietly start writing to a disk that gets discarded. Instead: the protocol now states thatnormalize/displaymust be pure and non-raising (both real stores already comply —CdoFileStoredoesposixpatharithmetic and never touches its client), the reader's declared-file derivation is guarded because a crew failing to load over a convenience default filename is indefensible, andbase_diranchoring stays unguarded on purpose because it is a containment guarantee and silently leaving the root relative would let a laterchdirmove the sandbox. Convenience degrades, guarantees don't.One thing I did not change: a bare
OSError("...")from a store still reports as its type rather than its message, becauseformat_error_for_displaypasses onlystrerrorthrough. That is #6692 holding a line worth holding — an OS-populatedOSErrorrenders its absolute filename intostr(). Stores wanting a legible message should raiseFileStoreError, whose text is preserved. Widening a security redaction from this PR seemed the wrong trade; happy to revisit separately.Testing
104 pass across the file tools, 40 of them in the new seam suite (from 21 at open), and every pre-existing file tool test is untouched — the refactor is behavior-preserving, which is the main thing to check.
The new tests stand in a store backed by a plain dict with no filesystem behind it. That's what proves the seam is real: a tool that reached past it to
open()oros.pathwould fail them. They assert the writer leaves the real cwd empty, that a file written by one tool is readable by the other (the round-trip that breaks across runs), that line windows work against a non-filesystem store, that containment is honoured in both directions, that both tools derive the same sandbox root from the samebase_dir, and that anOSErrormessage never carries an absolute path.This round adds: reconstruction through both
model_validateand a fullmodel_dumpround-trip (reader and writer), a store failing at each ofresolve/resolve_within/display/exists/ensure_parent/open_text/write_text, the redaction holding on the new failure path, and the three declared-path-across-a-rebuild cases above. Those last ones deliberately use the local store: the memory store'snormalizenever consults the process cwd, which is precisely why the earlier round-trip coverage could not see the issue.Rebased onto current
main. Two red checks are pre-existing and unrelated, both verified against a cleanorigin/maincheckout:teststrips a different pre-existing flake per run —tests/llms/test_tool_call_streaming.py(4 identical failures on baremain) on one run, and thetest_trace_enable_disableVCR cassette exhaustion on the next, which is the same flake this PR hit back in July before any of these changes and which passes locally on both this branch and cleanmain, andVulnerability Scanfails on dependency advisories (aiohttp, andnltkon main's own last run) — this PR adds no dependencies and touches no lockfile, so it cannot have introduced them. The advisory floors belong in their own PR per repo convention.ruff,ruff formatandmypyclean on the touched files.tool.specs.jsonpicks up the expanded class docstring — the generate-specs job committed that aschore: update tool specifications, and a local regenerate is now a no-op against it. The two suites that fail locally (test_oxylabs_toolsneeds an extra CI installs,test_mongodb_vector_search_toolreads from stdin) fail identically with this branch stashed.🤖 Generated with Claude Code
Note
Medium Risk
Refactors security-sensitive path sandboxing and all file I/O behind a new abstraction; default behavior is intended to be unchanged, but misconfigured custom stores or serialization edge cases could affect where agents read/write.
Overview
Introduces a
FileStoreseam soFileReadToolandFileWriterToolcan persist reads/writes outside ephemeral local disk without changing crew or tool arguments. Deployments register a factory viaregister_file_store_factory; unregistered or broken factories still useLocalFileStore(today’s filesystem behavior, largely moved behind the protocol).Both tools resolve paths, open files, and write through the bound store (once per tool instance).
FileReadToolmoves declared-path pinning and description generation intomodel_post_initsomodel_validate/ serialized crews rebuild with a store and default file semantics intact. StoreFileStoreError/OSErrorfailures are returned as agent-visible strings instead of aborting the step.Adds a dict-backed test store and a broad seam test suite (round-trips, containment, reconstruction, factory fallbacks, failure paths).
tool.specs.jsonpicks up the expandedFileReadTooldocstring.Reviewed by Cursor Bugbot for commit c870f96. Bugbot is set up for automated code reviews on this repo. Configure here.