Slack: ground DOCX attachments and govern uploads - #3
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds bounded DOCX extraction and integrates governed Slack uploads, Slack Connect attachment resolution, document ingestion, retry-limited downloads, and deduplicated thread attachment context with expanded tests. ChangesSlack artifact and attachment handling
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant SlackEvent
participant SlackAdapter
participant SlackFiles
participant DocumentExtractor
participant AgentPrompt
SlackEvent->>SlackAdapter: receive attachment metadata
SlackAdapter->>SlackFiles: resolve files_info and download bounded bytes
SlackFiles-->>SlackAdapter: attachment bytes
SlackAdapter->>DocumentExtractor: extract DOCX text
DocumentExtractor-->>SlackAdapter: text and truncation metadata
SlackAdapter->>AgentPrompt: add provenance and untrusted attachment content
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
3 |
unresolved-attribute |
1 |
First entries
tests/gateway/test_document_extract.py:6: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/document_extract.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `defusedxml`
tests/gateway/test_slack.py:629: [unresolved-attribute] unresolved-attribute: Attribute `kwargs` is not defined on `None` in union `_Call | None`
gateway/document_extract.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `defusedxml.common`
✅ Fixed issues (1):
| Rule | Count |
|---|---|
invalid-return-type |
1 |
First entries
gateway/platforms/slack.py:2933: [invalid-return-type] invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `bytes`
Unchanged: 5061 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Retry bytes trip size limit
- Changed size limit check to only use current attempt bytes and return actual file size instead of cumulative transfer across retries.
- ✅ Fixed: Char budget blocks all files
- Separated char budget check from hard limits and only apply it to DOCX files since they're the only type with text extraction.
- ✅ Fixed: XML bound trusts zip headers
- Changed to stream-read with a hard byte limit per chunk instead of trusting the declared file_size in zip headers.
Or push these changes by commenting:
@cursor push 1f13adad32
Preview (1f13adad32)
diff --git a/gateway/document_extract.py b/gateway/document_extract.py
--- a/gateway/document_extract.py
+++ b/gateway/document_extract.py
@@ -132,24 +132,29 @@ def extract_docx_text(
raise DocumentExtractionError(
"malformed_docx", "DOCX package is missing word/document.xml"
) from exc
- if info.file_size > max_xml_bytes:
- raise DocumentExtractionError(
- "docx_too_large",
- f"DOCX main document XML exceeds the {max_xml_bytes}-byte extraction limit",
- )
- document_xml = archive.read(info)
+ # Stream-read with a hard byte limit to defend against zip bombs
+ # (malicious files with false declared sizes in headers).
+ chunks: list[bytes] = []
+ bytes_read = 0
+ with archive.open(info) as entry:
+ while True:
+ chunk = entry.read(65536)
+ if not chunk:
+ break
+ bytes_read += len(chunk)
+ if bytes_read > max_xml_bytes:
+ raise DocumentExtractionError(
+ "docx_too_large",
+ f"DOCX main document XML exceeds the {max_xml_bytes}-byte extraction limit",
+ )
+ chunks.append(chunk)
+ document_xml = b"".join(chunks)
except DocumentExtractionError:
raise
except (BadZipFile, OSError, RuntimeError, ValueError) as exc:
raise DocumentExtractionError(
"malformed_docx", "DOCX package is malformed or unreadable"
) from exc
-
- if len(document_xml) > max_xml_bytes:
- raise DocumentExtractionError(
- "docx_too_large",
- f"DOCX main document XML exceeds the {max_xml_bytes}-byte extraction limit",
- )
try:
root = ElementTree.fromstring(document_xml)
except ElementTree.ParseError as exc:
@@ -132,24 +132,29 @@ def extract_docx_text(
raise DocumentExtractionError(
"malformed_docx", "DOCX package is missing word/document.xml"
) from exc
- if info.file_size > max_xml_bytes:
- raise DocumentExtractionError(
- "docx_too_large",
- f"DOCX main document XML exceeds the {max_xml_bytes}-byte extraction limit",
- )
- document_xml = archive.read(info)
+ # Stream-read with a hard byte limit to defend against zip bombs
+ # (malicious files with false declared sizes in headers).
+ chunks: list[bytes] = []
+ bytes_read = 0
+ with archive.open(info) as entry:
+ while True:
+ chunk = entry.read(65536)
+ if not chunk:
+ break
+ bytes_read += len(chunk)
+ if bytes_read > max_xml_bytes:
+ raise DocumentExtractionError(
+ "docx_too_large",
+ f"DOCX main document XML exceeds the {max_xml_bytes}-byte extraction limit",
+ )
+ chunks.append(chunk)
+ document_xml = b"".join(chunks)
except DocumentExtractionError:
raise
except (BadZipFile, OSError, RuntimeError, ValueError) as exc:
raise DocumentExtractionError(
"malformed_docx", "DOCX package is malformed or unreadable"
) from exc
-
- if len(document_xml) > max_xml_bytes:
- raise DocumentExtractionError(
- "docx_too_large",
- f"DOCX main document XML exceeds the {max_xml_bytes}-byte extraction limit",
- )
try:
root = ElementTree.fromstring(document_xml)
except ElementTree.ParseError as exc:
diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py
--- a/gateway/platforms/slack.py
+++ b/gateway/platforms/slack.py
@@ -3021,10 +3021,11 @@ async def _fetch_thread_context(
continue
remaining_bytes = MAX_THREAD_ATTACHMENT_BYTES - attachment_bytes
remaining_chars = MAX_THREAD_EXTRACTED_CHARS - extracted_chars
+
+ # Hard budget limits (file count, total bytes)
if (
attachment_count >= MAX_THREAD_ATTACHMENT_FILES
or remaining_bytes <= 0
- or remaining_chars <= 0
):
attachment_parts.append(
"[Slack thread attachment notice]\n"
@@ -3021,10 +3021,11 @@ async def _fetch_thread_context(
continue
remaining_bytes = MAX_THREAD_ATTACHMENT_BYTES - attachment_bytes
remaining_chars = MAX_THREAD_EXTRACTED_CHARS - extracted_chars
+
+ # Hard budget limits (file count, total bytes)
if (
attachment_count >= MAX_THREAD_ATTACHMENT_FILES
or remaining_bytes <= 0
- or remaining_chars <= 0
):
attachment_parts.append(
"[Slack thread attachment notice]\n"
@@ -3033,6 +3034,21 @@ async def _fetch_thread_context(
)
attachment_budget_exhausted = True
continue
+
+ # Char budget only applies to DOCX (text extraction)
+ filename = str(file_obj.get("name") or "")
+ _, ext = os.path.splitext(filename)
+ ext = ext.lower()
+ is_docx = (
+ ext == ".docx"
+ or mimetype == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ )
+ if is_docx and remaining_chars <= 0:
+ attachment_parts.append(
+ "[Slack thread attachment notice]\n"
+ "DOCX attachment skipped: text-extraction budget exhausted."
+ )
+ continue
attachment_count += 1
try:
block, downloaded_bytes, document_chars = (
@@ -3033,6 +3034,21 @@ async def _fetch_thread_context(
)
attachment_budget_exhausted = True
continue
+
+ # Char budget only applies to DOCX (text extraction)
+ filename = str(file_obj.get("name") or "")
+ _, ext = os.path.splitext(filename)
+ ext = ext.lower()
+ is_docx = (
+ ext == ".docx"
+ or mimetype == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ )
+ if is_docx and remaining_chars <= 0:
+ attachment_parts.append(
+ "[Slack thread attachment notice]\n"
+ "DOCX attachment skipped: text-extraction budget exhausted."
+ )
+ continue
attachment_count += 1
try:
block, downloaded_bytes, document_chars = (
@@ -3371,7 +3387,7 @@ async def _download_slack_file_bytes(
async for chunk in response.aiter_bytes():
data.extend(chunk)
attempt_bytes_consumed += len(chunk)
- if retry_bytes_consumed + attempt_bytes_consumed > max_bytes:
+ if attempt_bytes_consumed > max_bytes:
raise _SlackAttachmentError(
"Slack attachment download exceeded the "
f"{max_bytes}-byte limit.",
@@ -3371,7 +3387,7 @@ async def _download_slack_file_bytes(
async for chunk in response.aiter_bytes():
data.extend(chunk)
attempt_bytes_consumed += len(chunk)
- if retry_bytes_consumed + attempt_bytes_consumed > max_bytes:
+ if attempt_bytes_consumed > max_bytes:
raise _SlackAttachmentError(
"Slack attachment download exceeded the "
f"{max_bytes}-byte limit.",
@@ -3380,8 +3396,7 @@ async def _download_slack_file_bytes(
),
)
payload = bytes(data)
- consumed = retry_bytes_consumed + attempt_bytes_consumed
- return (payload, consumed) if return_consumed else payload
+ return (payload, attempt_bytes_consumed) if return_consumed else payload
response = await client.get(
url,
headers={"Authorization": f"Bearer {bot_token}"},
@@ -3380,8 +3396,7 @@ async def _download_slack_file_bytes(
),
)
payload = bytes(data)
- consumed = retry_bytes_consumed + attempt_bytes_consumed
- return (payload, consumed) if return_consumed else payload
+ return (payload, attempt_bytes_consumed) if return_consumed else payload
response = await client.get(
url,
headers={"Authorization": f"Bearer {bot_token}"},You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit f117d3674dad3b4cd64bce85e300870fc15ad16f. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
gateway/platforms/slack.py (2)
3352-3384: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueCumulative
retry_bytes_consumedshrinks the per-attempt budget, so a retried near-limit download fails with a misleading message.After a partial attempt consumes N bytes, the retry only has
max_bytes - Nof headroom and aborts with"download exceeded the {max_bytes}-byte limit"even though the file itself is within the limit. If the aggregate transfer budget is intentional (it appears to be, per the thread accounting), the message should say the transfer budget was exhausted rather than implying the file is oversized.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gateway/platforms/slack.py` around lines 3352 - 3384, The max_bytes overflow handling in the retry loop around retry_bytes_consumed should distinguish aggregate transfer-budget exhaustion from an oversized attachment. Preserve the cumulative accounting and limit check, but update the _SlackAttachmentError message to clearly state that the transfer budget was exceeded when retry_bytes_consumed contributes to the limit breach.
1285-1311: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffArchive scan reads and decodes every member in full.
For a 20 MB archive this decompresses each entry into memory and runs the full redaction regex suite over
payload.decode(...)twice (once on the container bytes, once per member) on the request path. The aggregatefile_sizecap bounds it, but consider streaming member reads with an early exit, or capping per-member scan length, if upload latency matters here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gateway/platforms/slack.py` around lines 1285 - 1311, Update the archive scan around ZipFile and _contains_secret to avoid reading and decoding each member in full on the request path. Stream member contents in bounded chunks through the secret detection logic, stopping immediately when a match is found, while preserving the aggregate expansion limit and rejecting any archive containing credentials.tests/gateway/test_slack_approval_buttons.py (1)
349-417: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBudget tests hardcode literal constant values — change-detector risk.
Both tests bake in the exact numeric values of
MAX_THREAD_ATTACHMENT_FILES(implied 10, viarange(11)+await_count == 10) andMAX_THREAD_ATTACHMENT_BYTES(implied via two20*1024*1024chunks) instead of deriving expectations from the constants themselves. If either budget is retuned, these tests break even though the enforcement logic is still correct.♻️ Suggested refactor: derive expectations from the actual constants
+from gateway.platforms.slack import MAX_THREAD_ATTACHMENT_FILES, MAX_THREAD_ATTACHMENT_BYTES + async def test_thread_attachment_count_budget_skips_excess_files(self): adapter = _make_adapter() mock_client = adapter._team_clients["T1"] files = [ { "id": f"F_{index}", "name": f"file-{index}.pdf", "mimetype": "application/pdf", "size": 4, "url_private_download": f"https://files.slack.com/file-{index}.pdf", } - for index in range(11) + for index in range(MAX_THREAD_ATTACHMENT_FILES + 1) ] ... - assert adapter._download_slack_file_bytes.await_count == 10 + assert adapter._download_slack_file_bytes.await_count == MAX_THREAD_ATTACHMENT_FILES assert "bounded attachment-context budget was reached" in context - assert "file_id=F_9" in context - assert "file_id=F_10" not in context + assert f"file_id=F_{MAX_THREAD_ATTACHMENT_FILES - 1}" in context + assert f"file_id=F_{MAX_THREAD_ATTACHMENT_FILES}" not in contextA similar derivation (e.g.,
bytes_consumed=MAX_THREAD_ATTACHMENT_BYTES // 2 + 1) would decouple the byte-budget test from the literal 20MB constant.As per coding guidelines, "Do not write change-detector tests that fail whenever data expected to change (model catalogs, config version numbers, enumeration counts, hardcoded lists) gets updated. Write tests that assert relationships and invariants instead."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/gateway/test_slack_approval_buttons.py` around lines 349 - 417, Update test_thread_attachment_count_budget_skips_excess_files to build MAX_THREAD_ATTACHMENT_FILES + 1 files and assert the download count against MAX_THREAD_ATTACHMENT_FILES instead of hardcoded 11 and 10. Update test_thread_byte_budget_counts_rejected_misreported_downloads to derive bytes_consumed from MAX_THREAD_ATTACHMENT_BYTES, such as half the budget plus one, while preserving the assertion that enforcement stops after the budget is reached.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@gateway/document_extract.py`:
- Around line 153-158: Update the DOCX XML parsing in the document extraction
flow around ElementTree.fromstring to use defusedxml’s safe parser for
user-supplied document_xml, while preserving the existing
DocumentExtractionError handling for malformed XML. Do not rely solely on the
size limit; ensure DTD and entity expansion attacks are blocked.
In `@gateway/platforms/slack.py`:
- Around line 2382-2387: Update the attachment condition in the inbound
processing flow around `_ingest_slack_document` so a URL alone does not select
the document path. Exclude attachments whose mimetype starts with `image/`,
`audio/`, or `video/`, matching `_fetch_thread_context`’s filtering, while
preserving processing for supported document mimetypes and recognized file
extensions.
- Around line 1215-1249: Update _prepare_local_upload to catch genuine OSError
failures from artifact-root setup and candidate.read_bytes(), while preserving
existing _SlackUploadPolicyError and FileNotFoundError behavior. Normalize these
filesystem failures into the adapter’s handled upload error type so send_video
and send_document continue returning SendResult instead of propagating
unexpected exceptions.
---
Nitpick comments:
In `@gateway/platforms/slack.py`:
- Around line 3352-3384: The max_bytes overflow handling in the retry loop
around retry_bytes_consumed should distinguish aggregate transfer-budget
exhaustion from an oversized attachment. Preserve the cumulative accounting and
limit check, but update the _SlackAttachmentError message to clearly state that
the transfer budget was exceeded when retry_bytes_consumed contributes to the
limit breach.
- Around line 1285-1311: Update the archive scan around ZipFile and
_contains_secret to avoid reading and decoding each member in full on the
request path. Stream member contents in bounded chunks through the secret
detection logic, stopping immediately when a match is found, while preserving
the aggregate expansion limit and rejecting any archive containing credentials.
In `@tests/gateway/test_slack_approval_buttons.py`:
- Around line 349-417: Update
test_thread_attachment_count_budget_skips_excess_files to build
MAX_THREAD_ATTACHMENT_FILES + 1 files and assert the download count against
MAX_THREAD_ATTACHMENT_FILES instead of hardcoded 11 and 10. Update
test_thread_byte_budget_counts_rejected_misreported_downloads to derive
bytes_consumed from MAX_THREAD_ATTACHMENT_BYTES, such as half the budget plus
one, while preserving the assertion that enforcement stops after the budget is
reached.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b19b619b-de19-4553-a5ff-cf5378ec7a2e
📥 Commits
Reviewing files that changed from the base of the PR and between f532c6b and f117d3674dad3b4cd64bce85e300870fc15ad16f.
📒 Files selected for processing (9)
agent/prompt_builder.pygateway/document_extract.pygateway/platforms/slack.pytests/gateway/test_document_extract.pytests/gateway/test_media_download_retry.pytests/gateway/test_send_multiple_images.pytests/gateway/test_slack.pytests/gateway/test_slack_approval_buttons.pytools/send_message_tool.py
15ea071 to
20c41e4
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
20c41e4 to
c548749
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Add bounded DOCX ingestion for current messages and prior thread attachments. Upload only exact validated snapshots from an explicitly enabled artifact workflow, with aggregate transfer budgets and retry accounting.\n\nRefs MER-141, MER-251, MER-267.
c548749 to
92ba6f8
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@gateway/platforms/slack.py`:
- Around line 1279-1339: Update _assert_upload_content_safe to validate the
provided data snapshot for archive checks instead of reopening candidate from
disk: wrap data with BytesIO and pass it to ZipFile. Add the required io.BytesIO
import, while preserving the existing archive limits, Office member checks, and
credential scanning behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e59ff3f0-bb1e-4acd-a1c6-cb74fc16e385
📥 Commits
Reviewing files that changed from the base of the PR and between f117d3674dad3b4cd64bce85e300870fc15ad16f and 92ba6f8.
📒 Files selected for processing (9)
agent/prompt_builder.pygateway/document_extract.pygateway/platforms/slack.pytests/gateway/test_document_extract.pytests/gateway/test_media_download_retry.pytests/gateway/test_send_multiple_images.pytests/gateway/test_slack.pytests/gateway/test_slack_approval_buttons.pytools/send_message_tool.py
🚧 Files skipped from review as they are similar to previous changes (2)
- tools/send_message_tool.py
- tests/gateway/test_media_download_retry.py


Implements MER-251 and MER-267, plus the Hermes substrate for MER-141. Adds bounded current-message and prior-thread document ingestion, local DOCX extraction, actual transfer accounting, retry handling, and default-off governed local uploads that send the exact validated byte snapshot.\n\nVerification: 293 focused Slack/document tests pass; ruff, compileall, and diff checks pass. The broader Hermes suite has a pre-existing local-keychain isolation failure in 14 Anthropic credential tests, unrelated to this diff.\n\nDeployment/acceptance gates remain in MER-266.
Note
Medium Risk
Changes security-sensitive Slack upload paths and injects untrusted attachment text into agent prompts, but limits are explicit, uploads are default-off, and behavior is heavily tested.
Overview
Tightens Slack file handling on both directions: inbound documents are ingested with limits and optional DOCX text grounding; outbound local uploads only work through a default-off governed artifact workflow.
Inbound: Adds
gateway/document_extract.pyfor bounded, local-only DOCX → Markdown extraction (headings, tables, size caps, typed errors). The Slack adapter centralizes attachment handling via_resolve_slack_file_object,_ingest_slack_document, and streaming downloads with byte budgets. Current-message attachments inject untrusted extracted DOCX text into the prompt; prior-thread fetches now include document provenance and extracted content under count/byte/char budgets.Outbound: Local
MEDIA:/ file uploads go through_prepare_local_upload(opt-in viaHERMES_SLACK_LOCAL_UPLOADS_ENABLED, paths underHERMES_SLACK_ARTIFACT_ROOTor~/.hermes/artifacts/slack, symlink-safe containment, magic-byte checks, credential/archive scans). Uploads send validated byte snapshots viacontent=instead of arbitrary filesystem paths. Slack platform hints andsend_message_tooldocs steer the model toward approved artifact paths only.Tests: New/expanded coverage for DOCX extraction, upload policy denials, DOCX injection, thread attachment budgets, and download retry/stream limits.
Reviewed by Cursor Bugbot for commit f117d3674dad3b4cd64bce85e300870fc15ad16f. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
MEDIA:<approved_path>from a trusted artifact tool.