Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/wework-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,14 @@ jobs:
chmod 0755 dist/wegent-executor
dist/wegent-executor --version

- name: Prepare bundled Codex
- name: Prepare bundled sidecars
working-directory: wework
env:
WEWORK_CODEX_TARGET: ${{ matrix.rust_target }}
run: pnpm run prepare:codex
WEWORK_DWS_TARGET: ${{ matrix.rust_target }}
run: |
pnpm run prepare:codex
pnpm run prepare:dws

- name: Sync Wework version files
working-directory: wework
Expand Down Expand Up @@ -608,11 +611,14 @@ jobs:
Copy-Item target/x86_64-pc-windows-msvc/release/wegent-executor.exe ../wework/src-tauri/binaries/wegent-executor-x86_64-pc-windows-msvc.exe
& ../wework/src-tauri/binaries/wegent-executor-x86_64-pc-windows-msvc.exe --version

- name: Prepare bundled Codex
- name: Prepare bundled sidecars
working-directory: wework
env:
WEWORK_CODEX_TARGET: x86_64-pc-windows-msvc
run: pnpm run prepare:codex
WEWORK_DWS_TARGET: x86_64-pc-windows-msvc
run: |
pnpm run prepare:codex
pnpm run prepare:dws

- name: Build Wework app bundle
shell: pwsh
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/wework-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,11 @@ jobs:
restore-keys: |
${{ runner.os }}-wework-desktop-e2e-

- name: Prepare real Codex binary
- name: Prepare bundled sidecars
working-directory: ./wework
run: pnpm run prepare:codex
run: |
pnpm run prepare:codex
pnpm run prepare:dws

- name: Run Wework desktop ${{ matrix.name }} E2E
env:
Expand Down Expand Up @@ -253,10 +255,11 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-wework-desktop-memory-e2e-

- name: Prepare real Codex binary
- name: Prepare bundled sidecars
working-directory: ./wework
run: |
pnpm run prepare:codex
pnpm run prepare:dws
codex_bin="$(find "$PWD/src-tauri/binaries/codex" -type f -path '*/bin/codex' -perm -u+x -print -quit)"
test -n "$codex_bin"
echo "CODEX_BIN=$codex_bin" >> "$GITHUB_ENV"
Expand Down
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ DEFAULT_TEAM_CHAT=wegent-chat#default
DEFAULT_TEAM_KNOWLEDGE=wegent-notebook#default
DEFAULT_TEAM_TASK=wegent-wework#default

# Cloud project ID that receives Wework feedback. Empty disables submission.
WEWORK_FEEDBACK_PROJECT_ID=
# Maximum uploaded diagnostic bundle size in MB.
WEWORK_FEEDBACK_MAX_BUNDLE_SIZE_MB=250

# Long-term memory configuration (mem0)
# Optional API key for mem0 service authentication
MEMORY_API_KEY=
Expand Down
2 changes: 2 additions & 0 deletions backend/app/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
device_chat_tasks,
devices,
dingtalk_docs,
feedback,
groups,
health,
im_sessions,
Expand Down Expand Up @@ -136,6 +137,7 @@
cloud_projects.router, prefix="/v1/cloud-projects", tags=["cloud-projects"]
)
api_router.include_router(deliveries.router, prefix="/v1", tags=["deliveries"])
api_router.include_router(feedback.router, prefix="/v1/feedback", tags=["feedback"])
api_router.include_router(api_keys.router, prefix="/api-keys", tags=["api-keys"])
api_router.include_router(devices.router, prefix="/devices", tags=["devices"])
api_router.include_router(
Expand Down
50 changes: 35 additions & 15 deletions backend/app/api/endpoints/deliveries.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
UploadFile,
status,
)
from fastapi.responses import Response
from sqlalchemy.orm import Session

