Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions application/single_app/agent_delegation_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,23 @@
try:
if agent_type == "local":
kernel, agent = _build_local_agent(target, settings)
from functions_orchestration_operations import (
current_operation_journal, integration_inputs_require_budget, require_integration_model_budget,
)
from semantic_kernel.filters import FilterTypes

Check warning on line 342 in application/single_app/agent_delegation_runtime.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.

journal = current_operation_journal()
if journal is not None:
kernel.add_filter(FilterTypes.FUNCTION_INVOCATION, journal.function_filter)
if integration_inputs_require_budget():
require_integration_model_budget(
getattr(agent, "model_token_budget", None), messages,

Check warning on line 349 in application/single_app/agent_delegation_runtime.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.

Check warning on line 349 in application/single_app/agent_delegation_runtime.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.

Check warning on line 349 in application/single_app/agent_delegation_runtime.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.
[metadata.model_dump(mode="json") for metadata in kernel.get_full_list_of_function_metadata()],
)
_check_cancelled(frame)
result = await _invoke_local(agent, messages)
if journal is not None:
journal.require_valid()
text = str(result) if result is not None else ""
model = getattr(agent, "deployment_name", None)
else:
Expand Down
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.320"
VERSION = "0.261.321"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
22 changes: 19 additions & 3 deletions application/single_app/functions_action_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from functions_action_manifest import bind_action_origin, is_retired_mcp_stdio
from functions_agent_delegation import AGENT_PLUGIN_TYPE, _identifier
from functions_control_center_dashboard import CONTROL_CENTER_ACTION_TYPE
from functions_m365_operations import orchestration_m365_capabilities
Comment thread
paullizer marked this conversation as resolved.


_SCOPES = frozenset({"personal", "group", "global"})
Expand Down Expand Up @@ -280,7 +281,7 @@
or name
or "Action"
)
return {
descriptor = {
"action_ref": _action_ref(scope_type, scope_id, action["id"]),
"id": action["id"],
"name": name,
Expand All @@ -291,6 +292,10 @@
"scope_id": scope_id,
"scope_label": _safe_text(scope_label, 200, private_values) or scope_type.title(),
}
capabilities = orchestration_m365_capabilities(action)
if capabilities:
descriptor["operation_capabilities"] = capabilities
return descriptor


def build_accessible_action_catalog(user_id, *, settings=None, user_groups=None, user_roles=None):
Expand Down Expand Up @@ -360,13 +365,24 @@
):
continue
private_values = _private_values(action)
projection.append({
entry = {
"action_ref": action_ref,
"display_name": _safe_text(action.get("display_name"), 200, private_values) or "Action",
"description": _safe_text(_description(action), 1000, private_values),
"type": _safe_text(action_type, 80, private_values),
"scope_label": _safe_text(action.get("scope_label"), 200, private_values),
})
}
if action.get("operation_capabilities"):
entry["operation_capabilities"] = [
{
key: _safe_text(value, 1000, private_values)
for key, value in capability.items()
if key in {"function_name", "description", "execution_intent", "delivery_mode"}
}
for capability in action["operation_capabilities"]
if isinstance(capability, dict)
]
projection.append(entry)
return projection


Expand Down
61 changes: 60 additions & 1 deletion application/single_app/functions_m365_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from functions_msgraph_operations import (
MSGRAPH_CAPABILITY_DEFINITIONS,
get_msgraph_enabled_function_names,

Check warning on line 11 in application/single_app/functions_m365_operations.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.
normalize_msgraph_calendar_send_options,
normalize_msgraph_mail_send_options,
)
Expand All @@ -21,6 +22,54 @@
})
M365_DIRECTORY_FUNCTIONS = frozenset({"search_users", "get_user_by_email"})


def orchestration_m365_manifest(action):
"""Saved function bounds shared by metadata discovery and step authorization."""

Check warning on line 27 in application/single_app/functions_m365_operations.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.
manifest = deepcopy(action)
if action.get("type") == "msgraph":

Check warning on line 29 in application/single_app/functions_m365_operations.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.
fields = action.get("additionalFields") or {}
functions = set(get_msgraph_enabled_function_names(

Check warning on line 31 in application/single_app/functions_m365_operations.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.
fields.get("msgraph_capabilities", action.get("msgraph_capabilities")),

Check warning on line 32 in application/single_app/functions_m365_operations.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.
))
if action.get("msgraph_capabilities") is not None:

