Skip to content

fix(shared-task): enable full attachment access with share tokens - #413

Merged
qdaxb merged 6 commits into
mainfrom
wegent/fix-shared-attachment
Feb 3, 2026
Merged

qdaxb merged 6 commits into
mainfrom
wegent/fix-shared-attachment

Conversation

@yixiangxx

@yixiangxx yixiangxx commented Feb 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR enables complete attachment access on shared task pages using share tokens, allowing users to view and download both user-uploaded
and AI-generated attachments without authentication.

Changes

Backend (Python)

backend/app/api/endpoints/adapter/attachments.py

  • Added share_token query parameter support to all attachment endpoints:
    • GET /api/attachments/{id} - Attachment metadata
    • GET /api/attachments/{id}/preview - Preview data
    • GET /api/attachments/{id}/download - File download
  • Updated _validate_share_token_access to handle two cases:
    • Attachments linked to subtasks (validate via task_id)
    • Attachments not linked to subtasks (validate via owner user_id)
  • Implemented dual authentication: JWT tokens OR share tokens
  • Added comprehensive logging for debugging share token validation
  • Changed error codes: 403 for denied access, 404 for not found

backend/app/core/security.py

  • Added get_current_user_optional dependency for optional JWT authentication
  • Enables endpoints to work with either JWT or share token

backend/tests/chat_shell/test_compression.py

  • Removed obsolete provider detection tests after LiteLLM migration
  • Fixed multimodal token count assertion (100 → 50 tokens)
  • All 38 compression tests now pass

Frontend (React/TypeScript)

Context Pattern for Share Token Propagation

  • Created ShareTokenContext for clean share token passing through component tree
  • Wrapped MessageBubble content with ShareTokenProvider
  • Components use useShareToken() hook to access share token

API Layer Updates

  • getAttachment() - Added optional shareToken parameter
  • getAttachmentPreview() - Added optional shareToken parameter
  • downloadAttachment() - Already had shareToken support
  • All APIs conditionally add share_token query param and omit Authorization header when using share token

Component Updates

  • AttachmentCard - Uses useShareToken() and passes to API calls
  • AttachmentEmbed - Uses useShareToken() and passes to API calls
  • AttachmentPreview - Passes shareToken prop through to components
  • ContextBadgeList - Renders attachments for both user and AI messages
  • MessageBubble - Provides share token context to all child components

Hook Updates

  • useAttachmentPreview - Accepts optional shareToken parameter
  • useAttachmentImage - Accepts optional shareToken parameter
  • Caching strategy: Uses simple attachmentId key (no token in cache key)

Problem Solved

Before this PR, attachments in shared tasks would fail with 401/404 errors because:

  1. Attachment endpoints required JWT authentication
  2. AI-generated attachments weren't being rendered in shared task view
  3. Attachments with subtask_id=0 couldn't be validated

Now:

  • ✅ All attachments work on shared task pages without login
  • ✅ Both user-uploaded and AI-generated attachments are accessible
  • ✅ Unlinked attachments (subtask_id=0) are validated via owner match
  • ✅ Clean React Context pattern avoids prop drilling

Summary by CodeRabbit

  • New Features

    • Public sharing of attachments via share tokens: view, preview, and download without signing in.
    • Frontend support for share tokens across attachment previews, images, downloads, and message UI; new share token context for propagation.
    • Optional authentication path: endpoints now accept either a user session or a share token.
  • Tests

    • Adjusted message-compression test thresholds and removed outdated provider-detection tests.

yixiangxx and others added 3 commits February 3, 2026 15:19
…asks

- Add share_token query parameter to attachment download endpoint
- Support dual authentication: JWT for logged-in users, share_token for public access
- Add get_current_user_optional helper for optional JWT authentication
- Update frontend attachment APIs to accept optional shareToken parameter
- Pass shareToken through MessageBubble -> ContextBadgeList -> AttachmentPreview
- Validate share_token against task ownership before allowing attachment access
This commit enables attachments in shared tasks to be accessed without
authentication using share tokens. Both user-uploaded attachments and
AI-generated attachments now work correctly on shared task pages.

Backend changes:
- Add share_token query parameter support to get_attachment endpoint
- Add share_token support to get_attachment_preview endpoint (already had it for download)
- Update _validate_share_token_access to handle attachments with subtask_id=0
  by verifying attachment owner matches task owner
