Repository navigation
[agentserver-core] Harden lifecycle endpoint auth and env-var validation - #49774
Ankit Sultania (ankitsul) wants to merge 3 commits into
Conversation
…idation The snapshot lifecycle endpoints (/_agent/before-snapshot, /_agent/after-restore and their aliases) were registered unconditionally on the agent port and accepted any session_env_overrides key. - Require a matching x-foundry-lifecycle-token header (constant-time comparison) when FOUNDRY_LIFECYCLE_TOKEN is set; return 401 otherwise. - Do not register the lifecycle routes when the process is not hosted and no token is configured; log the posture once at startup. - Capture is_hosted and the token once at startup so later environment updates cannot change them. - Validate session_env_overrides as a whole and fail closed: strict key pattern, count/size caps, 1 MiB body cap, and a deny list covering loader, interpreter, proxy, TLS trust, telemetry, platform identity and credential variables. - Bump to 2.3.1 and document the behaviour in CHANGELOG and README. Authored-by: GitHub Copilot CLI v1.0.105 Model: Claude Fable 5.1 (claude-fable-5.1) Co-authored-by: Copilot <[email protected]> Copilot-Session: a3850a99-7429-42b6-bd69-abc89d315204
|
Thank you for your contribution Ankit Sultania (@ankitsul)! We will review the pull request and get back to you soon. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). 7 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟡 Changes recommended
Lifecycle restores allow identity and authentication settings to be overridden, while validation gaps can cause failed restores or prevent subprocesses from starting.
4 open issues
What changed in this PR
This PR hardens snapshot lifecycle endpoints in azure-ai-agentserver-core for release 2.3.1, adding authentication controls and stricter restore-request validation while preserving hosted deployments without a token.
Changes:
- Adds startup-captured lifecycle token authentication and conditional route registration.
- Adds request-body limits and environment-override validation before restore processing.
- Expands lifecycle tests and documents compatibility changes.
| File | Description |
|---|---|
sdk/agentserver/azure-ai-agentserver-core/tests/test_snapshot_lifecycle.py |
Adds authentication, route-registration, and validation tests; updates existing lifecycle requests. |
sdk/agentserver/azure-ai-agentserver-core/README.md |
Documents token requirements, route availability, and override restrictions. |
sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md |
Records 2.3.1 hardening and compatibility changes. |
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_version.py |
Bumps the package version to 2.3.1. |
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_base.py |
Implements lifecycle authentication, conditional routes, and request validation. |
Review effort: Balanced
Ankit Sultania (ankitsul)
left a comment
There was a problem hiding this comment.
🤖 Multi-model review: AgentServer lifecycle endpoint hardening (MSRC 161856)
Recommendation: 🔄 Changes requested. This is a comment-only review because the PR is self-authored.
Several parts are good:
- constant-time token comparison;
- the 1 MiB body cap, enforced both on the declared length and while streaming;
- strict JSON-object parsing;
- denying identity,
LD_*andPYTHON*keys.
However, the production hosted path is still unauthenticated, and the env-override policy is a denylist that conflicts with Vienna's after-restore contract. Both need to be coordinated with the platform side before this closes MSRC 161856.
| Models | GPT-5.6 Sol, GPT-5.5, Gemini 4 Argon; arbiter and safety check by GPT-5.6 Terra |
| Size | |
| IcM | 31000000759303 (could not access the IcM from tooling; reviewed against the PR description) |
Active findings (4, inline)
| Severity | Finding |
|---|---|
| 🟠 High | Hosted mode without FOUNDRY_LIFECYCLE_TOKEN authorizes every lifecycle request, and Vienna doesn't send or provision the token yet (_base.py:827) |
| 🟠 High | The finite denylist admits process-control variables (BASH_ENV, NODE_OPTIONS, JAVA_TOOL_OPTIONS, DOTNET_STARTUP_HOOKS, …); switch to an allowlist (_base.py:85) |
| 🟡 Medium | The OTEL_ / APPLICATIONINSIGHTS_ / AZURE_ prefixes reject payloads that follow Vienna's documented after-restore contract (OTEL_RESOURCE_ATTRIBUTES, connection-backed AZURE_* keys), so restore would get a 400 (_base.py:88-90) |
| ⚪ Low | The oversized-body test only hits the declared-length check; the streaming limit is untested |
Dismissed after cross-review (confirmed by the GPT-5.6 Terra safety check)
- The token stays in
os.environ(flagged by Sol, dismissed by GPT-5.5). Agent code shares the process with the host and can readself._lifecycle_tokenanyway, and rotation is out of this contract. Popping it to avoid subprocess inheritance is optional defense in depth. assigned_session_idis not seeded from the boot-timeFOUNDRY_AGENT_SESSION_ID(flagged by Gemini, dismissed by GPT-5.5). This is pre-existing behavior, and its exploit depends on the hosted-without-token gap above.
These complement the existing Copilot comments (lines 95, 134, 891, 944) without duplicating them.
Review generated with multi-model cross-validation; reviewed by Ankit Sultania (@ankitsul).
…en env override policy - Fail closed when hosted lifecycle authentication is not configured, with an explicit temporary compatibility opt-out. - Validate restore environment overrides against startup-captured allow rules, deny sensitive process controls, and enforce UTF-8 byte limits. - Preserve startup hosted posture and enforce streaming request size limits with focused tests and documentation. Authored-by: GitHub Copilot CLI v1.0.105 Model: Claude Fable 5.1 Co-authored-by: Copilot <[email protected]> Copilot-Session: a3850a99-7429-42b6-bd69-abc89d315204
There was a problem hiding this comment.
🟡 Changes recommended
Restore can expose bearer tokens or redirect FTP traffic, reject newly assigned session GUIDs, and block hosted callers that upgrade without authentication configuration.
4 open issues
4 resolved since last review
Review effort: Balanced
| "NO_PROXY", | ||
| "REQUESTS_CA_BUNDLE", | ||
| "CURL_CA_BUNDLE", | ||
| "AZURE_TENANT_ID", |
| upper in _DENIED_ENV_KEYS | ||
| or upper.startswith(_DENIED_ENV_KEY_PREFIXES) |
| self._lifecycle_allowed_env_keys: frozenset[str] = ( | ||
| frozenset(os.environ) | ||
| | frozenset(platform_allowed_keys) | ||
| | frozenset({_SESSION_ID_ENV, "OTEL_RESOURCE_ATTRIBUTES"}) |
| if self._lifecycle_is_hosted and not self._lifecycle_allow_unauthenticated: | ||
| return create_error_response( | ||
| "lifecycle_authentication_not_configured", | ||
| "Lifecycle authentication is required but is not configured.", | ||
| status_code=403, |
…-hardening Resolve CHANGELOG.md and _version.py conflicts by folding the lifecycle hardening notes into the 2.3.1b1 (Unreleased) section introduced upstream. Authored-by: GitHub Copilot CLI v1.0.105 Model: Claude Fable 5.1 (claude-fable-5.1) Co-authored-by: Copilot <[email protected]> Copilot-Session: a3850a99-7429-42b6-bd69-abc89d315204
|
Ankit Sultania (@ankitsul) please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
There was a problem hiding this comment.
🟡 Changes recommended
Default authentication breaks existing hosted snapshot and restore calls, while restore allowlisting rejects valid session variables and deny rules permit proxy and credential overrides.
7 open issues
Azure credential override conflicts with documented deny policy · New Require hosted auth provisioning before upgrade Allow the session GUID during restore Reject all environment variables ending in _PROXY Block the Azure project endpoint alias Package version remains beta despite stable release notes · New Restore allowlist rejects valid session variables absent at startup · New
Review effort: Balanced
| "OTEL_EXPORTER_", | ||
| "APPLICATIONINSIGHTS_", | ||
| "FOUNDRY_PROJECT_", | ||
| "AZURE_CLIENT_", |
|
|
||
| ### Features Added | ||
|
|
||
| - Added `FOUNDRY_LIFECYCLE_TOKEN`. Hosted snapshot lifecycle endpoints require |
| if key not in allowed_keys: | ||
| raise ValueError(f"environment override {key!r} is not allowed") |


Description
Summary
azure-ai-agentserver-core2.3.0b1–2.3.0 register the snapshot lifecycle endpoints (POST /_agent/before-snapshot,POST /_agent/after-restoreand the/_before_snapshot//_after_restorealiases) unconditionally on the same port as the agent routes, andafter-restorecopies everysession_env_overridesentry intoos.environwith only basic shape checks. This PR hardens lifecycle endpoint authentication and environment-variable validation and ships it as 2.3.1.Change
FOUNDRY_LIFECYCLE_TOKEN. When set, lifecycle requests must present the same value in thex-foundry-lifecycle-tokenheader (compared withhmac.compare_digest); otherwise the host returns401 unauthorized_lifecycle_request.is_hostedand the token are captured once inAgentServerHost.__init__. When the process is not hosted and no token is configured, the/_agent/*routes are not registered at all (logged once atINFO). Hosted without a token logs aWARNINGand keeps the routes so existing platform callers continue to work until they send the header._parse_session_context:^[A-Za-z_][A-Za-z0-9_]*$, ≤ 256 chars; value ≤ 32 KiB, no NUL; ≤ 256 entries; request body ≤ 1 MiB (checked viacontent-lengthand while streaming).PATH,HOME,NO_PROXY,REQUESTS_CA_BUNDLE,CURL_CA_BUNDLE,FOUNDRY_HOSTING_ENVIRONMENT,FOUNDRY_LIFECYCLE_TOKEN; prefixesLD_,DYLD_,PYTHON,SSL_CERT_,OTEL_,APPLICATIONINSIGHTS_,FOUNDRY_PROJECT_,AZURE_,MSI_,IDENTITY_; suffix_PROXY.400before any environment or config mutation.is_hostedinstead of the (mutable) live config.2.3.1; CHANGELOG and README updated.Tests
sdk/agentserver/azure-ai-agentserver-core/tests/test_snapshot_lifecycle.py(53 → 115 cases):401, no env change, no session assignment, no handler call (all 4 paths)404); hosted + no token → routes present400, nothing applied400,os.environunchangedcontent-length→400Existing lifecycle tests were updated to set the token / header and to use
FOUNDRY_AGENT_VERSIONinstead of the now-deniedFOUNDRY_PROJECT_ENDPOINTas the config-hydration probe. Verified the new cases fail (54) without the source change.Ran locally (Python 3.14, venv with
pytest,httpx,pytest-asyncio):pytest sdk/agentserver/azure-ai-agentserver-core/tests→ 1387 passed, 1 failed (test_tracing.py::TestSetupDistroExport::test_managed_otlp_handles_mixed_signal_protocols, pre-existing, needs theotlp-grpcextra;test_otlp_protocol.pyandtest_tracing_e2e.pyskipped for the same reason). Changed files areblack -l 120clean.Compatibility
FOUNDRY_LIFECYCLE_TOKENand sendx-foundry-lifecycle-tokento enable authentication (platform-side plumbing is a separate change).FOUNDRY_LIFECYCLE_TOKEN; otherwise the routes are not registered.All SDK Contribution checklist:
General Guidelines and Best Practices
Testing Guidelines