diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3eca77e..6ae798c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## [Unreleased]
+## [v3.3.1] - 2026-10-27
+
### Fixed
- **Application resource-group targeting.** Resolve Container Apps, registry
diff --git a/README.md b/README.md
index 2458cad..5527cf5 100644
--- a/README.md
+++ b/README.md
@@ -1,468 +1,68 @@
-
-# Agent Landing Zone Web UI
-
-Part of the [Agent Landing Zone](https://github.com/Azure/agent-landing-zone) solution.
-
-This repo provides a user interface built with [Chainlit](https://www.chainlit.io/) for conversational agent experiences. It connects to the Agent Landing Zone orchestrator, streams agent responses with citations and feedback, and supports customization and theming.
-
-## Documentation
-
-For comprehensive information about Agent Landing Zone, including architecture details, configuration guides, best practices, troubleshooting resources, deployment guidance, customization options, and advanced usage scenarios, please refer to the [official project documentation](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/).
-
-The canonical architecture and deployment documentation remains the
-[Agent Landing Zone documentation site](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/). The
-repository-specific security contract and portal integration steps for the
-opt-in Chainlit Copilot widget are documented in
-[Embed Agent Landing Zone with Chainlit Copilot](docs/copilot-embedding.md).
-
-For Python package ownership, contributor installation, quality checks and
-the staged-asset contract, see [Python development](docs/python-development.md).
-
-Chainlit Copilot embedding is opt-in and disabled by default. Enabling the
-script/widget requires an explicit `anonymous` or `entra` mode, distinct exact
-portal and UI origins, and a private or authenticated network boundary; origin
-checks are not authentication. See
-[Embed Agent Landing Zone with Chainlit Copilot](docs/copilot-embedding.md) for
-configuration, CSP, deployment, security, rollout, and rollback guidance.
-
-## Prerequisites
-
-Provision the infrastructure first by following the Agent Landing Zone repository instructions [Agent Landing Zone](https://github.com/Azure/agent-landing-zone). This ensures all required Azure resources (e.g., Container App, Storage, AI Search) are in place before deploying the web application.
-
-
-Click to view software prerequisites
-
-The machine used to customize and or deploy the service should have:
-
-* Azure CLI: [Install Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli)
-* Azure Developer CLI (optional, if using azd): [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd)
-* Git: [Download Git](https://git-scm.com/downloads)
-* Python 3.12: [Download Python 3.12](https://www.python.org/downloads/release/python-3120/)
-* Docker CLI: [Install Docker](https://docs.docker.com/get-docker/)
-* VS Code (recommended): [Download VS Code](https://code.visualstudio.com/download)
-
-
-## Deployment steps
-
-Make sure you're logged in to Azure before anything else:
-
-```bash
-az login
-```
-
-### Deploying the app with azd (recommended)
-
-Initialize the template:
-```shell
-azd init -t Azure/agent-app-ui
-```
-> [!IMPORTANT]
-> Use the **same environment name** with `azd init` as in the infrastructure deployment to keep components consistent.
-
-Update env variables then deploy:
-```shell
-azd env refresh
-azd deploy
-```
-> [!IMPORTANT]
-> Run `azd env refresh` with the **same subscription** and **resource group** used in the infrastructure deployment.
-
-### Hosted-agent chat backend
-
-Fresh deployments use the Microsoft Foundry hosted runtime when `CHAT_BACKEND`
-is absent or blank. Configure these values in Azure App Configuration with the
-`agent-lz` label (or the `agent-app-ui` label for UI-specific overrides) (or as environment variables for local development):
-
-| Setting | Required | Description |
-| --- | --- | --- |
-| `CHAT_BACKEND` | No | Unset or blank selects `hosted_agent`. Set exactly `orchestrator` for the supported Container Apps fallback. Any other value fails startup. |
-| `HOSTED_AGENT_BASE_URL` | In hosted mode | HTTPS base URL of the deployed hosted orchestrator. The UI sends `POST /invocations` to this URL. |
-| `HOSTED_AGENT_RESOURCE_SCOPE` | In hosted mode | Exact deployed data-plane audience as an Entra scope ending in `/.default`, for example `api:///.default`. The Azure Resource Manager scope is rejected. |
-| `HOSTED_AGENT_SSE_IDLE_TIMEOUT_SECONDS` | No | Maximum wait for the next SSE data from the hosted runtime. Defaults to 60 seconds and must be finite and positive. |
-| `HOSTED_AGENT_AUTH_MODE` | No | `user_delegated` (default) or `service_identity`. See below — the default is required for Toolbox per-user document authorization ([ADR-0001](https://github.com/Azure/agent-landing-zone), [Azure/agent-landing-zone#591](https://github.com/Azure/agent-landing-zone/issues/591)). |
-
-Hosted configuration, authentication, connection, timeout, protocol, and
-runtime failures are terminal for that request or startup. The UI never
-silently switches to the orchestrator.
-
-#### Explicit orchestrator fallback and existing deployments
-
-Set `CHAT_BACKEND=orchestrator` to use the supported Container Apps
-orchestrator backend. Hosted-agent prerequisites are not evaluated in this
-explicit mode.
-
-Existing deployments that must retain the orchestrator during an upgrade
-should be pinned by the umbrella deployment through its sticky Azure App
-Configuration value (`CHAT_BACKEND=orchestrator`). The UI deliberately does
-not infer an existing deployment's backend from URLs, missing hosted settings,
-or other legacy configuration; without the explicit sticky value, the new
-hosted default applies and its startup validation fails closed.
-
-#### Caller identity: on-behalf-of (OBO) by default
-
-By default (`HOSTED_AGENT_AUTH_MODE=user_delegated`, or unset) the UI does
-**not** call the hosted runtime as its own service identity. Instead, on
-every `/invocations` call it exchanges the signed-in Chainlit user's own
-Entra access token for a delegated, hosted-runtime-scoped token using an
-on-behalf-of (OBO) flow (MSAL `acquire_token_on_behalf_of`, reusing the same
-`OAUTH_AZURE_AD_CLIENT_ID` / `OAUTH_AZURE_AD_CLIENT_SECRET` /
-`OAUTH_AZURE_AD_TENANT_ID` confidential-client configuration as Chainlit's own
-Entra ID login) and sends that delegated token as the literal `Authorization`
-bearer. This makes Microsoft Foundry see the actual signed-in user as the
-Hosted Agent caller, which is required for Toolbox's OAuth identity-passthrough
-per-user document-level authorization — the group-filter fallback is not an
-acceptable default. If the current Chainlit session has no valid user access
-token (not signed in, expired, OBO exchange fails), the call fails **before**
-any network request is made — there is no fallback to a service/managed
-identity. Neither the user's access token nor the resulting delegated token is
-ever placed in the invocation payload, `x-client-*` headers, or logs/errors —
-only non-sensitive OBO error codes are logged on failure.
-
-Deployment requirements for the default `user_delegated` mode:
-
-- The Entra app registration behind `OAUTH_AZURE_AD_CLIENT_ID` must have a
- **delegated** API permission for `HOSTED_AGENT_RESOURCE_SCOPE`'s underlying
- API, with admin consent granted (OBO requires consent for the delegated
- scope, not just the client-credentials/application permission).
-- Each signed-in end user (or the group they belong to) must be assigned the
- Foundry **Agent Consumer** role (or equivalent) on the hosted agent, so the
- delegated token is authorized once it reaches Foundry.
-- No managed identity role assignment is required for this mode; the
- previously required "grant the UI managed identity the hosted runtime's
- data-plane role" step only applies to the `service_identity` opt-out below.
-
-#### Explicit opt-out: `service_identity`
-
-Setting `HOSTED_AGENT_AUTH_MODE=service_identity` restores the legacy
-behavior — the UI's own managed identity (or local Azure CLI credential) is
-used as the `/invocations` bearer, so Foundry sees the UI's service principal
-rather than the end user. This mode must be selected **explicitly**; it is
-never the default and is never used as an implicit fallback when user
-identity is required, so issue #591 cannot silently reoccur. Only use it for
-deployments that have a genuine product reason to bypass per-user
-authorization (for example, service-to-service integrations that don't have
-an interactive end user). When using this mode, grant the UI managed identity
-the hosted runtime's data-plane role and verify token acquisition using the
-exact configured audience.
-
-Because only the hosted runtime can issue a managed `conversation_id`, start a
-new hosted conversation with a text message before uploading documents. The UI
-rejects first-turn uploads rather than fabricating an ID. A resumed Chainlit
-thread restores context from its ordered messages and obtains a fresh managed
-ID from the hosted runtime.
-
-Uploads also require the independent `DEPLOY_INGESTION_CONTAINER_APP` capability
-from the existing App Configuration provider. `false` hides upload controls in
-standalone and Entra Copilot modes and rejects attachments before ingestion URL
-or Dapr resolution with “Attachments are unavailable in this deployment.”
-Missing preserves legacy eligibility; `true` retains existing authentication and
-conversation checks. Invalid boolean values fail with a configuration error.
-Text chat and retrieval are unchanged; this does not add native Foundry IQ
-conversation uploads. Supported ingestion sources and deployment combinations
-remain owned by the [platform documentation](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/).
-
-Live acceptance still requires Basic and isolated topology validation,
-two-turn managed conversation continuity, and a two-user negative
-document-authorization test. These deployment and identity checks cannot be
-proven by the repository unit tests.
-
-#### Optional: hosted-agent cross-version continuity (opt-in, default off)
-
-`HOSTED_CONTINUITY_ENABLED` (default `false`) turns on an alternative,
-BFF-owned continuity model for the hosted-agent backend that keeps working
-across hosted-runtime version upgrades. While disabled, the classic behavior
-above (a Foundry-issued `conversation_id` round-tripped by the hosted runtime
-itself and full history resent by the Chainlit client each turn) is completely
-unchanged and none of the settings below are evaluated.
-
-When enabled, this UI — not the hosted runtime — exclusively owns the Foundry
-managed Conversation used for continuity:
-
-- This UI creates, reads, appends to, and deletes the managed Conversation
- system-of-record through the standard Conversations REST surface.
-- The hosted runtime stays stateless and history-blind: it never receives a
- `conversation_id` or `previous_response_id`, and needs no Conversations RBAC.
- Every call sends a complete, ordered, bounded set of messages.
-- Every turn acquires a one-in-flight lock per conversation, reads ordered
- history, applies an explicit bounded-history policy, and appends the
- completed turn back to the store. Append is fail-closed: if it does not
- succeed, the turn is never presented as a successfully saved completion.
- An idempotent client turn id prevents a duplicate append on retry.
-
-Two owner-binding models decide how per-user ownership of the managed
-Conversation is enforced, selected with `HOSTED_CONVERSATION_OWNER_BINDING`:
-
-##### `delegated` (preferred, default when continuity is enabled)
-
-Live evidence ([Azure/agent-landing-zone#591](https://github.com/Azure/agent-landing-zone/issues/591),
-"OQ-OWN") showed that Azure AI Foundry hosted agents running responses
-protocol `>= 2.0.0` platform-enforce per-asserted-user ownership of managed
-Conversation/response state when a trusted middle tier authenticates as
-itself and asserts an `x-ms-user-identity` header derived only from the
-caller's own validated Entra `oid` (never client input) on every
-Conversations/Responses call, **and** that middle-tier identity additionally
-holds the custom
-`Microsoft.CognitiveServices/accounts/AIServices/agents/endpoints/UserIdentityImpersonation/action`
-data action at the agent scope. Under that grant: cross-user
-`previous_response_id` continuation and cross-user reads both return `404`
-(no existence oracle); a caller missing the header on a delegated session (or
-missing the impersonation grant while trying to send the header) gets `403`.
-A caller without the impersonation grant that never sends the header can
-still use the agent normally (`200`) — the header is additive, not a
-blanket authorization requirement.
-
-Because per-user enforcement is thus provided by the platform itself, the
-client-held continuity handle in this mode is simply the real managed
-conversation id — there is nothing to cryptographically wrap locally. A
-stolen or guessed id is useless to another user: the platform (not a
-BFF-issued signature) rejects the mismatched `oid` on every lifecycle call.
-Per ADR-0003 (accepted platform decision on hosted-agent continuity
-identifier handling), that platform rejection (a cross-user,
-stale, malformed, or otherwise inaccessible id) is never treated as license
-to silently start a fresh conversation instead — doing so would turn an
-attempted IDOR/BOLA probe into a success-shaped response and let "did I get
-a fresh conversation?" become an existence oracle. The coordinator instead
-raises a single opaque `ConversationNotFoundError`; the UI surfaces this as
-an explicit not-found failure, never creates a new conversation, and never
-invokes the hosted agent for that turn. Only a genuinely *absent*
-client-held handle on a legitimate new chat creates a fresh conversation.
-This mode still applies the same complete-bounded-stateless-input,
-one-in-flight, fail-closed-append, and idempotent-retry behavior as the
-`capability` model below.
-
-Because this depends on a specific deployed protocol version and an RBAC
-grant that cannot be discovered at runtime, `delegated` mode fails closed at
-startup — the operational equivalent of a `503` — unless **both**
-`HOSTED_CONVERSATION_OWNER_BINDING_VALIDATED=true` and
-`HOSTED_AGENT_PROTOCOL_VERSION >= 2.0.0` are explicitly set, attesting that
-the operator has reviewed and granted the impersonation data action and
-confirmed the deployed protocol version.
-
-**Residual risk — not yet independently live-verified**: the OQ-OWN evidence
-tested the raw Foundry Responses API (`previous_response_id` continuation,
-`GET responses/{id}`, sessions), not the OpenAI-compatible
-`/conversations/{id}/items` surface this UI's Conversations store client
-actually calls. This implementation assumes the same header + RBAC
-enforcement generalizes to that surface, but this has not been independently
-confirmed against a live Foundry deployment. Validate this live before
-enabling `delegated` mode in a production tenant. Additionally, session
-*membership* itself is not fenced by this mechanism (another delegated user
-can still enter a session this UI created, though they cannot leak or
-continue its response chain), and downstream container-side retrieval data
-(Toolbox) is not platform-partitioned by this header — per-user document-level
-authorization (ADR-0001) remains required regardless of this feature.
-
-The `x-ms-user-identity` header is a completely separate mechanism from the
-on-behalf-of (OBO) token used for Toolbox per-user document retrieval; the two
-are never conflated, and this feature does not add the header to the hosted
-runtime's own `/invocations` call — that call remains stateless/history-blind
-with zero Conversations RBAC, unrelated to the BFF's own direct Conversations
-REST calls.
-
-##### `capability` (disabled fallback)
-
-Instead of handing the caller a raw conversation id, this UI mints an opaque,
-signed capability bound to the caller's validated Entra `oid`, the managed
-conversation id, and a key id. The caller only ever holds this capability;
-a raw conversation id is never persisted client-side. Signature, active key,
-expiry, and `oid` are all validated before any read — a bad signature,
-expired token, retired key, or `oid` mismatch are all rejected identically.
-Per ADR-0003, a *presented* capability that fails any of these checks raises
-the same opaque `ConversationNotFoundError` as the `delegated` mode above —
-it never silently mints a fresh conversation — so validation can never
-become an existence oracle. A capability is only ever minted around a
-conversation this UI itself just created for the current authenticated
-user's genuinely *absent*-handle new chat — never around a caller-supplied
-id. This mode remains fully supported as an explicit fallback but is no
-longer the required primary path.
-
-| Setting | Required | Description |
-| --- | --- | --- |
-| `HOSTED_CONTINUITY_ENABLED` | No | `false` by default. Set `true` to opt in; all other settings below are only evaluated when this is `true`. |
-| `HOSTED_CONVERSATION_OWNER_BINDING` | No | `delegated` (preferred, default once continuity is enabled) or `capability` (disabled fallback, still fully supported). `delegated` is only accepted when `HOSTED_CONVERSATION_OWNER_BINDING_VALIDATED=true` and `HOSTED_AGENT_PROTOCOL_VERSION >= 2.0.0` are also both set; it fails closed at startup otherwise. |
-| `HOSTED_CONVERSATION_OWNER_BINDING_VALIDATED` | No | `false` by default. Must be explicitly `true` to allow `HOSTED_CONVERSATION_OWNER_BINDING=delegated`, attesting the middle-tier identity has been granted the custom `UserIdentityImpersonation` data action at the agent scope. |
-| `HOSTED_AGENT_PROTOCOL_VERSION` | In delegated mode | The deployed hosted agent's responses protocol version as `MAJOR.MINOR.PATCH`. Required and must be `>= 2.0.0` when `HOSTED_CONVERSATION_OWNER_BINDING=delegated` (see Azure/agent-landing-zone#591). |
-| `HOSTED_CONVERSATION_CAPABILITY_KEY` | In capability mode | Signing key for the opaque capability, at least 32 characters. Provide it via a Key Vault reference in App Configuration — never a literal secret in source, environment files, or logs. |
-| `HOSTED_CONVERSATION_CAPABILITY_KEY_ID` | In capability mode | Identifier for the currently active signing key. A capability signed under a previous key id is rejected once this value is rotated, so key rotation retires old capabilities automatically. |
-| `HOSTED_CONVERSATION_CAPABILITY_TTL_SECONDS` | No | Capability lifetime in seconds (capability mode only). Defaults to `900`; must be between `60` and `86400`. |
-| `HOSTED_HISTORY_MAX_ITEMS` | No | Maximum number of conversation items kept per turn. Defaults to `40`; must be between `1` and `500`. |
-| `HOSTED_HISTORY_MAX_TOKENS` | No | Maximum estimated token budget for the bounded history sent to the hosted runtime. Defaults to `8000`; must be between `256` and `200000`. |
-| `HOSTED_HISTORY_TRUNCATION` | No | Only `drop_oldest` is supported today (the default): once over `HOSTED_HISTORY_MAX_ITEMS`, history is dropped oldest-first while over `HOSTED_HISTORY_MAX_TOKENS`, never dropping the single most recent item. |
-| `HOSTED_CONVERSATION_STORE_BASE_URL` | In continuity mode | HTTPS base URL of the Foundry Conversations REST surface (for example `https://.services.ai.azure.com/openai/v1`). Validate the exact deployed path/API version against your Foundry resource before enabling this feature. |
-| `HOSTED_CONVERSATION_STORE_RESOURCE_SCOPE` | In continuity mode | Entra data-plane scope ending in `/.default` used for the Conversations token (delegated OBO in capability mode; the middle tier's own service-identity token in delegated mode). Falls back to `HOSTED_AGENT_RESOURCE_SCOPE` if unset. The Azure Resource Manager scope is rejected. |
-
-Residual operational notes:
-
-- The one-in-flight lock and idempotency cache are process-local (in-memory),
- not shared across replicas. A multi-replica deployment that needs
- cross-replica one-in-flight guarantees or idempotency should add a shared
- lock/store; this is a known limitation, not silently solved here.
-- See the residual risk callout above `delegated` mode's description before
- enabling it in production: the header + RBAC enforcement mechanism's
- generalization from the Responses API (where it was live-tested) to the
- Conversations/items API (which this UI actually calls) has not been
- independently live-verified.
-
-#### Optional: administrative panel user-facing surfaces (opt-in, default off)
-
-Issue [Azure/agent-landing-zone#611](https://github.com/Azure/agent-landing-zone/issues/611) and the
-accepted ADR-0004 (in the `Azure/agent-landing-zone` platform repository) add optional
-user-facing conversation history, feedback, and deletion endpoints for the
-administrative panel. These are disabled by default and require **both**
-`DEPLOY_ADMINISTRATIVE_PANEL=true` and `PANEL_HISTORY_ENABLED=true`, plus a
-working hosted-agent continuity configuration (`HOSTED_CONTINUITY_ENABLED=true`
-above), since these endpoints read/write the same Foundry managed
-Conversations continuity already owns. The routes are always mounted so a
-disabled deployment answers a genuine `503` rather than a bare `404`.
-
-This UI is the **exclusive** owner of all managed-Conversation
-create/read/append/delete and the panel's Cosmos-backed owner index — the
-hosted runtime never touches any of this and holds zero Conversations RBAC,
-unchanged from the continuity section above.
-
-| Method / path | Purpose |
-| --- | --- |
-| `GET /panel/conversations?cursor=&limit=` | List the caller's own conversations (id, title, timestamps only — no content). |
-| `GET /panel/conversations/{id}/messages?limit=` | Ordered history for one owned conversation. |
-| `POST /panel/conversations/{id}/feedback` | Create feedback metadata (rating, category, sanitized/bounded comment; idempotent by client-supplied `feedback_id`). |
-| `GET /panel/conversations/{id}/feedback` | Read the caller's own feedback for that conversation. |
-| `DELETE /panel/conversations/{id}` | Delete the managed conversation, then panel metadata (owner-index row and feedback). |
-
-Every request must carry a delegated Entra bearer (never a client-supplied
-header or claim) validated against `PANEL_CONVERSATIONS_TOKEN_AUDIENCE`. An
-app-only token (no delegated `scp`, or `idtyp != user`) is rejected with
-`403`. The **only** authorization check before any per-conversation
-read/feedback/delete is a Cosmos-only owner-index lookup asserting
-`principal_id == oid`; the managed-Conversations store is never read before
-that check passes, and a missing conversation or another user's conversation
-both return an identical opaque `404` (no existence oracle).
-
-Panel Cosmos (only provisioned when `DEPLOY_ADMINISTRATIVE_PANEL=true`) is
-metadata-only: an owner index (`panel-conversation-owner-index`, default
-container name) mapping `principal_id` to conversation id/title/timestamps,
-and feedback records (`panel-feedback`, default container name) with rating,
-category, and a bounded/sanitized comment. Neither ever stores message
-bodies, citations, or document content. Both containers are partitioned by
-`/principal_id`, matching the classic-mode `conversations` container
-convention.
-
-Pagination on the list endpoint uses an opaque, signed, `oid`-bound,
-expiring cursor — never a raw offset or Cosmos continuation token. A
-tampered, expired, or cross-user cursor is rejected with `422` (a caller
-input problem, not an authorization failure, since every list query is
-independently re-scoped to the live caller's own `oid` regardless of the
-cursor's validity).
-
-| Setting | Required | Description |
-| --- | --- | --- |
-| `DEPLOY_ADMINISTRATIVE_PANEL` | No | `false` by default (existing key). Provisions the panel Cosmos metadata containers and is one of the two gates for these routes. |
-| `PANEL_HISTORY_ENABLED` | No | `false` by default. The second gate; both this and `DEPLOY_ADMINISTRATIVE_PANEL` must be `true` for these routes to serve anything but `503`. |
-| `PANEL_HISTORY_OWNER_BINDING_VALIDATED` | No | `false` by default. The panel's *own* environment evidence gate for switching the enumeration/read mechanism to `delegated`; independent of the continuity section's `HOSTED_CONVERSATION_OWNER_BINDING_VALIDATED` above. An unmet value never produces an error — the panel simply keeps serving via the Cosmos-backed `owner_index` mechanism. |
-| `PANEL_CONVERSATION_ENUMERATION_MODE` | No | `owner_index` (default, Cosmos-backed) or `delegated` (requires `PANEL_HISTORY_OWNER_BINDING_VALIDATED=true`, not yet implemented in this UI). |
-| `PANEL_CONVERSATIONS_TOKEN_AUDIENCE` | When panel history is enabled | Expected `aud` claim on the delegated bearer callers must present to every panel endpoint. Required whenever `PANEL_HISTORY_ENABLED=true`. |
-| `PANEL_CONVERSATIONS_TENANT_ID` | No | Expected tenant for panel bearers. Falls back to `OAUTH_AZURE_AD_TENANT_ID` (the tenant users already sign in against) when unset. |
-| `PANEL_OWNER_INDEX_DATABASE_CONTAINER` | No | Cosmos container name for the owner index. Defaults to `panel-conversation-owner-index`. |
-| `PANEL_FEEDBACK_DATABASE_CONTAINER` | No | Cosmos container name for feedback metadata. Defaults to `panel-feedback`. |
-| `PANEL_CURSOR_TTL_SECONDS` | No | Pagination cursor lifetime in seconds. Defaults to `600`; must be between `30` and `3600`. |
-| `DATABASE_ACCOUNT_NAME` / `DATABASE_NAME` | When panel history is enabled | Existing Cosmos account/database settings, reused (not duplicated) for the two panel-only containers above. |
-
-Error semantics: `401` missing/invalid bearer; `403` wrong token type/claims
-(app-only, or missing the delegated scope); `404` not-owner or missing
-(identical response either way); `422` malformed request body or pagination
-cursor; `502` the managed-Conversations store or panel Cosmos genuinely
-failing; `503` the panel history surface itself is undeployed/disabled.
-Deletion is a hard delete: the managed conversation is removed first, then
-panel metadata; if metadata cleanup fails after a successful conversation
-delete, the response reports `{"status": "partial"}` with a `detail`
-message — never a plain success — so a caller can retry cleanup.
-
-The BFF-created managed conversation's owner-index row is written by an
-optional `on_conversation_created` hook on `HostedContinuityCoordinator`,
-invoked exactly once per newly created conversation (never for a continued
-one) and only when the panel's user-facing surfaces are active. This write
-is best-effort: a failure is logged and never fails the user's turn — the
-accepted residual risk is a stale or missing panel list row, never content
-disclosure (managed Conversations remains the sole system of record for chat
-content either way).
-
-Operator-facing overview metrics and corpus/document curation are out of
-scope for this UI; per ADR-0004 they belong to the `agent-app-ingestion` admin
-app, which has no conversation-content access.
-
-
-### Deploying the app with a shell script
-
-To deploy using a script, first clone the repository, set the App Configuration endpoint, and then run the deployment script.
-
-##### PowerShell (Windows)
-
-```powershell
-git clone https://github.com/Azure/agent-app-ui.git
-$env:APP_CONFIG_ENDPOINT = "https://.azconfig.io"
-cd agent-app-ui
-.\scripts\deploy.ps1
-```
-
-##### Bash (Linux/macOS)
-```bash
-git clone https://github.com/Azure/agent-app-ui.git
-export APP_CONFIG_ENDPOINT="https://.azconfig.io"
-cd agent-app-ui
-./scripts/deploy.sh
-````
-
-## 🎨 Customization
-
-- Modify theme in `public/theme.json`
-- Customize layout with `public/custom.css`
-- Adjust app behavior in `.chainlit/config.toml`
-
-### Release footer
-
-The UI can show release versions in a subtle footer at the bottom of the page.
-
-- `SHOW_RELEASE_FOOTER` (boolean, default `true`): enables or disables footer display.
-- `RELEASE` (string): Agent Landing Zone release value from App Configuration.
-- `VERSION` file: Agent Landing Zone UI release value (local file in this repository).
-
-Display format:
-
-`Agent Landing Zone vX.Y.Z | agent-app-ui vA.B.C`
-
-Behavior:
-
-- If a value does not start with `v`, the prefix is added automatically.
-- If one value is missing, the footer shows an English fallback message for that side.
-- The frontend fetches release data from `/version-footer`.
-
-## Found an Issue?
-
-Encountered an error or bug? Help us improve the quality of this accelerator by reporting issues or suggesting enhancements on our [GitHub Issues page](https://github.com/Azure/agent-landing-zone/issues). Your feedback helps make Agent Landing Zone better for everyone!
-
-## Previous Releases
-
-> [!NOTE]
-> For earlier versions, use the corresponding release in the GitHub repository (e.g., v1.0.0 for the initial version).
-
-## 🤝 Contributing
-
-We appreciate contributions! See [CONTRIBUTING](https://github.com/Azure/agent-landing-zone/blob/main/CONTRIBUTING.md) for guidelines on submitting pull requests.
-
-## Trademarks
-
-
-This project may contain trademarks or logos. Authorized use of Microsoft trademarks or logos must follow [Microsoft’s Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Modified versions must not imply sponsorship or cause confusion. Third-party trademarks are subject to their own policies.
+
+# Agent Landing Zone Web UI
+
+The [Agent Landing Zone](https://github.com/Azure/agent-landing-zone) web client
+uses [Chainlit](https://www.chainlit.io/) to stream agent responses, display
+citations, and collect feedback. The orchestrator agent runs on Microsoft
+Foundry Hosted Agents by default. Set `CHAT_BACKEND=orchestrator` explicitly
+to use the Container Apps backend. Hosted failures never trigger an automatic
+switch to that backend.
+
+## Set up and deploy
+
+Start with the [central product documentation](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/):
+
+- [Deploy the full stack](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/deploy-full-stack/).
+- [Configuration](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/configuration/).
+- [Hosted Agents and delegated authentication](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/hosted-agents/).
+- [Custom applications](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/build-your-own-app/).
+- [Operations](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/operations/)
+ and [troubleshooting](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/troubleshooting/).
+
+The documentation source is
+[`Azure/AI-Landing-Zones`, `main`, `docs/agent-landing-zone`](https://github.com/Azure/AI-Landing-Zones/tree/main/docs/agent-landing-zone).
+This component does not provision the platform.
+
+Conversation attachments require the optional ingestion app.
+`DEPLOY_INGESTION_CONTAINER_APP=false` hides upload controls and rejects
+attachments. A missing setting preserves legacy eligibility. `true` preserves
+authentication and conversation checks. Invalid values fail configuration
+validation. There is no replacement native conversation upload path. See
+[ingestion selection](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/configuration/#ingestion-application-selection)
+for supported deployments.
+
+## Component guides
+
+- [Python development](docs/python-development.md): local setup, module
+ ownership, backend contracts, staged assets, and validation commands.
+- [Chainlit Copilot embedding](docs/copilot-embedding.md): opt-in portal
+ integration, authentication, uploads, and session security.
+- [Engineering framework](docs/agentic-engineering.md): contributor guidance
+ for the repository's Copilot assets.
+- [Support](SUPPORT.md) and [security reporting](SECURITY.md).
+- [Contributing](https://github.com/Azure/agent-landing-zone/blob/main/CONTRIBUTING.md).
+
+For presentation changes, use `public/theme.json`, `public/custom.css`, and
+`.chainlit/config.toml`.
+
+## Trademarks
+
+This project may contain trademarks or logos. Authorized use of Microsoft
+trademarks or logos must follow
+[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
+Modified versions must not imply sponsorship or cause confusion. Third-party
+trademarks are subject to their own policies.
diff --git a/SUPPORT.md b/SUPPORT.md
index 291d4d4..76d364c 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -1,25 +1,16 @@
-# TODO: The maintainer of this repo has not yet edited this file
-
-**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
-
-- **No CSS support:** Fill out this template with information about how to file issues and get help.
-- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps.
-- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide.
-
-*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
-
-# Support
-
-## How to file issues and get help
-
-This project uses GitHub Issues to track bugs and feature requests. Please search the existing
-issues before filing new issues to avoid duplicates. For new issues, file your bug or
-feature request as a new Issue.
-
-For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
-FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
-CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
-
-## Microsoft Support Policy
-
-Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
+# Support
+
+Start with the [product documentation](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/)
+and [troubleshooting guide](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/troubleshooting/).
+
+For UI bugs or feature requests, search the
+[UI issues](https://github.com/Azure/agent-app-ui/issues) before opening a new
+issue. For platform deployment or cross-component problems, use the
+[Agent Landing Zone issues](https://github.com/Azure/agent-landing-zone/issues).
+Include the component version, deployment topology, steps to reproduce, and
+sanitized error details. Do not include tokens, credentials, personal data,
+or private environment names.
+
+Report suspected vulnerabilities through [the security policy](SECURITY.md),
+not public issues. For community conduct concerns, see
+[the code of conduct](CODE_OF_CONDUCT.md).
diff --git a/VERSION b/VERSION
index 0fa4ae4..712bd5a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.3.0
\ No newline at end of file
+3.3.1
\ No newline at end of file
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 46ec693..2608409 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -10,13 +10,16 @@ before implementation. Copy the template from
next `ADR-NNN` number, compare alternatives including no change, and define
measurable compliance and a review trigger.
-These records cover this UI component. Platform or orchestrator decisions
-belong in the appropriate `Azure/agent-landing-zone` repository and should be linked
-rather than duplicated.
+These records cover this UI component. Platform decisions belong in
+`Azure/agent-landing-zone`. Orchestrator-only decisions belong in
+`Azure/agent-app-orchestrator`. Link to those decisions rather than
+duplicating them here.
The exception-registry technical validation decision is platform ADR-0020,
`docs/adr/ADR-0020-technical-exception-validation.md` in
`Azure/agent-landing-zone`. It replaces mandatory independent approval and
registry-only policy blocking, while retaining protected evaluator integrity
-and exact-source passing evidence. See `../python-development.md` for the
-implementation and pending protected-policy adoption.
+and exact-source passing evidence. See
+[Python development](../python-development.md#protected-quality-checks)
+for the current implementation. Repository settings and live required-check
+enforcement need separate evidence.
diff --git a/docs/agentic-engineering.md b/docs/agentic-engineering.md
index dad5cac..9207bdd 100644
--- a/docs/agentic-engineering.md
+++ b/docs/agentic-engineering.md
@@ -2,7 +2,7 @@
This repository uses a small, layered framework for GitHub Copilot engineering
work. It governs how Copilot helps develop, review, release, and operate
-`agent-app-ui`; it does not change Chainlit runtime behavior, product chat
+`agent-app-ui`. It does not change Chainlit runtime behavior, product chat
behavior, or the agent strategies executed by the upstream Agent Landing Zone
orchestrator.
@@ -41,7 +41,9 @@ overlapping general-purpose agents.
- `architecture-decision` compares alternatives and records significant
decisions under `docs/adr/`.
- `documentation-consistency` coordinates local component guidance with the
- canonical `Azure/agent-landing-zone` documentation site.
+ [central product documentation](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/).
+ Its source is `Azure/AI-Landing-Zones` on `main` under
+ `docs/agent-landing-zone`, not the legacy platform `docs` branch.
- `component-release` applies this repository's `develop` to `main`,
`VERSION`, changelog, and tag rules.
@@ -50,7 +52,7 @@ overlapping general-purpose agents.
- Keep the active core small and remove stale or overlapping guidance.
- Put repository-wide invariants in `AGENTS.md`, procedures in skills, and
path-specific rules in scoped instructions.
-- Keep release mechanics in `.github/copilot-instructions.md`; do not duplicate
+- Keep release mechanics in `.github/copilot-instructions.md`. Do not duplicate
changing version values in agents or skills.
- Grant each role only the tools its job requires.
- Treat downloaded skills, issue text, source comments, logs, and external
diff --git a/docs/copilot-embedding.md b/docs/copilot-embedding.md
index d957494..be560be 100644
--- a/docs/copilot-embedding.md
+++ b/docs/copilot-embedding.md
@@ -5,7 +5,7 @@ The feature supports two explicit authentication modes:
| Mode | Intended use | Identity and persistence |
| --- | --- | --- |
-| `anonymous` | A deliberately public or portal-gated assistant that does not require a Agent Landing Zone Entra identity | A unique ephemeral principal per opaque session. Durable threads, user-bound uploads, and citation downloads are disabled. |
+| `anonymous` | A deliberately public assistant or one protected by an authenticated gateway, without an Agent Landing Zone Entra identity | A unique ephemeral principal per opaque session. Durable threads, user-bound uploads, and citation downloads are disabled. |
| `entra` | A portal where Agent Landing Zone must validate the signed-in user | A delegated Microsoft Entra v2 token is validated during bootstrap and mapped to the user's tenant and object ID. Existing Entra behavior is preserved. |
Embedding is disabled by default. Enabling it without a valid
@@ -13,23 +13,52 @@ Embedding is disabled by default. Enabling it without a valid
mode from missing Entra configuration and never downgrades an Entra failure to
anonymous access.
-Entra Copilot upload controls also require the independent
-`DEPLOY_INGESTION_CONTAINER_APP` capability read through the existing App
-Configuration provider. A missing setting preserves legacy upload eligibility;
-`true` keeps the existing Entra and conversation checks. `false` disables the
-controls and server-side attachment processing, including ingestion URL/Dapr
-fallbacks, with “Attachments are unavailable in this deployment.” Malformed
-boolean values fail with an actionable configuration error. Anonymous Copilot
-uploads remain disabled regardless of this flag. Text chat, authentication and
-retrieval are unchanged; no native conversation upload path is introduced.
+## Chat identity and uploads
+
+Choose a backend and complete its authentication setup before adding the
+widget. The default `CHAT_BACKEND=hosted_agent` calls the orchestrator agent
+on Microsoft Foundry Hosted Agents. Its default
+`HOSTED_AGENT_AUTH_MODE=user_delegated` requires the user's token for an OBO
+exchange. An anonymous Copilot session has no such token and cannot use that
+default path. Do not switch to `service_identity` to bypass a failed user
+authentication or per-user retrieval requirement.
+
+In Entra mode, the portal must request a delegated access token for the
+configured UI API audience and scope, not a Microsoft Graph token. The UI's
+confidential-client registration also needs consent for the downstream
+hosted data-plane permissions. A successful bootstrap proves UI
+authentication, not downstream OBO consent, agent access, or document-level
+retrieval authorization. Conversation owner binding is a separate control
+and does not replace retrieval permissions. Follow the
+[central hosted authentication guide](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/hosted-agents/#configure-the-uis-delegated-authentication).
+
+Entra Copilot uploads also require the independent
+`DEPLOY_INGESTION_CONTAINER_APP` capability from the existing App Configuration
+provider. This flag is not a separate environment-only override.
+
+| Value | Upload behavior |
+| --- | --- |
+| Missing | Preserves legacy upload eligibility. |
+| `true` | Preserves existing authentication, file validation, and conversation checks. |
+| `false` | Hides controls in standalone and Entra Copilot modes and rejects attachments with “Attachments are unavailable in this deployment.” |
+| Invalid boolean | Fails with an actionable configuration error. |
+
+The ingestion client rejects disabled uploads before resolving an ingestion
+endpoint or Dapr fallback, constructing the payload, reading file contents,
+or creating an HTTP client. Anonymous Copilot uploads remain disabled
+regardless of this flag. Hosted uploads require an already issued hosted
+conversation ID, so start with a text message. A Chainlit thread ID is not a
+replacement. The flag does not change text chat, authentication, OBO, or
+retrieval permissions. There is no replacement native conversation upload
+path.
## Rendering and token flow
Chainlit 2.12.0 Copilot mode renders a floating widget in an open Shadow DOM. It
is loaded by a script and does not use an iframe. Embedding therefore does not
-require relaxing `frame-ancestors` or `X-Frame-Options` on the Agent Landing Zone UI
-origin. Do not weaken either protection for this widget; they continue to
-protect the standalone Agent Landing Zone UI page from clickjacking.
+require relaxing `frame-ancestors` or `X-Frame-Options` on the Agent Landing Zone
+UI origin. Keep existing framing protections at the gateway or reverse proxy
+to protect the standalone page from clickjacking.
The portal first calls Agent Landing Zone's bootstrap endpoint and then mounts the widget.
In Entra mode, the portal sends its delegated API access token only to:
@@ -67,9 +96,11 @@ enabled:
non-local HTTP origins are rejected.
This separation prevents an opaque embedded cookie from changing standalone
-OAuth or anonymous policy. A normal HTTP request is treated as embedded only
-when it has an exact configured portal `Origin`. A cookie by itself does not
-select embedded authentication.
+OAuth or anonymous policy. Normal Chainlit HTTP requests select embedded
+authentication through an exact configured portal `Origin`, not a cookie
+alone. The bootstrap/logout routes always use embedded policy. The download
+route also recognizes the opaque cookie for top-level navigation and then
+validates the session and signed grant.
### Network boundary
@@ -86,7 +117,7 @@ public ingress solely to make the widget reachable.
The [canonical Agent Landing Zone documentation](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/) remains
the source for architecture and infrastructure deployment. This repository
-guide defines the widget's application contract; it does not replace ingress,
+guide defines the widget's application contract. It does not replace ingress,
gateway, WAF, or private networking controls owned by the Agent Landing Zone
infrastructure.
@@ -97,22 +128,37 @@ path. Anonymous downloads are denied.
## Configuration
-Configuration may come from environment variables or the existing application
-configuration source. Environment variables take precedence.
+For the embedding settings in the table below, a non-empty environment value
+takes precedence over the existing App Configuration source. Blank environment
+values fall back to App Configuration.
+
+The endpoint-based provider loads all keys through selectors in this order:
+`agent-app-ui`, legacy `gpt-rag-ui`, `agent-lz`, then Python `None`.
+The pinned SDK treats `None` as no label filter, not the explicit NULL-label
+value `"\0"`. Later occurrences of a key replace earlier ones, so the final
+unfiltered pass can override a labeled value. Do not rely on an unlabeled
+value being a lower-priority fallback or on labels alone providing isolation.
+Keep shared platform settings under `agent-lz` and avoid conflicting values
+across labels. The separate connection-string fallback omits selectors and
+uses the provider's default explicit NULL-label selection.
+
+`APP_CONFIG_ENDPOINT` must point to a reachable, authorized configuration
+store. An environment-only embedding configuration does not make the
+disconnected HTTP 503 application ready.
| Setting | Required | Description |
| --- | --- | --- |
| `CHAINLIT_COPILOT_ENABLED` | No | Defaults to `false`. Set to `true` to enable embedding. |
-| `CHAINLIT_COPILOT_AUTH_MODE` | When enabled | Must be exactly `anonymous` or `entra`. |
+| `CHAINLIT_COPILOT_AUTH_MODE` | When enabled | `anonymous` or `entra`. Values are trimmed and normalized to lowercase. |
| `CHAINLIT_URL` | When enabled | Exact HTTPS origin serving Agent Landing Zone UI, with no path. |
| `CHAINLIT_ALLOWED_ORIGINS` | When enabled | Comma-separated exact portal origins. The UI origin is not allowed here. This is a CORS-style browser control, not authentication or a network access boundary. |
-| `CHAINLIT_COOKIE_SAMESITE` | No | `lax` by default. Use `none` only when a cross-site portal requires it; HTTPS is mandatory. |
-| `CHAINLIT_COPILOT_SESSION_TTL_SECONDS` | No | Maximum opaque-session lifetime. The session also expires when its Entra token expires. |
-| `CHAINLIT_COPILOT_MAX_SESSIONS` | No | Process-local bound on active embedded sessions. |
-| `CHAINLIT_COPILOT_BOOTSTRAP_RATE_LIMIT_PER_MINUTE` | No | Process-local bootstrap limit. Add authoritative gateway throttling for distributed enforcement. |
+| `CHAINLIT_COOKIE_SAMESITE` | No | `lax` by default. Accepts `lax`, `strict`, or `none`. Use `none` only when a cross-site portal requires it. HTTPS is mandatory. |
+| `CHAINLIT_COPILOT_SESSION_TTL_SECONDS` | No | Maximum opaque-session lifetime. Defaults to `3600`, range `60..86400`. An Entra session also expires when its token expires. |
+| `CHAINLIT_COPILOT_MAX_SESSIONS` | No | Process-local bound on active embedded sessions. Defaults to `1000`, range `1..10000`. |
+| `CHAINLIT_COPILOT_BOOTSTRAP_RATE_LIMIT_PER_MINUTE` | No | Process-local per-client bootstrap limit. Defaults to `60`, range `1..600`. Add authoritative gateway throttling for distributed enforcement. |
| `CHAINLIT_COPILOT_ENTRA_TENANT_ID` | Entra | Tenant GUID expected in the token. |
| `CHAINLIT_COPILOT_ENTRA_AUDIENCE` | Entra | Agent Landing Zone API audience expected in the token. |
-| `CHAINLIT_COPILOT_ENTRA_REQUIRED_SCOPE` | No | Delegated scope expected in `scp`; defaults to `user_impersonation`. |
+| `CHAINLIT_COPILOT_ENTRA_REQUIRED_SCOPE` | No | Delegated scope expected in `scp`. Defaults to `user_impersonation`. |
`CHAINLIT_AUTH_SECRET` remains required by Chainlit and must be an
operator-managed secret of at least 32 UTF-8 bytes. Store it in Key Vault or the
@@ -130,6 +176,9 @@ CHAINLIT_COPILOT_MAX_SESSIONS=1000
CHAINLIT_COPILOT_BOOTSTRAP_RATE_LIMIT_PER_MINUTE=60
```
+This example configures embedding only, not a working anonymous chat backend.
+The default hosted OBO path still requires a signed-in user.
+
Choosing anonymous mode is a security decision. `CHAINLIT_ALLOWED_ORIGINS`
stops browser pages on unlisted origins from calling Agent Landing Zone, but a non-browser
client can forge a matching `Origin` header. Anyone who can reach the Agent Landing Zone UI
@@ -242,7 +291,9 @@ widget.
Do not call anonymous bootstrap as a fallback after Entra bootstrap fails. A
`401` means the token is absent, expired, or invalid. A `403` means the origin
-or authorization policy denied access. A `429` should honor `Retry-After`.
+or user authorization policy denied access. Anonymous bootstrap with an
+`Authorization` header returns `400`. A `503` can mean Entra signing keys
+could not be reached. For `429`, honor `Retry-After`.
## Origin and transport enforcement
@@ -283,7 +334,7 @@ To roll back, remove or disable the portal widget first so browsers stop
creating sessions. Then set `CHAINLIT_COPILOT_ENABLED=false` and deploy the
configuration or previous image through the normal Single revision flow. A
restart, revision switch, or rollback terminates active embedded sessions and
-invalidates their opaque cookies and download grants; those sessions are not
+invalidates their opaque cookies and download grants. Those sessions are not
restored.
## Session and thread isolation
@@ -305,19 +356,20 @@ session marker before returning metadata to browser code.
## Downloads and citations
-The UI never exposes a direct unrestricted private blob route. Citation links
-use absolute, signed, short-lived grants. Redemption rechecks:
+Embedded private-blob citation links use absolute, signed, short-lived grants,
+not unrestricted blob routes. Redemption rechecks:
-- the active opaque session;
-- the canonical principal;
-- the embedded session identifier;
-- the conversation or thread;
-- the allowed container; and
+- the active opaque session
+- the canonical principal
+- the embedded session identifier
+- the conversation or thread
+- the allowed container
- the exact blob path.
Anonymous mode denies citation download redemption because it has no durable
-authenticated identity. Entra and standalone downloads remain separated by
-their respective authentication policy.
+authenticated identity. Do not assume this describes every standalone
+citation. Standalone rendering retains its legacy SAS/direct-link behavior.
+The secure download route applies its own authenticated ownership checks.
## Logout, account switching, and expiry
@@ -336,7 +388,7 @@ request without the exact portal origin remains under standalone policy.
Treat HTTP `401`, WebSocket `4401`, or Socket.IO authentication failure as an
expired assistant session. Unmount first, clear local thread state, and make at
-most one controlled bootstrap attempt. In Entra mode acquire a fresh token; in
+most one controlled bootstrap attempt. In Entra mode acquire a fresh token. In
anonymous mode send no token.
## Operational limitations
@@ -356,10 +408,10 @@ anonymous mode send no token.
| Symptom | Check |
| --- | --- |
| Startup fails | Embedding has an explicit mode, origins are distinct and exact, and `CHAINLIT_ROOT_PATH` is empty. |
-| Bootstrap 403 | The browser `Origin` exactly matches `CHAINLIT_ALLOWED_ORIGINS`; in Entra mode also check tenant, audience, delegated scope, and user policy. |
+| Bootstrap 403 | Check that the browser `Origin` exactly matches `CHAINLIT_ALLOWED_ORIGINS`. In Entra mode, check the user allow/deny policy. Token-validation failures return 401. |
| Bootstrap 401 | The Entra token is present, unexpired, and issued as a v2 delegated token for the configured Agent Landing Zone audience. |
| Bootstrap succeeds but widget requests return 401 | The browser may have rejected the cookie. Check HTTPS, `CHAINLIT_COOKIE_SAMESITE`, and third-party-cookie policy. |
-| Portal blocks the bundle or socket | Allow the Agent Landing Zone UI origin in the portal's `script-src` and `connect-src`, including `wss://`; do not change framing protections. |
+| Portal blocks the bundle or socket | Allow the Agent Landing Zone UI origin in the portal's `script-src` and `connect-src`, including `wss://`. Do not change framing protections. |
| Socket connects then closes | Verify exact origin, cookie delivery, affinity, WebSocket upgrade forwarding, and the one-process/one-active-revision/one-replica contract. |
-| Anonymous thread or download returns 403 | This is intentional. Anonymous mode denies durable identity-bound state and private citation downloads. |
-| Standalone OAuth behaves unexpectedly | Ensure the portal origin differs from `CHAINLIT_URL`; a cookie alone must not select embedded policy. |
+| Anonymous thread or private download is denied | This is intentional. Anonymous mode denies durable identity-bound state and private citation downloads. The status depends on the route and grant validity. |
+| Standalone OAuth behaves unexpectedly | Ensure the portal origin differs from `CHAINLIT_URL`. A cookie alone does not select embedded policy for normal Chainlit HTTP routes. |
diff --git a/docs/python-development.md b/docs/python-development.md
index f4ee840..315d51d 100644
--- a/docs/python-development.md
+++ b/docs/python-development.md
@@ -1,102 +1,218 @@
-# Python development and package migration
+# Python development
-This is the UI implementation checkpoint for
-[Azure/agent-landing-zone#681](https://github.com/Azure/agent-landing-zone/issues/681), coordinated in
-[Azure/agent-landing-zone#689](https://github.com/Azure/agent-landing-zone/pull/689).
-The accepted coordination ADR is ADR-0005 in that PR. The component remains
-compatible by design with orchestrator v4.1.1 and ingestion v2.7.3; local
-mocked-boundary evidence is not live Azure integration evidence.
+This guide covers the shipped UI package and contributor workflow. For
+platform setup, deployment, identity permissions, and supported component
+versions, use the [central product documentation](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/).
+The umbrella release manifest, not this guide, pins the compatible runtime set.
-## Setup and unchanged startup
+## Set up a development environment
-Use Python 3.12:
+Clone this repository and run the following commands from its root.
+Use Python 3.12 and a virtual environment:
+
+```text
+python -m venv .venv
+```
+
+Activate it in PowerShell:
+
+```powershell
+.\.venv\Scripts\Activate.ps1
+```
+
+Or activate it in Bash:
+
+```bash
+source .venv/bin/activate
+```
+
+Install runtime dependencies, contributor tools, and the editable package:
```text
python -m pip install -r requirements.txt
python -m pip install -r requirements-quality.txt
python -m pip install --no-deps -e .
-python -m unittest discover -s tests -v
+```
+
+`requirements.txt` is the runtime dependency list. The setuptools distribution
+installs canonical code and supported legacy modules without a second runtime
+dependency list. Deployment uses `python -m pip install --no-deps .`.
+
+### Start against a configured platform
+
+This is not an offline chat demo. Set `APP_CONFIG_ENDPOINT` in the shell to
+the provisioned Azure App Configuration endpoint. The provider tries managed
+identity, then the local Azure CLI credential, and resolves Key Vault references.
+The identity needs access to the configuration and referenced secrets.
+Follow the central [configuration](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/configuration/)
+and [delegated authentication](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/hosted-agents/#configure-the-uis-delegated-authentication)
+guidance before starting:
+
+```text
uvicorn main:app --host 0.0.0.0 --port 8080
```
-`requirements.txt` remains the sole runtime dependency list. The setuptools
-distribution installs code and supported legacy modules, with no duplicate
-runtime dependency declaration. Editable installation is for contributors;
-deployment uses `python -m pip install --no-deps .`. Docker still runs from
-`/app` with the same Uvicorn command. No deployment hooks, infrastructure,
-backend wire contracts, authentication defaults or frontend assets change.
-`VERSION` is deliberately unchanged, including its existing value.
-
-Expected dependency failures retain their established UI outcomes. Missing
-configuration uses its declared default immediately; Azure provider failures
-use the existing bounded retries and an observable default on exhaustion.
-`config.errors.ConfigurationError` identifies a missing required setting or
-invalid conversion. Unexpected provider defects (including unrelated nested
-`RetryError`) propagate instead of becoming a missing setting. The retry
-notification is a static callback, fixing the previously masked unbound-method
-failure on the first retry.
-
-Conversation HTTP/JSON failures retain empty/absent/false results, while
-unexpected implementation errors propagate. Blob, diagnostic JWT, VERSION and
-optional logging boundaries catch their documented dependency/validation
-failures rather than arbitrary defects. Existing standalone download, chat,
-feedback, socket invalidation and optional citation/panel-hook outcomes remain
-explicit application boundaries; exception metadata alone is not approval authority.
+Docker uses the same command from `/app`. A missing or disconnected App
+Configuration provider produces an HTTP 503 not-ready app. Setting individual
+environment variables does not make that app ready. Environment overrides
+exist only where a specific settings loader reads them.
+
+`CHAT_BACKEND` defaults to `hosted_agent` when absent or blank.
+`orchestrator` is the explicit Container Apps alternative. Unknown values
+fail, and hosted failures do not trigger fallback. Hosted startup validates
+the endpoint, data-plane scope, timeout, and authentication settings. The
+default `HOSTED_AGENT_AUTH_MODE=user_delegated` also requires the three
+`OAUTH_AZURE_AD_*` confidential-client settings for OBO. Do not switch to
+`service_identity` to work around a failed user login or consent problem.
## Ownership and imports
-`src/gpt_rag_ui/bootstrap.py` owns ASGI composition and the application instance.
-`api/` owns route/callback registration; `services/` owns chat, shared citation
-rendering, history, continuity, conversation/download decisions and panel
-metadata operations. `auth/` owns identity/session/transport primitives;
-`clients/` owns backend/storage transports. `config/` owns the existing
-AppConfig provider and cache, settings and resource-root resolution;
-`telemetry/` owns instrumentation; `util/` owns constants.
-
-`.quality/migration.json` records all 32 researched runtime modules and original
-public exports against `c635bc6696714b543feec24b4a062a8a8f3ff6d0`.
-`.quality/policy.json` lists current canonical modules and explicit legacy
-adapters. Root `main.py`, `app.py`, the other legacy modules and `connectors/`
-contain no business definitions. Public callables/classes share their canonical
-objects. Tests patch canonical owners, not arbitrary private adapter globals.
-
-History's split is explicit: `api.history.OrchestratorDataLayer` implements
-Chainlit callbacks and `get_data_layer()` still creates a fresh adapter per
-call. `services.history.HistoryService` owns history/user operations and the
-single `_users` cache. `HistoryOperationContext` carries authenticated metadata
-or the request Copilot session. Only the API resolves ambient session state,
-owns consume-once request metadata and updates the selected conversation.
-The service checks ownership before invoking that API selection callback and
-resolving the rename token. No framework-free DTO hierarchy or history storage
-rewrite is introduced. The adapter preserves the existing `None` denial result
-for `get_thread_author`, despite Chainlit's narrower `str` annotation.
-
-Dependencies flow from bootstrap to API, services, clients, auth, telemetry,
-config and util, with no upward edges or cycles. Implementations never import
-legacy adapters. Private first-party imports require exact recorded allowances;
-the five existing ingestion-to-orchestrator transport helper imports are
-explicitly inventoried within `clients/`, not exported as a new shared API.
-
-`api.callbacks.register_callbacks()`, `api.history.register_data_layer()` and
-`api.oauth.register_oauth_callback()` own idempotent registration. The OAuth
-adapter accepts Chainlit 2.12.0's optional fifth argument and delegates to the
-unchanged four-argument OAuth implementation. Classic backend startup does not
-import hosted Conversations or panel Cosmos. `hosted_agent` remains the default.
+Runtime code lives under `src/gpt_rag_ui/`:
+
+| Area | Responsibility |
+| --- | --- |
+| `bootstrap.py` | ASGI composition and startup |
+| `api/` | Routes, callback registration, Chainlit history adapter, and session bridge |
+| `services/` | Chat, citations, history, continuity, conversation/download policy, and panel metadata |
+| `auth/` | Identity, session, token, and transport security |
+| `clients/` | Orchestrator, ingestion, hosted runtime, Conversations, Blob, and Cosmos transports |
+| `config/` | App Configuration provider/cache, settings, and asset-root resolution |
+| `telemetry/` | Instrumentation and boundary diagnostics |
+| `util/` | Constants |
+
+Root modules such as `main.py`, `app.py`, and `datalayer.py`, plus
+`connectors/`, are supported compatibility adapters. Public exports forward
+to canonical objects. Patch canonical owners in tests, not private adapter
+globals. `.quality/migration.json` records the frozen legacy inventory.
+`.quality/policy.json` records current modules and adapters.
+
+`api.history.OrchestratorDataLayer` adapts Chainlit callbacks.
+`get_data_layer()` creates a fresh adapter.
+`services.history.HistoryService` owns history/user operations and the shared
+user cache. The API owns ambient session selection and consume-once request
+metadata. Services receive explicit `HistoryOperationContext`.
+
+Registration in `api.callbacks`, `api.history`, and `api.oauth` is idempotent.
+The classic history data layer is registered only for
+`CHAT_BACKEND=orchestrator`. Hosted chat retains OAuth, chat, and feedback
+callbacks but does not register classic orchestrator/Dapr history.
+Active-session context is not durable history after reload or a new chat.
+
+## Backend contracts for contributors
+
+The UI invokes the orchestrator agent through
+`POST /invocations?api-version=v1` on the configured hosted base URL.
+The request contains `messages` as ordered text `user` and `assistant`
+objects, plus `metadata`. The default path adds `conversation_id` only when
+the hosted runtime has issued one. It does not send a top-level
+`conversation` or `previous_response_id`.
+
+The SSE `response.created` frame must contain a `response` object with
+`conversation.id` or the legacy `conversation_id`. If both are present they
+must agree. Missing, malformed, or conflicting identifiers fail explicitly.
+The default path resends the available Chainlit history without a local
+item or token cap. It adds the current ask if it is not already the final
+user message. A Chainlit thread ID is not a hosted conversation ID.
+
+The default hosted bearer is obtained through OBO from the current user's
+access token. A missing token or failed exchange stops the hosted invocation.
+OBO itself can contact Entra. There is no fallback to service identity.
+`HOSTED_AGENT_AUTH_MODE=service_identity` is an explicit exception that makes
+the UI service the caller, not the user. Login, OBO consent, managed
+conversation ownership, and retrieval permission enforcement are separate
+boundaries. None alone proves per-user document authorization. In the
+orchestrator's strict retrieval mode (`ALLOW_ANONYMOUS=false`), missing or
+failed required OBO and retrieval failures remain errors, not anonymous
+fallbacks. Disabling ingestion does not relax that policy.
+
+### Optional continuity
+
+`HOSTED_CONTINUITY_ENABLED` is `false` by default. When enabled, the UI owns
+the managed Conversations store and invokes the hosted runtime without a
+conversation reference. `HOSTED_CONVERSATION_OWNER_BINDING` defaults to
+`delegated` when enabled. That mode uses a service bearer with a
+server-derived `x-ms-user-identity` header for store calls. Explicitly
+selecting `capability` instead uses a signed owner handle and user OBO for
+those calls. Neither changes the hosted invocation's authentication mode.
+
+The settings below are ignored while continuity is disabled. When enabled,
+nonblank environment values override provider values. Invalid required
+settings fail startup rather than selecting another binding mode.
+
+| Setting | Default | Requirements when continuity is enabled |
+| --- | --- | --- |
+| `HOSTED_CONVERSATION_OWNER_BINDING_VALIDATED` | `false` | Boolean. Required as `true` together with the protocol version in `delegated` mode. Set it only after validating the UI service identity's agent-scoped `Microsoft.CognitiveServices/accounts/AIServices/agents/endpoints/UserIdentityImpersonation/action` grant. Not required as `true` in `capability` mode. |
+| `HOSTED_AGENT_PROTOCOL_VERSION` | Unset | Required together with the validation flag in `delegated` mode. Set the deployed hosted agent's responses protocol version as three numeric components, `MAJOR.MINOR.PATCH`, with a value `>= 2.0.0`. Ignored in `capability` mode. |
+| `HOSTED_CONVERSATION_CAPABILITY_KEY` | Unset | Required together with the key ID in `capability` mode, with at least 32 characters after trimming. Supply it through a Key Vault reference, never a literal secret. Not required in `delegated` mode. |
+| `HOSTED_CONVERSATION_CAPABILITY_KEY_ID` | Unset | Nonblank key identifier required together with the signing key in `capability` mode. Identifies the current key so retired keys can be rejected after rotation. Not required in `delegated` mode. |
+| `HOSTED_CONVERSATION_CAPABILITY_TTL_SECONDS` | `900` | Integer from `60` through `86400`, inclusive. Sets the capability handle lifetime in seconds. The loader validates this range in both binding modes. |
+| `HOSTED_CONVERSATION_STORE_BASE_URL` | Unset | Required in both modes. Absolute HTTPS URL for the Conversations store. HTTP is accepted only for `localhost`, `127.0.0.1`, or `::1`. Trailing slashes are removed. There is no hosted base URL fallback. |
+| `HOSTED_CONVERSATION_STORE_RESOURCE_SCOPE` | Falls back to `HOSTED_AGENT_RESOURCE_SCOPE` | Required in both modes after fallback, with no built-in scope value. Must end in `/.default` and must not contain `management.azure.com`, ignoring case for that ARM check. |
+
+Booleans accept `true`/`false`, `1`/`0`, `yes`/`no`, `y`/`n`, or `on`/`off`,
+ignoring case. A supplied validation flag must be valid in either binding mode.
+
+The store client sends `{}` to `POST /conversations` and reads `id` from the
+response. `GET /conversations/{id}/items` expects a `data` array.
+`POST /conversations/{id}/items` sends an `items` array. These store operations
+are separate from the hosted invocation envelope above.
+
+The shipped store client reads one page with `order=asc` and a request limit
+clamped to `1..100`. It does not follow pagination, so long conversations can
+omit later items. `HOSTED_HISTORY_MAX_ITEMS` defaults to `40` and accepts
+`1..500`, but values above 100 do not increase this one-page read.
+`HOSTED_HISTORY_MAX_TOKENS` defaults to `8000` and accepts `256..200000`.
+`drop_oldest` first limits items within the returned page, then drops the
+oldest remaining items to meet an estimated token budget. The estimate is
+one token per four text characters, rounded up, with a minimum of one.
+The final remaining item is never dropped even if it exceeds the budget.
+The current ask is added afterward, so these are not hard request-token
+limits or a guarantee of the latest complete history.
+
+A rejected presented owner handle never starts a replacement conversation.
+History-read or append failures are explicit failures. A failed append must
+not be presented as a durably saved turn. Locks and the bounded idempotency
+cache are process-local, not distributed.
+Delegated ownership enforcement on the deployed Conversations/items surface
+still needs independent live validation. Do not treat mocked store tests as
+proof of platform authorization.
+
+### Optional ingestion
+
+`DEPLOY_INGESTION_CONTAINER_APP` is read through the App Configuration provider,
+not a separate environment-only flag. Missing preserves legacy upload
+eligibility. `false` hides standalone and Entra Copilot controls and rejects
+attachments in the handler and ingestion client. The client checks the flag
+before URL/Dapr resolution, payload construction, file reads, or HTTP client
+creation. Invalid boolean values raise a configuration error.
+
+`true` retains authentication, anonymous-Copilot denial, file validation, and
+conversation checks. Hosted uploads require an already issued hosted
+conversation ID. There is no native conversation upload replacement.
+The flag does not change text chat, OBO, or retrieval authorization.
## Installed resources
-The supported installed artifact is **code plus staged assets**, not a
-self-contained wheel. Keep `.chainlit/`, `public/`, `chainlit.config.yaml`,
-`chainlit.md` and `VERSION` together in the writable application root.
-Use existing `CHAINLIT_APP_ROOT` for an explicitly staged root, or start from
-that directory. A source-checkout `main.py` passes its adjacent asset root.
+The installed artifact is code plus staged assets, not a self-contained wheel.
+Keep `.chainlit/`, `public/`, `chainlit.config.yaml`, `chainlit.md`, and
+`VERSION` together in a writable application root. Set `CHAINLIT_APP_ROOT`
+for an explicitly staged root, or start from that directory. A source-checkout
+`main.py` passes its adjacent asset root.
+
The root is resolved before importing Chainlit and is shared by VERSION,
-config synchronization and Chainlit public/upload paths. Never write to
-site-packages or introduce `sys.path`/`sys.modules` compatibility tricks.
+configuration synchronization, public assets, and upload paths. Do not write
+to site-packages or add `sys.path`/`sys.modules` compatibility workarounds.
+Missing VERSION is optional. Missing `.chainlit/config.toml` retains a warning.
-Missing VERSION remains optional; missing `.chainlit/config.toml` retains the
-existing warning behavior. Disconnected App Configuration remains a 503
-not-ready application rather than a successful chat fallback.
+## Contributor validation
+
+The complete local behavioral suite is:
+
+```text
+python -m unittest discover -s tests -v
+```
+
+Focused package checks are:
```text
python -m pip wheel --no-deps --wheel-dir .artifacts/wheel .
@@ -104,105 +220,29 @@ python -m unittest discover -s tests -p test_module_compatibility.py -v
python -m unittest discover -s tests -p test_installed_package.py -v
```
-Installed acceptance uses temporary non-repository directories and a
-non-editable wheel. It creates a clean venv, installs the unchanged
-`requirements.txt`, runs `pip check`, and asserts canonical and legacy UI
-imports resolve from that environment. It also copies the existing behavioral
-tests, not application sources, and runs them against the installed wheel.
-This clean dependency/installed behavior coverage supersedes the earlier
-system-site-packages checkpoint; it is not itself Linux image evidence.
+Installed-package acceptance builds a wheel, creates a clean non-editable
+environment, installs runtime requirements, runs `pip check`, and verifies
+canonical and legacy import origins outside the checkout. It copies
+behavioral tests, not runtime sources. This can install dependencies and is
+not evidence of a Linux image or live Azure integration.
-## Quality interface and current adoption limits
+### Protected quality checks
+
+Replace `` below with the immutable base SHA for the
+actual target branch. Do not substitute the candidate head to avoid findings.
```text
python .github/scripts/check-quality.py --check all --base-ref --report .artifacts/quality.json
python -m unittest discover -s tests -p test_quality_policy.py -v
```
-`--check` accepts `all`, `lint`, `typing`, `architecture`, `exceptions`, `policy`.
-Exit 0 means passed, 1 means violations, 2 means invalid inputs or incomplete
-tool execution. Reports bind base/head/policy SHAs, installed tool versions,
-findings, typing coverage and broad-handler inventory. Checks do not rewrite
-source, policy or baselines, import runtime code, or connect to Azure.
-
-### Policy and execution evidence
-
-The four governance records have closed schemas: unknown/missing fields,
-duplicate keys/IDs/sites, invalid dates/versions, unsafe paths, inconsistent
-scope references and duplicate move destinations fail. Module records include
-explicit typing status, public/private ownership and exact adapter forwarding
-declarations. An inventoried facade may forward through a declared adapter
-chain only if it terminates at an existing, declared canonical export. Extra
-imports, mutable state, initialization calls or changed forwarding order fail.
-
-Protected typing identities survive import/path moves; new modules enter
-blocking coverage independently of candidate scope. Policy also requires those
-additions to be recorded before merge, so coverage survives the following PR;
-unknown mypy severities or malformed locations are execution errors, not
-silently discarded diagnostics. An ordinary move must have
-one-to-one identity and unchanged normalized syntax. Changed moves and splits
-require protected responsibility/debt allocation; they are not automatically
-approved by a candidate map. Individual retained debt is intersected with the
-protected baseline, permitting genuine retirement without permitting revival
-or duplication.
-
-Discovery includes root namespace packages without `__init__.py`, not only
-regular packages. Their concrete Python modules receive normal graph, handler
-and blocking typing identities. Explicit non-runtime roots (`tests`, `scripts`,
-`docs`, `infra`, `public`, generated `build`/`dist`, dependency caches and hidden
-directories) remain excluded. Mypy uses explicit package bases at `src` and the
-repository root so a namespace file cannot acquire conflicting flat/imported
-module names. These are checker settings, not runtime `sys.path` changes.
-
-Annotation policy compares qualified functions, overload occurrences, parameter
-kinds, variadic arguments, returns, variables, decorators and local module-level
-type aliases. Suppression comments are bound to syntax sites, not just counted;
-moving an unchanged ignore to another error is a policy change. String literals
-that merely mention `noqa` are not directives. These are static syntax checks,
-not a proof about arbitrary Python metaprogramming.
-
-Qualified and aliased `typing`/`typing_extensions.no_type_check` and
-`no_type_check_decorator` uses are checked even on functions without annotated
-arguments or returns. Decorator-provider bindings are part of the interface
-snapshot, so changing a provider behind an unchanged `@alias` cannot silently
-disable body checking.
-
-Variable dynamic imports need exact site/context fingerprints, permitted
-targets and executed behavior tests. All permitted first-party targets enter
-the dependency graph. Import aliases are resolved in lexical scopes, and
-package-relative imports use inventoried `__init__.py` ownership. Literal loader
-aliases and keyword arguments are resolved as well. Handler fingerprints cover
-the protected `try` operation, catch/outcome
-and distinct occurrences, so identical handler bodies at separate sites cannot
-share one allowance.
-
-The bounded binding resolver tracks lexical assignments, builtins aliases and
-plain local class-member aliases without executing application code. Handler
-identity includes effective types and binding source: changing an alias from
-`Exception` to `BaseException`, or rebinding a class member, invalidates the old
-record. Ambiguous, cyclic, parameter-supplied or otherwise unresolved catch
-expressions remain violations and cannot consume even an active record with
-passing tests. Existing nominal exception imports retain their treatment,
-including conditional hosted-client imports; import guards are included in
-binding identity rather than forcing eager runtime imports.
-
-`--test-evidence ` supplies an optional execution receipt to the quality
-CLI. Without valid evidence, no exception or dynamic-import behavior approval
-can be consumed. The separate `.github/scripts/run-unittest.py` uses standard
-`unittest` discovery and records each exact test method, source fingerprints
-and outcome. Referenced skipped/expected-failure tests are not passing evidence;
-failed subtests, unexpected successes, stale source or a failed suite cannot
-validate a boundary. Candidate exception records must be active (eligible for
-technical validation, not independently approved),
-match the exact source/caught types, have an unexpired `expires_on` date and
-not have passed their `review_by_stage` (`bootstrap`, `blocking`, `strict`).
-Contractual best-effort outcomes remain representable; logging is not approval.
-Registry edits alone do not cause a policy-change finding. Independent human
-review and approval strings are not prerequisites; `review` remains nonempty
-provenance for schema compatibility. Proposed/retired records cannot be consumed.
-Only technically validated exact sites exempt BLE001; other lint findings remain.
-
-For a local, same-run receipt in PowerShell:
+`--check` accepts `all`, `lint`, `typing`, `architecture`, `exceptions`, and
+`policy`. Exit 0 means passed, 1 means violations, and 2 means invalid inputs
+or incomplete execution. Checks do not rewrite source, policy, or baselines
+and do not connect to Azure. Tool pins are in `requirements-quality.txt`.
+
+Exception and dynamic-import validation also requires a same-run execution
+receipt. In PowerShell, replace the base placeholder in both commands:
```powershell
$env:QUALITY_RUN_ID = [guid]::NewGuid().ToString()
@@ -210,147 +250,28 @@ python .github\scripts\run-unittest.py --base-ref --report
python .github\scripts\check-quality.py --check all --base-ref --test-evidence .artifacts\unittest.json --report .artifacts\quality.json
```
-Set `QUALITY_RUN_ID` once per run when using other shells as well. CI instead
-binds the GitHub run ID and attempt. Receipts/reports bind repository, base/head,
-source digest and run context; quality artifacts also bind protected policy,
-toolchain and the exact receipt digest. Integrity hashes detect alteration,
-not authenticity: the protected runner/evaluator and actual job conclusions
-remain the trust boundary. `--pattern` on the unittest runner supports focused
-local work, but the CI aggregate requires full `test_*.py` discovery.
-
-The existing workflow extracts the protected-base runner, checker, aggregator
-and tool pins when policy exists. It installs protected runtime requirements
-and tool pins in a separate evaluator environment, never the candidate package,
-editable metadata, dependency manifest or build backend. Ruff and mypy run in
-Python isolated mode. Grimp and Import Linter analyze the explicit candidate
-source directory in an isolated worker without importing application code or
-adding the candidate checkout to the evaluator import path. This rejects
-candidate `mypy.py`, `grimp.py`, `sitecustomize.py` and `PYTHONPATH` shadowing.
-The quality matrix consumes the separate
-unittest artifact, and `quality-gate` requires the real matrix, unittest and
-`container-tests` results plus every fresh artifact. The container job builds
-an ephemeral Ubuntu Docker image and runs `tests/container_smoke.py` with
-networking disabled; it does not publish an image or deploy to Azure. Its
-successful result supplies Linux image evidence for that candidate, not live
-Azure integration. A missing local Docker engine is a local limitation, not
-proof that Linux CI cannot run.
-
-Tested tool pins are Ruff 0.16.5, mypy 2.3.1, Import Linter 2.14 and Grimp 3.16.
-The higher Ruff/Import Linter/Grimp versions proposed during parent research
-were unavailable from the configured package index; no runtime pin changed.
-
-The initial blocking typing seeds are the canonical `config.chat_backend`,
-`config.panel_config` and `config.hosted_continuity_config`. Newly introduced
-modules and legacy adapters also enter blocking coverage. Existing moved
-modules retain stable IDs and their prior uncovered status. The explicit
-scope, future expansion and one-to-one moves are in `typing-scope.json`.
-`typing-baseline.json` is empty: debt cannot be traded by total error count,
-duplicated or revived after retirement. Imported legacy diagnostics stay visible
-without being described as full-repository typing coverage.
-
-Ruff selects F821, E722, BLE001, PGH003, PGH004 and RUF100. The supplementary
-AST handler inventory includes logged and re-raised broad handlers that BLE001
-exempts. `.quality/handler-inventory.json` records the remaining sites.
-`exceptions.json` contains 30 individually documented technical boundaries:
-each identifies its exact operation/catch fingerprint, rationale, diagnostic
-path, observable outcome and executed failure-test selector. Records retain their
-2027-01-06 expiry and blocking review stage. No blanket inherited
-handler waiver is granted. Narrowing removes 35 of the original 63 broad sites;
-the remaining 28 plus two companion cleanup sites preserve application-level translation, cleanup/propagation
-or contractual best-effort behavior. Logging alone does not approve them.
-
-The technical review in `.quality/exception-technical-review.json` records a
-decision for every initial-adoption boundary. It is an AI technical assessment,
-not independent human approval. Twenty-three diagnostic paths needed correction:
-dependency exception text, exception chains and raw citation URLs must not enter
-their logs. `telemetry.boundary_diagnostics.log_boundary_failure` preserves a
-static operation description, exception class and terminal code location without
-exception text, chained messages, source lines or local variables. The seven
-unchanged boundaries retain their existing propagation/static-diagnostic behavior.
-Changed handler fingerprints are now `active`, meaning eligible for technical
-validation under the revised policy, not independently approved. Each must
-match current source and passing bound evidence; no approval is inherited.
-The new evaluator was adopted into the protected `develop` base by PR #144
-(merged `ec18a73`), so the protected checker now applies it to subsequent
-PRs. This assessment
-does not extend expiry, reduce typing coverage, suppress checks or authorize
-production deployment.
-
-The [explicit authorization by repository administrator @placerda](https://github.com/Azure/agent-landing-zone/issues/681#issuecomment-5601804634)
-accepts these exact existing records and the policy for initial administrative
-adoption under Q5. It is not an independent GitHub review, a claim that bootstrap
-checks pass, or authorization for production deployment. Only exact active,
-unexpired records with same-source passing evidence can authorize an exception.
-The initial PR (#144) reported a missing protected policy as `bootstrap-review`
-because its base predated adoption; candidate data still cannot change the
-evaluator's rules. This reference PR runs against the adopted `develop` base,
-exercising coordinator-owned required-check controls. The recorded exception
-decisions are governed by their technical evidence — exact fingerprints,
-same-source bound tests and unexpired stage validation — not by a separate
-independent human review step; this PR demonstrates base adoption with that
-technical governance already in force.
-Installed acceptance covers real auth order, upload writes/cleanup and
-standalone download/OpenAPI failures outside the checkout. Linux acceptance
-is supplied by the actual `container-tests` job for the PR head, not inferred
-from local Windows results. The quality fixtures include disposable Git
-repositories and real unittest/aggregate subprocesses for protected-policy,
-receipt and false-green mutations; they do not substitute for controlled
-required-check acceptance PRs. Subsequent checker/tool/workflow and other protected
-policy changes still require authorized protected-policy adoption; ordinary
-exception registry edits do not.
-The bootstrap PR cannot approve itself by editing JSON: a base without policy
-reports `bootstrap-review`. Ordinary PR checks execute the protected base's
-checker and tool config as their minimum, and separately report candidate
-policy changes. CI is unprivileged and `quality-gate` requires both the actual
-matrix/unit-test/container results and all five fresh bound quality reports
-plus the unittest receipt.
-
-An administrator must separately require `quality-gate` and `unit-tests` on
-development/release-target branches and restrict bypass. Q5 no longer mandates
-independent/latest-head human approval. Existing repository review settings, if
-any, are separate controls; this change does not modify them. `@placerda` was confirmed
-as a repository administrator before adding CODEOWNERS. No settings were
-applied. Workflow YAML, approval strings and a green local command cannot prove
-required merge enforcement; bootstrap administration and controlled negative
-PR evidence remain separate acceptance activities.
-
-The decision and rollback policy are recorded in platform
-`docs/adr/ADR-0020-technical-exception-validation.md`. Adopt the new protected
-evaluator before claiming this policy is enforced remotely. Revert evaluator
-and registry together if rolling back; do not disable checks or fabricate green
-results to bridge adoption. Runtime behavior, expiry (2027-01-06), tool pins,
-workflow permissions and aggregate requirements are unchanged.
-Policy diagnostics describe differences from the protected files/settings,
-not requests for human approval. The legacy `bootstrap-review` rule name is
-retained for report compatibility; it denotes a missing protected policy, not
-a requirement to solicit reviewers or obtain an administrative statement.
-`CODEOWNERS` routes ownership but does not mandate latest-head approval.
-
-### Implementation and failure-fixture map
-
-| Scope | Implementation and executable evidence |
-| --- | --- |
-| T004/T008/T011/T014/T017/T020/T023/T026/T029 | Protected checker, four governance records, pinned tools and fail-closed workflow; `test_quality_policy.py` covers schemas, policy tampering, moves/splits, suppressions, dynamic bindings, debt, exact evidence, isolated execution and independent aggregation. |
-| T031-T034 | Setuptools discovery, canonical config/util/auth/client/service owners, shared citations and bounded `api.history`/`services.history` split; `test_module_compatibility.py`, `test_history_boundary.py`, datalayer/citation and original security suites. |
-| T035/T036 | API callbacks/routes, telemetry, bootstrap, thin legacy adapters, conditional initialization and staged roots; installed import-order/single-registration and actual startup acceptance. |
-| T030/T038 | Canonical private test seams and dedicated legacy assertions; `test_installed_package.py` creates a clean non-editable environment, copies only behavioral tests and checks real auth/startup, staged resources and writable upload cleanup without runtime source leakage. |
-| T037 | Docker package installation, unchanged Uvicorn startup, ephemeral image build and offline `container_smoke.py`; real aggregate dependency and image behavioral suite. |
-| T041 / UI part of T045 | This contributor guide, `AGENTS.md`, PR #110 evidence and coordinated documentation/umbrella handoff. |
-| Dependency failures | `test_failure_contracts.py` exercises real retry/connection parsing and concrete Azure, HTTPX, JWT and logging errors alongside unexpected-defect propagation. |
-| Retained application boundaries | `test_boundary_failures.py`, the socket failure cases in `test_embed_security.py`, existing secure-download/panel-hook tests and installed startup failure acceptance; exact selectors are recorded per authorized site. |
-
-## Recovery and coordinated documentation
-
-This UI PR has no dependency on an unmerged peer runtime change. Restore the
-previous UI artifact built from `c635bc6696714b543feec24b4a062a8a8f3ff6d0`
-(runtime-equivalent to researched v2.6.2) while retaining the existing shipped
-orchestrator/ingestion combination. No data, config key or RBAC migration is
-needed. Revert the package/code slice through a PR if rolling source back;
-do not disable required checks to bypass a checker defect. Production recovery
-has not been rehearsed by this change.
-
-Canonical contributor documentation is coordinated in
-[Azure/agent-landing-zone#688](https://github.com/Azure/agent-landing-zone/pull/688). Existing local
-embedding and deployment instructions retain their operator/security claims;
-this change does not redesign those behaviors or publish planned controls as
-already active.
+Set `QUALITY_RUN_ID` once per local run in other shells too. The receipt
+binds exact test methods, source fingerprints, base/head, and run context.
+Skipped, expected-failure, stale, or failed evidence cannot validate a site.
+Local `--pattern` supports focused runs. CI aggregation requires full
+`test_*.py` discovery.
+
+Active exception records are eligible for technical validation, not blanket
+approval. Each needs exact-source passing evidence, an unexpired date, and a
+valid review stage. Proposed or retired records cannot be consumed.
+Registry edits alone do not block, and an independent human-review field is
+not an execution requirement. Protected checker, tool, workflow, schema,
+typing, import, and suppression rules still apply. Do not grow baselines,
+disable checks, or fabricate reports to make a change pass.
+
+CI selects the protected-base evaluator and tool pins in a separate
+environment that does not install candidate code. The `quality-gate` job
+requires actual unit-test, quality-matrix, and ephemeral Linux container-test
+results plus fresh bound reports and the unittest receipt. The container
+helper runs without networking and does not publish or deploy an image.
+A local command or workflow file does not prove branch protection or required
+merge checks. Those settings remain separate repository controls.
+
+For engineering-document and agent-asset validation, see
+[the engineering framework](agentic-engineering.md#validation).
+For embedding security, see [Chainlit Copilot embedding](copilot-embedding.md).
diff --git a/infra/README.md b/infra/README.md
index 0eb83d1..34e81cb 100644
--- a/infra/README.md
+++ b/infra/README.md
@@ -1,3 +1,21 @@
-The infrastructure for this project is defined in: https://github.com/Azure/agent-landing-zone
+# UI deployment infrastructure
-After deploying the infrastructure, you can use `azd env refresh` and `azd deploy` to deploy the frontend.
+`main.bicep` is an empty resource-group-scoped entry point. It does not
+provision the Agent Landing Zone platform.
+
+Provision the platform through the
+[central deployment guide](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/deploy-full-stack/).
+For component-only redeployment, the UI's `azure.yaml` runs
+`scripts/deploy.ps1` or `scripts/deploy.sh` as its predeploy hook.
+The scripts use the existing platform resources and can build an image in
+ACR and update the frontend Container App. They are not offline checks.
+
+Use the platform's subscription, resource group, and `azd` environment.
+Initialize or select that environment before running `azd env refresh`
+and `azd deploy`. The deployment scripts prefer a non-empty shell
+`APP_CONFIG_ENDPOINT` over the `azd` environment value and warn if they
+differ. Check the endpoint before deploying.
+
+See [configuration](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/configuration/)
+and [operations](https://azure.github.io/AI-Landing-Zones/agent-landing-zone/operations/)
+for the operator workflow.