From d88dadba3d7eaf24db026926cd49fb282c3b6277 Mon Sep 17 00:00:00 2001 From: Zhoujie628 Date: Tue, 13 Oct 2026 06:35:39 +0800 Subject: [PATCH] docs: clarify center authentication and notification lifecycle Signed-off-by: Zhoujie628 --- CHANGELOG.md | 6 ++-- DEVELOPER_GUIDE.md | 52 +++++++++++++++++++++++++++++++--- test_documentation_contract.py | 28 ++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 test_documentation_contract.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d1f88e8..3cdaf9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A `Notification-T` subscription whose remote never acknowledged stayed pending forever, pinning the caller's coroutine and the connection. The first acknowledgement is now bounded by `notification_ack_timeout_seconds` (default - 300s); SSE idleness after the acknowledgement is deliberately not bounded. + 300s). After acknowledgement, a separate no-transport-activity deadline applies; + heartbeats renew that deadline and notification result states do not close the stream. - `sse_normalization` patched `json_format.Parse` with a duplicate keyword argument, so every `StreamResponse` text parse raised `TypeError`. The patch now covers only `ParseDict`, the entry point that actually performs the @@ -94,7 +95,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `last_business_event_at`. This matches the Java engine's `NotificationSubscription`. - `NotificationSubscription.is_healthy()` judges liveness from transport activity, so a subscription that is only receiving heartbeats is no longer reported as dead. A - freshly opened subscription counts as healthy, since opening the stream is activity. + freshly opened subscription is not yet healthy: opening starts its idle budget but + is not received-byte evidence. Health becomes true after actual transport activity. - A notification stream is now **released after `send_timeout_seconds` without any transport activity** (configurable through `ExtensionSender(notification_idle_timeout_seconds=...)`), mirroring the Java engine's diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index e40b429..c5b166d 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -349,14 +349,58 @@ Remote A2A failures use `a2a.` or `a2a.http.` when no reason is `RegistryClient`, `load_psop`, and `search_psop` accept `connect_timeout_seconds` and `read_timeout_seconds` (both default to 30 seconds). Both must be positive. -## 10. Workflow retrieval +## 10. Workflow retrieval and center authentication + +Center authentication is independent from dispatched-agent `AuthProvider` credentials. +The orchestration public API uses the same account as web login: complete the first password +change in the UI, then `POST /api/v1/auth/token` and send the returned Bearer in headers. +Do not reuse registry credentials for the orchestration center, or put tokens in query strings. ```python -matches = await search_psop(base_url, intent, top_n=5, access_token=token) -workflow = await load_psop(base_url, matches[0].workflow_id, access_token=token) +import os +import httpx +from workflow_engine.client.ssl_context import create_ssl_context +from workflow_engine.registry.registry_client import RegistryClient, load_psop, search_psop + +# The same TLS material/policy must be used for login and resource requests. +tls_options = {"ssl_verify": True, "ca_certs_path": os.environ["CENTER_CA_FILE"]} +verify = create_ssl_context( + verify_server=tls_options["ssl_verify"], ca_certs_path=tls_options["ca_certs_path"] +) +base_url = os.environ["ORCH_URL"].rstrip("/") +intent = "Find a workflow matching the host request" +async with httpx.AsyncClient(verify=verify, timeout=30, follow_redirects=False) as client: + response = await client.post( + base_url + "/api/v1/auth/token", + json={"username": os.environ["ORCH_USERNAME"], "password": os.environ["ORCH_PASSWORD"]}, + ) + response.raise_for_status() + session = response.json() + token = session["access_token"] + expires_in = session["expires_in"] # host-owned cache deadline; never log session/token + +matches = await search_psop(base_url, intent, top_n=5, access_token=token, **tls_options) +if matches: + workflow = await load_psop( + base_url, matches[0].workflow_id, access_token=token, **tls_options + ) -registry = RegistryClient(registry_url) +registry = RegistryClient( + os.environ["REGISTRY_URL"], access_token=os.environ["REGISTRY_API_TOKEN"], **tls_options +) agent_cards = await registry.fetch_agent_cards() ``` +This is a single-session example, not an SDK-managed account provider. Python currently requires +the **host** to acquire/cache tokens against a monotonic deadline shorter than `expires_in`, +serialize concurrent refresh, and discard them after logout, password change or deletion. +For read-only retrieval, the host may renew once after 401 and retry once; never loop on 401, +refresh on 403/503, or blindly replay write/execution requests. Java's +`OrchestrationAccountAuth` implements that lifecycle; Python's PSOP helpers do not yet do so. +A host-owned provider can be added separately; do not claim automatic refresh from this example. + +Head/source capabilities and published packages are different delivery stages. Pin a released +version supported by the consuming center; an editable local install does not update a container +dependency, and this documentation change does not publish a new SDK. + `ssl_verify=False` is available for controlled development endpoints with untrusted certificates. It is scoped to the created HTTP client and does not change process-wide TLS defaults. diff --git a/test_documentation_contract.py b/test_documentation_contract.py new file mode 100644 index 0000000..802c876 --- /dev/null +++ b/test_documentation_contract.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Syntax and claims of the center-account/long-lived notification examples.""" +import ast +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + + +def test_center_login_example_uses_actual_top_level_response_and_separate_tokens(): + text = (ROOT / "DEVELOPER_GUIDE.md").read_text(encoding="utf-8") + begin = text.index("import os\nimport httpx") + end = text.index("\n```", begin) + snippet = text[begin:end] + ast.parse(snippet) + assert 'session = response.json()' in snippet + assert 'response.json()["data"]' not in snippet + assert 'access_token=os.environ["REGISTRY_API_TOKEN"]' in snippet + assert "**tls_options" in snippet + assert "Python's PSOP helpers do not yet do so" in text + + +def test_notification_changelog_does_not_invent_received_activity(): + text = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + assert "freshly opened subscription counts as healthy" not in text + assert "SSE idleness after the acknowledgement is deliberately not bounded" not in text + assert "notification result states do not close" in text