- Add detailed logging for debugging share token validation
- Change 404 to 403 for denied share token access for better error clarity

Frontend changes:
- Create ShareTokenContext for passing share token through component tree
- Update MessageBubble to wrap content with ShareTokenProvider
- Update AttachmentCard to use useShareToken and pass to API calls
- Update AttachmentEmbed to use useShareToken and pass to API calls
- Update getAttachment and getAttachmentPreview APIs to accept optional shareToken
- Update useAttachmentPreview hook to accept and pass shareToken parameter
- Maintain simple caching strategy using attachmentId as key (no token in cache key)

This fixes the issue where attachments in AI-generated messages were
returning 404/401 errors when viewed through shared task links.

Co-Authored-By: Claude <noreply@anthropic.com>
The TokenCounter implementation was updated in commit 522a58a to use
LiteLLM for unified token counting across all providers, removing the
explicit provider detection logic and `provider` attribute.

This commit updates the test suite to reflect the implementation changes:
- Remove test_detect_provider_openai (obsolete)
- Remove test_detect_provider_anthropic (obsolete)
- Remove test_detect_provider_google (obsolete)
- Fix test_count_message_multimodal assertion (100 -> 50 tokens)

The multimodal test was adjusted because LiteLLM accurately counts tokens
for the tiny test base64 image, resulting in ~98 tokens instead of the
previously estimated 100+ tokens.

All 38 compression tests now pass.
@coderabbitai

coderabbitai Bot commented Feb 3, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@yixiangxx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds share-token based public access to attachment endpoints and an optional JWT auth path; frontend receives and propagates a shareToken via a new ShareTokenContext and updated attachment APIs/hooks/components to support token-based access and conditional Authorization omission.

Changes

Cohort / File(s) Summary
Backend auth & attachment endpoints
backend/app/core/security.py, backend/app/api/endpoints/adapter/attachments.py
Added get_current_user_optional() (optional JWT) and added share_token query handling to get_attachment, get_attachment_preview, download_attachment plus new _validate_share_token_access() helper to validate token→attachment relationships.
Frontend API helpers
frontend/src/apis/attachments.ts
All attachment helper functions accept optional shareToken; URLs append ?share_token=... when provided and Authorization header is omitted when using shareToken.
Frontend context & hooks
frontend/src/contexts/ShareTokenContext.tsx, frontend/src/hooks/useAttachmentPreview.ts, frontend/src/hooks/useAttachmentImage.ts
New ShareTokenProvider and useShareToken(); hooks accept shareToken param, include it in fetch URLs, adjust auth header logic, and add shareToken to effect dependencies and error handling.
Frontend components (attachments & messaging)
frontend/src/components/common/AttachmentCard.tsx, frontend/src/components/common/AttachmentEmbed.tsx, frontend/src/features/tasks/components/message/MessageBubble.tsx, frontend/src/features/tasks/components/message/ContextBadgeList.tsx, frontend/src/features/tasks/components/input/AttachmentPreview.tsx
Threaded shareToken through components and props; MessageBubble gains shareToken? prop and wraps content with ShareTokenProvider; attachment components call updated APIs/hooks with the token.
Pages & tests
frontend/src/app/shared/task/page.tsx, backend/tests/chat_shell/test_compression.py
page.tsx extracts shareToken from URL and passes to MessageBubble; tests adjusted (compression threshold lowered, provider-detection tests removed).

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser (UI)
  participant Frontend as Frontend (React)
  participant Backend as API Server
  participant DB as Database / Services

  Browser->>Frontend: Load page with ?share_token=SHARE
  Frontend->>Frontend: ShareTokenProvider saves token in context
  Frontend->>Backend: GET /attachments/:id?share_token=SHARE
  Backend->>DB: validate share token -> find SharedTask / Attachment / Subtask
  DB-->>Backend: validation result (allowed / denied)
  alt allowed
    Backend-->>Frontend: 200 + attachment data / stream
    Frontend->>Browser: render attachment (no Auth header used)
  else denied
    Backend-->>Frontend: 403 / 404
    Frontend->>Browser: show access denied
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • feifei325
  • Micro66

Poem

🐰
I found a little token, bright and neat,
It lets attachments hop right to your seat.
No login thump — just share and there you go,
From burrow backend to the frontend show. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: enabling attachment access with share tokens. It accurately captures the core functionality addition across both backend and frontend.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wegent/fix-shared-attachment

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.

