Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
af9a94c
Fix delegated action-type governance to honor explicit item policies
Xeelee33 Sep 12, 2026
16f4625
Updated release notes with bug fixed in v0.261.025
Xeelee33 Sep 12, 2026
ff3749a
Add a controlled account-selection login flow for users who sign in
Xeelee33 Sep 16, 2026
b9a063a
Fix deployment authentication and safe post-configuration
paullizer Sep 17, 2026
0450720
Document deployment reliability release and tracking issue
paullizer Sep 17, 2026
cec657f
build(deps): bump azure/login
dependabot[bot] Sep 17, 2026
9d64601
Merge pull request #1488 from paullizer/fix/deployment-postconfig-rel…
paullizer Sep 17, 2026
b8a7541
Add Microsoft 365 source actions and delegated workflow access
paullizer Sep 18, 2026
1883af6
Sanitize workflow alert diagnostics and resolve CodeQL control-flow f…
paullizer Sep 18, 2026
c23e32c
Separate Microsoft 365 context from workflow connection authorization
paullizer Sep 18, 2026
601c294
Document resolution of intentional CodeQL interface findings
paullizer Sep 18, 2026
bdb668a
Fix Microsoft 365 agent streaming and Cosmos replacement writes
paullizer Sep 18, 2026
26e44a2
Fix Microsoft 365 in-chat connection and source onboarding
paullizer Sep 18, 2026
938f453
Fix group chat uploads for workspace owners, admins, and document man…
Xeelee33 Sep 18, 2026
537a259
Fix stored XSS in group document filename rendering
paullizer Sep 18, 2026
3507614
Merge pull request #1500 from paullizer/paullizer-group-filename-xss
paullizer Sep 18, 2026
795ca52
Merge branch 'Development' into feature/improve-cross-tenant-auth
Xeelee33 Sep 19, 2026
3d9079a
Merge branch 'Development' into bugfix/normalize_action_item_id
Xeelee33 Sep 19, 2026
98c4b0a
Fix Microsoft 365 consent callback scope validation
paullizer Sep 19, 2026
899da95
fix(tabular): support ANSI CSVs and reroute repeated tool failures
Xeelee33 Sep 19, 2026
67481bb
Merge branch 'Development' into bugfix/workspace_file_upload_csv_enco…
Xeelee33 Sep 19, 2026
73ff7d6
Remove MCP stdio support and harden action authorization
paullizer Sep 19, 2026
f5f2569
Add Microsoft 365 interactive reconnect and rejected-token recovery
paullizer Sep 19, 2026
df6ebbe
Address MCP CodeQL review findings
paullizer Sep 19, 2026
2dd6758
Merge Development and retain the MCP patch version
paullizer Sep 19, 2026
663b5f9
Clarify the expected manifest hashing failure
paullizer Sep 19, 2026
a46eb81
Merge pull request #1502 from Xeelee33/bugfix/workspace_file_upload_c…
paullizer Sep 19, 2026
607c5a8
Merge pull request #1479 from Xeelee33/bugfix/normalize_action_item_id
paullizer Sep 19, 2026
46a08cd
Merge branch 'Development' into feature/improve-cross-tenant-auth
paullizer Sep 19, 2026
78b1240
Merge pull request #1480 from Xeelee33/feature/improve-cross-tenant-auth
paullizer Sep 19, 2026
7f99b8d
Merge pull request #1490 from microsoft/dependabot/github_actions/git…
paullizer Sep 19, 2026
62176c9
Merge latest Development into the MCP authorization fix
paullizer Sep 19, 2026
5e1a44d
Merge pull request #1503 from microsoft/paullizer-mcp-stdio-removal
paullizer Sep 19, 2026
f5782c7
Populate verified model token catalog and budget contracts
paullizer Sep 19, 2026
dd430b7
Add validated model capacity overrides to endpoint editors
paullizer Sep 19, 2026
5d55300
Wire selected model budgets through agents and bounded retrieval
paullizer Sep 19, 2026
0fafc28
Merge origin/Development into paullizer-sharepoint-file-sync
paullizer Sep 19, 2026
e24c8cf
Address remaining M365 CodeQL review notes
paullizer Sep 19, 2026
8c681de
Fix Microsoft 365 chat action cards and confirmed delivery
paullizer Sep 20, 2026
79d4cfb
Remove unused group deletion result binding
paullizer Sep 20, 2026
f1be106
Merge pull request #1497 from microsoft/paullizer-sharepoint-file-sync
paullizer Sep 20, 2026
b56ccd3
Fix collaborative prompts without explicit AI targets
Xeelee33 Sep 21, 2026
11f2e7c
Merge pull request #1515 from Xeelee33/bugfix/sharing_conversation_br…
paullizer Sep 22, 2026
ca32a19
add-default-model-admin-setting - added new admin model and reasoning…
chadpalmer Sep 22, 2026
129ff06
add-default-model-admin-setting - fixed merge conflict.
chadpalmer Sep 22, 2026
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 .github/workflows/staging-azd-ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
esac

