A collaborative conversation is backed by a hidden source conversation whose
user_id is always the shared conversation creator. The collaboration stream
bridge proxies into chat_stream_api using that source conversation while keeping
the requesting participant's session, so every owner-equality check failed for
participants. The visible symptom was "Stream interrupted: Forbidden" with no
content whenever an invited user invoked the AI.
Four gates were involved:
- chat_stream_api returned 403 Forbidden, blocking all AI invocation by
participants. This only surfaced on explicit AI requests because shared
conversations default to ai_invocation_mode 'explicit_only'.
- The generated artifact write raised PermissionError, which
maybe_create_generated_file_output swallowed, so the file silently vanished.
- Group workspace document saves required Owner/Admin/DocumentManager.
- Artifact downloads were owner-only, so participants could not retrieve files
even once they existed.
Authorization is now resolved through a single collaboration-aware helper,
build_conversation_participation_context, mirroring the pattern already used by
chat file uploads in _resolve_chat_upload_context. Personal conversations with
no collaboration link remain strictly owner-only.
On top of that, downloadable files generated by a non-owner participant are now
staged for approval instead of failing. The artifact is written immediately in a
pending_approval state and released by an approver, so the model never re-runs.
The conversation owner approves in personal shared conversations; any group
Owner, Admin, or DocumentManager approves in group ones. A requester is never
their own approver. Denial deletes the stored blob and records the decision, and
anything left unapproved auto-denies after three days on the existing approval
expiry sweep. Only downloadable deliverables are gated; generated images and
charts stay ungated. Behavior is controlled by a new admin setting that defaults
to enabled.
Also fixed while in this path, both of which affect the reported case because
900 rows exceeds the 500-row inline export threshold:
- assert_generated_chat_artifact_is_published_for_user read the export run using
the caller's id as the partition key, so an approved large CSV queued by a
participant would have been unreadable by the owner. The run owner is now
recorded on the artifact.
- commit_generated_chat_artifact_publication_for_user and
delete_generated_chat_artifact_for_user carried the same owner-only check and
would have broken publication and rollback for participant-queued exports.
Group workspace writes are deliberately not staged. They feed the group search
index, so withholding them would mean withholding indexing. That path now
returns an actionable message naming the roles that can complete the request.
Version bumped to 0.260.004.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Problem
In a shared (collaborative) conversation, an invited participant asking the assistant to generate a CSV got:
The failure is broader than file generation. A collaborative conversation is backed by a hidden source conversation (
conversation_kind: 'collaboration_source') whoseuser_idis always the shared conversation creator./api/collaboration/conversations/<id>/streamproxies intochat_stream_apiusing that source conversation but keeps the requesting participant's session, so every owner-equality check failed for participants.Four gates were involved:
chat_stream_api→_authorize_personal_conversation_access403 Forbidden. Blocked all AI invocation by participants, not just files._upload_generated_chat_artifact_for_current_userPermissionError, swallowed bymaybe_create_generated_file_output, so the file silently vanished._resolve_group_upload_target_for_current_userUsercould not save generated documents to a group workspace._get_authorized_chat_artifact_messageGate 1 only surfaced on explicit AI requests because shared conversations default to
ai_invocation_mode: 'explicit_only'— ordinary participant messages never reach the bridge, which is why it looked file-specific.Changes
Authorization fix. All owner-only checks now resolve through one collaboration-aware helper,
build_conversation_participation_context, mirroring the pattern already used by chat file uploads in_resolve_chat_upload_context. Personal conversations with no collaboration link remain strictly owner-only.Approval gate. Downloadable files generated by a non-owner participant are written immediately in a
pending_approvalstate rather than failing, so approval is a cheap state flip and the model never re-runs.Owner/Admin/DocumentManagerapprovesrequire_shared_conversation_file_approval, default onAdditional bugs fixed
Both affect the reported case directly, since 900 rows exceeds the 500-row inline export threshold and takes the background export path:
assert_generated_chat_artifact_is_published_for_userread the export run using the caller's id as the partition key. A large CSV queued by a participant would have been unreadable by the owner even after approval. The run owner is now recorded on the artifact, with a fallback to the caller for pre-existing artifacts.commit_generated_chat_artifact_publication_for_useranddelete_generated_chat_artifact_for_usercarried the same owner-only check and would have broken publication and rollback for participant-queued exports.Three more were caught in review and fixed before this PR:
Admin/DocumentManagerwho was only a participant could stage a file and approve it themselves. The requester check now runs before the scope branch./api/enhanced_citations/tabular. That route streams any blob-backed file message after a single conversation-ownership check and never consulted the gate. A plain groupUserwho created a group shared conversation — explicitly not an approver — could fetch a staged CSV/XLSX directly. The gate now runs there before the blob is read, returning403rather than500.list_pending_generated_file_approvals_for_userappliedTOP @limitacross the whole messages container and filtered by approver afterwards in Python, so a tenant with >50 pending files could return an empty list to an approver with items waiting. Candidates are now narrowed to the caller's own approval scopes inside the query.Deliberate scope decision
Gate 3 (saving a generated document into a group workspace) is not staged. That path feeds the group search index, so holding it back means withholding indexing — a materially larger change. It now returns an actionable message naming the roles that can complete the request. The group scenario from the report — a member asking for a file in a group conversation — is fully covered by the approval flow, since those artifacts carry group scope and route to group document roles. Noted as follow-up in the feature doc.
Validation
test_shared_conversation_file_approval_fix.py(new)functional_tests/route_tests/(3 suites)test_generated_artifact_lifecycle_authorization.pytest_assistant_table_csv_artifact.pytest_generated_json_xml_exports.pytest_tabular_row_orchestration_scale.pyDevelopmentwas merged into this branch to resolve conflicts inconfig.pyandrelease_notes.md. The two suites that arrived with it —test_collaboration_mention_tab_autocomplete.py(5/5) andtest_chat_new_conversation_documents_drawer_reset.py(7/7) — pass against the merged code.Two existing tests that extract these functions via AST needed the new dependency stubbed and were updated:
test_generated_artifact_lifecycle_authorization.pyandtest_tabular_row_orchestration_scale.py.Confirmed against a clean baseline as pre-existing and unrelated to this change:
test_mixed_source_hardening.py,test_tabular_generated_output_exports.py, andtest_enhanced_citations_blob_and_collaboration_fix.py. The latter two fail on stale exact-version assertions pinned to0.241.144, which conflict with the repo rule against exact version equality in tests — worth a separate cleanup, left untouched here.Behavior change summary
Stream interrupted: Forbidden, no contentNotes
0.260.006(rebased over0.260.005fromDevelopment, which had already claimed0.260.004). Nothing underdeployers/changed, sodeployers/version.txtis untouched.static/js/chat/, usestextContentonly, Bootstrapd-noneand alerts, no CDN or dynamic imports.@swagger_route(security=get_auth_security()),@login_required,@user_required.docs/explanation/features/SHARED_CONVERSATION_FILE_APPROVALS.md,docs/explanation/fixes/SHARED_CONVERSATION_FILE_GENERATION_FORBIDDEN_FIX.md, release notes, and both indexes.Review focus
The diff touches shared conversation authorization used well beyond collaborative conversations. The highest-value review target is
build_conversation_participation_contextand its call sites. The invariant to check is that non-shared personal conversations remain strictly owner-only — asserted in the new tests, but worth a human read.