Skip to content

fix(teams): surface pre-signed content.downloadUrl for chat file attachments#136

Closed
tony-chinchill-ai wants to merge 1 commit into
Chinchill-AI:mainfrom
tony-chinchill-ai:fix/teams-attachment-downloadurl
Closed

fix(teams): surface pre-signed content.downloadUrl for chat file attachments#136
tony-chinchill-ai wants to merge 1 commit into
Chinchill-AI:mainfrom
tony-chinchill-ai:fix/teams-attachment-downloadurl

Conversation

@tony-chinchill-ai

@tony-chinchill-ai tony-chinchill-ai commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Problem

TeamsAdapter._create_attachment reads only the top-level contentUrl. For a file uploaded in a personal/group chat that points at the auth-required SharePoint/OneDrive item and returns 403 to an anonymous GET. The activity also carries content.downloadUrl — a short-lived pre-signed link that fetches with no auth header — which was being dropped.

Fix

Prefer content.downloadUrl when present so the file is readable without granting Files.Read.All.

Rebased onto main; single commit touching only the Teams adapter + its test, with a CHANGELOG entry under the 0.4.29a3 (unreleased) section (pyproject version bump left to the coordinating release PR per the unreleased-section convention). 46 Teams adapter tests pass, including a new regression test asserting the pre-signed URL is surfaced over the SharePoint contentUrl.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request implements end-to-end propagation of Slack file upload confirmations through the messaging pipeline, captures them in raw payloads, adds privacy safeguards for message history, and fixes Teams file attachment URL selection. Version is bumped to 0.4.27.2 and changes are documented in CHANGELOG.md.

Changes

Upload confirmation and raw propagation

Layer / File(s) Summary
Slack upload confirmation capture and augmentation
src/chat_sdk/adapters/slack/adapter.py, tests/test_slack_api.py
SlackAdapter.post_message now captures files_upload_v2 confirmation IDs from _upload_files() and augments RawMessage.raw with uploaded_file_ids across file-only, card, table, and text response branches. New _augment_raw_with_uploads() helper conditionally merges upload IDs only when uploads occurred. Tests verify file-only, text+files, and text-only message handling with and without the uploaded_file_ids key.
Raw payload propagation through post()
src/chat_sdk/thread.py, tests/test_thread_faithful.py
ThreadImpl.post() passes the adapter-returned RawMessage.raw into _create_sent_message(), which now accepts an optional raw parameter and forwards it into the constructed SentMessage. Test verifies the adapter's raw payload (specifically uploaded_file_ids) reaches the final SentMessage.raw.
Message history raw nulling
src/chat_sdk/chat.py
_MessageHistoryCache.append() explicitly sets raw to None before serializing messages for state adapter persistence, preventing enriched platform payloads and PII from being stored in message history.

Teams attachment fix

Layer / File(s) Summary
Teams downloadUrl preference
src/chat_sdk/adapters/teams/adapter.py, tests/test_teams_adapter.py
_create_attachment now prefers content.downloadUrl (pre-signed, anonymously readable) over top-level contentUrl (SharePoint auth-required) when both are present. Test verifies Teams file download attachments map to the pre-signed URL for proper file access.

Release

Layer / File(s) Summary
Version and changelog
CHANGELOG.md, pyproject.toml
Project version bumped to 0.4.27.2. CHANGELOG documents Slack upload confirmation propagation, message-history raw nulling, and corresponding test additions for patch release 0.4.27.1 (2026-05-29).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Chinchill-AI/chat-sdk-python#117: Core upload confirmation and raw propagation feature—both PRs modify SlackAdapter.post_message to add uploaded_file_ids into RawMessage.raw and update ThreadImpl.post()/_create_sent_message to propagate raw through to SentMessage.

Poem

🐰 Uploads confirmed, payloads flow,
Through adapters, threads, and raw they go,
History stays clean, no PII stored,
Teams downloads fix'd—anon links adored!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'fix(teams): surface pre-signed content.downloadUrl for chat file attachments' accurately describes the main change documented in the Teams adapter file, which now prefers content.downloadUrl over contentUrl for file attachments.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces several fixes and enhancements across adapters: it surfaces Slack-confirmed file IDs on RawMessage.raw, ensures _MessageHistoryCache nulls out raw before persisting to prevent storage inflation, and updates the Teams adapter to prefer pre-signed download URLs over SharePoint URLs to avoid 403 errors. While the version in pyproject.toml was bumped to 0.4.27.2 to include the Teams fix, there is no corresponding entry in CHANGELOG.md. Please apply the suggested changelog update to document this new version.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread CHANGELOG.md Outdated
@@ -1,5 +1,18 @@
# Changelog