Check warning on line 34 in application/single_app/functions_m365_operations.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.
functions.intersection_update(get_msgraph_enabled_function_names(action["msgraph_capabilities"]))
if action.get("enabled_functions") is not None:
functions.intersection_update(action["enabled_functions"])
manifest["enabled_functions"] = sorted(functions)
else:
manifest["enabled_functions"] = get_m365_enabled_function_names(action["type"], action)
return manifest


def orchestration_m365_capabilities(action):
"""Secret-free enabled operation descriptions, never a permission grant."""
action_type = action.get("type")
if action_type != "msgraph" and action_type not in M365_ACTION_DEFINITIONS:
return []
manifest = orchestration_m365_manifest(action)
definitions = (
MSGRAPH_CAPABILITY_DEFINITIONS if action_type == "msgraph"
else get_m365_function_definitions(action_type)
)
options = {**(action.get("additionalFields") or {}), **action}
if action_type != "msgraph":
options = normalize_m365_action_config(action_type, action)["additionalFields"]
capabilities = []
for definition in definitions:
name = definition["function_name"]
if name not in manifest["enabled_functions"]:
continue
entry = {
"function_name": name, "description": definition["description"],
"execution_intent": "operate" if name in M365_WRITE_FUNCTIONS else "gather",
}
if name == "send_mail":
entry["delivery_mode"] = normalize_msgraph_mail_send_options(options)["msgraph_mail_send_mode"]
elif name == "create_calendar_invite":
entry["delivery_mode"] = normalize_msgraph_calendar_send_options(options)["msgraph_calendar_send_mode"]
capabilities.append(entry)
return capabilities

