Skip to content

Add AsyncRequestBroker utility and shell execution audit log #8861

Description

Summary

Two related additions: (1) a small reusable async primitive for server-initiated pause-and-wait patterns, and (2) an audit log and fail-closed extension for shell tool execution. The broker is the transport layer that the audit/fail-closed behavior depends on.

We intend to take on implementation.

User need

When an autonomous agent executes shell commands, users in enterprise environments need two guarantees: an audit record of what ran, when, and what the outcome was; and a guarantee that if no approval UI is connected, commands do not execute silently. Today, ShellPolicy handles allowlist/denylist filtering, but (as documented in its own module docstring) it is explicitly not a security boundary — the actual security is approval_mode="always_require" plus Docker/Hyperlight sandboxing. The audit trail and fail-closed guarantee are not implemented.

Developer need

Any server-side pattern that needs to ask a question and wait for an asynchronous client response currently requires building a custom async primitive from scratch. There is no shared utility. Every team that needs pause-and-resume reinvents it independently, with different error handling, different timeout behavior, and different race-condition handling.

What's already there

  • Python: ShellPolicy (python/packages/tools/agent_framework_tools/shell/_policy.py) — layered denylist/allowlist with interruptible pattern matching. The module docstring explicitly states this is a UX pre-filter, not a security boundary. Shell tools exist in agent-framework-tools.
  • .NET: Shell tools in dotnet/src/Microsoft.Agents.AI.Tools.Shell/.
  • Both: approval_mode="always_require" is the stated default. The approval gate is declared but its async transport layer is not implemented. No audit log exists in either implementation.

Proposal

1. AsyncRequestBroker — a standalone async utility for server-initiated pause-and-wait:

Server side: emit a typed request, suspend until the client responds
Client side: POST the decision; broker resolves the suspended caller
Race conditions: handled via a bounded deque of settled request IDs

This primitive is used by both the shell governance extension below and by tool approval policy work (separate issue, if pursued).

2. Shell execution audit log — a structured record added to session state for every shell command that passes through the execution path:

{command, workdir, policy_decision, approval_decision, exit_code, timestamp}

policy_decision captures the ShellPolicy outcome. approval_decision captures the human response. This record is available in session state and can be retrieved after the session for compliance purposes.

3. Fail-closed when no transport is connected — when approval_mode="always_require" and no approval transport is reachable, execution is denied rather than proceeding. Makes the approval requirement enforceable, not aspirational.

Scope

  • Both Python and .NET implementations are in scope. The Python shell tools (python/packages/tools/agent_framework_tools/shell/) and .NET shell tools (dotnet/src/Microsoft.Agents.AI.Tools.Shell/) maintain feature parity; changes to one require equivalent changes in the other.
  • AsyncRequestBroker: new utility in core (Python and .NET)
  • Audit log and fail-closed: extension to the shell tool execution path in both languages
  • No changes to ShellPolicy itself — it is well-designed for its stated purpose

Notes

