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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.256"
VERSION = "0.261.257"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
15 changes: 13 additions & 2 deletions application/single_app/functions_collaboration.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
get_user_groups,
)
from functions_message_artifacts import filter_assistant_artifact_items
from functions_message_deletion import exclude_soft_deleted_messages
from functions_message_image_revisions import (
IMAGE_REVISIONS_METADATA_KEY,
resolve_image_message_content,
Expand Down Expand Up @@ -967,6 +968,8 @@
partition_key=source_conversation_id,
))
raw_messages = filter_assistant_artifact_items(raw_messages)
# Deleted while archiving was enabled: not part of the conversation being shared.

Check warning on line 971 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
raw_messages = exclude_soft_deleted_messages(raw_messages)

copied_messages = []
source_to_collaboration_message_ids = {}
Expand Down Expand Up @@ -1164,6 +1167,7 @@
partition_key=source_conversation_id,
))
raw_messages = filter_assistant_artifact_items(raw_messages)
raw_messages = exclude_soft_deleted_messages(raw_messages)

copied_messages = []
source_to_collaboration_message_ids = {}
Expand Down Expand Up @@ -1937,12 +1941,19 @@


def list_collaboration_messages(conversation_id):
"""Return a shared conversation's messages, oldest first.

Check warning on line 1944 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

A shared message is deleted outright, but a conversation shared from a personal one can

Check warning on line 1946 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
hold copies of messages that were deleted there while archiving was enabled; earlier
conversions copied them. They are not part of the conversation and are excluded.

Check warning on line 1948 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
Conversation deletion queries the container directly, so the copies still go with it.

Check warning on line 1949 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"""
query = 'SELECT * FROM c WHERE c.conversation_id = @conversation_id ORDER BY c.timestamp ASC'
return list(cosmos_collaboration_messages_container.query_items(
return exclude_soft_deleted_messages(list(cosmos_collaboration_messages_container.query_items(

Check warning on line 1952 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
query=query,
parameters=[{'name': '@conversation_id', 'value': conversation_id}],
partition_key=conversation_id,
))
)))


