booking-agent-cli 0.7.0__tar.gz → 0.9.0__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.
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/PKG-INFO +2 -2
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/README.md +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/pyproject.toml +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/__init__.py +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/backend/live.py +16 -6
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/defaults.env +6 -0
- booking_agent_cli-0.9.0/src/booking_agent/faults.py +154 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/namespace.py +1 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/observability.py +11 -4
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/platform_profile.py +11 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_capabilities.py +6 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_checkout.py +144 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_profile.py +25 -3
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/__init__.py +1 -1
- booking_agent_cli-0.9.0/src/booking_agent_cli/booking_summary.py +364 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/bundled_skill.md +61 -58
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/calendar_event.py +4 -12
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/calendar_preference.py +9 -23
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/catalog.py +26 -5
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/flow.py +979 -76
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/engine.py +2 -0
- booking_agent_cli-0.9.0/src/booking_agent_cli/fault_report_schema.py +67 -0
- booking_agent_cli-0.9.0/src/booking_agent_cli/fault_reports.py +252 -0
- booking_agent_cli-0.9.0/src/booking_agent_cli/gates.py +82 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/geo_eligibility.py +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/identity.py +7 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/main.py +15 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/output.py +6 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/payment_helpers.py +77 -22
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/session.py +58 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/support_bundle_schema.py +3 -4
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/support_report.py +40 -8
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/conftest.py +14 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/test_timezone_booking_e2e.py +6 -3
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/test_ucp_skill_flow_e2e.py +188 -3
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_booking_rejection.py +11 -5
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_connector.py +1 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_flow.py +37 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_preference.py +5 -5
- booking_agent_cli-0.9.0/tests/integration/test_cli_enforced_gates.py +660 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_declined_spt.py +1 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_gates.py +98 -69
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_home_visit_address.py +4 -3
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_install_integrity.py +2 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_link_auth.py +34 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_platform_profile.py +17 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_skill_integrity.py +2 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_support_report.py +2 -2
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/ucp_checkout_store.py +73 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/ucp_server.py +16 -5
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/offline_stack/stack.py +26 -5
- booking_agent_cli-0.9.0/tests/unit/test_fault_reports.py +275 -0
- booking_agent_cli-0.9.0/tests/unit/test_pay_at_service.py +249 -0
- booking_agent_cli-0.7.0/src/booking_agent_cli/booking_summary.py +0 -177
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/.gitignore +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/authorization.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/backend/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/filter.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/google.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/calendar/scope.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/circuit_breaker.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/config.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/discovery.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/headless.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/http_deadlines.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/http_retry.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/http_trace.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/merchant_pacing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/models/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/models/platform.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/models/wire.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/pop.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/profile_cache.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/registry_url.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/rest_client.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/secure_file.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/settings.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/signals.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/step_timing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/ucp_negotiation.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent/usp_methods.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/activity_log.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/booking_poll.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/calendar_connection.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/config.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/runtime.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/commands/update.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/config_loader.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/flow_timings.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/geocode.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/install_integrity.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/link_cli.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/payment_token.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/provenance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/reschedule.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/runtime.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/runtime_prepare.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/search_area.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/skill_bundle.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/skill_integrity.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/timezone_context.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/src/booking_agent_cli/updates.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/.env.tests +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/test_latency_soak.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/test_staging_registry_integration.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/e2e/ucp_native.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_activity_log.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_at_rest_hygiene.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_bulk_cancel.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_calendar_connection.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_checkout_signals.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_circuit_breaker.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_config.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_correlation_flow.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_discover_registry_rejection.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_evidence.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_reschedule.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_flow_step_timings.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_google_calendar_oauth.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_http_trace.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_identity_logout.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_lazy_oauth.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_link_cli_spt.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_namespace.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_oauth_listener.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_oauth_metadata.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_platform_key_pop.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_pop_keystore.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_profile_cache.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_regression_index.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_request_retry_semantics.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_runtime_prepare.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_search_area.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_session_compat.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_session_retention.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_skill_portability.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_timezone_context.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_cancel_result.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_checkout_validate.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_ucp_profile.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_update_command.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/integration/test_wheel_contents.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/SOURCE +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/availability.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/booking.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/catalog.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/paid_bookings.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/profile.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/registry.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/rest_common.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/usp.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/waitlist.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/spec_schemas/webhook_event.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/profiles.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/fixtures/services.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/backend.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/link_cli.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/mock_calendar.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/pop_business.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/mock/pop_transport.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/support/offline_stack/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_authorization_policy.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_booking_poll_credentials.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_booking_request_error.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_calendar_filter.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_catalog.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_check_version.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_conditional_writes.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_delivery_address.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_discovery.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_geo_eligibility.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_geocode.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_headless.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_http_deadlines.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_http_retry.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_live_search_registry.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_merchant_pacing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_mock_support.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_money_path_events.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_observability.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_outbound_member_names.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_package_layering.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_provenance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_search_filters.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_search_pricing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_search_responses.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_registry_url.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_release_notes.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_rest_client.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_scoped_method_authorization.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_session_credentials.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_spec_conformance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_step_timing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_capabilities.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_ucp_negotiation.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_updates.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/unit/test_wire_model_conformance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/010-alg-none.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/README.md +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.9.0}/tests/vectors/pop/SOURCE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: booking-agent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/wix/booking-agent
|
|
6
6
|
Project-URL: Documentation, https://github.com/wix/booking-agent/blob/master/booking-agent/README.md
|
|
@@ -78,7 +78,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
78
78
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
81
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.57.0/booking-agent/skills/booking-agent/SKILL.md
|
|
82
82
|
gh attestation verify SKILL.md --repo wix/booking-agent \
|
|
83
83
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
84
84
|
```
|
|
@@ -36,7 +36,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
36
36
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
39
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.57.0/booking-agent/skills/booking-agent/SKILL.md
|
|
40
40
|
gh attestation verify SKILL.md --repo wix/booking-agent \
|
|
41
41
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
42
42
|
```
|
|
@@ -33,6 +33,7 @@ from ..rest_client import (
|
|
|
33
33
|
USPTokenStore,
|
|
34
34
|
)
|
|
35
35
|
from ..headless import attach_registry_hit, mark_live_catalog_service, service_from_registry_hit
|
|
36
|
+
from ..faults import record_silent_fault
|
|
36
37
|
from ..authorization import BookingScopedCredential, evaluate, resolve_posture
|
|
37
38
|
from ..pop import PopKey, PopKeyStore
|
|
38
39
|
from ..ucp_profile import UcpNativeContext, fetch_and_resolve_business_profile
|
|
@@ -115,39 +116,41 @@ class LiveUSPBackend(USPBackend):
|
|
|
115
116
|
decision=decision,
|
|
116
117
|
)
|
|
117
118
|
|
|
119
|
+
# Discovery absorbs these failures, so they stay at debug level: a warning reaches
|
|
120
|
+
# stderr, which agent hosts merge into the stdout JSON they parse.
|
|
118
121
|
@staticmethod
|
|
119
122
|
def _log_profile_failure(profile_url: str, exc: BaseException) -> None:
|
|
120
123
|
if is_rate_limited(exc):
|
|
121
|
-
logger.
|
|
124
|
+
logger.debug(
|
|
122
125
|
"Profile resolution rate limited (HTTP 429) for %s",
|
|
123
126
|
profile_url,
|
|
124
127
|
)
|
|
125
128
|
return
|
|
126
129
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
127
|
-
logger.
|
|
130
|
+
logger.debug(
|
|
128
131
|
"Profile resolution HTTP %s for %s",
|
|
129
132
|
exc.response.status_code,
|
|
130
133
|
profile_url,
|
|
131
134
|
)
|
|
132
135
|
return
|
|
133
|
-
logger.
|
|
136
|
+
logger.debug("Profile resolution failed for %s: %s: %s", profile_url, type(exc).__name__, exc)
|
|
134
137
|
|
|
135
138
|
@staticmethod
|
|
136
139
|
def _log_service_failure(service_id: str, exc: BaseException) -> None:
|
|
137
140
|
if is_rate_limited(exc):
|
|
138
|
-
logger.
|
|
141
|
+
logger.debug(
|
|
139
142
|
"Service catalog fetch rate limited (HTTP 429) for %s",
|
|
140
143
|
service_id,
|
|
141
144
|
)
|
|
142
145
|
return
|
|
143
146
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
144
|
-
logger.
|
|
147
|
+
logger.debug(
|
|
145
148
|
"Service catalog fetch HTTP %s for %s",
|
|
146
149
|
exc.response.status_code,
|
|
147
150
|
service_id,
|
|
148
151
|
)
|
|
149
152
|
return
|
|
150
|
-
logger.
|
|
153
|
+
logger.debug("Service fetch failed: %s: %s", service_id, type(exc).__name__)
|
|
151
154
|
|
|
152
155
|
# -- Registry-level ------------------------------------------------------
|
|
153
156
|
|
|
@@ -184,6 +187,9 @@ class LiveUSPBackend(USPBackend):
|
|
|
184
187
|
self._log_profile_failure(profile_url, exc)
|
|
185
188
|
if is_rate_limited(exc):
|
|
186
189
|
raise
|
|
190
|
+
record_silent_fault(
|
|
191
|
+
"profile_resolve", exc, profile_url=profile_url, business_id=biz_id,
|
|
192
|
+
)
|
|
187
193
|
|
|
188
194
|
async def search_registry(self, intent: ServiceSearchIntent) -> List[USPService]:
|
|
189
195
|
if not self._cfg.usp_registry_url:
|
|
@@ -289,6 +295,10 @@ class LiveUSPBackend(USPBackend):
|
|
|
289
295
|
if is_rate_limited(exc):
|
|
290
296
|
rate_limited_exc = exc
|
|
291
297
|
raise
|
|
298
|
+
record_silent_fault(
|
|
299
|
+
"catalog_enrich", exc, profile_url=profile_url,
|
|
300
|
+
business_id=biz_id, service_id=r.service_id,
|
|
301
|
+
)
|
|
292
302
|
return attach_registry_hit(
|
|
293
303
|
service_from_registry_hit(r), r, rank=rank,
|
|
294
304
|
)
|
|
@@ -43,6 +43,12 @@ BOOKING_AGENT_DISCOVERY_CONCURRENCY=8
|
|
|
43
43
|
BOOKING_AGENT_PROFILE_CACHE_TTL_SECONDS=900
|
|
44
44
|
# 1 writes a redacted request log to <session dir>/http_trace.log.
|
|
45
45
|
BOOKING_AGENT_HTTP_TRACE=0
|
|
46
|
+
# 1 writes money-path JSON event lines to stderr. Off by default because many
|
|
47
|
+
# agent hosts merge stderr into the stdout JSON they parse.
|
|
48
|
+
BOOKING_AGENT_STDERR_EVENTS=0
|
|
49
|
+
# 0 stops automatic fault reports: faults the user never saw, about a business's
|
|
50
|
+
# profile or catalog or the assistant's own code, sent with no user data.
|
|
51
|
+
BOOKING_AGENT_FAULT_REPORTS=1
|
|
46
52
|
|
|
47
53
|
# --- Address -------------------------------------------------------------------
|
|
48
54
|
BOOKING_AGENT_GEOCODER_URL=https://nominatim.openstreetmap.org/search
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Faults the client absorbed, so the user never saw them.
|
|
2
|
+
|
|
3
|
+
Code that catches an exception and carries on (a business profile that cannot be
|
|
4
|
+
read during discovery, a catalog entry that cannot be fetched) calls
|
|
5
|
+
``record_silent_fault``. Warnings logged by the allowed modules during a command are
|
|
6
|
+
collected the same way. The CLI decides at the end of the command whether these
|
|
7
|
+
become automatic fault reports; nothing here does I/O.
|
|
8
|
+
|
|
9
|
+
Only structural facts are kept: the step, the exception type, a code-authored log
|
|
10
|
+
format string, and public identifiers of the business involved. Exception messages
|
|
11
|
+
and log arguments are never kept, because they can carry anything.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
from urllib.parse import urlsplit
|
|
19
|
+
|
|
20
|
+
#: Steps whose outcome never depends on the user's own accounts or data.
|
|
21
|
+
STEPS = frozenset({
|
|
22
|
+
"registry_search",
|
|
23
|
+
"profile_resolve",
|
|
24
|
+
"catalog_enrich",
|
|
25
|
+
"availability_prefetch",
|
|
26
|
+
"platform_profile",
|
|
27
|
+
"cli",
|
|
28
|
+
})
|
|
29
|
+
#: Loggers whose warnings count as degraded results. Link, payment, identity, buyer
|
|
30
|
+
#: details, and Google Calendar code is not here: their faults stay on the consented path.
|
|
31
|
+
WARNING_LOGGERS = (
|
|
32
|
+
"booking_agent.backend.live",
|
|
33
|
+
"booking_agent.discovery",
|
|
34
|
+
"booking_agent.headless",
|
|
35
|
+
"booking_agent.ucp_profile",
|
|
36
|
+
"booking_agent.profile_cache",
|
|
37
|
+
)
|
|
38
|
+
_MAX_RECORDED = 50
|
|
39
|
+
_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
|
40
|
+
|
|
41
|
+
_recorded: List[Dict[str, Any]] = []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _reason(exc: Optional[BaseException]) -> Optional[str]:
|
|
45
|
+
if exc is None:
|
|
46
|
+
return None
|
|
47
|
+
response = getattr(exc, "response", None)
|
|
48
|
+
status = getattr(response, "status_code", None)
|
|
49
|
+
if isinstance(status, int):
|
|
50
|
+
return f"http_{status}"
|
|
51
|
+
from booking_agent.ucp_profile import UcpProfileValidationError
|
|
52
|
+
|
|
53
|
+
if isinstance(exc, UcpProfileValidationError) and exc.args and isinstance(exc.args[0], str):
|
|
54
|
+
# Validation messages are written in this package and name only profile fields.
|
|
55
|
+
slug = _SLUG_RE.sub("_", exc.args[0].lower()).strip("_")
|
|
56
|
+
return slug[:64] or None
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _subject(profile_url: Optional[str], **ids: Any) -> Dict[str, Any]:
|
|
61
|
+
subject: Dict[str, Any] = {}
|
|
62
|
+
if profile_url:
|
|
63
|
+
try:
|
|
64
|
+
parts = urlsplit(profile_url)
|
|
65
|
+
except ValueError:
|
|
66
|
+
parts = None
|
|
67
|
+
if parts is not None and parts.scheme in ("http", "https") and parts.hostname:
|
|
68
|
+
subject["host"] = parts.hostname
|
|
69
|
+
subject["path"] = parts.path or "/"
|
|
70
|
+
for key, value in ids.items():
|
|
71
|
+
if value is not None:
|
|
72
|
+
subject[key] = value
|
|
73
|
+
return subject
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def record_silent_fault(
|
|
77
|
+
step: str,
|
|
78
|
+
exc: Optional[BaseException] = None,
|
|
79
|
+
*,
|
|
80
|
+
profile_url: Optional[str] = None,
|
|
81
|
+
business_id: Optional[str] = None,
|
|
82
|
+
service_id: Optional[str] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Note a fault this code absorbed. Never raises."""
|
|
85
|
+
try:
|
|
86
|
+
if len(_recorded) >= _MAX_RECORDED:
|
|
87
|
+
return
|
|
88
|
+
response = getattr(exc, "response", None)
|
|
89
|
+
status = getattr(response, "status_code", None)
|
|
90
|
+
_recorded.append({
|
|
91
|
+
"fault_class": "absorbed_exception",
|
|
92
|
+
"step": step if step in STEPS else "cli",
|
|
93
|
+
"exception_type": _qualname(exc),
|
|
94
|
+
"message_template": None,
|
|
95
|
+
"reason_code": _reason(exc),
|
|
96
|
+
"subject": _subject(
|
|
97
|
+
profile_url,
|
|
98
|
+
business_id=business_id,
|
|
99
|
+
service_ids=[service_id] if service_id else None,
|
|
100
|
+
http_status=status if isinstance(status, int) else None,
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _qualname(exc: Optional[BaseException]) -> Optional[str]:
|
|
108
|
+
if exc is None:
|
|
109
|
+
return None
|
|
110
|
+
cls = type(exc)
|
|
111
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _WarningCollector(logging.Handler):
|
|
115
|
+
"""Keeps the format string of each warning; the formatted text is never stored."""
|
|
116
|
+
|
|
117
|
+
def __init__(self) -> None:
|
|
118
|
+
super().__init__(level=logging.WARNING)
|
|
119
|
+
|
|
120
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
121
|
+
try:
|
|
122
|
+
if not record.name.startswith(WARNING_LOGGERS) or len(_recorded) >= _MAX_RECORDED:
|
|
123
|
+
return
|
|
124
|
+
exc = record.exc_info[1] if record.exc_info else None
|
|
125
|
+
_recorded.append({
|
|
126
|
+
"fault_class": "degraded_result",
|
|
127
|
+
"step": "cli",
|
|
128
|
+
"exception_type": _qualname(exc),
|
|
129
|
+
"message_template": record.msg if isinstance(record.msg, str) else None,
|
|
130
|
+
"reason_code": None,
|
|
131
|
+
"subject": {"logger": record.name},
|
|
132
|
+
})
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_collector: Optional[_WarningCollector] = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def install_warning_collector() -> None:
|
|
141
|
+
"""Collect allowed warnings instead of printing them to stderr (idempotent)."""
|
|
142
|
+
global _collector
|
|
143
|
+
if _collector is not None:
|
|
144
|
+
return
|
|
145
|
+
_collector = _WarningCollector()
|
|
146
|
+
for name in ("booking_agent", "booking_agent_cli"):
|
|
147
|
+
logging.getLogger(name).addHandler(_collector)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def drain() -> List[Dict[str, Any]]:
|
|
151
|
+
"""Every fault recorded so far, cleared."""
|
|
152
|
+
out = list(_recorded)
|
|
153
|
+
_recorded.clear()
|
|
154
|
+
return out
|
|
@@ -17,6 +17,7 @@ CAP_CATALOG = "dev.usp-protocol.services.catalog"
|
|
|
17
17
|
CAP_AVAILABILITY = "dev.usp-protocol.services.availability"
|
|
18
18
|
CAP_BOOKINGS = "dev.usp-protocol.services.bookings"
|
|
19
19
|
CAP_PAID_BOOKINGS = "dev.usp-protocol.services.paid_bookings"
|
|
20
|
+
CAP_PAY_AT_SERVICE = "dev.usp-protocol.services.pay_at_service"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class NamespaceOriginError(ValueError):
|
|
@@ -9,6 +9,8 @@ from typing import Any, Callable, Dict, Optional
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
|
+
from booking_agent import settings
|
|
13
|
+
|
|
12
14
|
# Wire name for the flow join key. Optional on the USP HTTP binding (Track S of
|
|
13
15
|
# spec issue #160): send when bound, never require it on responses, never fail a
|
|
14
16
|
# call because a peer omitted or ignored it.
|
|
@@ -35,9 +37,14 @@ _FORBIDDEN_FIELD_KEYS = frozenset({
|
|
|
35
37
|
})
|
|
36
38
|
|
|
37
39
|
_correlation_id: ContextVar[Optional[str]] = ContextVar("booking_agent_correlation_id", default=None)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _default_sink(record: Dict[str, Any]) -> None:
|
|
43
|
+
if settings.get_bool("BOOKING_AGENT_STDERR_EVENTS"):
|
|
44
|
+
sys.stderr.write(json.dumps(record, default=str) + "\n")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_event_sink: Callable[[Dict[str, Any]], None] = _default_sink
|
|
41
48
|
|
|
42
49
|
|
|
43
50
|
def set_event_sink(sink: Callable[[Dict[str, Any]], None]) -> None:
|
|
@@ -49,7 +56,7 @@ def set_event_sink(sink: Callable[[Dict[str, Any]], None]) -> None:
|
|
|
49
56
|
def reset_event_sink() -> None:
|
|
50
57
|
"""Restore the default stderr event sink."""
|
|
51
58
|
global _event_sink
|
|
52
|
-
_event_sink =
|
|
59
|
+
_event_sink = _default_sink
|
|
53
60
|
|
|
54
61
|
|
|
55
62
|
def new_correlation_id() -> str:
|
|
@@ -88,6 +88,17 @@ def platform_profile_doc() -> Dict[str, Any]:
|
|
|
88
88
|
"extends": "dev.ucp.shopping.checkout",
|
|
89
89
|
}
|
|
90
90
|
],
|
|
91
|
+
"dev.usp-protocol.services.pay_at_service": [
|
|
92
|
+
{
|
|
93
|
+
"version": "2026-08-20",
|
|
94
|
+
"spec": (
|
|
95
|
+
"https://usp-protocol.dev/specification"
|
|
96
|
+
"#pay-at-service-settlement-extension"
|
|
97
|
+
),
|
|
98
|
+
"schema": "https://usp-protocol.dev/schemas/services/pay_at_service.json",
|
|
99
|
+
"extends": "dev.ucp.shopping.checkout",
|
|
100
|
+
}
|
|
101
|
+
],
|
|
91
102
|
"dev.ucp.common.payment.terms": [
|
|
92
103
|
{
|
|
93
104
|
"version": "2026-08-25",
|
|
@@ -28,6 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
from typing import Any, Callable, Dict, List
|
|
29
29
|
|
|
30
30
|
from .ucp_negotiation import _capabilities_from_doc
|
|
31
|
+
from .namespace import CAP_PAY_AT_SERVICE
|
|
31
32
|
from .ucp_profile import CAP_PAID_BOOKINGS, UcpProfileValidationError
|
|
32
33
|
|
|
33
34
|
|
|
@@ -91,9 +92,14 @@ def _paid_bookings_handler(echoed_versions: List[str], platform_supported: List[
|
|
|
91
92
|
_validate_versions_supported(CAP_PAID_BOOKINGS, echoed_versions, platform_supported)
|
|
92
93
|
|
|
93
94
|
|
|
95
|
+
def _pay_at_service_handler(echoed_versions: List[str], platform_supported: List[str]) -> None:
|
|
96
|
+
_validate_versions_supported(CAP_PAY_AT_SERVICE, echoed_versions, platform_supported)
|
|
97
|
+
|
|
98
|
+
|
|
94
99
|
# namespace -> handler. Register new capabilities (e.g. AP2) here.
|
|
95
100
|
CAPABILITY_HANDLERS: Dict[str, CapabilityHandler] = {
|
|
96
101
|
CAP_PAID_BOOKINGS: _paid_bookings_handler,
|
|
102
|
+
CAP_PAY_AT_SERVICE: _pay_at_service_handler,
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import uuid
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from typing import Any, Dict, List, Optional
|
|
10
11
|
|
|
@@ -489,6 +490,129 @@ def immediate_schedule_amount(term: Dict[str, Any]) -> Optional[int]:
|
|
|
489
490
|
return amount
|
|
490
491
|
|
|
491
492
|
|
|
493
|
+
PAY_AT_SERVICE_CAPABILITY = "dev.usp-protocol.services.pay_at_service"
|
|
494
|
+
SETTLEMENT_MODE_OFFLINE = "offline"
|
|
495
|
+
#: Disclosure code the business emits for a pay-at-service checkout (USP 11.3.5 S8).
|
|
496
|
+
PAYMENT_DUE_AT_SERVICE_CODE = "payment_due_at_service"
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def is_offline_settled(checkout: Any) -> bool:
|
|
500
|
+
"""True when the checkout declares ``settlement.mode == "offline"`` (USP 11.3.3)."""
|
|
501
|
+
settlement = checkout.get("settlement") if isinstance(checkout, dict) else None
|
|
502
|
+
return isinstance(settlement, dict) and settlement.get("mode") == SETTLEMENT_MODE_OFFLINE
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _instants_equal(a: Any, b: Any) -> bool:
|
|
506
|
+
"""Compare two RFC 3339 instants, tolerating a differing UTC offset spelling."""
|
|
507
|
+
if a == b:
|
|
508
|
+
return True
|
|
509
|
+
try:
|
|
510
|
+
left = datetime.fromisoformat(str(a).replace("Z", "+00:00"))
|
|
511
|
+
right = datetime.fromisoformat(str(b).replace("Z", "+00:00"))
|
|
512
|
+
except ValueError:
|
|
513
|
+
return False
|
|
514
|
+
if left.tzinfo is None or right.tzinfo is None:
|
|
515
|
+
return False
|
|
516
|
+
return left == right
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _is_at_service_term(term: Any) -> bool:
|
|
520
|
+
schedules = term.get("schedules") if isinstance(term, dict) else None
|
|
521
|
+
return (
|
|
522
|
+
isinstance(schedules, list)
|
|
523
|
+
and bool(schedules)
|
|
524
|
+
and all(isinstance(s, dict) and s.get("type") == "at_service" for s in schedules)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def validate_offline_settlement(
|
|
529
|
+
checkout: Dict[str, Any],
|
|
530
|
+
*,
|
|
531
|
+
negotiated_capabilities: Optional[Dict[str, Any]] = None,
|
|
532
|
+
persisted_term: Optional[Dict[str, Any]] = None,
|
|
533
|
+
slot_start: Optional[str] = None,
|
|
534
|
+
) -> Dict[str, Any]:
|
|
535
|
+
"""Validate a pay-at-service checkout (USP 11.3) and return its selected term.
|
|
536
|
+
|
|
537
|
+
Fails closed on anything the extension does not define: an unknown settlement
|
|
538
|
+
mode, a settlement without the terms it names (S1), more than one offline term
|
|
539
|
+
(S2), a ``due_at`` that is not the slot start (S3), or a selected term other
|
|
540
|
+
than the offline one (S4 applies only when the offline term is selected).
|
|
541
|
+
"""
|
|
542
|
+
settlement = checkout.get("settlement")
|
|
543
|
+
if not isinstance(settlement, dict):
|
|
544
|
+
raise UcpProfileValidationError("Checkout settlement must be an object.")
|
|
545
|
+
if settlement.get("mode") != SETTLEMENT_MODE_OFFLINE:
|
|
546
|
+
raise UcpProfileValidationError(
|
|
547
|
+
f"Checkout settlement mode {settlement.get('mode')!r} is not supported."
|
|
548
|
+
)
|
|
549
|
+
term_id = settlement.get("term_id")
|
|
550
|
+
if not isinstance(term_id, str) or not term_id:
|
|
551
|
+
raise UcpProfileValidationError("Checkout settlement is missing term_id.")
|
|
552
|
+
caps = negotiated_capabilities or {}
|
|
553
|
+
missing = [
|
|
554
|
+
cap for cap in (PAY_AT_SERVICE_CAPABILITY, PAYMENT_TERMS_CAPABILITY) if cap not in caps
|
|
555
|
+
]
|
|
556
|
+
if missing:
|
|
557
|
+
raise UcpProfileValidationError(
|
|
558
|
+
"Checkout declared pay-at-service settlement without negotiated capabilities "
|
|
559
|
+
f"{missing!r}."
|
|
560
|
+
)
|
|
561
|
+
payment = checkout.get("payment") if isinstance(checkout.get("payment"), dict) else {}
|
|
562
|
+
terms = payment.get("terms")
|
|
563
|
+
selected_id = payment.get("selected_term_id")
|
|
564
|
+
if not isinstance(terms, list) or not terms or not selected_id:
|
|
565
|
+
raise UcpProfileValidationError(
|
|
566
|
+
"Checkout settlement requires payment.terms and payment.selected_term_id."
|
|
567
|
+
)
|
|
568
|
+
named = [t for t in terms if isinstance(t, dict) and t.get("id") == term_id]
|
|
569
|
+
if len(named) != 1:
|
|
570
|
+
raise UcpProfileValidationError(
|
|
571
|
+
"Checkout settlement.term_id must match exactly one payment.terms[].id."
|
|
572
|
+
)
|
|
573
|
+
term = named[0]
|
|
574
|
+
offline_terms = [t for t in terms if _is_at_service_term(t)]
|
|
575
|
+
if len(offline_terms) != 1 or offline_terms[0] is not term:
|
|
576
|
+
raise UcpProfileValidationError(
|
|
577
|
+
"A pay-at-service checkout must offer exactly one pay-at-service term, "
|
|
578
|
+
"and settlement.term_id must name it."
|
|
579
|
+
)
|
|
580
|
+
if selected_id != term_id:
|
|
581
|
+
raise UcpProfileValidationError(
|
|
582
|
+
"The pay-at-service term is not the selected payment term, so the booking "
|
|
583
|
+
"cannot be completed without a payment instrument."
|
|
584
|
+
)
|
|
585
|
+
total = checkout_total_minor_units(checkout)
|
|
586
|
+
if total is None:
|
|
587
|
+
raise UcpProfileValidationError("Checkout totals must include a type=total amount.")
|
|
588
|
+
amounts: list[int] = []
|
|
589
|
+
for schedule in term["schedules"]:
|
|
590
|
+
amount = _schedule_amount(schedule)
|
|
591
|
+
if amount is None or amount <= 0:
|
|
592
|
+
raise UcpProfileValidationError(
|
|
593
|
+
"Each pay-at-service schedule must carry a positive integer amount."
|
|
594
|
+
)
|
|
595
|
+
amounts.append(amount)
|
|
596
|
+
due_at = schedule.get("due_at")
|
|
597
|
+
if not due_at:
|
|
598
|
+
raise UcpProfileValidationError(
|
|
599
|
+
"Pay-at-service schedule must set due_at to the slot start."
|
|
600
|
+
)
|
|
601
|
+
if slot_start and not _instants_equal(due_at, slot_start):
|
|
602
|
+
raise UcpProfileValidationError(
|
|
603
|
+
"Pay-at-service due_at must equal the booked slot start."
|
|
604
|
+
)
|
|
605
|
+
if sum(amounts) != int(total):
|
|
606
|
+
raise UcpProfileValidationError(
|
|
607
|
+
"Pay-at-service schedule amounts must sum to the checkout total."
|
|
608
|
+
)
|
|
609
|
+
if persisted_term is not None and persisted_term != term:
|
|
610
|
+
raise UcpProfileValidationError(
|
|
611
|
+
"The payment term changed since it was quoted. Do not complete this booking."
|
|
612
|
+
)
|
|
613
|
+
return term
|
|
614
|
+
|
|
615
|
+
|
|
492
616
|
def validate_payment_terms(
|
|
493
617
|
checkout: Dict[str, Any],
|
|
494
618
|
*,
|
|
@@ -496,13 +620,33 @@ def validate_payment_terms(
|
|
|
496
620
|
require_terms: bool = False,
|
|
497
621
|
persisted_term: Optional[Dict[str, Any]] = None,
|
|
498
622
|
slot_start: Optional[str] = None,
|
|
623
|
+
expect_offline_settlement: bool = False,
|
|
499
624
|
) -> Optional[Dict[str, Any]]:
|
|
500
625
|
"""Validate UCP payment terms on a checkout. Returns the selected term or None.
|
|
501
626
|
|
|
502
627
|
Deposits require the negotiated terms capability, exactly one selected term,
|
|
503
628
|
schedules that sum to checkout total, exactly one positive immediate amount,
|
|
504
629
|
and an at-service balance with due_at.
|
|
630
|
+
|
|
631
|
+
A checkout routed as pay-at-service (``expect_offline_settlement``) must carry
|
|
632
|
+
an offline ``settlement`` and is validated by :func:`validate_offline_settlement`;
|
|
633
|
+
any other route that receives a ``settlement`` fails closed rather than being
|
|
634
|
+
treated as an ordinary charge.
|
|
505
635
|
"""
|
|
636
|
+
has_settlement = "settlement" in checkout
|
|
637
|
+
if has_settlement != expect_offline_settlement:
|
|
638
|
+
raise UcpProfileValidationError(
|
|
639
|
+
"Checkout carries a settlement this booking was not routed for."
|
|
640
|
+
if has_settlement
|
|
641
|
+
else "This booking was routed as pay-at-service but the checkout has no settlement."
|
|
642
|
+
)
|
|
643
|
+
if has_settlement:
|
|
644
|
+
return validate_offline_settlement(
|
|
645
|
+
checkout,
|
|
646
|
+
negotiated_capabilities=negotiated_capabilities,
|
|
647
|
+
persisted_term=persisted_term,
|
|
648
|
+
slot_start=slot_start,
|
|
649
|
+
)
|
|
506
650
|
payment = checkout.get("payment") if isinstance(checkout.get("payment"), dict) else {}
|
|
507
651
|
terms = payment.get("terms") if isinstance(payment, dict) else None
|
|
508
652
|
has_terms = isinstance(terms, list) and len(terms) > 0
|
|
@@ -20,9 +20,16 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
20
20
|
from pydantic.alias_generators import to_camel
|
|
21
21
|
|
|
22
22
|
from .authorization import AuthorizationPolicy, parse_policy
|
|
23
|
-
from .namespace import
|
|
23
|
+
from .namespace import (
|
|
24
|
+
CAP_CATALOG,
|
|
25
|
+
CAP_PAID_BOOKINGS,
|
|
26
|
+
CAP_PAY_AT_SERVICE,
|
|
27
|
+
SVC_SERVICES,
|
|
28
|
+
check_origin_binding,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
CAP_CHECKOUT = "dev.ucp.shopping.checkout"
|
|
32
|
+
CAP_PAYMENT_TERMS = "dev.ucp.common.payment.terms"
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
class UcpTransportBinding(BaseModel):
|
|
@@ -110,6 +117,20 @@ class UcpNativeContext(BaseModel):
|
|
|
110
117
|
def requires_paid_bookings(self) -> bool:
|
|
111
118
|
return CAP_PAID_BOOKINGS in self.profile.capabilities
|
|
112
119
|
|
|
120
|
+
def declares_pay_at_service(self) -> bool:
|
|
121
|
+
"""True when the business declares everything the pay-at-service checkout path needs.
|
|
122
|
+
|
|
123
|
+
USP 11.3.2: a business routing ``at_service`` through a checkout declares
|
|
124
|
+
``pay_at_service`` *and* ``dev.ucp.common.payment.terms``; declaring the
|
|
125
|
+
first without the second is malformed, so it is not treated as declared.
|
|
126
|
+
"""
|
|
127
|
+
caps = self.profile.capabilities
|
|
128
|
+
return (
|
|
129
|
+
CAP_PAY_AT_SERVICE in caps
|
|
130
|
+
and CAP_PAYMENT_TERMS in caps
|
|
131
|
+
and CAP_CHECKOUT in caps
|
|
132
|
+
)
|
|
133
|
+
|
|
113
134
|
def usp_authorization(self) -> Optional[AuthorizationPolicy]:
|
|
114
135
|
"""The ``config.authorization`` policy on the ``dev.usp-protocol.services`` binding.
|
|
115
136
|
|
|
@@ -148,10 +169,11 @@ class UcpNativeContext(BaseModel):
|
|
|
148
169
|
continue
|
|
149
170
|
if "authorization" in binding:
|
|
150
171
|
logger.warning(
|
|
151
|
-
|
|
172
|
+
"%s binding declares a sibling 'authorization' member, "
|
|
152
173
|
"which USP prohibits; the policy MUST be published as "
|
|
153
174
|
"config.authorization. Treating this business as publishing no "
|
|
154
|
-
"policy."
|
|
175
|
+
"policy.",
|
|
176
|
+
SVC_SERVICES,
|
|
155
177
|
)
|
|
156
178
|
misplaced = True
|
|
157
179
|
continue
|