Commit Graph

9 Commits

  • Python: refactor FoundryHostedAgentHistoryProvider onto Foundry SDK (#5637)
    * refactor(foundry_hosting): build FoundryHostedAgentHistoryProvider on azure.ai.agentserver SDK
    
    Rebuilds the Foundry hosted-agent history provider on top of
    ``azure.ai.agentserver``'s ``FoundryStorageProvider`` instead of the
    in-house ``_HttpStorageBackend``. Splits the monolithic ``_responses.py``
    into focused modules:
    
    - ``_history_provider.py`` — new ``FoundryHostedAgentHistoryProvider``
      that talks to the SDK's ``FoundryStorageProvider``, threads
      ``response_id`` / ``previous_response_id`` through ``ContextVar``s via
      ``bind_request_context``, and lifts host-bound isolation keys
      (``x-agent-{user,chat}-isolation-key``) from the optional
      ``agent_framework_hosting`` package into a provider-local
      ``IsolationContext`` so the storage layer carries the correct
      partition keys without channels having to know about them.
    - ``_shared.py`` — extracts all SDK ``Item`` / ``OutputItem`` ↔
      framework ``Message`` conversion helpers into one place so both
      ``_responses.py`` and the new history provider can share them.
      Restores ``_convert_file_data`` for inline ``input_file`` payloads,
      and the hosted-MCP routing for ``custom_tool_call_output`` items
      whose ``call_id`` carries the ``mcp_*`` prefix.
    - ``_ids.py`` — shared id helpers.
    - ``_responses.py`` — shrinks ~700 lines, re-exports converters for
      back-compat with existing tests.
    - ``tests/test_history_provider.py`` — exercises the new provider
      against a fake SDK backend; the host-isolation test is gated on the
      optional ``agent_framework_hosting`` import.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * feat(foundry_hosting): add local_storage_root for file-based dev history
    
    Adds an optional `local_storage_root: str | Path | None` parameter to
    `FoundryHostedAgentHistoryProvider`. When set and the provider is
    running outside a Foundry Hosted Agent container, conversations are
    persisted to JSONL files via `agent_framework.FileHistoryProvider`
    laid out as:
    
      {root}/{user_key or '~none'}/{chat_key or '~none'}/{session_id}.jsonl
    
    Hosted mode (FOUNDRY_HOSTING_ENVIRONMENT set) ignores the option with a
    one-time INFO log so Foundry storage always wins on the platform. The
    in-memory fallback is unchanged when the option is omitted.
    
    Path safety: isolation segments are validated against the same character
    allowlist FileHistoryProvider uses for session-id stems and
    base64-url-encoded with a reserved "~iso-" prefix when unsafe. "~none"
    sentinel for missing keys can never collide with a real isolation key
    (real keys starting with "~" are encoded). The resolved target dir is
    also re-checked to be inside the configured root.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * fix(foundry_hosting): address PR-1 review comments
    
    - _shared.py:_capture_raw narrows `except Exception` to `except TypeError`
      and emits a WARNING with traceback so the lossy fallback to a
      synthesized round-trip is observable. Mirrors the reviewer suggestion.
    
    - _history_provider.py:save_messages narrows `except Exception` to
      `except FoundryStorageError` so only storage-validation failures
      (4xx/5xx, opaque server errors) are swallowed. Network / TLS / auth
      / payload-builder bugs propagate so the caller can retry / alert.
      Adds an instance-level `failed_writes` counter operators can poll
      for silent-drop visibility.
    
    - _history_provider.py id-stamping loop: drops the
      `contextlib.suppress(AttributeError, TypeError)` around
      `item.id = new_id` so SDK contract changes surface in the test
      suite instead of silently corrupting the chain (the storage backend
      rejects the entire `create_response` with HTTP 500 when synthetic
      prefix-based ids leak through). `import contextlib` removed.
    
    - tests:
      * Unit-cover `foundry_response_id` / `foundry_response_id_factory` /
        `foundry_item_id` so SDK `IdGenerator` contract changes are caught
        locally.
      * Cover the `save_messages` wire payload: required-by-storage fields
        (`background`, `parallel_tool_calls`, `instructions`,
        `agent_reference`), env-var-driven stamping (`FOUNDRY_AGENT_NAME` /
        `FOUNDRY_AGENT_VERSION` / `FOUNDRY_AGENT_SESSION_ID` /
        `MODEL_DEPLOYMENT_NAME` with `AZURE_AI_MODEL_DEPLOYMENT_NAME`
        fallback), and the rule that `model` / `agent_session_id` /
        `agent_reference.version` are omitted (not stamped to `None`) when
        their env vars are unset.
      * Cover the `FOUNDRY_AGENT_SESSION_ID` last-resort chain anchor on
        both the get and save paths, including the prefix gate that blocks
        non-`caresp_*`/`resp_*` values from reaching storage, and the
        precedence rule that a host binding wins over the env.
      * Replace the old `test_save_messages_swallows_backend_errors` with
        two tests asserting the new contract: storage errors are swallowed
        and bump `failed_writes`; everything else propagates and leaves the
        counter at zero.
    
    141 unit tests pass; mypy + pyright + ruff clean.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * fix(foundry_hosting): address PR-1 round-2 review comments
    
    - Hosted detection now delegates to AgentConfig.from_env().is_hosted so
      a future Foundry SDK rename of FOUNDRY_HOSTING_ENVIRONMENT propagates
      automatically; drop the local _ENV_FOUNDRY_HOSTING_ENVIRONMENT
      constant.
    - Drop the FOUNDRY_AGENT_SESSION_ID fallback in both get_messages and
      save_messages: per the SDK it identifies the *container instance*,
      not the conversation, so chaining off it would silently merge
      unrelated conversations across container restarts. The host-bound
      previous_response_id (set by ResponsesChannel) is the only
      authoritative anchor; the env value is still stamped into the
      persisted envelope's agent_session_id for operator correlation.
    - Update module docstring + replace TestFoundryAgentSessionIdAnchor
      with assertions for the new contract (env var ignored as anchor,
      still stamped onto persisted envelope, host binding wins).
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * refactor(foundry_hosting): reconcile with upstream main (#5851, #5666)
    
    Brings the FoundryHostedAgentHistoryProvider refactor branch back into
    sync with the foundry_hosting changes that have landed on upstream
    main since PR-1 was opened:
    
    * #5851 (path traversal in checkpoint storage, CWE-22).
      The workflow-host code in ``_responses.py`` builds a
      ``FileCheckpointStorage`` from a caller-controlled ``context_id``
      (``previous_response_id`` / ``conversation_id`` / ``response_id``).
      Switch both call sites to route through
      ``_checkpoint_storage_for_context``, which rejects separators,
      NUL bytes, drive letters, absolute paths, and all-dot segments,
      and enforces ``is_relative_to(root)`` before any directory is
      created.
    
    * #5666 (function approval flow).
      Make the SDK-Item → AF-Message conversion helpers in ``_shared.py``
      async and accept an optional ``approval_storage`` keyword:
    
      - ``_items_to_messages`` / ``_item_to_message`` /
        ``_item_to_message_inner``
      - ``_output_items_to_messages`` / ``_output_item_to_message`` /
        ``_output_item_to_message_inner``
    
      For ``mcp_approval_request`` / ``mcp_approval_response`` items the
      helpers now load the original function-call Content from the
      approval storage (via ``ApprovalStorage.load_approval_request``)
      instead of synthesising a placeholder. This matches upstream
      semantics and lets approval round-trips reconstruct the real
      payload.
    
      The ``ApprovalStorage`` Protocol moves to ``_shared.py`` so the
      conversion helpers can reference it without pulling in
      ``_responses.py`` (which would create a circular import). The
      concrete ``InMemoryFunctionApprovalStorage`` and
      ``FileBasedFunctionApprovalStorage`` stay in ``_responses.py``
      next to the host that owns them, and re-export
      ``ApprovalStorage`` from ``_shared`` for compatibility.
    
      The workflow-host streaming path passes its own
      ``self._approval_storage`` into ``_to_outputs`` so approval
      requests are saved at emit time.
    
    * Bump ``_history_provider.FoundryHostedAgentHistoryProvider.get_messages``
      to ``await`` the now-async ``_output_items_to_messages`` call.
    
    No public API change beyond the new keyword-only ``approval_storage``
    parameter on the four conversion entry points.
    
    Validation:
    - uv run poe check-packages -P foundry_hosting (lint + pyright clean)
    - uv run poe mypy -P foundry_hosting (clean)
    - uv run poe test -P foundry_hosting (183 passed, 1 skipped)
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Python: Show more authentication methods in Foundry Toolbox MCP (#5719)
    * Show more authentication methods in Foundry Toolbox MCP
    
    * Remove hardcoded toolbox version num
    
    * Add Foundry MCP OAuth consent handling
    
    * Use message instead of the dedicated item type
    
    * Go back to using OAuthConsentRequestOutputItem
    
    * WIP: sample testing
    
    * Update error code
    
    * Address review on Foundry Toolbox MCP samples
    
    Reviewed feedback addressed:
    
    - Drop the branch-pinned `git+https://...@feature/...` entries from
      `04_foundry_toolbox/requirements.txt`; restore the simple comment + `mcp`
      runtime dep. The git pins were only useful while iterating on the PR and
      shouldn't ship. (eavanvalkenburg)
    
    - Fix the `/toolsets/` typo in both `04_foundry_toolbox/README.md` and
      `06_files/README.md`. Verified empirically against the
      research_toolbox in the test workspace: the toolbox MCP gateway lives at
      `/toolboxes/{name}/mcp?api-version=v1` and requires the
      `Foundry-Features: Toolboxes=V1Preview` header. `/toolsets/{name}/mcp`
      returns 403 with `preview_feature_required: Toolsets=V1Preview` (a
      different opt-in feature).
    
    - Wrap `httpx.AsyncClient(...)` in `async with ... as http_client:` in both
      samples so the connection pool is cleaned up. (Copilot reviewer)
    
    - Make the `TOOLBOX_NAME` env var consistent in both samples. Previously the
      tool name silently fell back to `"toolbox"` when `TOOLBOX_NAME` was unset,
      but `resolve_toolbox_endpoint()` still required `TOOLBOX_NAME` and would
      raise `KeyError`. The samples now resolve the endpoint once and derive the
      tool name from the resolved URL when `TOOLBOX_NAME` isn't set, so the
      local tool name always matches the upstream toolbox identity regardless
      of which env var the user set. (Copilot reviewer)
    
    - Rename `_responses.is_consent_error` to `consent_url_from_error`: the
      helper returns `str | None` (the consent URL), not a bool, so the new
      name matches behavior. Update the test class accordingly. (eavanvalkenburg)
    
    - Tighten `_handle_inner_agent`'s lazy-entry catch from `Exception` to
      `AgentFrameworkException`, the type the MCP layer actually wraps consent
      errors in via `MCPStreamableHTTPTool.__aenter__` →
      `ToolExecutionException(inner_exception=mcp_error)`. Network failures,
      cancellations, and other non-framework exceptions now propagate normally
      instead of being briefly caught and re-raised. The test helper
      `_make_consent_error` is updated to use `ToolExecutionException` so it
      matches the real-world wrapping. (eavanvalkenburg)
    
    - Clarify the `github_pat` description in `agent.manifest.yaml` to note
      it's only needed when the PAT-based connection (`github-mcp-pat-conn`)
      is chosen; users selecting the OAuth2 connection (`github-mcp-oauth-conn`)
      can leave it empty. (Copilot reviewer)
    
    Validation: ran both samples end-to-end against a real Foundry toolbox
    (`research_toolbox`) -- the samples connect successfully and the agent
    lists the toolbox's MCP tools (`api_specs___fetch_azure_rest_api_docs`,
    etc.). `uv run poe test -P foundry_hosting` passes (119 tests), pyright +
    mypy clean.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * docs: fix broken Foundry samples link in 04_foundry_toolbox README
    
    The previous URL pointed to an old location of the toolbox supported-scenarios
    doc; the doc moved to /samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md
    and the old /samples/python/toolbox/azd path now 404s.
    
    Caught by the markdown-link-check CI step.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Python: Reject path-traversal context ids in Foundry Hosting Checkpoint Storage (#5851)
    * Reject path-traversal context ids in foundry workflow checkpoint storage
    
    Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/fca3aae6-50eb-4726-8baf-2718217d4e79
    
    Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
    
    * Address PR review feedback: clarify URL-decode comment, isolate test root, add e2e workflow rejection tests
    
    Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/832f45a6-c01e-4da9-bf85-1ba7b5f302e6
    
    Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
    
    * Clarify MSRC repro padding length in regression test
    
    Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/832f45a6-c01e-4da9-bf85-1ba7b5f302e6
    
    Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
    
    * add E2E http test for checkpoint context id rejection
    
    Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/730258ef-2781-4a7d-b7cf-b5c40c11defc
    
    Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
    Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
    Co-authored-by: Jacob Alber <jaalber@microsoft.com>
  • Python: Add support for function approval flow in Foundry hosted agent (#5666)
    * Add support for function approval flow in Foundry hosted agent
    
    * Address comments
    
    * Address comments
    
    * Address comments
  • Python: Fix hosted MCP replay producing orphan function_call_output (#5581)
    * Python: Fix hosted MCP replay producing orphan function_call_output
    
    Resolves part of #5546. After a turn ran a hosted MCP / Foundry-toolbox-MCP
    tool, the next turn's replayed input array carried a function_call_output
    with an mcp_* call_id and no matching function_call, and the Responses API
    returned a 400.
    
    Two layers covered here:
    
    * Chat-client serialize layer (packages/openai): adds mcp_server_tool_call
      and mcp_server_tool_result cases to _prepare_message_for_openai and
      _prepare_content_for_openai. Pairs are coalesced via a post-pass into a
      single mcp_call input item carrying both arguments and output. Orphan
      results are dropped (debug-logged) rather than serialized as orphan
      function_call_output, which is what the Responses API rejected.
    
    * Host read layer (packages/foundry_hosting): _item_to_message and
      _output_item_to_message now route custom_tool_call_output whose
      call_id.startswith("mcp_") to Content.from_mcp_server_tool_result.
      Non-mcp_ call_ids continue to produce Content.from_function_result.
      Symmetric with the host write-side choice for hosted-MCP results.
    
    Two further fixes (agentserver SDK additions, host write-side single-item
    emission) remain tracked on the issue and depend on an SDK release.
    
    * Python: Fix pyright unknown-type in _stringify_mcp_output
    
    cast(Sequence[Any], output) after the isinstance check so pyright stops
    flagging the loop variable as unknown. Also normalizes a couple of
    em-dashes in docstrings I introduced in the prior commit.
    
    * Python: Harden _stringify_mcp_output for dict-shaped MCP outputs
    
    Address Copilot review on PR #5581. Today the helper falls back to
    str() for any non-string, non-text-attribute entry, which produces
    Python repr (single-quoted dicts) for the canonical MCP raw-JSON
    text-content shape `{"type": "text", "text": "..."}` and any other
    dict-shaped output.
    
    Three small changes:
    
    * List-entry path: prefer plain string entries, then `.text` attribute
      (Content objects), then `entry["text"]` for Mapping entries in the
      canonical MCP shape, then JSON-encode anything else.
    * Final fallback: `json.dumps(output, default=str)` so Mappings and
      scalars produce valid JSON rather than Python repr.
    * Two new unit tests covering the dict-with-text shape and the
      non-text-dict JSON fallback.
    
    * Python: Suppress mypy redundant-cast on _stringify_mcp_output narrowing
    
    The cast is needed by pyright (reportUnknownVariableType) but mypy
    considers it redundant after the preceding isinstance narrowing.
    Pyright's behavior is correct for the strict-mode reporting we run,
    so keep the cast and silence mypy on the line.
  • Python: Update hosting agent samples + fixes (#5485)
    * Update foundry hosting samples
    
    * Add file data type support
    
    * Fix file content and add more tests
    
    * Fix README
    
    * Address comments
    
    * Fix int tests
    
    * remove temp
  • Python: update FoundryAgent for hosted agent sessions (#5447)
    * fixes to FoundryAgent to connect to new hosted agents
    
    Co-authored-by: Copilot <copilot@github.com>
    
    * fix mypy
    
    Co-authored-by: Copilot <copilot@github.com>
    
    * Python: remove Foundry service session helpers
    
    Remove the public hosted-agent service session CRUD helpers from FoundryAgent and drop the related feature-stage inventory entry.
    
    Update the hosted-agent sample to create and delete service sessions directly through the preview AIProjectClient APIs, and tighten a few test harnesses surfaced by full workspace validation.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * fix from merge
    
    * fix hosted env detection
    
    Co-authored-by: Copilot <copilot@github.com>
    
    * reverted sample update
    
    * fix tests and code
    
    Co-authored-by: Copilot <copilot@github.com>
    
    * remove aenter
    
    * skipping some tests
    
    Co-authored-by: Copilot <copilot@github.com>
    
    ---------
    
    Co-authored-by: Copilot <copilot@github.com>
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Python: Upgrade hosting server dependency and add more type support (#5459)
    * Upgrade hosting server dependency and add more type support
    
    * Comments
  • Python: Foundry hosted agent V2 (#5379)
    * Python: Wrapper + Samples 1st (#5177)
    
    * Experiment
    
    * Update dependency and add non streaming
    
    * Add more samples
    
    * Rename samples
    
    * Add invocations
    
    * Comments 1
    
    * Comments 2
    
    * Comments 3
    
    * Improve README
    
    * Add local shell sample
    
    * WIP: Add eval and memory samples
    
    * Update user agent prefix
    
    * Update user agent prefix doc
    
    * Update dependency (#5215)
    
    * Add tests and more content types (#5235)
    
    * Add tests
    
    * fix tests and sample
    
    * Fix formatting
    
    * Remove function approval contents
    
    * Python: Refine samples and upgrade packages (#5261)
    
    * Refine samples and upgrade pacakges
    
    * Upgrade to a new package that fixes a bug
    
    * Update model env var
    
    * Move samples (#5281)
    
    * Python: Upgrade agentserver packages (#5284)
    
    * Upgrade agentserver packages
    
    * Fix new types
    
    * Python: Add special handling for workflows (#5298)
    
    * Add special handling for workflows
    
    * Address comments
    
    * Improve samples (#5372)
    
    * Python: Add more types (#5378)
    
    * Add more type supports
    
    * Upgrade packages
    
    * Remove TODOs in README
    
    * Fix README
    
    * Comments and mypy
    
    * User agent scoped
    
    * Fix README
    
    * Fix pre commit
    
    * Fix pre commit 2
    
    * Fix pre commit 3
    
    * Fix pre commit 4
    
    * Fix pre commit 5
    
    * Fix pre commit 6
    
    * Add azure-monitor-opentelemetry to dev deps
    
    Fixes Samples & Markdown CI failure. The PR's new transitive dep on
    azure-monitor-opentelemetry-exporter (via azure-ai-agentserver-core) makes
    pyright resolve the azure.monitor.opentelemetry namespace, flipping the
    check_md_code_blocks diagnostic for `configure_azure_monitor` from
    reportMissingImports (filtered) to reportAttributeAccessIssue (not filtered).
    Installing the umbrella azure-monitor-opentelemetry package in dev makes
    pyright resolve the symbol correctly, matching the install guidance the
    observability README already gives users.
    
    ---------
    
    Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>