Previously, the shared task page only displayed the text output but not
the tool blocks (Bash, Read, Edit, Write, etc.) because the Message
object was missing the result field containing blocks data.

This commit passes the full result object from PublicSubtaskData to
MessageBubble component, enabling MixedContentView to render tool blocks
for shared tasks.

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/features/tasks/components/message/MessageBubble.tsx (1)

1726-1752: ⚠️ Potential issue | 🟠 Major

Include shareToken in memo equality check.

MessageBubble can skip re-render when shareToken changes, leaving ShareTokenProvider with stale token. Add shareToken to the comparison.

✅ Proposed fix
-      prevProps.isRegenerating === nextProps.isRegenerating
+      prevProps.isRegenerating === nextProps.isRegenerating &&
+      prevProps.shareToken === nextProps.shareToken
frontend/src/components/common/AttachmentEmbed.tsx (1)

101-103: ⚠️ Potential issue | 🟠 Major

Pass shareToken to HtmlPreview.

HtmlPreview is rendered without shareToken at line 102, but shareToken is available in AttachmentEmbed via useShareToken(). Since HtmlPreview downloads the attachment and currently uses only the user token (not shareToken), shared-task access will fail. Update HtmlPreview to accept and use shareToken.

🔧 Suggested fix
-      ) : isHtml ? (
-        <HtmlPreview attachmentId={attachmentId} filename={data.filename} />
+      ) : isHtml ? (
+        <HtmlPreview attachmentId={attachmentId} filename={data.filename} shareToken={shareToken} />
       ) : previewText ? (
🤖 Fix all issues with AI agents
In `@backend/app/api/endpoints/adapter/attachments.py`:
- Around line 419-439: In download_attachment, when a share_token is provided
and _validate_share_token_access(db, attachment_id, share_token) returns False
you must raise HTTPException(status_code=403, detail="Invalid share token")
instead of letting it fall through to the final 404; update the branch where
share_token is checked (the block that calls _validate_share_token_access and
context_service.get_context_optional) to explicitly return a 403 on invalid
token before continuing, leaving the existing _get_attachment_context and other
authentication branches unchanged.
🧹 Nitpick comments (6)
frontend/src/contexts/ShareTokenContext.tsx (1)

16-24: Consider memoizing the context value to prevent unnecessary re-renders.

The context value { shareToken } creates a new object on each render, which could cause unnecessary re-renders in consumers. For a simple string value this is a minor optimization, but worth considering if many components consume this context.

♻️ Optional: Memoize context value
+'use client'
+
+import React, { createContext, useContext, useMemo } from 'react'
+
 interface ShareTokenContextValue {
   /** Share token for public access (no login required) */
   shareToken?: string
 }

 const ShareTokenContext = createContext<ShareTokenContextValue>({})

 export function ShareTokenProvider({
   children,
   shareToken,
 }: {
   children: React.ReactNode
   shareToken?: string
 }) {
-  return <ShareTokenContext.Provider value={{ shareToken }}>{children}</ShareTokenContext.Provider>
+  const value = useMemo(() => ({ shareToken }), [shareToken])
+  return <ShareTokenContext.Provider value={value}>{children}</ShareTokenContext.Provider>
 }
frontend/src/apis/attachments.ts (1)

337-343: getAttachmentPreviewUrl and getAttachmentDownloadUrl are identical.

Both functions construct the same URL (/api/attachments/${attachmentId}/download). Consider consolidating to avoid duplication.

♻️ Consolidate duplicate functions
 /**
- * Get image preview URL for an attachment
+ * Get attachment download/preview URL
  *
  * `@param` attachmentId - Attachment ID
  * `@param` shareToken - Optional share token for public access
- * `@returns` Preview URL
+ * `@returns` Download URL (used for both preview and download)
  */
-export function getAttachmentPreviewUrl(attachmentId: number, shareToken?: string): string {
+export function getAttachmentDownloadUrl(attachmentId: number, shareToken?: string): string {
   const baseUrl = `${API_BASE_URL}/api/attachments/${attachmentId}/download`
   if (shareToken) {
     return `${baseUrl}?share_token=${encodeURIComponent(shareToken)}`
   }
   return baseUrl
 }

-/**
- * Get attachment download URL
- *
- * `@param` attachmentId - Attachment ID
- * `@param` shareToken - Optional share token for public access
- * `@returns` Download URL
- */
-export function getAttachmentDownloadUrl(attachmentId: number, shareToken?: string): string {
-  const baseUrl = `${API_BASE_URL}/api/attachments/${attachmentId}/download`
-  if (shareToken) {
-    return `${baseUrl}?share_token=${encodeURIComponent(shareToken)}`
-  }
-  return baseUrl
-}
+/** `@deprecated` Use getAttachmentDownloadUrl instead */
+export const getAttachmentPreviewUrl = getAttachmentDownloadUrl
frontend/src/hooks/useAttachmentPreview.ts (1)

17-20: Cache key doesn't account for shareToken.

The cache uses only attachmentId as the key (line 42), but the same attachment could theoretically return different data based on authentication method. If the response differs between authenticated and share-token access, cached data could be incorrect.

In practice this is likely fine since:

  1. Attachment content is the same regardless of auth method
  2. A user typically accesses via one method consistently

Consider documenting this assumption or adding shareToken to the cache key if responses could differ.

Also applies to: 41-42, 64-64

frontend/src/hooks/useAttachmentImage.ts (1)

66-72: Stale closure in cleanup - blobUrl revocation is ineffective.

The cleanup function captures blobUrl from the closure at the time the effect runs. Since blobUrl is initially null and only set asynchronously, this cleanup will always see null or a stale value.

The second useEffect (lines 74-80) correctly handles cleanup, so this isn't a bug, but the revocation logic in the first effect's cleanup is misleading/dead code.

♻️ Remove ineffective cleanup logic
     return () => {
       isMounted = false
-      if (blobUrl) {
-        URL.revokeObjectURL(blobUrl)
-      }
     }
   }, [attachmentId, enabled, shareToken])