_LEGACY_DEFINITIONS = {
definition["function_name"]: definition
for definition in MSGRAPH_CAPABILITY_DEFINITIONS
Expand Down Expand Up @@ -470,7 +519,17 @@
if denial:
return denial
with self._operation_context(function.__name__):
result = function(self, *args, **kwargs)
# A journal exists only inside an explicit orchestration operation step.
from functions_orchestration_operations import current_operation_journal
Comment thread
paullizer marked this conversation as resolved.

journal = current_operation_journal()
if journal is not None and function.__name__ in M365_WRITE_FUNCTIONS:
parameters = {key: value for key, value in arguments.items() if key != "self"}
result = journal.call_m365(
self, function.__name__, parameters, lambda: function(self, *args, **kwargs),
)
else:
result = function(self, *args, **kwargs)
source = get_m365_operation_source(function.__name__, self._action_type)
if source and isinstance(result, dict):
result.setdefault("source", source)
Expand Down
19 changes: 16 additions & 3 deletions application/single_app/functions_m365_pending_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@
def configure_m365_pending_delivery(
*, container, context_scope, log_event, transport_factory=M365Transport, notification_sender=None,
capture_agent_reference=None, view_authorizer=None, conversation_authorizer=None,
capture_step_reference=None,
):
_dependencies.update(
container=container, context_scope=context_scope, log_event=log_event,
transport_factory=transport_factory,
notification_sender=notification_sender,
capture_agent_reference=capture_agent_reference,
capture_step_reference=capture_step_reference,
view_authorizer=view_authorizer,
conversation_authorizer=conversation_authorizer,
)
Expand Down Expand Up @@ -106,6 +108,11 @@ def capture_workflow_delivery(user_id, action_id, workflow_id, run_id):
"id": context.workflow_id, "user_id": context.actor_user_id, "group_id": context.group_id,
}
else:
capture_step = _dependencies.get("capture_step_reference")
step = capture_step(context, action_id) if capture_step is not None else None
if step is not None:
delivery.update(kind="orchestration", step_ref=step)
return delivery
capture = _dependencies.get("capture_agent_reference")
if capture is None or not context.conversation_id or not context.request_id:
raise M365PolicyError("m365_delivery_context_invalid", "A selected conversation agent is required for action review.")
Expand Down Expand Up @@ -138,7 +145,13 @@ def has_verified_delivery_binding(action):
}
return bool(
delivery.get("version") == DELIVERY_BINDING_VERSION
and delivery.get("kind") in {"chat", "workflow"} and delivery.get("action_id")
and delivery.get("kind") in {"chat", "workflow", "orchestration"} and delivery.get("action_id")
and (
delivery.get("kind") != "orchestration"
or isinstance(delivery.get("step_ref"), dict)
and delivery["step_ref"].get("run_id") and delivery["step_ref"].get("step_id")
and isinstance(delivery["step_ref"].get("selection"), dict)
)
and delivery.get("action_type") in {"msgraph", "m365_email", "m365_calendar"}
and action.get("operation") in allowed_operations[delivery["action_type"]]
and snapshot.get("data_user_id") == action.get("user_id")
Expand Down Expand Up @@ -263,7 +276,7 @@ def dispatch_m365_pending_delivery(
due = datetime.fromisoformat(action["auto_send_at_utc"].replace("Z", "+00:00"))
if due > now:
return action, _delivery_error("delivery_not_due", "The scheduled delivery time has not arrived.")
if (action["m365_execution"].get("kind") == "chat"
if (action["m365_execution"].get("kind") in {"chat", "orchestration"}
and now > due + timedelta(seconds=DELIVERY_RECOVERY_GRACE_SECONDS)):
return action, _delivery_error("delivery_schedule_expired", "The delivery window expired. Review and send it manually.")
delivery = action["m365_execution"]
Expand Down Expand Up @@ -431,7 +444,7 @@ def dispatch_due_m365_deliveries(*, limit=25):
continue
delivery = action.get("m365_execution")
delivery = delivery if isinstance(delivery, dict) else {}
if delivery.get("kind") == "chat":
if delivery.get("kind") in {"chat", "orchestration"}:
due = datetime.fromisoformat(action["auto_send_at_utc"].replace("Z", "+00:00"))
if due + timedelta(seconds=DELIVERY_RECOVERY_GRACE_SECONDS) > now:
continue
Expand Down
89 changes: 69 additions & 20 deletions application/single_app/functions_m365_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
M365_LEGACY_OPERATION_SOURCES,
M365_WRITE_FUNCTIONS,
get_m365_enabled_function_names,
orchestration_m365_manifest,
)
from functions_msgraph_operations import get_msgraph_enabled_function_names
from functions_m365_connections import preflight_m365_chat_authentication
Expand Down Expand Up @@ -550,20 +551,7 @@

def _step_action_manifest(action):
"""A saved action's own enabled functions, with no agent overlay."""
manifest = dict(action)
if action.get("type") == "msgraph":
fields = action.get("additionalFields") or {}
functions = set(get_msgraph_enabled_function_names(
fields.get("msgraph_capabilities", action.get("msgraph_capabilities")),
))
if action.get("msgraph_capabilities") is not None:
functions.intersection_update(get_msgraph_enabled_function_names(action["msgraph_capabilities"]))
if action.get("enabled_functions") is not None:
functions.intersection_update(action["enabled_functions"])
manifest["enabled_functions"] = sorted(functions)
else:
manifest["enabled_functions"] = get_m365_enabled_function_names(action["type"], action)
return manifest
return orchestration_m365_manifest(action)


def step_selected_m365_manifests(selection, actor_user_id):
Expand Down Expand Up @@ -618,6 +606,14 @@
return readable


def _step_m365_manifests(selection, actor_user_id):
manifests = step_selected_m365_manifests(selection, actor_user_id)
intent = selection.get("execution_intent", "gather")
if intent not in {"gather", "operate"}:
raise M365PolicyError("m365_action_selection_unavailable", "The step intent is invalid.")
return manifests if intent == "operate" else _readable_m365_manifests(manifests)


def _begin_step_m365_request(context, origin):
"""Mark a plan step's request running. A retry of the step reuses the same record."""
container = cosmos_m365_execution_runs_container
Expand Down Expand Up @@ -719,8 +715,8 @@

A plan step runs in a fresh request context, so the chat request's Microsoft 365
context never reaches it. This context uses the signed-in user as actor and data user,
selects only the step's own action or agent, and never enables send, invite or
read-state functions. In a shared conversation it carries that conversation's real
selects only the step's own action or agent. Only explicit operate intent admits
configured send, invite or read-state functions. In a shared conversation it carries that conversation's real
audience; the user's own request is their consent to share the sources it reads there.
Delegated sign-in is checked here, before any model call or Microsoft Graph request. An
agent step whose agent loads no Microsoft 365 action gets no context and leaves no
Expand All @@ -747,10 +743,10 @@
shared_by_request=shared is not None,
)
install_m365_context(context)
manifests = _readable_m365_manifests(selected)
manifests = _step_m365_manifests(selection, user_id)
if not manifests:
if selected:
raise M365PolicyError("m365_read_only_step", "Plans can only read Microsoft 365 data.")
raise M365PolicyError("m365_read_only_step", "This step only gathers Microsoft 365 information.")
raise M365PolicyError("m365_action_not_selected", "No Microsoft 365 action is selected for this step.")
_begin_step_m365_request(context, origin)
status, approval_id = "failed", None
Expand Down Expand Up @@ -797,6 +793,55 @@
if key in selection
}

