Skip to content
Closed
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
13 changes: 8 additions & 5 deletions docs/specs/004-python-function-calling-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,12 @@ Before acting on a model function-call batch, the loop classifies every actionab
call aborts the complete batch before approval state changes or execution. Otherwise approval-required and Host-owned
calls are returned together in model order, while session-backed executable siblings remain deferred. An incomplete
session-backed mixed approval/Host response remains pending without executing a deferred call; a stateless incomplete
response is rejected. Correlation is scoped to the active mixed batch so completed or abandoned historical Host calls
remain unchanged. `ToolApprovalMiddleware` may resolve approval requests through standing or automatic policies, but
it preserves non-approval user-input requests and does not split manual approvals away from their Host-owned siblings.
response is rejected, including when no response is supplied, message roles vary within the model output, or a newer
standalone request follows the incomplete batch. Stateless discovery selects the latest unresolved mixed batch, so a
completed batch remains inert on later turns. Correlation is scoped to the active mixed batch so completed or abandoned
historical Host calls remain unchanged.
`ToolApprovalMiddleware` may resolve approval requests through standing or automatic policies, but it preserves
non-approval user-input requests and does not split or reorder manual approvals relative to their Host-owned siblings.

### Reasoning-bound function-call groups

Expand Down Expand Up @@ -612,15 +615,15 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Fatal call mixed with pauses | Complete-batch classification raises before approval or execution, independent of call order. | `packages/core/tests/core/test_function_invocation_logic.py::test_mixed_batch_fatal_unknown_precedes_every_pause` |
| Approval and Host-owned calls | Both pause types are returned in model order; a session-backed partial response remains pending across serialization; historical Host calls do not participate; a complete response executes the exact approved arguments once. | `test_mixed_batch_returns_approval_and_host_pause_in_model_order`, `test_mixed_batch_requires_complete_responses_before_execution`, `test_active_mixed_pause_ignores_historical_host_requests` |
| Approval and Host-owned calls | Both pause types are returned in model order; a session-backed partial response remains pending across serialization; a stateless zero-response or partial response fails closed across message roles and cannot be hidden by a newer standalone request; completed mixed batches remain inert; historical Host calls do not participate; a complete response executes the exact approved arguments once. | `test_mixed_batch_returns_approval_and_host_pause_in_model_order`, `test_mixed_batch_requires_complete_responses_before_execution`, `test_stateless_split_mixed_batch_rejects_incomplete_replay_before_execution`, `test_stateless_mixed_batch_across_message_roles_requires_complete_responses`, `test_later_standalone_request_does_not_hide_incomplete_stateless_mixed_batch`, `test_completed_split_stateless_mixed_batch_is_inert_on_later_turn`, `test_active_mixed_pause_ignores_historical_host_requests` |
| Safe and approval-required calls in one batch | Hidden safe calls replay only with the matching visible approval. | `packages/core/tests/core/test_harness_tool_approval.py::test_mixed_batch_hides_already_approved_request_until_approval_replay` |
| Restored approval state | Serialized `ToolApprovalState` restores mixed-batch behavior. | `test_mixed_batch_accepts_restored_tool_approval_state` |
| Unrelated turn before approval | Hidden calls do not execute on an unrelated turn. | `test_hidden_mixed_batch_requests_do_not_replay_on_unrelated_turn` |
| Multiple abandoned batches | Hidden calls replay only for the matching batch. | `test_hidden_mixed_batch_requests_replay_only_for_matching_visible_approval` |
| Queued approvals | One unresolved approval is surfaced per run without premature execution. | `test_tool_approval_middleware_queues_multiple_approval_requests`, `test_tool_approval_middleware_queues_streamed_approval_requests` |
| Middleware state plus hidden core state | State saves do not discard hidden mixed-batch calls. | `test_tool_approval_middleware_preserves_hidden_mixed_batch_requests` |
| Auto-approval callback | Callback receives the original function call and executes the approved set once. | `test_tool_approval_middleware_auto_approval_rule_receives_function_call` |
| Approval policy with Host-owned sibling | Reordering the same calls has the same outcome; auto-approved and safe calls remain deferred until Host input arrives; manual approvals stay in the mixed batch; an approved Host tool returns to Host-owned handling before local execution. | `test_tool_approval_middleware_mixed_batch_is_order_independent`, `test_tool_approval_middleware_auto_approves_with_host_pause_and_cached_safe_call`, `test_tool_approval_middleware_keeps_manual_approvals_together_with_host_pause`, `test_tool_approval_middleware_policy_approval_reclassifies_host_tool` |
| Approval policy with Host-owned sibling | Reordering the same calls has the same outcome; auto-approved and safe calls remain deferred until Host input arrives; separately streamed Host input stays immediately visible while later manual approvals remain in model order and in the same batch; an approved Host tool returns to Host-owned handling before local execution. | `test_tool_approval_middleware_mixed_batch_is_order_independent`, `test_tool_approval_middleware_auto_approves_with_host_pause_and_cached_safe_call`, `test_tool_approval_middleware_keeps_manual_approvals_together_with_host_pause`, `test_tool_approval_middleware_preserves_split_streamed_mixed_batch_order`, `test_tool_approval_middleware_streams_standalone_user_input_immediately`, `test_tool_approval_middleware_policy_approval_reclassifies_host_tool` |
| Shared call budget | Auto-approved re-entry does not reset `max_function_calls`, and every executed approval group counts even when it pauses for input. | `test_tool_approval_middleware_auto_approved_loops_share_function_call_budget`, `test_approval_resume_user_input_counts_toward_function_call_budget` |
| Standing tool rule | Tool-level approval applies only to later matching tools. | `test_tool_approval_middleware_always_approve_tool_rule` |
| Forged standing rule | An unbound or substituted hosted response cannot create a standing middleware approval rule for caller-selected metadata. | `test_tool_approval_middleware_drops_forged_standing_approval`, `test_tool_approval_middleware_rebinds_hosted_standing_approval` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ internal sealed class ContentToProcess
/// <summary>
/// Creates a new instance of ContentToProcess.
/// </summary>
/// <param name="contentEntries">The content to send and its associated ids.</param>
/// <param name="contentEntry">The content to send and its associated id.</param>
/// <param name="activityMetadata">Metadata about the activity performed with the content.</param>
/// <param name="deviceMetadata">Metadata about the device that produced the content.</param>
/// <param name="integratedAppMetadata">Metadata about the application integrating with Purview.</param>
/// <param name="protectedAppMetadata">Metadata about the application being protected by Purview.</param>
public ContentToProcess(
List<ProcessContentMetadataBase> contentEntries,
ProcessContentMetadataBase contentEntry,
ActivityMetadata activityMetadata,
DeviceMetadata deviceMetadata,
IntegratedAppMetadata integratedAppMetadata,
ProtectedAppMetadata protectedAppMetadata)
{
this.ContentEntries = contentEntries;
this.ContentEntries = [contentEntry];
this.ActivityMetadata = activityMetadata;
this.DeviceMetadata = deviceMetadata;
this.IntegratedAppMetadata = integratedAppMetadata;
Expand Down
36 changes: 36 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.Purview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,39 @@ catch (PurviewException e)
this._logger.LogError(e, "Purview middleware threw an exception.")
}
```

## Security Considerations

### Identity is a trusted input

Purview evaluates DLP policy **for a specific user**. The identity this integration resolves therefore
decides *which* policy is applied, and it is resolved in this order:

1. The user id from the configured `TokenCredential`'s token, when the credential resolves to a user.
2. The `userId` argument passed to the processor.
3. `ChatMessage.AdditionalProperties["userId"]`.
4. `ChatMessage.AuthorName`, when it is a GUID.

Only source 1 is verified. Sources 2-4 are supplied by the hosting application, so **a host must not
populate them from data that has crossed a trust boundary**. If an end user, an upstream service or a
model response can influence `AdditionalProperties["userId"]` or `AuthorName`, that party can select a
different user's DLP policy - typically one with weaker rules - and evade enforcement. Where identity
must come from a request, derive it from a validated token on the server, never from the request body.
Prefer a user-delegated credential (source 1) whenever possible.

`PurviewAppLocation` in `PurviewSettings` is trusted in the same way: it selects which policy locations
apply and must be configured by the host, not by the caller.

### Fail-closed behaviour

Policy evaluation fails closed. If no user id can be resolved, or the tenant or app location cannot be
determined, `PurviewRequestException` is thrown rather than letting content through unevaluated. Use
`IgnoreExceptions` if you deliberately want availability over enforcement - but understand that it
disables enforcement for every error, not just transient ones.

### What is evaluated

Every content item on a message is submitted for evaluation, not just its text: `DataContent` is sent
as Purview binary content, and `FunctionCallContent`, `FunctionResultContent` and other structured
content are serialized to text. Only `UsageContent` is skipped, because it carries token counts rather
than user data.
Loading
Loading