The second useEffect (lines 74-80) already handles proper cleanup when blobUrl changes or on unmount.

backend/app/core/security.py (1)

676-737: Trim get_current_user_optional and align tracing utilities.

This function is ~60 lines and manually manages spans. Consider extracting token verification/user lookup into a helper (or reuse get_current_user_from_token) and switching to the standard tracing decorators/helpers so it stays under 50 lines and follows the project’s tracing conventions.

As per coding guidelines, “Function length: Max 50 lines per function (preferred)” and “OpenTelemetry tracing: Use @trace_async() for async functions, @trace_sync() for sync functions, add_span_event() to add events, set_span_attribute() to set attributes”.

backend/app/api/endpoints/adapter/attachments.py (1)

121-193: Move share-token validation into a service and split helpers.

This helper now embeds substantial access-control logic (~70 lines) in the endpoint layer. Consider relocating it to app/services/ (e.g., shared_task or attachment service) and splitting subtask vs unlinked-attachment validation into smaller helpers to keep the API layer thin.

As per coding guidelines, “Backend: Add endpoint in app/api/, schema in app/schemas/, logic in app/services/” and “Function length: Max 50 lines per function (preferred)”.

Comment on lines +419 to +439
# Method 1: Share token authentication (no login required)
if share_token:
has_access = _validate_share_token_access(db, attachment_id, share_token)
if has_access:
# Get context for share token access
context = context_service.get_context_optional(
db=db,
context_id=attachment_id,
)
if context is None:
raise HTTPException(status_code=404, detail="Attachment not found")
# Method 2: JWT token authentication (existing logic)
elif current_user:
context = _get_attachment_context(db, attachment_id, current_user)
has_access = True
else:
# No authentication provided
raise HTTPException(status_code=401, detail="Authentication required")

if not has_access:
raise HTTPException(status_code=404, detail="Attachment not found")

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.

⚠️ Potential issue | 🟡 Minor

Return 403 when share_token is invalid.

In download_attachment, an invalid share token currently falls through to the final 404, unlike the 403 used in the other endpoints. Add the explicit 403 branch to keep error semantics consistent.

🛠️ Suggested fix
     if share_token:
         has_access = _validate_share_token_access(db, attachment_id, share_token)
         if has_access:
             # Get context for share token access
             context = context_service.get_context_optional(
                 db=db,
                 context_id=attachment_id,
             )
             if context is None:
                 raise HTTPException(status_code=404, detail="Attachment not found")