- name: Azure login
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
Expand Down
73 changes: 71 additions & 2 deletions application/single_app/agent_logging_chat_completion.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@

# agent_logging_chat_completion.py
from contextlib import aclosing
from copy import deepcopy
import json
import logging
from pydantic import Field
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.const import DEFAULT_SERVICE_NAME
from semantic_kernel.functions import KernelArguments
from functions_m365_agent_continuation import (
m365_agent_continuation,
m365_agent_stream_continuation,
)
from functions_appinsights import log_event
from functions_model_capabilities import ModelTokenBudget, ModelTokenBudgetError
from functions_model_budget_runtime import prepare_model_execution_settings
import datetime
import re


class LoggingChatCompletionAgent(ChatCompletionAgent):
display_name: str | None = Field(default=None)
default_agent: bool = Field(default=False)
is_global: bool = Field(default=False)
tool_invocations: list = Field(default_factory=list)
deployment_name: str | None = Field(default=None)
azure_endpoint: str | None = Field(default=None)
api_version: str | None = Field(default=None)
model_token_budget: ModelTokenBudget | None = Field(default=None, exclude=True)

def __init__(self, *args, display_name=None, default_agent=False, deployment_name=None, azure_endpoint=None, api_version=None, **kwargs):
def __init__(self, *args, display_name=None, default_agent=False, deployment_name=None, azure_endpoint=None, api_version=None, model_token_budget=None, **kwargs):
# Remove these from kwargs so the base class doesn't see them
kwargs.pop('display_name', None)
kwargs.pop('default_agent', None)
Expand All @@ -29,8 +41,58 @@ def __init__(self, *args, display_name=None, default_agent=False, deployment_nam
self.deployment_name = deployment_name
self.azure_endpoint = azure_endpoint
self.api_version = api_version
self.model_token_budget = model_token_budget
# tool_invocations is now properly declared as a Pydantic field

def _merge_arguments(self, override_args):
base = self.arguments if self.arguments is not None else KernelArguments()
values = dict(base)
execution_settings = deepcopy(base.execution_settings or {})
if override_args is not None:
values.update(override_args)
overrides = deepcopy(override_args.execution_settings or {})
if self.model_token_budget is not None and self.service is not None:
service_id = self.service.service_id
if set(overrides) - {service_id, DEFAULT_SERVICE_NAME}:
raise ModelTokenBudgetError(
"model_context_invalid", "Select an agent bound to the requested model service."
)
default = overrides.pop(DEFAULT_SERVICE_NAME, None)
if default is not None and service_id not in overrides:
default.service_id = service_id
overrides[service_id] = default
execution_settings.update(overrides)
return KernelArguments(settings=execution_settings, **values)

async def _get_chat_completion_service_and_settings(self, kernel, arguments):
service, settings = await super()._get_chat_completion_service_and_settings(kernel, arguments)
if self.model_token_budget is None:
return service, settings
if (
self.service is not None and service is not self.service
or self.deployment_name is not None and service.ai_model_id != self.deployment_name
):
raise ModelTokenBudgetError(
"model_context_invalid", "The selected service does not match this agent's model budget."
)
override_model = getattr(settings, "ai_model_id", None)
if override_model and override_model != service.ai_model_id:
raise ModelTokenBudgetError(
"model_context_invalid", "Select a model with matching budget metadata instead of overriding its request identifier."
)
try:
settings, _ = prepare_model_execution_settings(
settings, self.model_token_budget,
tools_enabled=bool(kernel.get_full_list_of_function_metadata()),
)
except ModelTokenBudgetError as error:
log_event(
"[SK_LOADER] Agent model budget configuration is invalid.",
extra={"agent_id": self.id, "code": error.code}, level=logging.ERROR,
)
raise
return service, settings

def log_tool_execution(self, tool_name, arguments=None, result=None):
"""Manual method to log tool executions. Can be called by plugins."""
tool_citation = {
Expand Down Expand Up @@ -131,6 +193,7 @@ def extract_tool_invocations_from_history(self, chat_history):
"""
return [] # Plugin invocation logger handles this now

@m365_agent_continuation
async def invoke(self, *args, **kwargs):
# Clear previous tool invocations
self.tool_invocations = []
Expand Down Expand Up @@ -206,6 +269,12 @@ async def invoke(self, *args, **kwargs):
}
)

@m365_agent_stream_continuation
async def invoke_stream(self, *args, **kwargs):
async with aclosing(super().invoke_stream(*args, **kwargs)) as stream:
async for response in stream:
yield response

def _capture_tool_invocations_simplified(self, args, response):
"""
SIMPLIFIED: Basic fallback citation capture.
Expand Down
54 changes: 53 additions & 1 deletion application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@
from route_backend_collaboration import register_route_backend_collaboration
from route_backend_data_management import register_route_backend_data_management
from route_backend_msgraph_pending_actions import register_route_backend_msgraph_pending_actions
from route_backend_m365 import configure_m365_routes, register_route_backend_m365
from functions_m365_approvals import configure_m365_approvals
from functions_m365_connections import configure_m365_connection_authorization
from functions_m365_execution import configure_m365_execution, validate_m365_workflow_context
from functions_m365_file_runtime import configure_m365_file_runtime
from functions_m365_request_resume import queue_approved_chat
from functions_m365_runtime import (
authorize_m365_conversation_audit,
complete_m365_request,
configure_m365_history_runtime,
configure_m365_pending_delivery_runtime,
resolve_m365_action_config,
resolve_m365_action_selection,
resolve_m365_audit_conversation_id,
resolve_m365_workflow_binding,
validate_m365_approval_decision,
validate_m365_workflow_execution,
)
from route_inbound_mcp import register_route_inbound_mcp
from route_enhanced_citations import register_enhanced_citations_routes
from plugin_validation_endpoint import plugin_validation_admin_bp, plugin_validation_bp
Expand Down Expand Up @@ -1149,8 +1167,14 @@ def index():

# Convert Markdown to HTML safely
landing_html = markdown_filter(landing_text)
signed_in_account = get_signed_in_account_display(session.get("user"))

return render_template('index.html', app_settings=public_settings, landing_html=landing_html)
return render_template(
'index.html',
app_settings=public_settings,
landing_html=landing_html,
signed_in_account=signed_in_account,
)

@public_app_bp.route('/robots933456.txt')
@swagger_route(security=get_auth_security())
Expand Down Expand Up @@ -1296,6 +1320,34 @@ def list_semantic_kernel_plugins():

# ------------------- API MS Graph Pending Action Routes -
register_route_blueprint('backend_msgraph_pending_actions', register_route_backend_msgraph_pending_actions, user_required_blueprint)
configure_m365_approvals(decision_validator=validate_m365_approval_decision)
configure_m365_execution(
workflow_validator=validate_m365_workflow_execution,
action_config_resolver=resolve_m365_action_config,
workflow_binding_resolver=resolve_m365_workflow_binding,
action_selection_resolver=resolve_m365_action_selection,
)
configure_m365_connection_authorization(validate_m365_workflow_context)
configure_m365_routes(
conversation_authorizer=authorize_m365_conversation_audit,
decision_callback=queue_approved_chat,
audit_conversation_resolver=resolve_m365_audit_conversation_id,
)
configure_m365_history_runtime()
configure_m365_file_runtime()
configure_m365_pending_delivery_runtime(app.test_request_context)
register_route_blueprint('backend_m365', register_route_backend_m365, user_required_blueprint)


@app.after_request
def finalize_m365_json_request(response):
if response.is_json:
payload = response.get_json()
success = response.status_code < 400 and isinstance(payload, dict) and not (
payload.get("error") or payload.get("pending") or payload.get("success") is False
)
complete_m365_request(success=success)
return response

# ------------------- API Documents Routes ---------------
register_route_blueprint('backend_documents', register_route_backend_documents, user_required_blueprint)
Expand Down
115 changes: 108 additions & 7 deletions application/single_app/background_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from azure.core import MatchConditions

from config import cosmos_settings_container, exceptions
from config import cosmos_m365_execution_runs_container, cosmos_settings_container
from functions_appinsights import log_event
from functions_control_center import (
calculate_next_control_center_auto_refresh_run,
Expand Down Expand Up @@ -52,7 +52,27 @@
update_group_workflow_runtime_fields,
)
from functions_settings import get_settings, is_group_workflows_enabled_for_group, update_settings
from functions_workflow_runner import create_workflow_run_id, run_group_workflow, run_personal_workflow
from functions_m365_workflow_binding import (
M365_ACTIVE_STATES,
workflow_result_is_waiting,
workflow_result_runtime_status,
)
from functions_m365_approvals import get_m365_approval_service
from functions_m365_connections import configure_m365_connection_authorization, get_m365_connection_service
from functions_m365_continuations import resume_pending_workflows
from functions_m365_execution import configure_m365_execution, validate_m365_workflow_context
from functions_m365_file_runtime import configure_m365_file_runtime
from functions_m365_runtime import (
configure_m365_pending_delivery_runtime,
load_current_workflow,
resolve_m365_action_config,
resolve_m365_action_selection,
resolve_m365_workflow_binding,
validate_m365_approval_decision,
validate_m365_workflow_execution,
)
from functions_workflow_runner import _get_workflow_runner_app, create_workflow_run_id, run_group_workflow, run_personal_workflow
from functions_m365_pending_delivery import dispatch_due_m365_deliveries


def _get_lock_holder_id():
Expand Down Expand Up @@ -295,6 +315,8 @@ def check_retention_policy_once():
from functions_retention_policy import execute_retention_policy

try:
# Retention can run before the workflow scheduler initializes delivery.
configure_m365_pending_delivery_runtime(_get_workflow_runner_app().test_request_context)
results = execute_retention_policy(manual_execution=False)
if results.get('success'):
print(
Expand Down Expand Up @@ -527,10 +549,83 @@ def run_cosmos_throughput_autoscale_loop():
time.sleep(sleep_seconds)


def check_m365_workflow_continuations_once():
approval_service = get_m365_approval_service()
approval_service.decision_validator = validate_m365_approval_decision
configure_m365_execution(
workflow_validator=validate_m365_workflow_execution,
action_config_resolver=resolve_m365_action_config,
workflow_binding_resolver=resolve_m365_workflow_binding,
action_selection_resolver=resolve_m365_action_selection,
)
configure_m365_connection_authorization(validate_m365_workflow_context)
configure_m365_file_runtime()
configure_m365_pending_delivery_runtime(_get_workflow_runner_app().test_request_context)
dispatch_due_m365_deliveries()

def can_resume(job, approval):
workflow = load_current_workflow(job["workflow_ref"])
return (
workflow.get("active_run_id") == job.get("run_id")
and workflow.get("status") in M365_ACTIVE_STATES
and workflow.get("m365_run_as_user_id") == job.get("user_id")
and (approval is None or approval.get("subject_user_id") == job.get("user_id"))
)

def connection_ready(job):
from config import TENANT_ID
connection = get_m365_connection_service().current_connection(job["user_id"], TENANT_ID)
if not connection or connection.get("status") != "connected":
return False
granted = {scope.rsplit("/", 1)[-1].lower() for scope in connection.get("authorized_scopes") or []}
return all(scope.rsplit("/", 1)[-1].lower() in granted for scope in job.get("required_scopes") or [])

def execute(job):
workflow = load_current_workflow(job["workflow_ref"])
settings = get_settings()
group_id = workflow.get("group_id")
if group_id:
if not is_group_workflows_enabled_for_group(settings, group_id):
raise PermissionError("Group workflows are no longer enabled for this group.")
lock_name = f"group_workflow_run_{group_id}_{workflow['id']}"
else:
if not settings.get("allow_user_workflows", False):
raise PermissionError("Personal workflows are no longer enabled.")
lock_name = f"workflow_run_{workflow['id']}"
lock = acquire_distributed_task_lock(lock_name, lease_seconds=900)
if not lock:
raise RuntimeError("The workflow is already executing.")
try:
runner = run_group_workflow if group_id else run_personal_workflow
result = runner(
workflow, trigger_source="m365_approval",
actor_user_id=job.get("actor_user_id"), run_id=job["run_id"],
)
updates = dict(result.get("workflow_updates") or {})
updates["status"] = workflow_result_runtime_status(result)
if not workflow_result_is_waiting(result):
updates["next_run_at"] = compute_next_run_at(
workflow, from_time=datetime.now(timezone.utc),
)
if group_id:
update_group_workflow_runtime_fields(group_id, workflow["id"], updates)
else:
update_personal_workflow_runtime_fields(workflow["user_id"], workflow["id"], updates)
return result
finally:
release_distributed_task_lock(lock)

return resume_pending_workflows(
cosmos_m365_execution_runs_container, approval_service,
execute=execute, can_resume=can_resume, log_event=log_event,
connection_ready=connection_ready,
)


def check_due_workflows_once():
"""Execute scheduled personal and group workflows that are due."""
settings = get_settings()
results = []
results = check_m365_workflow_continuations_once()

if settings.get('allow_user_workflows', False):
due_workflows = get_due_personal_workflows(limit=20)
Expand All @@ -549,6 +644,8 @@ def check_due_workflows_once():
refreshed_workflow = get_personal_workflow(user_id, workflow_id)
if not refreshed_workflow:
continue
if refreshed_workflow.get('status') in M365_ACTIVE_STATES:
continue
trigger_type = str(refreshed_workflow.get('trigger_type') or '').strip().lower()
if trigger_type not in {'interval', 'file_sync'} or not refreshed_workflow.get('is_enabled', False):
continue
Expand Down Expand Up @@ -584,8 +681,9 @@ def check_due_workflows_once():
run_id=active_run_id,
)
update_fields = dict(result.get('workflow_updates') or {})
update_fields['status'] = 'idle'
update_fields['next_run_at'] = compute_next_run_at(refreshed_workflow, from_time=datetime.now(timezone.utc))
update_fields['status'] = workflow_result_runtime_status(result)
if not workflow_result_is_waiting(result):
update_fields['next_run_at'] = compute_next_run_at(refreshed_workflow, from_time=datetime.now(timezone.utc))
update_personal_workflow_runtime_fields(user_id, workflow_id, update_fields)
results.append({'scope': 'personal', 'workflow_id': workflow_id, 'success': bool(result.get('success'))})
except Exception as exc:
Expand Down Expand Up @@ -633,6 +731,8 @@ def check_due_workflows_once():
refreshed_workflow = get_group_workflow(group_id, workflow_id)
if not refreshed_workflow:
continue
if refreshed_workflow.get('status') in M365_ACTIVE_STATES:
continue
trigger_type = str(refreshed_workflow.get('trigger_type') or '').strip().lower()
if trigger_type not in {'interval', 'file_sync'} or not refreshed_workflow.get('is_enabled', False):
continue
Expand Down Expand Up @@ -668,8 +768,9 @@ def check_due_workflows_once():
run_id=active_run_id,
)
update_fields = dict(result.get('workflow_updates') or {})
update_fields['status'] = 'idle'
update_fields['next_run_at'] = compute_next_run_at(refreshed_workflow, from_time=datetime.now(timezone.utc))
update_fields['status'] = workflow_result_runtime_status(result)
if not workflow_result_is_waiting(result):
update_fields['next_run_at'] = compute_next_run_at(refreshed_workflow, from_time=datetime.now(timezone.utc))
update_group_workflow_runtime_fields(group_id, workflow_id, update_fields)
results.append({'scope': 'group', 'group_id': group_id, 'workflow_id': workflow_id, 'success': bool(result.get('success'))})
except Exception as exc:
Expand Down
Loading
Loading