from app.api.dependencies import get_db
from app.core.config import settings
from app.core.security import get_current_user
from app.models.delivery import Delivery
from app.models.user import User
Expand Down Expand Up @@ -48,6 +50,10 @@
from app.services.delivery import delivery_service
from app.services.loop_items import loop_item_service
from app.services.loop_items.external_provider import external_loop_item_provider
from app.services.loop_items.provider_router import (
loop_item_attachment_provider_router,
loop_item_provider_router,
)

router = APIRouter()

Expand Down Expand Up @@ -242,14 +248,8 @@ def create_loop_item(
current_user: User = Depends(get_current_user),
) -> LoopItemResponse:
project = cloud_project_service.get(db, project_id, current_user.id)
if project.task_provider in {"github", "gitlab"}:
return LoopItemResponse.model_validate(
external_loop_item_provider.create(
db, project_id, current_user.id, current_user.user_name, values
)
)
item = loop_item_service.create(db, project_id, current_user.id, values)
return _loop_item_response(db, item, current_user)
created = loop_item_provider_router.create(db, project, current_user, values)
return LoopItemResponse.model_validate(created.values)


@router.post(
Expand Down Expand Up @@ -334,8 +334,9 @@ def list_loop_item_attachments(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> list[LoopItemAttachmentResponse]:
external_loop_item_provider.ensure_shadow(db, item_id, current_user.id)
attachments = loop_item_service.list_attachments(db, item_id, current_user.id)
attachments = loop_item_attachment_provider_router.list(
db, item_id, current_user.id
)
return [LoopItemAttachmentResponse.model_validate(item) for item in attachments]


Expand All @@ -350,14 +351,14 @@ def add_loop_item_attachment(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> LoopItemAttachmentResponse:
external_loop_item_provider.ensure_shadow(db, item_id, current_user.id)
attachment = loop_item_service.add_attachment(
attachment = loop_item_attachment_provider_router.add(
db,
item_id,
current_user.id,
file.filename or "attachment",
file.content_type or "application/octet-stream",
file.file,
settings.DELIVERY_MAX_ASSET_SIZE_MB * 1024 * 1024,
)
return LoopItemAttachmentResponse.model_validate(attachment)

Expand All @@ -371,9 +372,28 @@ def access_loop_item_attachment(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> LoopItemAttachmentAccessResponse:
loop_item_attachment_provider_router.require_access(
db, attachment_id, current_user.id
)
return LoopItemAttachmentAccessResponse(
url=loop_item_service.attachment_access_url(db, attachment_id, current_user.id),
expires_in_seconds=900,
url=f"wegent://attachments/{attachment_id}",
expires_in_seconds=0,
)


@router.get("/loop-item-attachments/{attachment_id}/content")
def read_loop_item_attachment(
attachment_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Response:
content, content_type, filename = loop_item_attachment_provider_router.content(
db, attachment_id, current_user.id
)
return Response(
content=content,
media_type=content_type,
headers={"Content-Disposition": f'inline; filename="{filename}"'},
)
Comment on lines +384 to 397

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Serving attachment bytes inline with a provider-supplied media_type and unescaped filename is risky.

Two concerns on this new endpoint:

  1. content_type originates from the uploaded file (file.content_type at Line 359 for local attachments). Serving text/html or image/svg+xml inline from the API origin allows stored XSS against any session-authenticated browser context.
  2. filename is unsanitized user input; a " or CR/LF in the display name corrupts or injects response headers.

Prefer Content-Disposition: attachment with RFC 5987 encoding, plus X-Content-Type-Options: nosniff.

🔒 Proposed fix
+from urllib.parse import quote as _url_quote
     content, content_type, filename = loop_item_attachment_provider_router.content(
         db, attachment_id, current_user.id
     )
+    safe_name = _url_quote(filename or "attachment")
     return Response(
         content=content,
         media_type=content_type,
-        headers={"Content-Disposition": f'inline; filename="{filename}"'},
+        headers={
+            "Content-Disposition": f"attachment; filename*=UTF-8''{safe_name}",
+            "X-Content-Type-Options": "nosniff",
+        },
     )
📝 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
@router.get("/loop-item-attachments/{attachment_id}/content")
def read_loop_item_attachment(
attachment_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Response:
content, content_type, filename = loop_item_attachment_provider_router.content(
db, attachment_id, current_user.id
)
return Response(
content=content,
media_type=content_type,
headers={"Content-Disposition": f'inline; filename="{filename}"'},
)
from urllib.parse import quote as _url_quote
`@router.get`("/loop-item-attachments/{attachment_id}/content")
def read_loop_item_attachment(
attachment_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Response:
content, content_type, filename = loop_item_attachment_provider_router.content(
db, attachment_id, current_user.id
)
safe_name = _url_quote(filename or "attachment")
return Response(
content=content,
media_type=content_type,
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{safe_name}",
"X-Content-Type-Options": "nosniff",
},
)
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 387-387: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


[warning] 388-388: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 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 `@backend/app/api/endpoints/deliveries.py` around lines 384 - 397, Update
read_loop_item_attachment to serve the file with Content-Disposition attachment
instead of inline, encode the provider-supplied filename using RFC 5987 rather
than interpolating it directly, and add X-Content-Type-Options: nosniff to the
response headers while preserving the returned content and media type.



Expand All @@ -385,7 +405,7 @@ def delete_loop_item_attachment(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> None:
loop_item_service.delete_attachment(db, attachment_id, current_user.id)
loop_item_attachment_provider_router.delete(db, attachment_id, current_user.id)


@router.get(
Expand Down
45 changes: 45 additions & 0 deletions backend/app/api/endpoints/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 Weibo, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Authenticated Wework feedback endpoint."""

import json

from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
from pydantic import ValidationError
from sqlalchemy.orm import Session

from app.api.dependencies import get_db
from app.core.security import get_current_user
from app.models.user import User
from app.schemas.feedback import FeedbackCreate, FeedbackResponse
from app.services.feedback_service import feedback_service

router = APIRouter()


@router.post("", response_model=FeedbackResponse, status_code=status.HTTP_201_CREATED)
def submit_feedback(
report_id: str = Form(...),
title: str = Form(...),
description: str = Form(""),
context: str = Form("{}"),
bundle: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> FeedbackResponse:
try:
parsed_context = json.loads(context)
values = FeedbackCreate(
report_id=report_id,
title=title,
description=description,
context=parsed_context,
)
except (json.JSONDecodeError, ValidationError) as error:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(error)) from error
if not isinstance(parsed_context, dict):
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY, "context must be an object"
)
return feedback_service.submit(db, current_user, values, bundle)
4 changes: 4 additions & 0 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,10 @@ def parse_rag_runtime_mode(cls, v: Any) -> str | dict[str, str]:
DEFAULT_TEAM_WEWORK: str = (
"wegent-wework#default" # Default team for WeWork workbench
)
# Cloud project that receives authenticated Wework feedback submissions.
# An empty value disables the feedback channel.
WEWORK_FEEDBACK_PROJECT_ID: str = ""
WEWORK_FEEDBACK_MAX_BUNDLE_SIZE_MB: int = 250

# JSON configuration for MCP servers (similar to Claude Desktop format)
# Example:
Expand Down
2 changes: 0 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ def _format_forwarded_headers_for_log(headers) -> str:

def _get_mcp_lifespan_servers():
from app.mcp_server.server import (
delivery_mcp_server,
interactive_form_question_mcp_server,
knowledge_mcp_server,
prompt_optimization_mcp_server,
Expand All @@ -105,7 +104,6 @@ def _get_mcp_lifespan_servers():
("interactive_form_question", interactive_form_question_mcp_server),
("Prompt optimization", prompt_optimization_mcp_server),
("Subscription", subscription_mcp_server),
("Delivery", delivery_mcp_server),
]
if settings.EXTERNAL_KNOWLEDGE_MCP_ENABLED:
from app.mcp_server.server import external_knowledge_mcp_server
Expand Down
Loading
Loading