Skip to content

fix(code-executors): preserve hard-linked output files (#7906) - #7932

Open
OKAN-SOFTWARE wants to merge 2 commits into
google:mainfrom
OKAN-SOFTWARE:fix/7906-hardlinked-output-files
Open

OKAN-SOFTWARE wants to merge 2 commits into
google:mainfrom
OKAN-SOFTWARE:fix/7906-hardlinked-output-files

Conversation

@OKAN-SOFTWARE

@OKAN-SOFTWARE OKAN-SOFTWARE commented Nov 2, 2026 •

Copy link
Copy Markdown

Link to Issue or Description of Change

Closes #7906

Problem:

ContainerCodeExecutor silently drops output files represented as TAR hard-link entries because _get_output_files() only processes regular files using TarInfo.isfile().

Solution:

  • Support regular files and hard links using member.isfile() or member.islnk().
  • Preserve both hard-linked filenames and their original contents.
  • Continue excluding symbolic links.
  • Enforce the output-size limit after resolving hard links to prevent excessive duplicated output.
  • Add regression tests covering hard-link extraction, symbolic-link exclusion, and output-size protection.

Testing Plan

Unit Tests:

  • Added regression tests for the fix.
  • All tests in the affected test module pass locally.

Results:

Environment Passed Skipped Failed
Python 3.11 (Windows) 42 6 0
Python 3.12 (Windows) 42 6 0

Additional validation:

  • Ruff: Passed
  • isort: Passed
  • Pyink: Passed
  • Git whitespace checks: Passed

Full Test Suite:

The full suite was attempted using Tox. Python 3.11 collected 22,640 tests but stopped at an A2A client-factory test with:

AttributeError: 'CustomFactory' object has no attribute '_httpx_client'

This failure occurs outside the modified container executor files. The complete multi-version suite has not passed locally.

Manual End-to-End (E2E) Tests:

Live Docker integration testing has not yet been performed. The regression tests use mocked Docker archives.

For manual verification, execute code that creates two hard-linked files inside the container's output directory using os.link(), then verify that both filenames appear in CodeExecutionResult.output_files with identical contents.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my code.
  • I have added explanatory comments where needed.
  • I have added regression tests that demonstrate the fix.
  • Targeted unit tests pass locally.
  • Full multi-version test suite passes locally.
  • Manual end-to-end testing completed.

Additional Context

The issue author confirmed that this contribution is welcome and offered to review the pull request.

The Google Individual Contributor License Agreement is on file, and the GitHub CLA check has passed.

@BlueRaddish BlueRaddish left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new islnk() branch also follows hard links to symbolic-link members, so a valid output archive can now abort the whole result instead of ignoring its symlinks.

For example, keep an ordinary outputs/ordinary.txt in the archive, then add:

symlink = tarfile.TarInfo('outputs/symbolic.txt')
symlink.type = tarfile.SYMTYPE
symlink.linkname = 'missing-target'
tar.addfile(symlink)

alias = tarfile.TarInfo('outputs/hard-symlink.txt')
alias.type = tarfile.LNKTYPE
alias.linkname = 'outputs/symbolic.txt'
tar.addfile(alias)

At head 63048908976ef965aaefaa01e2e2c366ab9c6626, tar.extractfile(alias) follows the symbolic target and raises KeyError: "linkname 'outputs/missing-target' not found". _get_output_files() does not catch it, so execute_code() loses the successful execution result and the unrelated ordinary output. Parent 95d8610e returns that ordinary file and ignores both links. If the symlink instead points to ordinary.txt, the head exports hard-symlink.txt with the target's data, although the direct symlink is excluded.

This is not restricted to a fabricated archive type: Moby's archive producer records hard links for non-directory entries, and its Unix handling explicitly accounts for hard links to symlinks. A hard-linked symlink inode should remain excluded along with other symlinks, rather than being treated as a regular-file output.

Please check the resolved hard-link target's entry type before extracting, excluding symbolic-link targets while preserving the ordinary hard-link contents and expansion budget. A regression for the archive above would cover the result-loss case.

Verification: I compared five real TAR fixtures against the exact parent/head execute_code, _execute_code_locked, _prepare_directories, _get_output_files, and MIME helper source, with the original File/input/result dataclasses. Ordinary files and direct-symlink exclusion remain controls on both revisions; regular-file hard links gain the intended second output on the head. The two hard-linked-symlink cases produce the differences described above. These were Windows/Python 3.13.14 CPU checks with the Docker I/O boundary recorded and stubbed; I did not start Docker, execute generated code, import the complete ADK package, or run the owning/full tox suites. OpenAI Codex assisted with this review.

@OKAN-SOFTWARE

Copy link
Copy Markdown
Author

Thanks for catching this edge case.

Addressed in commit 64e049bf6. Hard links are now exported only when they resolve to regular-file entries. Hard links to symbolic links are excluded without losing unrelated output files.

Added regression coverage for broken and valid symlink targets, ordinary hard links, and chained hard links.

Validation:

  • Regression reproduced before the fix and passed afterward.
  • 43 tests passed, 6 skipped (Windows/Python 3.12, with a test-process-only CSV MIME override).
  • Ruff, isort, Pyink, and Git whitespace checks passed.

The full multi-version suite and live Docker E2E tests have not been rerun for this correction.

Ready for another review. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ContainerCodeExecutor drops output files that are hard links

3 participants