+        else:
+            raise HTTPException(status_code=403, detail="Share token access denied")
🤖 Prompt for AI Agents
In `@backend/app/api/endpoints/adapter/attachments.py` around lines 419 - 439, In
download_attachment, when a share_token is provided and
_validate_share_token_access(db, attachment_id, share_token) returns False you
must raise HTTPException(status_code=403, detail="Invalid share token") instead
of letting it fall through to the final 404; update the branch where share_token
is checked (the block that calls _validate_share_token_access and
context_service.get_context_optional) to explicitly return a 403 on invalid
token before continuing, leaving the existing _get_attachment_context and other
authentication branches unchanged.

@qdaxb
qdaxb merged commit 0f0e9aa into main Feb 3, 2026
13 checks passed
@qdaxb
qdaxb deleted the wegent/fix-shared-attachment branch February 3, 2026 09:15
FicoHu pushed a commit that referenced this pull request Feb 3, 2026
Merge main branch changes including:
- Share token for attachments (#413)
- Skill selector for chat interface (#402)
- Subscription monitor fix (#408)

Conflict resolved in backend/app/core/security.py by keeping both:
- get_current_user_flexible_for_executor (from this branch)
- get_current_user_optional (from main)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
FicoHu pushed a commit that referenced this pull request Feb 3, 2026
* fix(shared-task): enable attachment preview and download for shared tasks

- Add share_token query parameter to attachment download endpoint
- Support dual authentication: JWT for logged-in users, share_token for public access
- Add get_current_user_optional helper for optional JWT authentication
- Update frontend attachment APIs to accept optional shareToken parameter
- Pass shareToken through MessageBubble -> ContextBadgeList -> AttachmentPreview
- Validate share_token against task ownership before allowing attachment access

* fix(attachments): add share token support for public attachment access

This commit enables attachments in shared tasks to be accessed without
authentication using share tokens. Both user-uploaded attachments and
AI-generated attachments now work correctly on shared task pages.

Backend changes:
- Add share_token query parameter support to get_attachment endpoint
- Add share_token support to get_attachment_preview endpoint (already had it for download)
- Update _validate_share_token_access to handle attachments with subtask_id=0
  by verifying attachment owner matches task owner
- Add detailed logging for debugging share token validation
- Change 404 to 403 for denied share token access for better error clarity

Frontend changes:
- Create ShareTokenContext for passing share token through component tree
- Update MessageBubble to wrap content with ShareTokenProvider
- Update AttachmentCard to use useShareToken and pass to API calls
- Update AttachmentEmbed to use useShareToken and pass to API calls
- Update getAttachment and getAttachmentPreview APIs to accept optional shareToken
- Update useAttachmentPreview hook to accept and pass shareToken parameter
- Maintain simple caching strategy using attachmentId as key (no token in cache key)

This fixes the issue where attachments in AI-generated messages were
returning 404/401 errors when viewed through shared task links.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(backend): remove obsolete provider detection tests in compression

The TokenCounter implementation was updated in commit 522a58a to use
LiteLLM for unified token counting across all providers, removing the
explicit provider detection logic and `provider` attribute.

This commit updates the test suite to reflect the implementation changes:
- Remove test_detect_provider_openai (obsolete)
- Remove test_detect_provider_anthropic (obsolete)
- Remove test_detect_provider_google (obsolete)
- Fix test_count_message_multimodal assertion (100 -> 50 tokens)

The multimodal test was adjusted because LiteLLM accurately counts tokens
for the tiny test base64 image, resulting in ~98 tokens instead of the
previously estimated 100+ tokens.

All 38 compression tests now pass.

* fix(shared-task): display tool usage in shared task page

Previously, the shared task page only displayed the text output but not
the tool blocks (Bash, Read, Edit, Write, etc.) because the Message
object was missing the result field containing blocks data.

This commit passes the full result object from PublicSubtaskData to
MessageBubble component, enabling MixedContentView to render tool blocks
for shared tasks.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: 添加OpenAI Anthropic Google模型提供者检测测试

---------

Co-authored-by: yixiangxx <3120662+yixiangxx@users.noreply.github.com>
Co-authored-by: yixiangxx <yixiang1@staff.weibo.com>
Co-authored-by: Claude <noreply@anthropic.com>
fengkuizhi pushed a commit that referenced this pull request Mar 3, 2026
fix(shared-task): enable full attachment access with share tokens (#413)

See merge request weibo_rd/common/wecode/wegent!1353
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