Skip to content

Python: add AsyncRequestBroker and shell command audit log - #9166

Open
Mark Licata (marklicata) wants to merge 6 commits into
microsoft:mainfrom
marklicata:agent-async-request-broker-python
Open

Mark Licata (marklicata) wants to merge 6 commits into
microsoft:mainfrom
marklicata:agent-async-request-broker-python

Conversation

@marklicata

@marklicata Mark Licata (marklicata) commented Oct 7, 2026 •

Copy link
Copy Markdown

Motivation & Context

Two foundations for safe autonomous agent infrastructure:

  1. AsyncRequestBroker: a generic asyncio pause-and-wait primitive that server-side code can use to suspend on a request ID and await a client response, without polling. Tool approval policies and similar features build on it.
  2. ShellAuditLog: a compliance-grade record of every shell command evaluation (command text, policy decision, denial reason, and post-execution exit code). Enterprise deployments need this for governance and forensics.

This is the Python half of the work previously proposed in #8995, split per language at the reviewers' request. The .NET counterpart (AsyncRequestBroker<TResult>) is #9167.

Description & Review Guide

What are the major changes?

  • AsyncRequestBroker[T] (agent_framework._harness._request_broker): request_async(request_id, timeout?), resolve(request_id, result), reject(request_id, exc). A late-resolve race is handled via a bounded settled-ID deque (256 entries). Marked @experimental.
  • ShellAuditLog / ShellCommandRecord: new module agent_framework_tools/shell/_audit.py
  • LocalShellTool.audit_log: records the policy decision on every run(), then updates exit_code after execution

What is the impact of these changes?

Additive. Wiring the broker into the tool approval pipeline (fail-closed always_require execution) is intentionally deferred to the approval-policy work.

What do you want reviewers to focus on?

  • Broker race handling: a late resolve() returns True (already settled); a second request_async() for the same ID raises ValueError
  • Audit log: exit_code is None for denied commands and when the executor raises

Related Issue

Part of #8861, together with #9167 for .NET. The issue is closed manually once both PRs have merged.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Confirmed broker lifecycle and audit-history integrity issues remain unresolved.

3 open findings
What changed in this PR

Adds Python foundations for asynchronous client responses and shell-command auditing. Approval-pipeline integration remains deferred.

Changes:

  • Introduces and exports experimental AsyncRequestBroker.
  • Records shell policy decisions and execution exit codes.
  • Adds broker and shell-audit tests.
File Description
python/​packages/​tools/​tests/​test_local_shell_tool.py Tests command audit records.
python/​packages/​tools/​agent_framework_tools/​shell/​_tool.py Records policy decisions and execution outcomes.
python/​packages/​tools/​agent_framework_tools/​shell/​_audit.py Defines audit records and their in-memory log.
python/​packages/​tools/​agent_framework_tools/​shell/​__init__.py Exports audit types.
python/​packages/​core/​tests/​core/​test_request_broker.py Tests broker responses, timeouts, and duplicate IDs.
python/​packages/​core/​agent_framework/​_harness/​_request_broker.py Implements asynchronous request coordination.
python/​packages/​core/​agent_framework/​__init__.pyi Adds typed broker exports.
python/​packages/​core/​agent_framework/​__init__.py Adds runtime broker exports.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

…snapshots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark Licata (@marklicata) The language split addresses the scope concern from #8995, but the user-facing use case for AsyncRequestBroker is still not clear from this PR. Can we add a small sample showing a server request being awaited and a client resolving or rejecting it?

…l audit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marklicata

Copy link
Copy Markdown
Author

Eduard van Valkenburg (@eavanvalkenburg) Added a sample in e5cd62d: python/samples/02-agents/harness/async_request_broker.py. It runs without an LLM: server-side code awaits broker.request(...) for approval, the simulated client resolves one request and rejects another (the server handles the rejection), and a third request times out with no answer.

