cloud-dog-api-kit 0.13.0__tar.gz → 0.13.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloud_dog_api_kit-0.13.2/.docs-manifest.yml +6 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/CHANGELOG.md +13 -0
- cloud_dog_api_kit-0.13.2/DATA-MODEL.md +33 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/PKG-INFO +1 -1
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/README.md +1 -1
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/__init__.py +2 -1
- cloud_dog_api_kit-0.13.2/cloud_dog_api_kit/agent/__init__.py +11 -0
- cloud_dog_api_kit-0.13.2/cloud_dog_api_kit/agent/session.py +113 -0
- cloud_dog_api_kit-0.13.2/cloud_dog_api_kit/agent/strategy.py +36 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_sdk.py +23 -3
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/tool_router.py +1 -1
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/transport.py +144 -1
- cloud_dog_api_kit-0.13.2/cloud_dog_api_kit/web/__init__.py +23 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/web/proxy.py +61 -3
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/pyproject.toml +1 -1
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.36_MCPTransportHelpers/test_mcp_transport.py +54 -3
- cloud_dog_api_kit-0.13.2/tests/unit/UT1.40_AgentSession/test_agent_session.py +158 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.47_WebApiProxyHeaders/test_web_proxy_headers.py +62 -2
- cloud_dog_api_kit-0.13.2/tests/unit/UT_B5A_SyncClass/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_B5B_ClientSDK/test_client_sdk.py +36 -1
- cloud_dog_api_kit-0.13.2/working/w28b-309/diff-check.log +0 -0
- cloud_dog_api_kit-0.13.2/working/w28b-309/focused-tests.log +39 -0
- cloud_dog_api_kit-0.13.2/working/w28b-309/regression-tests.log +33 -0
- cloud_dog_api_kit-0.13.2/working/w28b-309/secret-grep.log +0 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/git-diff-check.log +0 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/import-smoke.log +3 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/platform-agent-tests.log +2 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/platform-api-kit-tests.log +33 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/platform-cache-tests.log +2 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/platform-jobs-tests.log +3 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/platform-llm-tests.log +2 -0
- cloud_dog_api_kit-0.13.2/working/w28b-310/ps96-greps.log +0 -0
- cloud_dog_api_kit-0.13.0/cloud_dog_api_kit/web/__init__.py +0 -7
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/.gitignore +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/AGENT-INSTRUCTION-FIX-API-KIT.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/ARCHITECTURE.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/BUILD.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/LICENCE +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/LICENSE +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/NOTICE +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/REQUIREMENTS.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/TESTS.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/adoption_test.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/a2a/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/a2a/card.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/a2a/events.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/a2a/gateway.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/a2a/skill_audit.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/auth/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/auth/dependency.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/auth/rbac.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/auth/service_auth.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/clients/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/clients/circuit_breaker.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/clients/http_client.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/clients/retry.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/compat/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/compat/envelope.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/compat/profile.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/compat/routes.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/config.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/correlation/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/correlation/context.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/correlation/middleware.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/envelopes/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/envelopes/error.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/envelopes/success.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/errors/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/errors/exceptions.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/errors/handler.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/errors/taxonomy.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/factory.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/idempotency/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/idempotency/middleware.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/idempotency/store.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/lifecycle/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/lifecycle/hooks.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/lifecycle/shutdown.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/async_jobs.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_transport/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_transport/base.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_transport/exceptions.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_transport/http_jsonrpc.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_transport/legacy_sse.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_transport/stdio.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/client_transport/streamable_http.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/contract.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/error_mapper.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/gateway.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/legacy_sse.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/session.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/sync_handler.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/mcp/tool_audit.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/middleware/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/middleware/cors.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/middleware/logging.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/middleware/request_size_limit.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/middleware/timeout.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/middleware/timing.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/openapi/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/openapi/customise.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/openapi/route.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/routers/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/routers/crud.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/routers/health.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/routers/jobs.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/routers/version.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/schemas/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/schemas/envelopes.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/schemas/filters.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/schemas/pagination.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/streaming/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/streaming/events.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/streaming/jsonl.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/streaming/sse.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/conformance.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/fixtures.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/flows/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/flows/auth_flow.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/flows/crud_flow.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/flows/job_flow.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/testing/flows/streaming_flow.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/traceability_ids.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/versioning/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/versioning/header.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/webhook/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/cloud_dog_api_kit/webhook/signature.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/docs/ARCHITECTURE.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/docs/CONFIGURATION.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/docs/EXAMPLES.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/examples/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/examples/fastapi_app/README.md +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/examples/fastapi_app/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/examples/fastapi_app/app.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/application/AT1.7_MigrationPatternLegacyToVersioned/test_migration_pattern.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/conftest.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/env-AT +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/env-IT +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/env-ST +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/env-UT +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/integration/IT1.8_MCPGatewayIntegration/test_mcp_gateway_integration.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/integration/IT1.9_MCPClientTransports/test_mcp_client_transports.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/integration/IT1_10_A2AEventsCommonPackage/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/integration/IT1_10_A2AEventsCommonPackage/test_a2a_events_common_package.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/quality/QT_PUBLISH_COMPLIANCE/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/quality/QT_PUBLISH_COMPLIANCE/test_publish_compliance.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/quality/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/security/SEC1.1_AuthEnforcement/test_auth_enforcement.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/security/SEC1.2_RBACEnforcement/test_rbac_enforcement.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/security/SEC1.3_TenantIsolation/test_tenant_sec.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/security/SEC1.4_InputValidation/test_input_validation.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/security/SEC1.5_SecretRedaction/test_secret_redaction.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/security/SEC1.6_ErrorSanitisation/test_error_sanitisation.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.10_IdempotencyEndToEnd/test_idempotency_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.11_HTTPClientEndToEnd/test_http_client_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.12_MCPFullTransportFlow/test_mcp_transport_flow.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.13_StartupLifecycleIntegration/test_startup_lifecycle_integration.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.1_FullMiddlewareStack/test_full_stack.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.2_ErrorFlowEndToEnd/test_error_flow.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.3_PaginationEndToEnd/test_pagination_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.4_AuthEndToEnd/test_auth_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.5_HealthStatusEndToEnd/test_health_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.6_AppFactory/test_app_factory.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.7_CRUDFlowEndToEnd/test_crud_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.8_JobFlowEndToEnd/test_job_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/system/ST1.9_StreamingEndToEnd/test_streaming_e2e.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/test_traceability_ids.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.10_ServiceAuth/test_service_auth.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.11_CorrelationContext/test_correlation_context.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.12_CorrelationMiddleware/test_correlation_middleware.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.13_PaginationModels/test_pagination_models.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.14_PaginationDependency/test_pagination_dep.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.15_FilterHelpers/test_filters.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.16_CRUDRouter/test_crud_router.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.17_HealthRouter/test_health_router.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.18_ReadyLiveEndpoints/test_ready_live.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.19_JobRouter/test_job_router.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.1_SuccessEnvelope/test_success_envelope.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.20_SSEStreaming/test_sse.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.21_JSONLStreaming/test_jsonl.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.22_SSEEventModel/test_sse_event.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.23_HTTPClient/test_http_client.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.24_RetryPolicy/test_retry_policy.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.25_CORSHelper/test_cors.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.26_RequestLogging/test_request_logging.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.27_TimeoutMiddleware/test_timeout.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.28_IdempotencyMiddleware/test_idempotency.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.29_IdempotencyStore/test_idem_store.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.2_ErrorEnvelope/test_error_envelope.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.30_VersionHeader/test_version_header.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.31_OpenAPICustomise/test_openapi.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.32_MCPGateway/test_mcp_gateway.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.33_A2AGateway/test_a2a_gateway.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.34_TestFixtures/test_fixtures.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.35_ConformanceValidators/test_conformance.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.37_MCPClientTransports/test_base.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.37_MCPClientTransports/test_http_jsonrpc.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.37_MCPClientTransports/test_legacy_sse.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.37_MCPClientTransports/test_stdio.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.37_MCPClientTransports/test_streamable_http.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.37_MCPToolRouter/test_mcp_tool_router.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.38_StartupLifecycleHooks/test_lifecycle_hooks.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.39_LegacyEnvelopeMiddleware/test_legacy_envelope.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.3_ErrorTaxonomy/test_error_taxonomy.py +0 -0
- {cloud_dog_api_kit-0.13.0/tests/unit/UT_B5A_SyncClass → cloud_dog_api_kit-0.13.2/tests/unit/UT1.40_AgentSession}/__init__.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.40_LegacyRouteAdapter/test_legacy_route_adapter.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.41_MCPSessionLifecycle/test_mcp_session.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.42_ProfileContextMiddleware/test_profile_context.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.43_WebhookSignatureVerification/test_webhook_signature.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.44_RequestSizeLimit/test_request_size_limit.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.45_GracefulShutdown/test_graceful_shutdown.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.46_MCPContractRegistration/test_mcp_contract_registration.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.4_ErrorExceptions/test_error_exceptions.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.5_ErrorHandler/test_error_handler.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.6_AuthDependency/test_auth_dependency.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.7_BearerAuth/test_bearer_auth.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.8_RBACHelpers/test_rbac.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT1.9_TenantIsolation/test_tenant.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_A2AEvents/test_a2a_events.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_A2AEvents/test_http_ingest_adapter.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_A2AEvents/test_persistent_event_broadcaster.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_A2AEvents/test_rest_poll_adapter.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_A2AEvents/test_websocket_adapter.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_A2aSkillAudit/test_a2a_skill_audit.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_B5A_SyncClass/test_sync_class.py +0 -0
- {cloud_dog_api_kit-0.13.0 → cloud_dog_api_kit-0.13.2}/tests/unit/UT_McpToolAudit/test_mcp_tool_audit.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.2 - 2026-06-05
|
|
4
|
+
- MCP `sync-with-progress` dispatch now allows the full PS-95 five-minute Mode 2
|
|
5
|
+
sync budget instead of clamping fallback execution to the short 55-second
|
|
6
|
+
budget. This lets progress-aware clients receive the final tool result for
|
|
7
|
+
migrated long-call services after the old Traefik timeout window.
|
|
8
|
+
|
|
9
|
+
## 0.13.1 - 2026-06-05
|
|
10
|
+
- MCP streamable HTTP transport now emits `notifications/progress` SSE frames for
|
|
11
|
+
`sync-with-progress` tools when the client advertises `text/event-stream`, then
|
|
12
|
+
returns the final JSON-RPC tool result on the same stream. This fixes W28D-323
|
|
13
|
+
Mode 2 progress-aware client compatibility for migrated services while keeping
|
|
14
|
+
stock JSON clients on the existing final-result response path.
|
|
15
|
+
|
|
3
16
|
## 0.12.4 - 2026-05-04
|
|
4
17
|
- `WebApiProxy._build_headers` now performs a case-insensitive merge of caller-supplied headers with caller-wins semantics. Previously, Python dict case-sensitivity allowed BOTH `X-API-Key` (the proxy's service key) and `x-api-key` (the caller's forwarded header from FastAPI request iteration) to coexist; httpx emitted both as separate header lines and Starlette's first-match selection caused the service key to override the caller's authenticated identity downstream — breaking RBAC such that any developer making a proxied request impersonated admin. The fix removes any existing case-insensitive duplicate of an incoming header before adding the caller's value, preserving the caller's identity for downstream authorisation. Version skips 0.12.3 because that internal-PyPI release was reverted (untested + bundled an unrelated shared-client change). Resolves the D-RBAC root cause documented in W28A-#A51-REPORT-2026-05-04.md Section D.
|
|
5
18
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
template-id: T-PKG-DMT
|
|
3
|
+
template-version: 1.0
|
|
4
|
+
applies-to: DATA-MODEL.md
|
|
5
|
+
registry: package
|
|
6
|
+
required: must-have
|
|
7
|
+
when-applicable: ""
|
|
8
|
+
template-last-updated: 2026-06-12
|
|
9
|
+
template-owner: platform-standards
|
|
10
|
+
|
|
11
|
+
project: platform-api-kit
|
|
12
|
+
public: true
|
|
13
|
+
doc-last-updated: 2026-06-12
|
|
14
|
+
doc-git-commit: no-git
|
|
15
|
+
doc-git-branch: main
|
|
16
|
+
doc-source-shas: []
|
|
17
|
+
doc-age-policy: indefinite
|
|
18
|
+
doc-conformance-stamp: 2026-06-12T12:00:00Z
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# platform-api-kit — DATA-MODEL
|
|
22
|
+
|
|
23
|
+
> **Template version:** T-PKG-DMT v1.0
|
|
24
|
+
|
|
25
|
+
## 1. Exported types
|
|
26
|
+
| Type | Shape | Purpose |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
|
|
29
|
+
## 2. Persisted state
|
|
30
|
+
If the package owns persisted state, describe schema.
|
|
31
|
+
|
|
32
|
+
## 3. Cross-references
|
|
33
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
@@ -133,7 +133,7 @@ pip install cloud-dog-api-kit
|
|
|
133
133
|
- `ruff check cloud_dog_api_kit tests` -> `All checks passed!`
|
|
134
134
|
- `ruff format --check cloud_dog_api_kit tests examples` -> `143 files already formatted`
|
|
135
135
|
- `python -m build` -> fails in network-restricted environment (isolated `hatchling` install unavailable)
|
|
136
|
-
-
|
|
136
|
+
- `<platform-config>/.venv/bin/python -m build --no-isolation` -> built `.tar.gz` and `.whl`
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
@@ -33,7 +33,7 @@ Public API:
|
|
|
33
33
|
|
|
34
34
|
from __future__ import annotations
|
|
35
35
|
|
|
36
|
-
__version__ = "0.
|
|
36
|
+
__version__ = "0.13.2"
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def create_app(*args, **kwargs): # type: ignore[no-untyped-def]
|
|
@@ -109,6 +109,7 @@ from cloud_dog_api_kit.openapi import configure_openapi
|
|
|
109
109
|
from cloud_dog_api_kit.versioning import VersionHeaderMiddleware
|
|
110
110
|
from cloud_dog_api_kit.webhook import WebhookSignatureMiddleware, compute_webhook_signature
|
|
111
111
|
from cloud_dog_api_kit.web.proxy import WebApiProxy
|
|
112
|
+
from cloud_dog_api_kit.agent import AgentSession, AgentStrategy, SessionState, parse_prefer_strategy
|
|
112
113
|
|
|
113
114
|
__all__ = [
|
|
114
115
|
"create_app",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Agent session lifecycle and strategy parsing (W28B-309, PS-96)."""
|
|
2
|
+
|
|
3
|
+
from cloud_dog_api_kit.agent.session import AgentSession, SessionState
|
|
4
|
+
from cloud_dog_api_kit.agent.strategy import AgentStrategy, parse_prefer_strategy
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AgentSession",
|
|
8
|
+
"AgentStrategy",
|
|
9
|
+
"SessionState",
|
|
10
|
+
"parse_prefer_strategy",
|
|
11
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Agent session lifecycle (W28B-309, PS-96 §5).
|
|
2
|
+
|
|
3
|
+
An AgentSession tracks a multi-turn agent interaction across
|
|
4
|
+
create → (suspend ↔ resume)* → close transitions. Memory
|
|
5
|
+
persistence is delegated to cloud_dog_cache interfaces.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable, Optional
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SessionState(str, Enum):
|
|
18
|
+
"""Agent session lifecycle states."""
|
|
19
|
+
CREATED = "created"
|
|
20
|
+
ACTIVE = "active"
|
|
21
|
+
SUSPENDED = "suspended"
|
|
22
|
+
CLOSED = "closed"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_TRANSITIONS: dict[SessionState, set[SessionState]] = {
|
|
26
|
+
SessionState.CREATED: {SessionState.ACTIVE, SessionState.CLOSED},
|
|
27
|
+
SessionState.ACTIVE: {SessionState.SUSPENDED, SessionState.CLOSED},
|
|
28
|
+
SessionState.SUSPENDED: {SessionState.ACTIVE, SessionState.CLOSED},
|
|
29
|
+
SessionState.CLOSED: set(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AgentSession:
|
|
35
|
+
"""Lifecycle-managed agent session with memory persistence hooks."""
|
|
36
|
+
|
|
37
|
+
session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
38
|
+
state: SessionState = SessionState.CREATED
|
|
39
|
+
strategy: Optional[str] = None
|
|
40
|
+
memory: dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
|
42
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
|
43
|
+
_on_persist: Optional[Callable[["AgentSession"], None]] = field(default=None, repr=False)
|
|
44
|
+
|
|
45
|
+
def _transition(self, target: SessionState) -> None:
|
|
46
|
+
allowed = _TRANSITIONS.get(self.state, set())
|
|
47
|
+
if target not in allowed:
|
|
48
|
+
raise ValueError(f"Invalid transition: {self.state.value} → {target.value}")
|
|
49
|
+
self.state = target
|
|
50
|
+
self.updated_at = datetime.now(tz=timezone.utc)
|
|
51
|
+
|
|
52
|
+
# ── Lifecycle hooks ──
|
|
53
|
+
|
|
54
|
+
def create(self, *, strategy: Optional[str] = None) -> "AgentSession":
|
|
55
|
+
"""Activate a newly created session."""
|
|
56
|
+
self._transition(SessionState.ACTIVE)
|
|
57
|
+
if strategy:
|
|
58
|
+
self.strategy = strategy
|
|
59
|
+
self._persist()
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def suspend(self, *, checkpoint: dict[str, Any] | None = None) -> "AgentSession":
|
|
63
|
+
"""Suspend the session, persisting current memory."""
|
|
64
|
+
self._transition(SessionState.SUSPENDED)
|
|
65
|
+
if checkpoint:
|
|
66
|
+
self.memory["_checkpoint"] = checkpoint
|
|
67
|
+
self._persist()
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def resume(self) -> "AgentSession":
|
|
71
|
+
"""Resume a suspended session."""
|
|
72
|
+
self._transition(SessionState.ACTIVE)
|
|
73
|
+
self.memory.pop("_checkpoint", None)
|
|
74
|
+
self._persist()
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def close(self) -> "AgentSession":
|
|
78
|
+
"""Close the session permanently."""
|
|
79
|
+
self._transition(SessionState.CLOSED)
|
|
80
|
+
self._persist()
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
# ── Memory ──
|
|
84
|
+
|
|
85
|
+
def set_memory(self, key: str, value: Any) -> None:
|
|
86
|
+
"""Store a value in session memory."""
|
|
87
|
+
self.memory[key] = value
|
|
88
|
+
self._persist()
|
|
89
|
+
|
|
90
|
+
def get_memory(self, key: str, default: Any = None) -> Any:
|
|
91
|
+
"""Retrieve a value from session memory."""
|
|
92
|
+
return self.memory.get(key, default)
|
|
93
|
+
|
|
94
|
+
# ── Persistence ──
|
|
95
|
+
|
|
96
|
+
def set_persist_hook(self, hook: Callable[["AgentSession"], None]) -> None:
|
|
97
|
+
"""Register a persistence callback (e.g., cloud_dog_cache write)."""
|
|
98
|
+
self._on_persist = hook
|
|
99
|
+
|
|
100
|
+
def _persist(self) -> None:
|
|
101
|
+
if self._on_persist is not None:
|
|
102
|
+
self._on_persist(self)
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> dict[str, Any]:
|
|
105
|
+
"""Serialise for cache/storage."""
|
|
106
|
+
return {
|
|
107
|
+
"session_id": self.session_id,
|
|
108
|
+
"state": self.state.value,
|
|
109
|
+
"strategy": self.strategy,
|
|
110
|
+
"memory": self.memory,
|
|
111
|
+
"created_at": self.created_at.isoformat(),
|
|
112
|
+
"updated_at": self.updated_at.isoformat(),
|
|
113
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Agent strategy selection via Prefer header (W28B-309, PS-96 §3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentStrategy(str, Enum):
|
|
10
|
+
"""PS-96 agent execution strategies."""
|
|
11
|
+
SIMPLE = "simple"
|
|
12
|
+
REACT = "react"
|
|
13
|
+
CODEACT = "codeact"
|
|
14
|
+
SUBAGENT_ROUTER = "subagent_router"
|
|
15
|
+
RLM = "rlm"
|
|
16
|
+
REFLEXION = "reflexion"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_prefer_strategy(prefer_header: Optional[str]) -> Optional[AgentStrategy]:
|
|
20
|
+
"""Parse agent-strategy from RFC 7240 Prefer header.
|
|
21
|
+
|
|
22
|
+
Example: ``Prefer: agent-strategy=react``
|
|
23
|
+
|
|
24
|
+
Returns None if no agent-strategy preference found or value unknown.
|
|
25
|
+
"""
|
|
26
|
+
if not prefer_header:
|
|
27
|
+
return None
|
|
28
|
+
for token in prefer_header.split(","):
|
|
29
|
+
token = token.strip()
|
|
30
|
+
if token.lower().startswith("agent-strategy="):
|
|
31
|
+
value = token.split("=", 1)[1].strip().strip('"').lower()
|
|
32
|
+
try:
|
|
33
|
+
return AgentStrategy(value)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return None
|
|
36
|
+
return None
|
|
@@ -64,9 +64,18 @@ class AsyncJobClient:
|
|
|
64
64
|
)
|
|
65
65
|
return job_id
|
|
66
66
|
|
|
67
|
-
async def wait(
|
|
67
|
+
async def wait(
|
|
68
|
+
self,
|
|
69
|
+
job_id: str,
|
|
70
|
+
*,
|
|
71
|
+
timeout: float | None = None,
|
|
72
|
+
on_progress: Callable[[float, float | None, str | None], Awaitable[None]] | None = None,
|
|
73
|
+
) -> dict[str, Any]:
|
|
68
74
|
"""Poll a job until completion or timeout.
|
|
69
75
|
|
|
76
|
+
If on_progress is provided, invokes it with (progress_pct, total, message)
|
|
77
|
+
each time a poll returns progress data (Mode 2 client-side).
|
|
78
|
+
|
|
70
79
|
Returns the final job status dict with 'result' on success.
|
|
71
80
|
Raises TimeoutError with the job_id if timeout expires.
|
|
72
81
|
"""
|
|
@@ -74,6 +83,17 @@ class AsyncJobClient:
|
|
|
74
83
|
while True:
|
|
75
84
|
status = await self._poll_job(job_id)
|
|
76
85
|
state = str(status.get("status", "")).lower()
|
|
86
|
+
|
|
87
|
+
# Emit progress if callback provided and status has progress data
|
|
88
|
+
if on_progress and "progress" in status:
|
|
89
|
+
prog = status["progress"]
|
|
90
|
+
if isinstance(prog, dict):
|
|
91
|
+
await on_progress(
|
|
92
|
+
float(prog.get("percentage", 0)),
|
|
93
|
+
prog.get("total"),
|
|
94
|
+
prog.get("message"),
|
|
95
|
+
)
|
|
96
|
+
|
|
77
97
|
if state in ("completed", "succeeded"):
|
|
78
98
|
return status
|
|
79
99
|
if state in ("failed", "cancelled", "timeout", "dead_lettered"):
|
|
@@ -109,13 +129,13 @@ class AsyncJobClient:
|
|
|
109
129
|
if _is_budget_exceeded(response):
|
|
110
130
|
job_id = _extract_job_id_from_error(response)
|
|
111
131
|
if job_id:
|
|
112
|
-
return await self.wait(job_id, timeout=timeout)
|
|
132
|
+
return await self.wait(job_id, timeout=timeout, on_progress=on_progress)
|
|
113
133
|
raise ValueError("Budget exceeded but no job_id in error response")
|
|
114
134
|
|
|
115
135
|
# Async/job_id response (Mode 3)
|
|
116
136
|
job_id = _extract_job_id(response)
|
|
117
137
|
if job_id:
|
|
118
|
-
return await self.wait(job_id, timeout=timeout)
|
|
138
|
+
return await self.wait(job_id, timeout=timeout, on_progress=on_progress)
|
|
119
139
|
|
|
120
140
|
# Fallback: treat entire response as sync result
|
|
121
141
|
return {"status": "completed", "result": response}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
|
+
import asyncio
|
|
28
29
|
import inspect
|
|
29
30
|
import json
|
|
30
31
|
import sys
|
|
@@ -44,7 +45,7 @@ from cloud_dog_api_kit.mcp.async_jobs import AsyncJobStore
|
|
|
44
45
|
from cloud_dog_api_kit.mcp.error_mapper import map_legacy_mcp_payload
|
|
45
46
|
from cloud_dog_api_kit.mcp.legacy_sse import LegacySSEBroker, LegacySSEConfig
|
|
46
47
|
from cloud_dog_api_kit.mcp.session import SESSION_HEADER, McpSessionManager
|
|
47
|
-
from cloud_dog_api_kit.mcp.tool_router import ToolContract, normalise_tool_registry
|
|
48
|
+
from cloud_dog_api_kit.mcp.tool_router import SYNC_CLASS_PROGRESS, ToolContract, normalise_tool_registry
|
|
48
49
|
|
|
49
50
|
TransportModes = set[str]
|
|
50
51
|
ToolCallable = Callable[[dict[str, Any], Request], Awaitable[Any] | Any]
|
|
@@ -198,6 +199,41 @@ def _mcp_tool_call_payload(result: Any) -> dict[str, Any]:
|
|
|
198
199
|
return payload
|
|
199
200
|
|
|
200
201
|
|
|
202
|
+
def _accepts_event_stream(request: Request) -> bool:
|
|
203
|
+
return "text/event-stream" in request.headers.get("accept", "").lower()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _sse_frame(data: dict[str, Any], *, event: str | None = None) -> str:
|
|
207
|
+
lines: list[str] = []
|
|
208
|
+
if event:
|
|
209
|
+
lines.append(f"event: {event}")
|
|
210
|
+
encoded = json.dumps(data, default=str, ensure_ascii=True)
|
|
211
|
+
for line in encoded.splitlines() or [""]:
|
|
212
|
+
lines.append(f"data: {line}")
|
|
213
|
+
return "\n".join(lines) + "\n\n"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _mcp_progress_notification(
|
|
217
|
+
*,
|
|
218
|
+
progress_token: str,
|
|
219
|
+
progress: float,
|
|
220
|
+
total: float | None,
|
|
221
|
+
message: str,
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
params: dict[str, Any] = {
|
|
224
|
+
"progressToken": progress_token,
|
|
225
|
+
"progress": progress,
|
|
226
|
+
"message": message,
|
|
227
|
+
}
|
|
228
|
+
if total is not None:
|
|
229
|
+
params["total"] = total
|
|
230
|
+
return {
|
|
231
|
+
"jsonrpc": "2.0",
|
|
232
|
+
"method": "notifications/progress",
|
|
233
|
+
"params": params,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
201
237
|
async def _maybe_await(value: Any) -> Any:
|
|
202
238
|
if inspect.isawaitable(value):
|
|
203
239
|
return await value
|
|
@@ -289,6 +325,18 @@ async def _resolve_tool_context(
|
|
|
289
325
|
return context
|
|
290
326
|
|
|
291
327
|
|
|
328
|
+
def _progress_token_from_params(params: dict[str, Any], session_id: str, request_id: Any) -> str:
|
|
329
|
+
meta = params.get("_meta")
|
|
330
|
+
if isinstance(meta, dict):
|
|
331
|
+
token = meta.get("progressToken")
|
|
332
|
+
if token is not None and str(token).strip():
|
|
333
|
+
return str(token)
|
|
334
|
+
token = params.get("progressToken")
|
|
335
|
+
if token is not None and str(token).strip():
|
|
336
|
+
return str(token)
|
|
337
|
+
return f"{session_id}:{request_id if request_id is not None else uuid4().hex}"
|
|
338
|
+
|
|
339
|
+
|
|
292
340
|
async def _dispatch_payload(
|
|
293
341
|
tools: dict[str, ToolContract],
|
|
294
342
|
payload: dict[str, Any],
|
|
@@ -665,6 +713,101 @@ async def _dispatch_payload(
|
|
|
665
713
|
response.headers["X-Mcp-Session-Created"] = "true" if created else "false"
|
|
666
714
|
return response
|
|
667
715
|
|
|
716
|
+
if (
|
|
717
|
+
payload.get("jsonrpc") == "2.0"
|
|
718
|
+
and modern_jsonrpc
|
|
719
|
+
and tool.sync_class == SYNC_CLASS_PROGRESS
|
|
720
|
+
and wait_value is not False
|
|
721
|
+
and _accepts_event_stream(request)
|
|
722
|
+
):
|
|
723
|
+
rpc_id = payload.get("id")
|
|
724
|
+
progress_token = _progress_token_from_params(params, session.session_id, rpc_id)
|
|
725
|
+
budget_total = max(float(tool.sync_budget_seconds or 0.0), 1.0)
|
|
726
|
+
|
|
727
|
+
async def _event_stream() -> Any:
|
|
728
|
+
started = asyncio.get_running_loop().time()
|
|
729
|
+
yield _sse_frame(
|
|
730
|
+
_mcp_progress_notification(
|
|
731
|
+
progress_token=progress_token,
|
|
732
|
+
progress=0.0,
|
|
733
|
+
total=budget_total,
|
|
734
|
+
message=f"Started {tool.name}",
|
|
735
|
+
),
|
|
736
|
+
event="message",
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
task = asyncio.create_task(_call_tool(tool, arguments, request, tool_context))
|
|
740
|
+
last_reported_second = 0
|
|
741
|
+
try:
|
|
742
|
+
while not task.done():
|
|
743
|
+
await asyncio.sleep(1.0)
|
|
744
|
+
elapsed = asyncio.get_running_loop().time() - started
|
|
745
|
+
reported_second = int(elapsed)
|
|
746
|
+
if reported_second <= last_reported_second:
|
|
747
|
+
continue
|
|
748
|
+
last_reported_second = reported_second
|
|
749
|
+
yield _sse_frame(
|
|
750
|
+
_mcp_progress_notification(
|
|
751
|
+
progress_token=progress_token,
|
|
752
|
+
progress=min(float(elapsed), budget_total),
|
|
753
|
+
total=budget_total,
|
|
754
|
+
message=f"Running {tool.name}",
|
|
755
|
+
),
|
|
756
|
+
event="message",
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
result = await task
|
|
760
|
+
yield _sse_frame(
|
|
761
|
+
_mcp_progress_notification(
|
|
762
|
+
progress_token=progress_token,
|
|
763
|
+
progress=budget_total,
|
|
764
|
+
total=budget_total,
|
|
765
|
+
message=f"Completed {tool.name}",
|
|
766
|
+
),
|
|
767
|
+
event="message",
|
|
768
|
+
)
|
|
769
|
+
tool_payload = _mcp_tool_call_payload(result)
|
|
770
|
+
response_payload = _jsonrpc_response(rpc_id, result=tool_payload)
|
|
771
|
+
if is_messages_path:
|
|
772
|
+
response_payload = _messages_compat_payload(
|
|
773
|
+
jsonrpc_payload=response_payload,
|
|
774
|
+
legacy_result=result,
|
|
775
|
+
request_id=request_id,
|
|
776
|
+
correlation_id=correlation_id,
|
|
777
|
+
)
|
|
778
|
+
yield _sse_frame(response_payload, event="message")
|
|
779
|
+
except Exception as exc:
|
|
780
|
+
if not task.done():
|
|
781
|
+
task.cancel()
|
|
782
|
+
yield _sse_frame(
|
|
783
|
+
_mcp_progress_notification(
|
|
784
|
+
progress_token=progress_token,
|
|
785
|
+
progress=budget_total,
|
|
786
|
+
total=budget_total,
|
|
787
|
+
message=f"Failed {tool.name}",
|
|
788
|
+
),
|
|
789
|
+
event="message",
|
|
790
|
+
)
|
|
791
|
+
yield _sse_frame(
|
|
792
|
+
_jsonrpc_response(
|
|
793
|
+
rpc_id,
|
|
794
|
+
error={
|
|
795
|
+
"code": -32000,
|
|
796
|
+
"message": str(exc) or f"Tool failed: {tool.name}",
|
|
797
|
+
},
|
|
798
|
+
),
|
|
799
|
+
event="message",
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
response = StreamingResponse(
|
|
803
|
+
_event_stream(),
|
|
804
|
+
media_type="text/event-stream",
|
|
805
|
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
806
|
+
)
|
|
807
|
+
response.headers[SESSION_HEADER] = session.session_id
|
|
808
|
+
response.headers["X-Mcp-Session-Created"] = "true" if created else "false"
|
|
809
|
+
return response
|
|
810
|
+
|
|
668
811
|
result = await _call_tool(tool, arguments, request, tool_context)
|
|
669
812
|
if payload.get("jsonrpc") == "2.0" and modern_jsonrpc:
|
|
670
813
|
tool_payload = _mcp_tool_call_payload(result)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
"""Web server utilities for cloud_dog_api_kit."""
|
|
4
|
+
|
|
5
|
+
from .proxy import (
|
|
6
|
+
PRINCIPAL_ROLE_HEADER,
|
|
7
|
+
PRINCIPAL_SOURCE_HEADER,
|
|
8
|
+
PRINCIPAL_SOURCE_WEBUI,
|
|
9
|
+
PRINCIPAL_USER_HEADER,
|
|
10
|
+
ProxyResponse,
|
|
11
|
+
SessionPrincipal,
|
|
12
|
+
WebApiProxy,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"PRINCIPAL_ROLE_HEADER",
|
|
17
|
+
"PRINCIPAL_SOURCE_HEADER",
|
|
18
|
+
"PRINCIPAL_SOURCE_WEBUI",
|
|
19
|
+
"PRINCIPAL_USER_HEADER",
|
|
20
|
+
"ProxyResponse",
|
|
21
|
+
"SessionPrincipal",
|
|
22
|
+
"WebApiProxy",
|
|
23
|
+
]
|
|
@@ -25,6 +25,11 @@ Related Tests: UT1.XX_WebApiProxy
|
|
|
25
25
|
|
|
26
26
|
Recent Changes (max 10):
|
|
27
27
|
- 2026-04-08: W28A-849 — Initial implementation.
|
|
28
|
+
- 2026-06-10: PS-82 §8.3 — canonical session-principal forwarding. Adds the
|
|
29
|
+
SessionPrincipal carrier + per-call ``principal=`` forwarding so a web-proxy
|
|
30
|
+
NEVER substitutes its service/service-admin key for a user-originated request
|
|
31
|
+
(the W28A-890 / notification 3-of-76 identity-collapse defect class). The
|
|
32
|
+
backend enforces RBAC on the forwarded principal.
|
|
28
33
|
"""
|
|
29
34
|
|
|
30
35
|
from __future__ import annotations
|
|
@@ -37,6 +42,44 @@ import httpx
|
|
|
37
42
|
|
|
38
43
|
logger = logging.getLogger(__name__)
|
|
39
44
|
|
|
45
|
+
# --- PS-82 §8.3 canonical session-principal forwarding headers -------------------
|
|
46
|
+
# The ONE canonical set of headers a web-proxy uses to forward the authenticated
|
|
47
|
+
# session principal (identity + role) to the backend. Services MUST NOT invent
|
|
48
|
+
# their own header names — the backend trusts exactly these on the internal hop.
|
|
49
|
+
PRINCIPAL_USER_HEADER = "X-Request-User"
|
|
50
|
+
PRINCIPAL_ROLE_HEADER = "X-Request-Role"
|
|
51
|
+
PRINCIPAL_SOURCE_HEADER = "X-Request-Source"
|
|
52
|
+
#: Marks a request as originating from an authenticated WebUI session (vs a raw
|
|
53
|
+
#: service-to-service call). The backend uses this to require the forwarded
|
|
54
|
+
#: principal instead of materialising the service-key identity.
|
|
55
|
+
PRINCIPAL_SOURCE_WEBUI = "webui"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class SessionPrincipal:
|
|
60
|
+
"""The authenticated WebUI session principal forwarded to the backend (PS-82 §8.3).
|
|
61
|
+
|
|
62
|
+
Carries the *user's* identity + role so the backend enforces RBAC on the real
|
|
63
|
+
caller. A web-proxy MUST construct one of these from the session for EVERY
|
|
64
|
+
user-originated call site and pass it to the proxy, so the service/service-admin
|
|
65
|
+
key is never substituted for a user request (the identity-collapse defect).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
user: str
|
|
69
|
+
role: str = "user"
|
|
70
|
+
|
|
71
|
+
def headers(self) -> dict[str, str]:
|
|
72
|
+
"""Return the canonical forwarding headers for this principal."""
|
|
73
|
+
user = str(self.user or "").strip()
|
|
74
|
+
if not user:
|
|
75
|
+
return {}
|
|
76
|
+
role = str(self.role or "user").strip().lower() or "user"
|
|
77
|
+
return {
|
|
78
|
+
PRINCIPAL_SOURCE_HEADER: PRINCIPAL_SOURCE_WEBUI,
|
|
79
|
+
PRINCIPAL_USER_HEADER: user,
|
|
80
|
+
PRINCIPAL_ROLE_HEADER: role,
|
|
81
|
+
}
|
|
82
|
+
|
|
40
83
|
|
|
41
84
|
@dataclass
|
|
42
85
|
class ProxyResponse:
|
|
@@ -118,17 +161,28 @@ class WebApiProxy:
|
|
|
118
161
|
)
|
|
119
162
|
|
|
120
163
|
def _build_headers(
|
|
121
|
-
self,
|
|
164
|
+
self,
|
|
165
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
166
|
+
principal: SessionPrincipal | None = None,
|
|
122
167
|
) -> dict[str, str]:
|
|
123
|
-
"""Build request headers with API key and
|
|
168
|
+
"""Build request headers with API key, optional extras, and session principal.
|
|
124
169
|
|
|
125
170
|
Caller-supplied headers take precedence on case-insensitive name match,
|
|
126
171
|
so a forwarded X-API-Key from an authenticated user overrides the proxy's
|
|
127
172
|
service key (preserves caller identity for downstream RBAC).
|
|
173
|
+
|
|
174
|
+
When ``principal`` is supplied (PS-82 §8.3), the canonical
|
|
175
|
+
``X-Request-Source``/``X-Request-User``/``X-Request-Role`` forwarding headers
|
|
176
|
+
are added so the backend authorises the REAL caller. The proxy's
|
|
177
|
+
service/service-admin key still travels as the transport credential for the
|
|
178
|
+
internal hop, but the backend MUST enforce RBAC on the forwarded principal —
|
|
179
|
+
never collapse the user to the service-admin identity.
|
|
128
180
|
"""
|
|
129
181
|
headers: dict[str, str] = {}
|
|
130
182
|
if self._api_key:
|
|
131
183
|
headers[self._api_key_header] = self._api_key
|
|
184
|
+
if principal is not None:
|
|
185
|
+
headers.update(principal.headers())
|
|
132
186
|
if extra_headers:
|
|
133
187
|
for incoming_key, incoming_value in extra_headers.items():
|
|
134
188
|
normalised = incoming_key.lower()
|
|
@@ -146,6 +200,7 @@ class WebApiProxy:
|
|
|
146
200
|
params: dict[str, Any] | None = None,
|
|
147
201
|
headers: Mapping[str, str] | None = None,
|
|
148
202
|
cookies: dict[str, str] | None = None,
|
|
203
|
+
principal: SessionPrincipal | None = None,
|
|
149
204
|
) -> ProxyResponse:
|
|
150
205
|
"""Send a proxied request to the API server.
|
|
151
206
|
|
|
@@ -156,12 +211,15 @@ class WebApiProxy:
|
|
|
156
211
|
params: Query parameters.
|
|
157
212
|
headers: Additional headers to forward.
|
|
158
213
|
cookies: Cookies to forward (for session-based auth proxying).
|
|
214
|
+
principal: The authenticated WebUI session principal (PS-82 §8.3).
|
|
215
|
+
MUST be supplied for every user-originated call site so the backend
|
|
216
|
+
enforces RBAC on the real caller instead of the service-admin key.
|
|
159
217
|
|
|
160
218
|
Returns:
|
|
161
219
|
ProxyResponse with status_code, data, and optional error.
|
|
162
220
|
"""
|
|
163
221
|
url = f"{self._base_url}{path}"
|
|
164
|
-
merged_headers = self._build_headers(headers)
|
|
222
|
+
merged_headers = self._build_headers(headers, principal=principal)
|
|
165
223
|
try:
|
|
166
224
|
async with httpx.AsyncClient(
|
|
167
225
|
verify=self._verify_tls,
|