def validate_step_reference(reference, context):
# The stored origin is a locator, not authority; reload its exact owned plan.
from functions_orchestration_runs import get_orchestration_run
Comment thread
paullizer marked this conversation as resolved.

if not isinstance(reference, dict) or not isinstance(reference.get("selection"), dict):
raise M365PolicyError("m365_delivery_context_invalid", "The orchestration origin is unavailable.")
run = get_orchestration_run(
reference.get("run_id"), context.actor_user_id,
conversation_id=context.conversation_id, strict=True,
)
selection = reference["selection"]
step = next((
item for item in (run or {}).get("plan", {}).get("steps", [])
if item.get("step_id") == reference.get("step_id")
), None)
arguments = (step or {}).get("arguments") or {}
selected_agent = selection.get("agent") or {}
matches = (
step and step.get("enabled", True) and arguments.get("execution_intent") == "operate"
and selection.get("execution_intent") == "operate"
and (
step.get("capability_id") == "action_invoke" and selection.get("kind") == "action"
and arguments.get("action_ref") == selection.get("action_ref")
or step.get("capability_id") == "agent_invoke" and selection.get("kind") == "agent"
and arguments.get("agent_name") == selected_agent.get("name")
)
)
if not matches or run.get("status") == "cancelled" or run.get("superseded_by_run_id"):
raise M365PolicyError("m365_context_changed", "The originating plan changed or stopped.")
return deepcopy(selection)

def capture_step_reference(context, action_id):
selection = getattr(g, M365_STEP_SELECTION_KEY, None) if has_request_context() else None
if selection is None:
return None
if action_id not in resolve_m365_action_selection(context):
raise M365PolicyError("m365_action_not_selected", "The outgoing action is not selected.")
record = cosmos_m365_execution_runs_container.read_item(
context.request_id, partition_key=context.data_user_id,
)
origin = record.get("orchestration") or {}
if record.get("actor_user_id") != context.actor_user_id or record.get("conversation_id") != context.conversation_id:
raise M365PolicyError("m365_delivery_context_invalid", "The step's request binding changed.")
reference = {
"run_id": origin.get("run_id"), "step_id": origin.get("step_id"), "selection": deepcopy(selection),
}
validate_step_reference(reference, context)
return reference

def authorize_conversation(viewer, conversation_id):
return resolve_m365_audit_conversation_id(viewer, conversation_id)

Expand Down Expand Up @@ -885,7 +930,10 @@
conversation, _access, shared = _conversation_access(context.actor_user_id, context.conversation_id)
if conversation is None or _audience_version(conversation, shared) != context.audience_version:
raise M365PolicyError("m365_context_changed", "The conversation or audience changed. Prepare the action again.")
g.m365_selected_agent_ref = deepcopy(delivery.get("agent_ref") or {})
if delivery.get("kind") == "orchestration":
g.m365_step_selection = validate_step_reference(delivery.get("step_ref"), context)
else:
g.m365_selected_agent_ref = deepcopy(delivery.get("agent_ref") or {})
with m365_execution_context(context):
if context.workflow_id and not validate_m365_workflow_execution(context):
raise M365PolicyError("m365_workflow_changed", "The workflow or destination changed after this delivery was prepared.")
Expand Down Expand Up @@ -913,6 +961,7 @@
container=cosmos_msgraph_pending_actions_container,
context_scope=delivery_context, log_event=log_event, notification_sender=notify_delivery,
capture_agent_reference=capture_agent_reference, view_authorizer=authorize_view,
capture_step_reference=capture_step_reference,
conversation_authorizer=authorize_conversation,
)

Expand Down Expand Up @@ -948,7 +997,7 @@
return manifests
step = getattr(g, M365_STEP_SELECTION_KEY, None) if has_request_context() else None
if step is not None:
return _readable_m365_manifests(step_selected_m365_manifests(step, context.actor_user_id))
return _step_m365_manifests(step, context.actor_user_id)
selection = getattr(g, "m365_selected_agent_ref", None) if has_request_context() else None
if not selection:
return []
Expand Down
Loading
Loading