This does not change the security model for shell execution — DockerShellTool and HyperlightCodeActProvider remain the correct answer for untrusted model-generated commands at the OS level. The audit log and fail-closed behavior add the compliance and enforcement layers that sit above sandboxing.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 29, 2026
  2. changed the title [-]Add AsyncRequestBroker utility and shell execution audit log[/-] [+]Python: Add AsyncRequestBroker utility and shell execution audit log[/+] on Sep 29, 2026
  3. changed the title [-]Python: Add AsyncRequestBroker utility and shell execution audit log[/-] [+]Add AsyncRequestBroker utility and shell execution audit log[/+] on Sep 29, 2026
  4. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 29, 2026
  5. GitSerge-crypto commented on Oct 13, 2026

    @GitSerge-crypto

    For the audit-log half of this: durability alone doesn't satisfy the
    enterprise case — a shell audit log that anyone with write access to the
    store can edit after the fact has no audit value once tampered. It would be
    worth making the records tamper-evident from day one rather than bolting
    integrity on later:

    • each shell execution appends one record: argv hash, cwd, exit code,
      stdout/stderr hash, timestamps, and the identity that authorized the run
    • every record also stores the hash of the previous record, forming a chain
      — verification is a linear replay that detects any retroactive edit or
      deletion
    • the chain lives in the same pluggable sink as the records, so the
      guarantee doesn't depend on external infrastructure

    We run a hash-chained provenance ledger this way for agent tool executions
    (Provenance Data Records, sha256-linked) and the overhead is negligible —
    one extra hash per record. If the audit log here lands as designed, carrying
    record_hash + prev_hash in the schema turns "an audit record exists"
    into "this audit record is provably unmodified", which is usually what the
    compliance reviewer actually asks for.

  6. eavanvalkenburg commented on Oct 14, 2026

    @eavanvalkenburg
    Member

    Mark Licata (@marklicata) I'm not quite sure on the AsyncRequestBroker setup, not that I do not see the value, but whether or not it belongs inside MAF.

    We deliberately made the choice that things like approvals and other HITL from workflows end the loop and return runtime control to the calling application in order to allow anyone to implement the how you ask for that human response completely open, because the range of options are just to large to make assumptions (from input from the command line in a script all the way to leveraging things like logic apps and approval systems that might take days to resolve). And then we have the setup where you supply the approval/response back to the agent/workflow through the run command with the session/checkpoint. So this has two consequences, 1) you can shut down your entire application and 2) this gives the developer freedom to implement it in a way that fits, you called it out:

    Any server-side pattern that needs to ask a question and wait for an asynchronous client response currently requires building a custom async primitive from scratch. There is no shared utility. Every team that needs pause-and-resume reinvents it independently, with different error handling, different timeout behavior, and different race-condition handling.

    but that is actually the design, because while the requirement is there, the number of possible options is far too vast to capture in a single abstraction in my view, and even if that is not the case, the question remains: does that need to be in MAF itself? Or would a general purpose vehicle that you can customize regardless of the type of content that goes through and back actually make more sense? And this actually shows in the sample you added in python/samples/02-agents/harness/async_request_broker.py where there is nothing specific to MAF happening!

  7. marklicata commented on Oct 14, 2026

    @marklicata
    Author

    Thanks for the detailed pushback, Eduard van Valkenburg (@eavanvalkenburg). I think a couple things got conflated, let me know if what I have below makes sense to you (or not).

    The broker primitive itself
    You're right that the sample has no MAF imports, so perhaps it belongs in a 'shared' or 'utils' directory? There's a pattern for this sort of cross-cutting utility on the .NET side (dotnet/src/Shared). But not on the Python side, which is more a gap than teams starting from zero. "Not in core" and "doesn't need to exist" are different conclusions. The sample can stay a reference implementation for now, but I would argue the broker being framework-agnostic is an argument for where, not whether.

    The fail-closed enforcement
    This one I don't think can be punted to a utility library, because MAF already owns the approval lifecycle in core. ToolApprovalMiddleware manages queued_approval_requests and collected_approval_responses in session state.
    When approval_mode="always_require" is set and no transport is connected, the execution silently proceeds. That's MAF's own declared contract with no enforcement arm. I would argue the fail-closed guarantee belongs in ToolApprovalMiddleware, not in a companion library, because MAF already took on the approval state management.

    The audit log
    Falls in between. It's schema-aware of MAF concepts (policy_decision, approval_decision) so it belongs wherever the shell tools live.

    So overall: agreed the broker can/should live outside core. IMO the fail-closed and audit log are MAF's responsibility regardless of where the broker lands.

    Thoughts on this thinking?

    It's actually a good design question about the bar for something landing in MAF core vs. a companion layer.

  8. eavanvalkenburg commented on Oct 15, 2026

    @eavanvalkenburg
    Member

    Mark Licata (@marklicata) So, indeed, the question for a broker is where not whether, and I'll think about and discuss with the team on what would be the best place for this in Python.

    WRT, fail-closed: I rechecked current main, and MAF is already fail-closed for local tool approvals. An always_require call is paused and surfaced as a function_approval_request; execution resumes only when the response matches a framework-recorded pending request in the same authoritative AgentSession. Unmatched or sessionless approval responses are ignored by default.

    This area was updated recently in #8750, which added the default binding of approval responses to framework-issued requests and tightened replayed-history handling.

    The enforcement belongs to MAF core and currently lives primarily in the function-invocation layer. ToolApprovalMiddleware complements it by managing queued prompts, collected responses, and standing approval rules.

    Audit log, I haven't looked closely at, will do that today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

pythonUsage: [Issues, PRs], Target: Python

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions