Repository navigation
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 inagent-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
ShellPolicyitself — 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.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
[-]Add AsyncRequestBroker utility and shell execution audit log[/-][+]Python: Add AsyncRequestBroker utility and shell execution audit log[/+][-]Python: Add AsyncRequestBroker utility and shell execution audit log[/-][+]Add AsyncRequestBroker utility and shell execution audit log[/+]GitSerge-crypto commentedon Oct 13, 2026
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:
stdout/stderr hash, timestamps, and the identity that authorized the run
— verification is a linear replay that detects any retroactive edit or
deletion
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_hashin the schema turns "an audit record exists"into "this audit record is provably unmodified", which is usually what the
compliance reviewer actually asks for.
eavanvalkenburg commentedon Oct 14, 2026
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
howyou 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: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.pywhere there is nothing specific to MAF happening!marklicata commentedon Oct 14, 2026
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.
ToolApprovalMiddlewaremanagesqueued_approval_requestsandcollected_approval_responsesin 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 inToolApprovalMiddleware, 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.
eavanvalkenburg commentedon Oct 15, 2026
Mark Licata (@marklicata) So, indeed, the question for a broker is
wherenotwhether, 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. Analways_requirecall is paused and surfaced as afunction_approval_request; execution resumes only when the response matches a framework-recorded pending request in the same authoritativeAgentSession. 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.
ToolApprovalMiddlewarecomplements it by managing queued prompts, collected responses, and standing approval rules.Audit log, I haven't looked closely at, will do that today.