def persist_collaboration_message(
Expand Down
3 changes: 3 additions & 0 deletions application/single_app/functions_mcp_server_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
sort_documents,
)
from functions_message_artifacts import filter_assistant_artifact_items
from functions_message_deletion import is_soft_deleted_message
from functions_personal_workflows import (
compute_next_run_at,
get_personal_workflow,
Expand Down Expand Up @@ -286,6 +287,8 @@ def _legacy_message_is_visible(message_item):
metadata = message_item.get("metadata", {}) if isinstance(message_item.get("metadata"), dict) else {}
if metadata.get("is_generated_chat_artifact", False):
return False
if is_soft_deleted_message(message_item):
return False
thread_info = metadata.get("thread_info", {}) if isinstance(metadata.get("thread_info"), dict) else {}
active_thread = thread_info.get("active_thread")
return active_thread is True or active_thread is None or "active_thread" not in thread_info
Expand Down
48 changes: 48 additions & 0 deletions application/single_app/functions_message_deletion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# functions_message_deletion.py
"""Helpers for soft-deleted chat messages.

With conversation archiving enabled, deleting a chat message keeps its document in the

Check warning on line 4 in application/single_app/functions_message_deletion.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
messages container with ``metadata.is_deleted`` set, rather than removing it. The delete
route also masks the message, but only as a fail-safe: masking is a separate, reversible

Check warning on line 6 in application/single_app/functions_message_deletion.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
user action, and a deleted message must not come back as a masked placeholder, be copied

Check warning on line 7 in application/single_app/functions_message_deletion.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
into a fork or shared conversation, be searchable, or reach the model.

Check warning on line 8 in application/single_app/functions_message_deletion.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.

Every reader that shows, copies, searches, or sends stored messages to a model therefore
has to drop soft-deleted documents itself. These helpers keep that check in one place.
"""

from collections.abc import Mapping


SOFT_DELETE_METADATA_FIELDS = (
'is_deleted',
'deleted_by_user_id',
'deleted_timestamp',
)

# For Cosmos queries that must exclude soft-deleted messages server-side, such as TOP 1
# lookups where filtering afterwards could leave nothing. The container alias must be `c`.
NOT_SOFT_DELETED_COSMOS_FILTER = (
'(NOT IS_DEFINED(c.metadata.is_deleted) OR c.metadata.is_deleted != true)'
)


def is_soft_deleted_message(message):
"""Return True when a stored chat message was deleted while archiving was enabled."""
if not isinstance(message, Mapping):
return False
metadata = message.get('metadata')
return isinstance(metadata, Mapping) and metadata.get('is_deleted') is True


def exclude_soft_deleted_messages(messages):
"""Return the messages that were not soft-deleted, preserving their order."""
return [message for message in messages or [] if not is_soft_deleted_message(message)]


def strip_soft_delete_metadata(metadata):
"""Remove deletion markers from metadata copied onto a new message, in place."""
if isinstance(metadata, dict):
for field_name in SOFT_DELETE_METADATA_FIELDS:
metadata.pop(field_name, None)
return metadata
3 changes: 3 additions & 0 deletions application/single_app/functions_orchestration_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from functions_action_catalog import build_action_planner_projection
from functions_assist_references import sanitize_reference_label
from functions_message_block_revisions import resolve_block_sources_in_content
from functions_message_deletion import is_soft_deleted_message
from functions_message_masking import remove_masked_content
from functions_orchestration_registry import (
CAPABILITY_ACTION_INVOKE,
Expand Down Expand Up @@ -1783,6 +1784,8 @@ def normalize_history_message(message):
raise ConversationContextError('The conversation contains invalid thread metadata.')
if (
metadata.get('masked')
# Deleted while archiving was enabled; its mask is only the delete route's fail-safe.
or is_soft_deleted_message(message)
or metadata.get('is_generated_chat_artifact')
# Answers built from a stored workflow result, or inheriting one, stay out of
# orchestration history until orchestration can re-check that result itself.
Expand Down
5 changes: 4 additions & 1 deletion application/single_app/functions_simplechat_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
)
from functions_orchestration_output_store import OutputStorageError, OutputUnavailableError
from functions_m365_action_cards import strip_pending_action_references
from functions_message_deletion import exclude_soft_deleted_messages
from functions_personal_workflows import save_personal_workflow
from functions_public_workspaces import (
check_public_workspace_status_allows_operation,
Expand Down Expand Up @@ -461,7 +462,9 @@ def _collect_fork_documents(
all_documents: List[Dict[str, Any]],
selected_message_id: str,
) -> List[Dict[str, Any]]:
ordered_documents = sorted(all_documents, key=_message_fork_sort_key)
# Messages deleted while archiving was enabled are not part of the conversation and are
# not copied. The caller still compares the full set to detect concurrent changes.
ordered_documents = sorted(exclude_soft_deleted_messages(all_documents), key=_message_fork_sort_key)
timeline_documents = [
document
for document in ordered_documents
Expand Down
22 changes: 21 additions & 1 deletion application/single_app/route_backend_chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@
remove_masked_content,
resolve_mask_display_name,
)
from functions_message_deletion import (
NOT_SOFT_DELETED_COSMOS_FILTER,
exclude_soft_deleted_messages,
is_soft_deleted_message,
)
from functions_message_visual_styles import (
UNSET as VISUAL_STYLE_HEIGHT_UNSET,
VisualStyleError,
Expand Down Expand Up @@ -2628,9 +2633,12 @@ def _resolve_prior_turn_history_window(settings=None):


def _read_recent_assistant_messages(conversation_id, message_limit):
# A deleted reply's tool results and wording must not carry into later turns. Excluded in
# the query so TOP counts only replies that still exist, and checked again as a backstop.
query = (
f'SELECT TOP {int(message_limit)} c.id, c.conversation_id, c.role, c.content, c.metadata, c.agent_citations, c.hybrid_citations FROM c '
'WHERE c.conversation_id = @conversation_id AND c.role = @role '
f'AND {NOT_SOFT_DELETED_COSMOS_FILTER} '
'ORDER BY c.timestamp DESC'
)
messages = list(cosmos_messages_container.query_items(
Expand All @@ -2641,6 +2649,7 @@ def _read_recent_assistant_messages(conversation_id, message_limit):
],
partition_key=conversation_id,
))
messages = exclude_soft_deleted_messages(messages)
return _sanitize_saved_analysis_history(messages)


Expand Down Expand Up @@ -26634,6 +26643,11 @@ def mask_message_api(message_id):

message_doc = message_results[0]

# Deleted while archiving was enabled. Its mask is the delete route's fail-safe,
# so clearing it here would reveal deleted content to the reader and the model.
if is_soft_deleted_message(message_doc):
return jsonify({'error': 'Message not found'}), 404

conversation_id = message_doc.get('conversation_id')

# Verify ownership - only the message author can mask their message
Expand Down Expand Up @@ -27014,7 +27028,8 @@ def _find_originating_user_request(conversation_id, message_doc):

The nearest preceding user message. "Make it match what we discussed" nearly always
means the request the diagram came from, and passing that one message is enough for it
to mean something without sending the whole conversation to redraw a flowchart.
to mean something without sending the whole conversation to redraw a flowchart. A
deleted message is skipped, so its text never reaches the model.

Best effort: an edit works perfectly well with no grounding at all, so a failure here is
swallowed rather than failing the edit.
Expand All @@ -27028,6 +27043,7 @@ def _find_originating_user_request(conversation_id, message_doc):
'SELECT TOP 1 c.content FROM c '
'WHERE c.conversation_id = @conversation_id '
"AND c.role = 'user' AND c.timestamp < @timestamp "
f'AND {NOT_SOFT_DELETED_COSMOS_FILTER} '
'ORDER BY c.timestamp DESC'
),
parameters=[
Expand Down Expand Up @@ -28408,6 +28424,10 @@ def build_conversation_history_segments(
filtered_messages = filter_assistant_artifact_items(all_messages or [])
filtered_messages = hydrate_agent_citations_from_artifacts(filtered_messages, artifact_payload_map)
ordered_messages = sort_messages_by_thread(filtered_messages)
# Dropped after ordering, so the surviving messages keep the order the thread chain gives
# them. A deleted message must not reach the summary or the recent window, nor take a slot
# in the history limit; its mask is only a fail-safe, not what keeps it out.
ordered_messages = exclude_soft_deleted_messages(ordered_messages)

total_messages = len(ordered_messages)
num_recent_messages = min(total_messages, conversation_history_limit)
Expand Down
8 changes: 7 additions & 1 deletion application/single_app/route_backend_collaboration.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
copy_message_mask_metadata,
resolve_mask_display_name,
)
from functions_message_deletion import NOT_SOFT_DELETED_COSMOS_FILTER, is_soft_deleted_message
from functions_message_block_revisions import (
BLOCK_REVISIONS_METADATA_KEY,
MAX_CHAT_CONTENT_LENGTH as BLOCK_MAX_CHAT_CONTENT_LENGTH,
Expand Down Expand Up @@ -903,7 +904,7 @@ def _find_collaboration_originating_request(conversation_id, message_doc):
The nearest preceding human message in the shared thread. Only that one message is used, and
it is passed to the model as background rather than as instructions — sending a whole shared
conversation to redraw one flowchart would be expensive and would hand the model a great
deal of other people's writing.
deal of other people's writing. A message copied in already deleted is skipped.

Best effort: an edit works perfectly well with no grounding at all, so a failure here is
swallowed rather than failing the edit.
Expand All @@ -917,6 +918,7 @@ def _find_collaboration_originating_request(conversation_id, message_doc):
'SELECT TOP 1 c.content FROM c '
'WHERE c.conversation_id = @conversation_id '
"AND c.role = 'user' AND c.timestamp < @timestamp "
f'AND {NOT_SOFT_DELETED_COSMOS_FILTER} '
'ORDER BY c.timestamp DESC'
),
parameters=[
Expand Down Expand Up @@ -1953,6 +1955,10 @@ def mask_collaboration_message_api(conversation_id, message_id):
message_doc = get_collaboration_message(message_id)
if str(message_doc.get('conversation_id') or '').strip() != str(conversation_id or '').strip():
return jsonify({'error': 'Collaborative message not found'}), 404
# A copy of a message deleted before the conversation was shared. Unmasking it would
# reveal deleted content, and the sync below would unmask the original too.
if is_soft_deleted_message(message_doc):
return jsonify({'error': 'Collaborative message not found'}), 404

_assert_user_can_mask_collaboration_message(current_user['user_id'], message_doc)
user_display_name = resolve_mask_display_name(current_user)
Expand Down
Loading
Loading