diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4a1543d8b..1026ffbdce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,8 +139,7 @@ jobs: github.repository == 'docker/docker-agent' && github.event.repository.fork == false && ( (github.event_name == 'pull_request' && - github.event.pull_request.base.repo.full_name == github.repository && - github.event.pull_request.head.repo.full_name == github.repository) || + github.event.pull_request.base.repo.full_name == github.repository) || (github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/heads/') && (github.ref != 'refs/heads/main' || inputs.image_mode == 'build-only') && @@ -265,68 +264,6 @@ jobs: GIT_TAG=pr GIT_COMMIT=dev - # Fork PRs cannot use repository variables or OIDC; build without credentials. - build-image-fork: - if: >- - github.repository == 'docker/docker-agent' && - github.event_name == 'pull_request' && - github.event.pull_request.base.repo.full_name == github.repository && - github.event.pull_request.head.repo.full_name != github.repository - permissions: - contents: read - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - with: - driver: docker-container - cache-binary: false - - - name: Build image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - context: . - platforms: ${{ matrix.platform }} - outputs: type=cacheonly - push: false - load: false - sbom: false - provenance: false - github-token: '' - build-args: | - GIT_TAG=pr - GIT_COMMIT=dev - - - name: Build template - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - context: . - target: template - platforms: ${{ matrix.platform }} - outputs: type=cacheonly - push: false - load: false - sbom: false - provenance: false - github-token: '' - build-args: | - GIT_TAG=pr - GIT_COMMIT=dev - build-and-push-image: if: >- github.repository == 'docker/docker-agent' && diff --git a/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml index 99a67bf1fb..b16bf9704b 100644 --- a/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.anthropic.com - body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' + body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' url: https://api.anthropic.com/v1/messages method: POST response: @@ -55,7 +55,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.anthropic.com - body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","type":"text"}],"role":"user"},{"content":[{"id":"toolu_012gmfqnoTX8c5aV3vMWUnas","input":{"path":"testdata/working_dir"},"name":"list_directory","cache_control":{"type":"ephemeral"},"type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_012gmfqnoTX8c5aV3vMWUnas","is_error":false,"cache_control":{"type":"ephemeral"},"content":[{"text":"FILE README.me\n","type":"text"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' + body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","type":"text"}],"role":"user"},{"content":[{"id":"toolu_012gmfqnoTX8c5aV3vMWUnas","input":{"path":"testdata/working_dir"},"name":"list_directory","cache_control":{"type":"ephemeral"},"type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_012gmfqnoTX8c5aV3vMWUnas","is_error":false,"cache_control":{"type":"ephemeral"},"content":[{"text":"FILE README.me\n","type":"text"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' url: https://api.anthropic.com/v1/messages method: POST response: diff --git a/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml index b955ef09b4..6588b8dcb4 100644 --- a/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml @@ -9,7 +9,7 @@ interactions: content_length: 0 host: generativelanguage.googleapis.com body: | - {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} + {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} form: alt: - sse @@ -33,7 +33,7 @@ interactions: content_length: 0 host: generativelanguage.googleapis.com body: | - {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"},{"parts":[{"functionCall":{"args":{"path":"testdata/working_dir"},"name":"list_directory"},"thoughtSignature":"c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I="}],"role":"model"},{"parts":[{"functionResponse":{"name":"call_3df8565b-a1ef-4490-95f9-5d94296d7687","response":{"result":"FILE README.me\n"}}}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} + {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"},{"parts":[{"functionCall":{"args":{"path":"testdata/working_dir"},"name":"list_directory"},"thoughtSignature":"c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I="}],"role":"model"},{"parts":[{"functionResponse":{"name":"call_3df8565b-a1ef-4490-95f9-5d94296d7687","response":{"result":"FILE README.me\n"}}}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} form: alt: - sse diff --git a/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml index 65fb6f20a7..ab0a21e909 100644 --- a/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.mistral.ai - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.mistral.ai/v1/chat/completions method: POST response: @@ -34,7 +34,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.mistral.ai - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"D9WYdiHxV","function":{"arguments":"{\"path\": \"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"D9WYdiHxV","role":"tool"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"D9WYdiHxV","function":{"arguments":"{\"path\": \"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"D9WYdiHxV","role":"tool"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.mistral.ai/v1/chat/completions method: POST response: diff --git a/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml b/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml index 29fc44f7d3..7702ab6f12 100644 --- a/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml +++ b/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: @@ -54,7 +54,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_dsl9jWekN0H1do1ClfeyR1iA","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_dsl9jWekN0H1do1ClfeyR1iA","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_dsl9jWekN0H1do1ClfeyR1iA","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_dsl9jWekN0H1do1ClfeyR1iA","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: diff --git a/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml index 50930716b2..36600c9291 100644 --- a/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: @@ -54,7 +54,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_I1tmAsYKD7bveEpXORJwVFgs","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_I1tmAsYKD7bveEpXORJwVFgs","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_I1tmAsYKD7bveEpXORJwVFgs","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_I1tmAsYKD7bveEpXORJwVFgs","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: diff --git a/e2e/testdata/cassettes/TestExec_ToolCallsNeedAcceptance.yaml b/e2e/testdata/cassettes/TestExec_ToolCallsNeedAcceptance.yaml index 0f19ed2a19..8c90b57c5d 100644 --- a/e2e/testdata/cassettes/TestExec_ToolCallsNeedAcceptance.yaml +++ b/e2e/testdata/cassettes/TestExec_ToolCallsNeedAcceptance.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"}],"model":"gpt-5-mini","reasoning":{"summary":"detailed"},"tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}' + body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"}],"model":"gpt-5-mini","reasoning":{"summary":"detailed"},"tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}' url: https://api.openai.com/v1/responses method: POST response: @@ -622,7 +622,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tools\n\n- Relative paths resolve from the working directory; absolute paths and \"..\" work as expected\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"},{"arguments":"{\"content\":\"Hello, World!\",\"path\":\"hello.txt\"}","call_id":"call_GdIrV330GhotzkZcKWdlNbPV","name":"write_file","type":"function_call"},{"output":"The user rejected the tool call.","call_id":"call_GdIrV330GhotzkZcKWdlNbPV","type":"function_call_output"}],"model":"gpt-5-mini","reasoning":{"summary":"detailed"},"tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}' + body: '{"input":[{"content":[{"text":"You are a knowledgeable assistant that can write test files.","type":"input_text"}],"role":"system"},{"content":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","type":"input_text"}],"role":"system"},{"content":"Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.","role":"user"},{"arguments":"{\"content\":\"Hello, World!\",\"path\":\"hello.txt\"}","call_id":"call_GdIrV330GhotzkZcKWdlNbPV","name":"write_file","type":"function_call"},{"output":"The user rejected the tool call.","call_id":"call_GdIrV330GhotzkZcKWdlNbPV","type":"function_call_output"}],"model":"gpt-5-mini","reasoning":{"summary":"detailed"},"tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","type":"function"}],"stream":true}' url: https://api.openai.com/v1/responses method: POST response: diff --git a/pkg/fake/proxy.go b/pkg/fake/proxy.go index 965fe09969..615c5f40b5 100644 --- a/pkg/fake/proxy.go +++ b/pkg/fake/proxy.go @@ -294,6 +294,9 @@ func DefaultMatcher(onError func(err error)) recorder.MatcherFunc { toolChoiceRegex := regexp.MustCompile(`"tool_choice":"[^"]*",?`) // Normalize prompt-file paths (they are machine-specific absolute paths). promptFileRegex := regexp.MustCompile(`Instructions from: (?:[^\\"\r\n]|\\\\)+`) + // Normalize the working directory stated in the filesystem toolset + // instructions (machine-specific absolute path). + workingDirRegex := regexp.MustCompile(`The working directory is \\"(?:[^\\"\r\n]|\\\\)+\\"`) return func(r *http.Request, i cassette.Request) bool { if r.Body == nil || r.Body == http.NoBody { @@ -325,12 +328,14 @@ func DefaultMatcher(onError func(err error)) recorder.MatcherFunc { normalizedReq = reasoningRegex.ReplaceAllString(normalizedReq, "") normalizedReq = toolChoiceRegex.ReplaceAllString(normalizedReq, "") normalizedReq = promptFileRegex.ReplaceAllString(normalizedReq, "Instructions from: FILE") + normalizedReq = workingDirRegex.ReplaceAllString(normalizedReq, "The working directory is WD") normalizedCassette := callIDRegex.ReplaceAllString(i.Body, "call_ID") normalizedCassette = maxTokensRegex.ReplaceAllString(normalizedCassette, "") normalizedCassette = thinkingConfigRegex.ReplaceAllString(normalizedCassette, "") normalizedCassette = reasoningRegex.ReplaceAllString(normalizedCassette, "") normalizedCassette = toolChoiceRegex.ReplaceAllString(normalizedCassette, "") normalizedCassette = promptFileRegex.ReplaceAllString(normalizedCassette, "Instructions from: FILE") + normalizedCassette = workingDirRegex.ReplaceAllString(normalizedCassette, "The working directory is WD") return normalizedReq == normalizedCassette } diff --git a/pkg/tools/builtin/filesystem/filesystem.go b/pkg/tools/builtin/filesystem/filesystem.go index bcd837415c..1fab47cc94 100644 --- a/pkg/tools/builtin/filesystem/filesystem.go +++ b/pkg/tools/builtin/filesystem/filesystem.go @@ -15,6 +15,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "strings" "sync" "unicode/utf8" @@ -249,9 +250,12 @@ func (t *ToolSet) Close() error { func (t *ToolSet) Instructions() string { var b strings.Builder - b.WriteString(`## Filesystem Tools - -- Relative paths resolve from the working directory; absolute paths and ".." work as expected + b.WriteString("## Filesystem Tools\n") + if t.workingDir != "" { + fmt.Fprintf(&b, "\n- The working directory is %q; relative paths resolve from it", t.workingDir) + } + b.WriteString(` +- Absolute paths must match the host OS (e.g. C:\... on Windows, /... on Unix) - Prefer read_multiple_files over sequential read_file calls - Use search_files_content to locate code or text across files - Use exclude patterns in searches and max_depth in directory_tree to limit output`) @@ -634,9 +638,9 @@ func (t *ToolSet) executePostEditCommands(ctx context.Context, filePath string) // resolvePath resolves a path relative to the working directory. // A leading "~" or "~/" is expanded to the user's home directory so that // LLM-supplied paths like "~/file.txt" work without the agent having to -// know the user's home directory upfront. Relative paths (including ".") -// are joined with the working directory. Absolute paths and paths -// starting with ".." are used as-is. +// know the user's home directory upfront. Absolute paths are used as-is; +// everything else (including "." and "..") is joined with the working +// directory. // // resolvePath does NOT enforce the allow- or deny-lists; callers should use // [resolveAndCheckPath] when those checks are required (i.e. for any path @@ -652,6 +656,43 @@ func (t *ToolSet) resolvePath(path string) string { return filepath.Clean(filepath.Join(t.workingDir, path)) } +// checkForeignPath rejects paths that are absolute in another OS's syntax +// (e.g. "/mnt/c/..." on Windows, "C:\..." on Unix). filepath.IsAbs does not +// recognize them, so resolvePath would silently join them onto the working +// directory and the caller would get a baffling not-found for a mangled +// path. An explicit "./" prefix bypasses the check for the rare legitimate +// file whose name matches one of these shapes. +func checkForeignPath(path, goos string) error { + if goos == "windows" { + switch { + // "//server/share" and "\\server\share" are valid UNC paths. + case strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//"): + return fmt.Errorf("path %q looks like a POSIX absolute path, but this host runs Windows; use a Windows path (e.g. C:\\...) or a path relative to the working directory", path) + case strings.HasPrefix(path, `\`) && !strings.HasPrefix(path, `\\`): + return fmt.Errorf("path %q is rooted but has no drive letter; use a full Windows path (e.g. C:\\...) or a path relative to the working directory", path) + case isDriveRelative(path): + return fmt.Errorf("path %q is drive-relative; use a full Windows path (e.g. C:\\...) or a path relative to the working directory", path) + } + return nil + } + if hasDrivePrefix(path) && len(path) > 2 && (path[2] == '/' || path[2] == '\\') { + return fmt.Errorf("path %q looks like a Windows absolute path, but this host runs %s; use a POSIX path (e.g. /home/...) or a path relative to the working directory", path, goos) + } + return nil +} + +// hasDrivePrefix reports whether path starts with a drive letter and colon. +func hasDrivePrefix(path string) bool { + return len(path) >= 2 && path[1] == ':' && + ('a' <= path[0] && path[0] <= 'z' || 'A' <= path[0] && path[0] <= 'Z') +} + +// isDriveRelative reports whether path is like "C:" or "C:foo" — relative +// to the current directory of drive C:, which filepath.Join cannot represent. +func isDriveRelative(path string) bool { + return hasDrivePrefix(path) && (len(path) == 2 || path[2] != '/' && path[2] != '\\') +} + // resolveAndCheckPath is the canonical entry point used by every filesystem // handler that operates on a user-supplied path. It resolves the path against // the working directory and validates the result against the allow- and @@ -695,6 +736,9 @@ func (t *ToolSet) resolveAndCheckPath(path string) (string, error) { if t.sandboxBroken { return "", errors.New("filesystem toolset is disabled due to invalid allow/deny list configuration") } + if err := checkForeignPath(path, runtime.GOOS); err != nil { + return "", err + } resolved := t.resolvePath(path) if t.agentsIgnore.Match(resolved) { @@ -716,6 +760,12 @@ func (t *ToolSet) resolveAndCheckPath(path string) (string, error) { return resolved, nil } +// resolutionHint tells the model where a failed path actually pointed, so +// it can correct a wrong base instead of retrying the same path. +func (t *ToolSet) resolutionHint(resolved string) string { + return fmt.Sprintf(" (resolved to %q; relative paths resolve from the working directory %q)", resolved, t.workingDir) +} + // rootedAccess returns the [*os.Root] handle and rooted (slash-separated) // name for resolved when the allow-list is configured. // @@ -969,6 +1019,9 @@ func (t *ToolSet) handleEditFile(ctx context.Context, args EditFileArgs) (*tools content, err := t.readFile(resolvedPath) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return tools.ResultError("not found" + t.resolutionHint(resolvedPath)), nil + } return tools.ResultError(fmt.Sprintf("Error reading file: %s", err)), nil } @@ -1013,6 +1066,9 @@ func (t *ToolSet) handleListDirectory(ctx context.Context, args ListDirectoryArg entries, err := t.readDir(resolvedPath) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return tools.ResultError("not found" + t.resolutionHint(resolvedPath)), nil + } return tools.ResultError(fmt.Sprintf("Error reading directory: %s", err)), nil } @@ -1082,7 +1138,7 @@ func (t *ToolSet) handleReadFile(ctx context.Context, args ReadFileArgs) (*tools if err != nil { var errMsg string if errors.Is(err, fs.ErrNotExist) { - errMsg = "not found" + errMsg = "not found" + t.resolutionHint(resolvedPath) } else { errMsg = err.Error() } @@ -1091,6 +1147,7 @@ func (t *ToolSet) handleReadFile(ctx context.Context, args ReadFileArgs) (*tools Output: errMsg, IsError: true, Meta: ReadFileMeta{ + Path: args.Path, Error: errMsg, }, }, nil @@ -1318,7 +1375,7 @@ func (t *ToolSet) handleReadMultipleFiles(ctx context.Context, args ReadMultiple if err != nil { errMsg := err.Error() if errors.Is(err, fs.ErrNotExist) { - errMsg = "not found" + errMsg = "not found" + t.resolutionHint(resolvedPath) } contents = append(contents, PathContent{ Path: path, diff --git a/pkg/tools/builtin/filesystem/filesystem_paths_test.go b/pkg/tools/builtin/filesystem/filesystem_paths_test.go index 4259730968..28f9095b08 100644 --- a/pkg/tools/builtin/filesystem/filesystem_paths_test.go +++ b/pkg/tools/builtin/filesystem/filesystem_paths_test.go @@ -1,8 +1,10 @@ package filesystem import ( + "fmt" "os" "path/filepath" + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -46,6 +48,59 @@ func TestFilesystemTool_DefaultIsUnrestricted(t *testing.T) { assert.Equal(t, want, resolved) } +func TestCheckForeignPath(t *testing.T) { + t.Parallel() + tests := []struct { + path string + goos string + reject bool + }{ + {"/mnt/c/Users/x/compose.yml", "windows", true}, + {`\foo\bar`, "windows", true}, + {"C:foo", "windows", true}, + {"C:", "windows", true}, + {`C:\Users\x`, "windows", false}, + {"C:/Users/x", "windows", false}, + {"//server/share", "windows", false}, + {`\\server\share`, "windows", false}, + {"./mnt/c/x", "windows", false}, + {"rel/path", "windows", false}, + {"~/file.txt", "windows", false}, + {`C:\Users\x`, "linux", true}, + {"C:/Users/x", "darwin", true}, + {"/mnt/c/Users/x", "linux", false}, + {`./C:\weird`, "linux", false}, + {"C:foo", "linux", false}, + {"rel/path", "linux", false}, + } + for _, tt := range tests { + t.Run(tt.goos+" "+tt.path, func(t *testing.T) { + err := checkForeignPath(tt.path, tt.goos) + if tt.reject { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +// A foreign-OS absolute path must be rejected with guidance instead of +// silently joining onto the working directory (issue seen on Windows with +// "/mnt/c/..." resolving to "\mnt\c\..."). +func TestResolveAndCheckPath_RejectsForeignPath(t *testing.T) { + t.Parallel() + tool := New(t.TempDir()) + + foreign := `C:\Users\x\compose.yml` + if runtime.GOOS == "windows" { + foreign = "/mnt/c/Users/x/compose.yml" + } + _, err := tool.resolveAndCheckPath(foreign) + require.Error(t, err) + assert.Contains(t, err.Error(), "relative to the working directory") +} + func TestFilesystemTool_AllowList_DotMeansWorkingDir(t *testing.T) { t.Parallel() tmpDir := t.TempDir() @@ -351,11 +406,16 @@ func TestFilesystemTool_Instructions_MentionsRestrictions(t *testing.T) { t.Parallel() wd := t.TempDir() - // Default instructions: no restriction text. + // Default instructions: working directory stated (%q-quoted), no + // restriction text. plain := New(wd).Instructions() + assert.Contains(t, plain, fmt.Sprintf("%q", wd)) assert.NotContains(t, plain, "restricted") assert.NotContains(t, plain, "must not access") + // No working directory configured: no dangling `The working directory is ""`. + assert.NotContains(t, New("").Instructions(), "working directory is") + // With an allow-list: instructions mention the restriction. allowed := newTestToolSet(t, wd, WithAllowList([]string{".", "~"})).Instructions() assert.Contains(t, allowed, "restricted") diff --git a/pkg/tools/builtin/filesystem/filesystem_test.go b/pkg/tools/builtin/filesystem/filesystem_test.go index 3c7f7fa9dc..1165e5bf5f 100644 --- a/pkg/tools/builtin/filesystem/filesystem_test.go +++ b/pkg/tools/builtin/filesystem/filesystem_test.go @@ -3,6 +3,7 @@ package filesystem import ( "bytes" "encoding/json" + "fmt" "image" "image/color" "image/jpeg" @@ -178,7 +179,12 @@ func TestFilesystemTool_ReadFile(t *testing.T) { Path: "nonexistent.txt", }) require.NoError(t, err) - assert.Equal(t, "not found", result.Output) + assert.Contains(t, result.Output, "not found") + // The hint must name the resolved path and the working directory so the + // model can correct a wrong base instead of retrying the same path. + // %q-quoted, so Windows separators appear escaped in the output. + assert.Contains(t, result.Output, fmt.Sprintf("%q", filepath.Join(tmpDir, "nonexistent.txt"))) + assert.Contains(t, result.Output, fmt.Sprintf("%q", tmpDir)) } // TestFilesystemTool_ReadFile_LineRange is a regression test for issue @@ -401,7 +407,7 @@ func TestFilesystemTool_ReadImageFile(t *testing.T) { result, err = tool.handleReadFile(t.Context(), ReadFileArgs{Path: "missing.png"}) require.NoError(t, err) assert.True(t, result.IsError) - assert.Equal(t, "not found", result.Output) + assert.Contains(t, result.Output, "not found") } func TestFilesystemTool_ReadMultipleFiles(t *testing.T) { @@ -456,7 +462,7 @@ func TestFilesystemTool_ListDirectory(t *testing.T) { Path: "nonexistent", }) require.NoError(t, err) - assert.Contains(t, result.Output, "Error reading directory") + assert.Contains(t, result.Output, "not found") } // TestFilesystemTool_ListDirectoryEmpty pins the empty-directory message: