Repository navigation
feature: support repository archive assets in inject_asset - #63
Conversation
📊 PR Complexity Assessment: Moderate (Score: 3/5)This PR adds a new asset type to the inject_asset agent: 🔍 Complexity Drivers
🎯 Suggested Attention Areas
|
Handle the `v3.asset.file.repository_archive` selector alongside the existing repository selector. The archive is downloaded from `content_url` or extracted from embedded `content` bytes, and lands in the shared `/code` volume the same way a cloned repository does. An archive that cannot be extracted is logged and skipped rather than emitted. `repository_archive` is ported from agent_cloud_inject_asset, keeping its extraction guards: format detection by header signature (zip, tar and its compressed variants, 7z), rejection of members resolving outside the destination, caps on archive size, total uncompressed size and member count, and extraction into a staging directory so a partial failure leaves nothing behind. The per-asset directory naming from the cloud agent is left out. It exists to keep multi-asset scans from colliding, and this agent injects a single asset per scan volume. Requires ostorlab >= 2.10.0, the first release publishing the repository archive proto.
da3597a to
9dd4c56
Compare
There was a problem hiding this comment.
Code Review Summary
Total Issues Found: 5
Critical Issues (Bugs): 1
Security Issues: 1
Suggestions: 3
This PR adds repository archive download/extraction support (tar, zip, 7z) with security guards (member-count caps, size limits, path checks, tar filter="data") plus a new ArchiveDownloadError type and test suite. The design intent is sound and the zip/tar handling is solid, but the patch needs fixes before merge:
- Critical (Bug): The exception tuples in
extract_content/download_archivedo not cover realistic corruption errors from the archive libraries (EOFError from truncated gzip, zlib.error from corrupt deflate streams, lzma.LZMAError, py7zr CrcError/DecompressionError). These escape all handlers up through_emit_assetand crash the entire agentstart(), aborting injection of every asset in the scan — directly violating the documented skip-on-failure contract. - Security: The 7z branch lacks any symlink containment. py7zr materializes symlink members as real symlinks, so a crafted archive (e.g.,
link -> ../../..followed bylink/evil) passes the lexical filename check and writes outside the destination, with the escaping symlink also relocated into/code. - Suggestion: The new
ArchiveDownloadErrorcreates a second exception root inproviders/errors.py, contradicting the module docstring and the single-base convention. - Suggestion: The 7z branch's guard wiring (member count, size cap, path check) has zero test coverage, so regressions would pass CI.
- Suggestion: The
membersparameter should be annotated withcollections.abc.Sequence[str]per project convention.
Key themes are fail-closed archive extraction, exception-boundary translation (library errors must be normalized to domain errors at the _extract boundary), and format-parity in both defenses and tests.
Translate every archive-library failure into ArchiveDownloadError at the _extract boundary and in both outer handlers, rather than enumerating exception types. A truncated tar.gz raised EOFError, which matched none of the caught types and escaped _persist_repository_archive and the handler in _emit_asset, crashing start() and aborting injection of every asset in the scan instead of skipping the one bad archive. Audit extracted symlinks for the 7z branch. The lexical member-path check runs before extraction and cannot see link semantics, and unlike tar (filter="data") and zip (extractall sanitization) nothing constrained 7z symlink members. Current py7zr rejects escaping symlinks itself; auditing the staging directory keeps that guarantee ours rather than depending on an unpinned transitive version. Give the provider errors a single Error base so both failure families can be caught as one type, and refresh the module docstring that still claimed every failure surfaced as CloneError. Cover the 7z guard wiring, which had no tests, and annotate the member list as collections.abc.Sequence[str]. Restore the unpinned ostorlab[agent] requirement and the 2.0.3 agent version; the unpinned requirement resolves to 2.10.0, which ships the repository archive proto.
There was a problem hiding this comment.
Re-review: 2 remaining findings (1 bug, 1 security)
Fixes from the previous round verified at head 67eb588: the exception-boundary translation, the 7z post-extraction symlink audit, the Sequence[str] annotation, and the new 7z guard tests are all in place. The missing tests/providers/errors_test.py issue is already tracked in the errors.py thread (#3981739117) and is not duplicated here.
Two newly confirmed issues remain:
- agent/repository_archive.py:36 (Bug): the lzma-alone signature requires a zero dictionary size, which no real encoder emits, so every valid lzma-compressed tar falls through to "Unsupported repository archive format" — the advertised lzma support is non-functional.
- agent/repository_archive.py:168 (Security): the member cap fires only after
ZipFile/py7zrmaterialize their full member lists at open, so a crafted archive under the 5 GB cap can exhaust memory before_check_member_countruns; the "lazy enumeration" guarantee holds only for tar.
Event is COMMENT — not mergeable until these are addressed.
There was a problem hiding this comment.
Review follow-up
1 new issue found (maintainability / test design). No critical bugs or security issues in this pass.
- tests/repository_archive_test.py:339 — the 7z symlink-escape audit is unit-tested through a private seam. Make it reachable via the public surface: expose the helper as a public module-level function, or drive it through
extract_contentwith the py7zr extraction call mocked at the external library boundary so an escaping symlink is materialized before the audit runs — while keeping the assertion that the resultingArchiveDownloadErrorleaves the destination empty.
Note: the missing tests/providers/errors_test.py follow-up (the two error-hierarchy assertions and the one-test-file-per-module convention) is already tracked on the agent/providers/errors.py thread from commit 645f046 and remains unresolved at the PR head, so it is not duplicated here.
There was a problem hiding this comment.
Re-review at head f46c40c. The previously reported fixes verified: boundary exception translation to ArchiveDownloadError, the 7z symlink audit via the public check_extracted_symlinks seam, LZMA-alone signature detection, the pre-materialization ZIP EOCD/ZIP64 and 7z header-size caps, and the Sequence[str] annotation. Two issues remain open:
- Security — the new EOCD guard's record selection diverges from zipfile's own parser (65,557-byte scan window with no record validation vs zipfile's 65,558-byte window plus comment-length-to-EOF acceptance rule), leaving a craftable bypass at
file_size - 65_558that re-opens the exact pre-materialization memory-exhaustion window the guard was added to close. Align with CPython_EndRecData's acceptance rule. - Coverage —
tests/providers/errors_test.py, described as added in 645f046, is still absent at head, so theErrorre-parenting shared-contract change ships with zero test coverage; the ZIP64 sub-branch of the EOCD guard is also untested.
Details in the inline comments.
|
|
||
|
|
||
| class CloneError(Exception): | ||
| class Error(Exception): |
There was a problem hiding this comment.
Commit the missing tests/providers/errors_test.py — it is still absent at head f46c40c (fetching it returns 404 and it is not in the PR's changed-file list), even though the 645f046 reply described it as added. Pin the two properties that reply promised: every persistence failure is catchable as Error, and ArchiveDownloadError is not a CloneError (which is what keeps the existing except CloneError in _emit_asset correctly scoped to repository failures only). The Error re-parenting is a shared-contract change currently shipped with zero test coverage, and the project convention requires one test file per source module (agent/providers/errors.py → tests/providers/errors_test.py). If the intent was to reuse an existing module instead, fold those two assertions into a tests/providers test file.
| with archive_path.open("rb") as archive_file: | ||
| archive_file.seek(0, 2) | ||
| archive_size = archive_file.tell() | ||
| trailer_size = min(archive_size, 22 + 65535) |
There was a problem hiding this comment.
Align this guard's record selection with zipfile's own parser (CPython _EndRecData) so the guard and ZipFile.__init__ can never disagree about the authoritative EOCD. The guard scans the last 65,557 bytes with no validation of the found record, while zipfile scans the last 65,558 bytes and only accepts a record whose declared comment length reaches exactly EOF. A crafted archive whose EOCD starts precisely at file_size - 65_558 is invisible to this guard (the signature's first byte falls outside the 65,557-byte window, so rfind cannot match it and the guard returns without enforcing) yet is accepted by zipfile, which then materializes the full >1M-entry central directory before _check_member_count can fire — re-opening the exact pre-materialization memory window this check was added to close. Use the same 65,558-byte window and treat a record as authoritative only when EOCD start + 22 + declared comment size == archive_size (mirroring zipfile's acceptance rule, including the empty-comment fast path), keeping the ZIP64 fallback; records the parser will reject can simply be skipped here since ZipFile then fails closed with BadZipFile. Also add a regression test for the ZIP64 sub-branch (count == 0xFFFF → locator → ZIP64 EOCD), which currently has no coverage.
Adds support for
v3.asset.file.repository_archive, allowing scans to target source archives instead of live repositories.What it does
content_urlarchives or extracts embeddedcontentbytes into the shared/codevolume.ArchiveDownloadError; invalid archives are logged and skipped without aborting the scan.Verification
oxo agent build -f ostorlab.yaml -o ostorlab --force. The resulting image wasagent_ostorlab_inject_asset:v2.0.3(616c3b8061a7).8ca1f7aa62dece9142146d356fc3ddb3ab06e735.139completed with statusDone./code.v3.asset.file.repository_archive.