@agent-framework-automation agent-framework-automation Bot added the documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs label Oct 13, 2026
…roker-python

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark Licata (@marklicata) Looking only at the enhanced shell-tool audit logging: python/packages/tools/agent_framework_tools/shell/_tool.py:263 records the configured LocalShellTool._workdir, not the directory where the command actually executes. In persistent mode the shell can cd and retain a different cwd across calls, while audit records keep reporting the constructor workdir; with workdir=None, execution inherits a concrete process cwd but the record stores None. That makes the compliance/forensics trail inaccurate for identical relative commands.

Please record the effective execution cwd. For stateless execution, persist the concrete cwd used (os.getcwd() when omitted). For persistent execution, track/capture the shell cwd at the command boundary, or avoid claiming a concrete cwd when it can drift. Add coverage for a persistent session that changes directory before an audited command.

@eavanvalkenburg

Copy link
Copy Markdown
Member

Mark Licata (@marklicata) I would like to split the request-handling example out of this PR and develop it as a standalone sample, rather than adding AsyncRequestBroker to core or exporting it from agent_framework.

This follows the discussion in #8861: #8861 (comment). MAF deliberately returns control to the application for approvals and other human input, then accepts the response on a later run with the session/checkpoint. The application owns the transport and how long it waits; it should remain possible to shut the process down between those steps. The existing local-tool approval path is already fail-closed, as clarified in #8861 (comment).

Please make the separate sample show more extensive application-owned request handling around that existing contract: receive a pending request, deliver it to a simulated client, correlate the reply, and resume the agent with the same session. Include approval/rejection and timeout/cancellation or late-response handling so readers can see where those policies belong. Any in-memory broker used to illustrate the transport should stay local to the sample, with its process-lifetime limitation made explicit. It should not replace the framework pause-and-resume contract or require a new public broker API.

That gives developers a useful reference without committing the framework to one request-handling mechanism.

@eavanvalkenburg

Copy link
Copy Markdown
Member

Mark Licata (@marklicata) Separately, I am not yet convinced that the shell audit feature needs its own public logging abstraction rather than standard Python logging.

As implemented, ShellAuditLog is an unbounded in-memory list owned by a tool instance. The main extra capability is inspecting that list programmatically. Python logging already supports timestamps, exception information, structured fields through extra, and application-configured handlers for destinations and retention. What requirement do ShellAuditLog and ShellCommandRecord satisfy that structured logging cannot reasonably cover?

Neither an in-memory list nor Python logging alone provides a compliance audit trail. The current implementation has no session correlation or persistence, does not capture human approval decisions, and leaves failed or cancelled executions with exit_code=None, indistinguishable from an attempt that is still running. It also drops the executor timeout flag. My earlier comment about recording the effective working directory rather than the configured one still applies: #9166 (review).

My preference for a first version is structured events through standard Python logging for policy decisions and execution outcomes, with accurate working-directory, exit-code, timeout, and failure information. The application can configure collection, storage, and retention. Raw command text should be opt-in or redacted because it can contain secrets. Exceptions and cancellation must still propagate normally, and approval auditing belongs at the layer that owns approvals rather than being inferred by the shell tool.

If there is a concrete need for a supported, programmatically queryable execution history beyond logging, please describe that use case before we add these public APIs. Otherwise, I would avoid the new recorder types and the compliance claims for this version.

…th Python logging

- Remove AsyncRequestBroker from agent_framework public API and core harness;
  any in-process broker belongs in the application, not the framework
- Remove ShellAuditLog and ShellCommandRecord public types; shell tool now emits
  structured events through logging.getLogger('agent_framework.shell.audit') with
  shell.policy and shell.execution records
- Fix effective workdir: record os.getcwd() for stateless mode when no workdir
  configured, instead of None
- Fix outcome tracking: try/finally ensures exit_code and outcome are always logged
  even on exception or cancellation
- Rewrite async_request_broker.py sample to show MAF pause-and-resume approval
  contract with a locally-defined transport, not a framework import
- Addresses eavanvalkenburg feedback on microsoft#8861

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

This branch was previously deployed

1 inactive deployment
github-app-auth — 36a9e565 Deployed Nov 3, 2026 by marklicata via add_label #27015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants