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 @@ -96,7 +96,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.250.212"
VERSION = "0.250.213"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
12 changes: 9 additions & 3 deletions application/single_app/functions_group_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
WORKFLOW_TASK_RUNNER_TYPES,
WORKFLOW_TRIGGER_TYPES,
_build_default_model_summary,
_normalize_alert_priority,
_normalize_bool,
_normalize_document_action_config,
_normalize_schedule,
Expand All @@ -48,6 +47,7 @@
get_workflow_max_tasks,
)
from functions_settings import get_settings, normalize_model_endpoints
from functions_workflow_alerts import normalize_workflow_alert_settings


GROUP_WORKFLOW_MEMBER_ROLES = ("Owner", "Admin", "DocumentManager", "User")
Expand Down Expand Up @@ -479,9 +479,12 @@ def save_group_workflow(group_id, workflow_data, actor_user_id, user_info=None):
),
default=False,
) if url_access_enabled else False
alert_priority = _normalize_alert_priority(
workflow_data.get('alert_priority', (existing_workflow or {}).get('alert_priority', 'none'))
alert_settings = normalize_workflow_alert_settings(
workflow_data,
existing_workflow=existing_workflow,
task_ids=[task.get('id') for task in tasks],
)
alert_priority = alert_settings['alert_priority']
error_handling = _normalize_workflow_error_handling(workflow_data, existing_workflow=existing_workflow)
default_chat_capabilities_enabled = (
(existing_workflow or {}).get('chat_capabilities_enabled', False)
Expand Down Expand Up @@ -566,6 +569,9 @@ def save_group_workflow(group_id, workflow_data, actor_user_id, user_info=None):
'URL Access authorized at',
) if url_access_authorized else '',
'alert_priority': alert_priority,
'alert_mode': alert_settings['alert_mode'],
'alert_rules': alert_settings['alert_rules'],
'alert_evaluation': alert_settings['alert_evaluation'],
'schedule': schedule,
'document_action': document_action,
'analyze': analyze,
Expand Down
41 changes: 40 additions & 1 deletion application/single_app/functions_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
WORKFLOW_ALERT_NOTIFICATION_TYPE = 'workflow_priority_alert'
KEY_VAULT_SECRET_REMINDER_NOTIFICATION_TYPE = 'key_vault_secret_expiring'
WORKFLOW_ALERT_PRIORITY_CONFIG = {
'info': {
'icon': 'bi-info-circle',
'color': 'info',
},
'low': {
'icon': 'bi-bell',
'color': 'info',
Expand All @@ -41,7 +45,16 @@
'icon': 'bi-exclamation-triangle',
'color': 'danger',
},
'critical': {
'icon': 'bi-exclamation-octagon',
'color': 'danger',
},
}
# A run that errored is presented differently from a run that found something,
# independently of how loud the owner made the severity.
WORKFLOW_ALERT_FAILURE_CATEGORY = 'failure'
WORKFLOW_ALERT_FAILURE_ICON = 'bi-x-octagon'
WORKFLOW_ALERT_DELIVERY_NOTIFY_ONLY = 'notify_only'

# Notification type registry for extensibility
NOTIFICATION_TYPES = {
Expand Down Expand Up @@ -212,13 +225,32 @@ def _get_workflow_alert_priority(notification):
return priority


def _get_workflow_alert_category(notification):
metadata = notification.get('metadata') or {}
category = str(metadata.get('category') or 'alert').strip().lower()
return category if category in {'alert', WORKFLOW_ALERT_FAILURE_CATEGORY} else 'alert'


def _get_workflow_alert_delivery(notification):
"""Return the delivery style, defaulting to popup so legacy alerts still interrupt."""
metadata = notification.get('metadata') or {}
delivery = str(metadata.get('delivery') or '').strip().lower()
return delivery if delivery in {'notify_only', 'popup'} else 'popup'


def _get_notification_type_config(notification):
notification_type = notification.get('notification_type')
if notification_type == WORKFLOW_ALERT_NOTIFICATION_TYPE:
return WORKFLOW_ALERT_PRIORITY_CONFIG.get(
priority_config = WORKFLOW_ALERT_PRIORITY_CONFIG.get(
_get_workflow_alert_priority(notification),
NOTIFICATION_TYPES[WORKFLOW_ALERT_NOTIFICATION_TYPE],
)
if _get_workflow_alert_category(notification) == WORKFLOW_ALERT_FAILURE_CATEGORY:
return {
'icon': WORKFLOW_ALERT_FAILURE_ICON,
'color': priority_config.get('color', 'danger'),
}
return priority_config

return NOTIFICATION_TYPES.get(
notification_type,
Expand Down Expand Up @@ -700,6 +732,8 @@ def get_user_notifications(user_id, page=1, per_page=20, include_read=True, incl
notif['type_config'] = _get_notification_type_config(notif)
if notif.get('notification_type') == WORKFLOW_ALERT_NOTIFICATION_TYPE:
notif['priority'] = _get_workflow_alert_priority(notif)
notif['category'] = _get_workflow_alert_category(notif)
notif['delivery'] = _get_workflow_alert_delivery(notif)

filtered_notifications.append(notif)

Expand Down Expand Up @@ -825,11 +859,16 @@ def get_unread_workflow_priority_notifications(user_id, limit=5):
continue
if user_id in notification.get('read_by', []):
continue
# Quiet severities stay in the notification bell instead of interrupting.
if _get_workflow_alert_delivery(notification) == WORKFLOW_ALERT_DELIVERY_NOTIFY_ONLY:
continue

notification['message'] = _get_notification_display_message(notification)
notification['is_read'] = False
notification['is_dismissed'] = False
notification['priority'] = _get_workflow_alert_priority(notification)
notification['category'] = _get_workflow_alert_category(notification)
notification['delivery'] = _get_workflow_alert_delivery(notification)
notification['type_config'] = _get_notification_type_config(notification)
unread_notifications.append(notification)

Expand Down
11 changes: 9 additions & 2 deletions application/single_app/functions_personal_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from functions_global_agents import get_global_agents
from functions_personal_agents import get_personal_agents
from functions_settings import get_settings, get_user_settings, normalize_model_endpoints
from functions_workflow_alerts import normalize_workflow_alert_settings


WORKFLOW_TRIGGER_TYPES = {'manual', 'interval', 'file_sync'}
Expand Down Expand Up @@ -720,9 +721,12 @@ def save_personal_workflow(user_id, workflow_data, actor_user_id=None):
),
default=False,
) if url_access_enabled else False
alert_priority = _normalize_alert_priority(
workflow_data.get('alert_priority', (existing_workflow or {}).get('alert_priority', 'none'))
alert_settings = normalize_workflow_alert_settings(
workflow_data,
existing_workflow=existing_workflow,
task_ids=[task.get('id') for task in tasks],
)
alert_priority = alert_settings['alert_priority']
error_handling = _normalize_workflow_error_handling(workflow_data, existing_workflow=existing_workflow)
default_chat_capabilities_enabled = (
(existing_workflow or {}).get('chat_capabilities_enabled', False)
Expand Down Expand Up @@ -798,6 +802,9 @@ def save_personal_workflow(user_id, workflow_data, actor_user_id=None):
'URL Access authorized at',
) if url_access_authorized else '',
'alert_priority': alert_priority,
'alert_mode': alert_settings['alert_mode'],
'alert_rules': alert_settings['alert_rules'],
'alert_evaluation': alert_settings['alert_evaluation'],
'schedule': schedule,
'document_action': document_action,
'analyze': analyze,
Expand Down
57 changes: 56 additions & 1 deletion application/single_app/functions_simplechat_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"create_personal_conversation": "create_personal_conversation",
"create_personal_workflow": "create_personal_workflow",
"create_personal_collaboration_conversation": "create_personal_collaboration_conversation",
"raise_workflow_alert": "raise_workflow_alert",
}
SIMPLECHAT_CAPABILITY_DEFINITIONS = [
{
Expand Down Expand Up @@ -191,12 +192,24 @@
"label": "Create Personal Collaborative Conversations",
"description": "Create a personal collaborative conversation and invite other users.",
},
{
"key": "raise_workflow_alert",
"function_name": SIMPLECHAT_CAPABILITY_TO_FUNCTION["raise_workflow_alert"],
"label": "Raise Workflow Alerts",
"description": "Raise an alert signal during a workflow run so the workflow's alert rules can notify the owner.",
# Opt-in: an agent that can create notifications should be granted that

Check warning on line 200 in application/single_app/functions_simplechat_operations.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.
# deliberately rather than inheriting it when the capability was added.
"default_enabled": False,
},
]
CONVERSATION_ACCESS_ERROR = "Conversation not found or access denied"


def get_default_simplechat_capabilities() -> Dict[str, bool]:
return {definition["key"]: True for definition in SIMPLECHAT_CAPABILITY_DEFINITIONS}
return {
definition["key"]: bool(definition.get("default_enabled", True))
for definition in SIMPLECHAT_CAPABILITY_DEFINITIONS
}


def normalize_simplechat_capabilities(raw_capabilities: Any = None) -> Dict[str, bool]:
Expand Down Expand Up @@ -952,6 +965,48 @@
}


def raise_workflow_alert_for_current_user(
severity: str = "medium",
title: str = "",
reason: str = "",
signal_name: str = "",
) -> Dict[str, Any]:
"""Raise an alert signal for the workflow run the caller is executing inside.

The signal is only recorded while a workflow run is active. Outside a run the
call is refused so a normal chat cannot fabricate a workflow notification.
"""
# Imported lazily because functions_workflow_runner imports this module.
from functions_workflow_runner import (
is_workflow_alert_signal_scope_active,
record_workflow_alert_signal,
)

if not is_workflow_alert_signal_scope_active():
raise PermissionError(

Check warning on line 986 in application/single_app/functions_simplechat_operations.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.
"Workflow alerts can only be raised while a workflow run is executing."
)

signal = record_workflow_alert_signal(
severity=severity,
title=title,
reason=reason,
signal_name=signal_name,
)
if not signal:
raise PermissionError(

Check warning on line 997 in application/single_app/functions_simplechat_operations.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.
"Workflow alerts can only be raised while a workflow run is executing."
)

return {
"signal": signal,
"message": (
f"Recorded a {signal.get('severity')} workflow alert signal. "
"The workflow's alert rules decide whether it notifies the owner."
),
}


def add_conversation_message_for_current_user(
conversation_id: str,
content: str,
Expand Down
17 changes: 17 additions & 0 deletions application/single_app/functions_workflow_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from datetime import datetime, timezone

from functions_workflow_alerts import describe_alert_condition, resolve_workflow_alert_config


def _normalize_text(value):
return str(value or '').strip()
Expand Down Expand Up @@ -49,13 +51,27 @@ def _serialize_workflow(workflow):
if not isinstance(workflow, dict):
return None

alert_config = resolve_workflow_alert_config(workflow)
return {
'id': workflow.get('id'),
'name': workflow.get('name'),
'description': workflow.get('description'),
'runner_type': workflow.get('runner_type'),
'trigger_type': workflow.get('trigger_type'),
'alert_priority': workflow.get('alert_priority'),
'alert_mode': alert_config.get('alert_mode'),
'alert_rules': [
{
'id': rule.get('id'),
'name': rule.get('name'),
'enabled': rule.get('enabled', True),
'severity': rule.get('severity'),
'delivery': rule.get('delivery'),
'condition_summary': describe_alert_condition(rule.get('condition')),
}
for rule in alert_config.get('alert_rules') or []
if isinstance(rule, dict)
],
'conversation_id': workflow.get('conversation_id'),
}

Expand Down Expand Up @@ -95,6 +111,7 @@ def _serialize_run(run_record):
'agent_display_name': run_record.get('agent_display_name'),
'response_preview': run_record.get('response_preview'),
'error': run_record.get('error'),
'alert_decision': run_record.get('alert_decision') or {},
}


Expand Down
Loading
Loading