Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
52 changes: 48 additions & 4 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,14 +349,58 @@ Remote A2A failures use `a2a.<reason>` or `a2a.http.<status>` 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.
28 changes: 28 additions & 0 deletions test_documentation_contract.py
Original file line number Diff line number Diff line change
@@ -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
Loading