## 0.4.27.1 (2026-05-29)

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.

medium

The version in pyproject.toml has been bumped to 0.4.27.2 to include the Teams pre-signed download URL fix, but there is no corresponding entry in the changelog for this new version. Please add a changelog entry for 0.4.27.2 to document this fix.

Suggested change
## 0.4.27.1 (2026-05-29)
## 0.4.27.2 (2026-05-30)
Python-only patch on the 0.4.27 line. No upstream version change.
### Fixes
- **Teams now prefers pre-signed content.downloadUrl for chat file attachments** — Files uploaded in personal/group chats point to auth-required SharePoint/OneDrive items and return 403 to anonymous GET requests. Surfacing the pre-signed content.downloadUrl ensures the file is readable without requiring Files.Read.All permissions.
## 0.4.27.1 (2026-05-29)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5bce385f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

files = extract_files(message)
if files:
await self._upload_files(files, channel, thread_ts or None)
uploaded_file_ids = await self._upload_files(files, channel, thread_ts or None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse Slack upload IDs from the completion response

When posting files through the real Slack SDK, this newly surfaced value will usually be [] even after successful uploads because _upload_files() only extracts IDs from a nested files[].files[].id shape, while files_upload_v2 returns completion data as files: [{id: ...}] (and may also add file for single uploads). In the text+files and file-only paths introduced here, consumers gating on SentMessage.raw["uploaded_file_ids"] will see an empty confirmation despite Slack having accepted the files; the new tests mock the nested shape, so they don't catch the production response shape.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_thread_faithful.py (1)

263-285: PostableMarkdown(files=...) is valid; adjust only for clarity
PostableMarkdown is a dataclass with both attachments and files fields, so PostableMarkdown(markdown="report", files=[]) won’t raise a constructor TypeError. Since this test’s mocked adapter.post_message ignores the PostableMarkdown payload anyway, files vs attachments doesn’t affect the assertion (you can simplify/align for readability).

🤖 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/test_thread_faithful.py` around lines 263 - 285, The test
test_should_propagate_adapter_raw_onto_sent_message uses
PostableMarkdown(markdown="report", files=[]) which is valid but confusing;
change the PostableMarkdown instantiation to use the dataclass's attachments
field (e.g., PostableMarkdown(markdown="report", attachments=[])) for clarity
and consistency with other tests and the PostableMarkdown definition, keeping
everything else (custom_post, adapter.post_message, RawMessage returned and
assertions against result.raw) unchanged.
🤖 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 `@CHANGELOG.md`:
- Around line 3-14: The changelog header currently reads "## 0.4.27.1
(2026-05-29)" but the package was published as 0.4.27.2; update the CHANGELOG.md
top section to match the shipped version by renaming the header to "## 0.4.27.2
(2026-05-29)" or add a new "## 0.4.27.2" entry that contains the same release
notes, ensuring the header string "## 0.4.27.1 (2026-05-29)" is replaced or
superseded so the changelog aligns with the pyproject.toml version bump.

In `@src/chat_sdk/adapters/teams/adapter.py`:
- Line 933: The current assignment url = download_url or att.get("contentUrl")
uses truthiness and will incorrectly fall back when download_url is an empty
string; change it to use an explicit None check so url is download_url if
download_url is not None, otherwise att.get("contentUrl") — update the
expression around the url variable where download_url and att.get("contentUrl")
are used (in the Teams adapter code) to use the form download_url if
download_url is not None else att.get("contentUrl").

---

Nitpick comments:
In `@tests/test_thread_faithful.py`:
- Around line 263-285: The test
test_should_propagate_adapter_raw_onto_sent_message uses
PostableMarkdown(markdown="report", files=[]) which is valid but confusing;
change the PostableMarkdown instantiation to use the dataclass's attachments
field (e.g., PostableMarkdown(markdown="report", attachments=[])) for clarity
and consistency with other tests and the PostableMarkdown definition, keeping
everything else (custom_post, adapter.post_message, RawMessage returned and
assertions against result.raw) unchanged.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a9d7f9b7-fe2b-4c76-90e1-88456049b96d

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd86e7 and d5bce38.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • pyproject.toml
  • src/chat_sdk/adapters/slack/adapter.py
  • src/chat_sdk/adapters/teams/adapter.py
  • src/chat_sdk/chat.py
  • src/chat_sdk/thread.py
  • tests/test_slack_api.py
  • tests/test_teams_adapter.py
  • tests/test_thread_faithful.py

Comment thread CHANGELOG.md Outdated
# item and returns 403 to an anonymous GET, so prefer the pre-signed URL.
content = att.get("content")
download_url = content.get("downloadUrl") if isinstance(content, dict) else None
url = download_url or att.get("contentUrl")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use explicit None check instead of truthiness operator.

Line 933 uses download_url or att.get("contentUrl") which can incorrectly fall back to contentUrl if download_url is an empty string (falsy but not None). As per coding guidelines, use x if x is not None else default to avoid truthiness traps.

🔧 Suggested fix
-        url = download_url or att.get("contentUrl")
+        url = download_url if download_url is not None else att.get("contentUrl")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
url = download_url or att.get("contentUrl")
url = download_url if download_url is not None else att.get("contentUrl")
🤖 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 `@src/chat_sdk/adapters/teams/adapter.py` at line 933, The current assignment
url = download_url or att.get("contentUrl") uses truthiness and will incorrectly
fall back when download_url is an empty string; change it to use an explicit
None check so url is download_url if download_url is not None, otherwise
att.get("contentUrl") — update the expression around the url variable where
download_url and att.get("contentUrl") are used (in the Teams adapter code) to
use the form download_url if download_url is not None else
att.get("contentUrl").

Source: Coding guidelines

@tony-chinchill-ai tony-chinchill-ai force-pushed the fix/teams-attachment-downloadurl branch from d5bce38 to 073fd2b Compare June 13, 2026 01:55
@tony-chinchill-ai

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Rebased onto main and slimmed to teams-only. The earlier slack (files_upload_v2) and chat (SentMessage.raw null-out) commits landed on main independently via #117, so they're dropped here — this PR is now a single commit touching only src/chat_sdk/adapters/teams/adapter.py + its test + a CHANGELOG entry. The prior CHANGES_REQUESTED comments were against files no longer in the diff.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the current PR diff
gh pr diff 136 --repo Chinchill-AI/chat-sdk-python

echo "=== PR FILES CHANGED ==="
gh pr view 136 --repo Chinchill-AI/chat-sdk-python --json files --jq '.files[].path'

Length of output: 5000


🏁 Script executed:

#!/bin/bash
# Read the teams adapter and its test
cat src/chat_sdk/adapters/teams/adapter.py

echo "=== TEST FILE ==="
cat tests/test_teams_adapter.py

Length of output: 147084


🏁 Script executed:

#!/bin/bash
# Check CHANGELOG
cat CHANGELOG.md | head -60

Length of output: 6368



@tony-chinchill-ai: The rebased, teams-only diff looks good. Here's what I reviewed:


src/chat_sdk/adapters/teams/adapter.py_create_attachment

content = att.get("content")
download_url = content.get("downloadUrl") if isinstance(content, dict) else None
url = download_url or att.get("contentUrl")
  • ✅ The isinstance(content, dict) guard is correct — Teams uses plain string content for text/html attachments, so the guard prevents an AttributeError on those.
  • download_url or att.get("contentUrl") correctly falls back when downloadUrl is absent. An empty-string downloadUrl from Teams would also fall back, which is fine — an empty URL is unusable regardless.
  • ✅ The pre-signed URL propagates into fetch_metadata["url"] and the fetch_data closure, so downloads work without an auth header on the happy path.
  • ✅ The pre-signed host (contoso.sharepoint.com) satisfies _is_trusted_teams_download_url's .sharepoint.com suffix check.

One non-blocking note (pre-existing, not introduced here): fetch_metadata["url"] will hold the short-lived pre-signed URL. If the attachment is serialised to message history and rehydrate_attachment is called later (e.g. on a cached message), the URL will likely have expired by then. This is a pre-existing characteristic of how Teams issues these tokens and isn't made worse by this PR — just worth tracking as a follow-up if live rehydration of old file attachments becomes a use case.


tests/test_teams_adapter.pytest_prefers_pre_signed_download_url_over_sharepoint_content_url

  • ✅ Exercises the exact failure mode described in the PR: both contentUrl and content.downloadUrl present, assert the pre-signed URL wins.
  • ✅ Descriptive assertion message makes future failures self-documenting.

CHANGELOG.md

  • ✅ Entry lands under 0.4.29a3 (unreleased) and is consistent with the existing note that pyproject.toml is left unchanged pending a coordinating bump PR.

Everything looks clean. The prior CHANGES_REQUESTED comments were against files that are no longer in the diff. LGTM. 🐇

[approve]

…chments

TeamsAdapter._create_attachment read only the top-level contentUrl, which for a
file uploaded in a personal/group chat points at the auth-required
SharePoint/OneDrive item and returns 403 to an anonymous GET. The activity also
carries content.downloadUrl, a short-lived pre-signed link that fetches with no
auth header. Prefer it when present so the file is readable without granting
Files.Read.All.

Rebased onto main (0.4.29a3 line); CHANGELOG entry added, pyproject version bump
left to the coordinating release PR per the unreleased-section convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tony-chinchill-ai tony-chinchill-ai force-pushed the fix/teams-attachment-downloadurl branch from 073fd2b to 7e51b5d Compare June 13, 2026 02:08
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@tony-chinchill-ai

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@tony-chinchill-ai

Copy link
Copy Markdown
Contributor Author

@patrick-chinchill this is ready to merge when you have a minute.

What it is: a single commit (7e51b5d) touching only adapters/teams/adapter.py + its test (plus a CHANGELOG entry under 0.4.29a3), rebased onto current main. CI is green — Lint & Type Check, test 3.12, and test 3.13 all pass.

Why it shows BLOCKED / CHANGES_REQUESTED (and why that is safe to dismiss): the CodeRabbit review and the gemini/codex comments are all pinned to the old pre-rebase commit d5bce385, whose slack/thread changes were dropped here because they already landed on main independently via #117. No bot has re-reviewed the current 7e51b5d, and CodeRabbit did not re-trigger on the force-push.

Asks:

  1. Dismiss the stale CodeRabbit CHANGES_REQUESTED (it is on the deleted commit d5bce385).
  2. Merge.
  3. Cut/extend the 0.4.29a3-line tag so chinchill can pin to a real upstream version — the PR intentionally leaves pyproject.toml unbumped per the unreleased-section convention, so the coordinating release PR cuts the bump.

Context: this unblocks inbound Teams file reads. The adapter was dropping the pre-signed content.downloadUrl and surfacing the auth-required SharePoint contentUrl, which 403s on an anonymous GET.

patrick-chinchill added a commit that referenced this pull request Jun 20, 2026
…attachments (#167)

A SharePoint/OneDrive file shared in a personal or group chat arrives as a
`application/vnd.microsoft.teams.file.download.info` attachment that carries
BOTH a top-level `contentUrl` (the SharePoint/OneDrive item, which returns 403
to an anonymous GET) and a nested `content.downloadUrl` — a short-lived
pre-signed link the Bot Framework docs say to issue an HTTP GET against.

Upstream Teams (`adapter-teams/src/index.ts:833`, `const url = att.contentUrl`)
and our pre-fix adapter read `contentUrl` only, so the attachment is
undownloadable (`fetch_data` 403s). `_create_attachment` now prefers
`content.downloadUrl` for that attachment type; every other attachment
(inline images, etc.) keeps the upstream `contentUrl`-first path.

The resulting URL flows through the unchanged `_build_teams_fetch_data` /
`rehydrate_attachment` SSRF allowlist — SharePoint/OneDrive download hosts are
already covered by `*.sharepoint.com` / `*.onedrive.com`, so no host was added.

Justified Python-only divergence (hard-UX-failure: attachment cannot be
downloaded). Documented in docs/UPSTREAM_SYNC.md Known Non-Parity; to be filed
upstream. Supersedes stale PR #136.

Tests: TestFileDownloadInfoAttachment — downloadUrl preferred over a 403-ing
contentUrl, downloadUrl used when contentUrl absent, regular attachment with a
competing downloadUrl unchanged, downloadUrl flows through the trusted fetch,
and the downloadUrl path is still gated by the SSRF allowlist. Each fails on a
plausible mutation (verified against contentUrl-only revert, over-eager
downloadUrl-for-all, and dropped-SSRF-gate mutations).
@patrick-chinchill

Copy link
Copy Markdown
Collaborator

Incorporated and superseded by #167 (merged) — rebuilt to current standards: content-type-scoped content.downloadUrl preference for file.download.info attachments (so regular attachments are untouched), SSRF allowlist verified (*.sharepoint.com/*.onedrive.com already covered), documented as a Known Non-Parity divergence. Thanks @tony-chinchill-ai.

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.

2 participants