booking-agent-cli 0.7.0__tar.gz → 0.8.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.
Files changed (224) hide show
  1. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/PKG-INFO +2 -2
  2. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/README.md +1 -1
  3. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/pyproject.toml +1 -1
  4. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/__init__.py +1 -1
  5. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/namespace.py +1 -0
  6. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/platform_profile.py +11 -0
  7. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_capabilities.py +6 -0
  8. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_checkout.py +144 -0
  9. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_profile.py +22 -1
  10. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/__init__.py +1 -1
  11. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/booking_summary.py +15 -1
  12. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/bundled_skill.md +18 -16
  13. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/catalog.py +26 -5
  14. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/flow.py +217 -8
  15. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/engine.py +2 -0
  16. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/payment_helpers.py +77 -22
  17. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_ucp_skill_flow_e2e.py +154 -0
  18. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_platform_profile.py +17 -0
  19. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/ucp_checkout_store.py +73 -0
  20. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/ucp_server.py +16 -5
  21. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/offline_stack/stack.py +26 -5
  22. booking_agent_cli-0.8.0/tests/unit/test_pay_at_service.py +249 -0
  23. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/.gitignore +0 -0
  24. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/authorization.py +0 -0
  25. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/backend/__init__.py +0 -0
  26. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/backend/live.py +0 -0
  27. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/__init__.py +0 -0
  28. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/filter.py +0 -0
  29. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/google.py +0 -0
  30. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
  31. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/scope.py +0 -0
  32. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/circuit_breaker.py +0 -0
  33. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/config.py +0 -0
  34. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/defaults.env +0 -0
  35. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/discovery.py +0 -0
  36. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/headless.py +0 -0
  37. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/http_deadlines.py +0 -0
  38. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/http_retry.py +0 -0
  39. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/http_trace.py +0 -0
  40. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/merchant_pacing.py +0 -0
  41. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/models/__init__.py +0 -0
  42. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/models/platform.py +0 -0
  43. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/models/wire.py +0 -0
  44. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/observability.py +0 -0
  45. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/pop.py +0 -0
  46. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/profile_cache.py +0 -0
  47. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/registry_url.py +0 -0
  48. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/rest_client.py +0 -0
  49. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/secure_file.py +0 -0
  50. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/settings.py +0 -0
  51. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/signals.py +0 -0
  52. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/step_timing.py +0 -0
  53. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_negotiation.py +0 -0
  54. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/usp_methods.py +0 -0
  55. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/activity_log.py +0 -0
  56. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/booking_poll.py +0 -0
  57. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/calendar_connection.py +0 -0
  58. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/calendar_event.py +0 -0
  59. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/calendar_preference.py +0 -0
  60. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
  61. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/__init__.py +0 -0
  62. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/config.py +0 -0
  63. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
  64. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/runtime.py +0 -0
  65. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/update.py +0 -0
  66. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/config_loader.py +0 -0
  67. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/flow_timings.py +0 -0
  68. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/geo_eligibility.py +0 -0
  69. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/geocode.py +0 -0
  70. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/identity.py +0 -0
  71. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/install_integrity.py +0 -0
  72. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/link_cli.py +0 -0
  73. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/main.py +0 -0
  74. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/output.py +0 -0
  75. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/payment_token.py +0 -0
  76. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/provenance.py +0 -0
  77. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/reschedule.py +0 -0
  78. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/runtime.py +0 -0
  79. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/runtime_prepare.py +0 -0
  80. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/search_area.py +0 -0
  81. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/session.py +0 -0
  82. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/skill_bundle.py +0 -0
  83. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/skill_integrity.py +0 -0
  84. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/support_bundle_schema.py +0 -0
  85. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/support_report.py +0 -0
  86. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/timezone_context.py +0 -0
  87. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/updates.py +0 -0
  88. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/.env.tests +0 -0
  89. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/conftest.py +0 -0
  90. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/__init__.py +0 -0
  91. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
  92. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_latency_soak.py +0 -0
  93. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_staging_registry_integration.py +0 -0
  94. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_timezone_booking_e2e.py +0 -0
  95. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
  96. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/ucp_native.py +0 -0
  97. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/__init__.py +0 -0
  98. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_activity_log.py +0 -0
  99. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_at_rest_hygiene.py +0 -0
  100. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_booking_rejection.py +0 -0
  101. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_bulk_cancel.py +0 -0
  102. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_connection.py +0 -0
  103. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_connector.py +0 -0
  104. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_flow.py +0 -0
  105. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_preference.py +0 -0
  106. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_checkout_signals.py +0 -0
  107. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_circuit_breaker.py +0 -0
  108. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_config.py +0 -0
  109. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_correlation_flow.py +0 -0
  110. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_declined_spt.py +0 -0
  111. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_discover_registry_rejection.py +0 -0
  112. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_evidence.py +0 -0
  113. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_gates.py +0 -0
  114. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_reschedule.py +0 -0
  115. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_step_timings.py +0 -0
  116. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_google_calendar_oauth.py +0 -0
  117. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_home_visit_address.py +0 -0
  118. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_http_trace.py +0 -0
  119. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_identity_logout.py +0 -0
  120. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_install_integrity.py +0 -0
  121. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_lazy_oauth.py +0 -0
  122. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_link_auth.py +0 -0
  123. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_link_cli_spt.py +0 -0
  124. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_namespace.py +0 -0
  125. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_oauth_listener.py +0 -0
  126. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_oauth_metadata.py +0 -0
  127. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_platform_key_pop.py +0 -0
  128. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_pop_keystore.py +0 -0
  129. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_profile_cache.py +0 -0
  130. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_regression_index.py +0 -0
  131. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
  132. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_request_retry_semantics.py +0 -0
  133. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_runtime_prepare.py +0 -0
  134. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_search_area.py +0 -0
  135. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_session_compat.py +0 -0
  136. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_session_retention.py +0 -0
  137. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_skill_integrity.py +0 -0
  138. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_skill_portability.py +0 -0
  139. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_support_report.py +0 -0
  140. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_timezone_context.py +0 -0
  141. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_cancel_result.py +0 -0
  142. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
  143. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
  144. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_checkout_validate.py +0 -0
  145. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_profile.py +0 -0
  146. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_update_command.py +0 -0
  147. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_wheel_contents.py +0 -0
  148. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/SOURCE +0 -0
  149. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
  150. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/availability.json +0 -0
  151. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/booking.json +0 -0
  152. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
  153. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/catalog.json +0 -0
  154. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/paid_bookings.json +0 -0
  155. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/profile.json +0 -0
  156. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/registry.json +0 -0
  157. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/rest_common.json +0 -0
  158. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/usp.json +0 -0
  159. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/waitlist.json +0 -0
  160. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/webhook_event.json +0 -0
  161. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/__init__.py +0 -0
  162. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/__init__.py +0 -0
  163. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/profiles.py +0 -0
  164. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
  165. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/services.py +0 -0
  166. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/__init__.py +0 -0
  167. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/backend.py +0 -0
  168. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/link_cli.py +0 -0
  169. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/mock_calendar.py +0 -0
  170. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/pop_business.py +0 -0
  171. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/pop_transport.py +0 -0
  172. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/offline_stack/__init__.py +0 -0
  173. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/__init__.py +0 -0
  174. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_authorization_policy.py +0 -0
  175. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_booking_poll_credentials.py +0 -0
  176. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_booking_request_error.py +0 -0
  177. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_calendar_filter.py +0 -0
  178. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_catalog.py +0 -0
  179. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_check_version.py +0 -0
  180. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_conditional_writes.py +0 -0
  181. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_delivery_address.py +0 -0
  182. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_discovery.py +0 -0
  183. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_geo_eligibility.py +0 -0
  184. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_geocode.py +0 -0
  185. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_headless.py +0 -0
  186. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_http_deadlines.py +0 -0
  187. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_http_retry.py +0 -0
  188. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_live_search_registry.py +0 -0
  189. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_merchant_pacing.py +0 -0
  190. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_mock_support.py +0 -0
  191. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_money_path_events.py +0 -0
  192. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
  193. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_observability.py +0 -0
  194. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_outbound_member_names.py +0 -0
  195. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_package_layering.py +0 -0
  196. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_provenance.py +0 -0
  197. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_search_filters.py +0 -0
  198. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_search_pricing.py +0 -0
  199. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_search_responses.py +0 -0
  200. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_url.py +0 -0
  201. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_release_notes.py +0 -0
  202. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_rest_client.py +0 -0
  203. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_scoped_method_authorization.py +0 -0
  204. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_session_credentials.py +0 -0
  205. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_spec_conformance.py +0 -0
  206. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_step_timing.py +0 -0
  207. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_capabilities.py +0 -0
  208. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
  209. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
  210. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_negotiation.py +0 -0
  211. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_updates.py +0 -0
  212. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_wire_model_conformance.py +0 -0
  213. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
  214. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
  215. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
  216. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
  217. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
  218. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
  219. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
  220. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
  221. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
  222. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/010-alg-none.json +0 -0
  223. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/README.md +0 -0
  224. {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.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.7.0
3
+ Version: 0.8.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.55.0/booking-agent/skills/booking-agent/SKILL.md
81
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.56.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.55.0/booking-agent/skills/booking-agent/SKILL.md
39
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.56.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
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "booking-agent-cli"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -3,4 +3,4 @@
3
3
  Ships in the booking-agent-cli wheel and never imports ``booking_agent_cli``.
4
4
  """
5
5
 
6
- __version__ = "0.7.0"
6
+ __version__ = "0.8.0"
@@ -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):
@@ -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 CAP_CATALOG, CAP_PAID_BOOKINGS, SVC_SERVICES, check_origin_binding
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
 
@@ -1,3 +1,3 @@
1
1
  """booking_agent_cli: typer-based stateful CLI for USP booking flows."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.8.0"
@@ -104,7 +104,7 @@ def price_text(session: Any, price_info: Dict[str, Any]) -> Optional[str]:
104
104
  return "Free"
105
105
  if not price:
106
106
  return None
107
- if route == "at_service":
107
+ if route in ("at_service", "pay_at_service"):
108
108
  return f"{price}, paid to the business at the appointment (nothing is charged now)"
109
109
  if route == "deposit_required":
110
110
  return (
@@ -151,6 +151,18 @@ def payment_context_text(session: Any, total_display: Optional[str]) -> str:
151
151
  return text
152
152
 
153
153
 
154
+ def pay_at_service_line(session: Any) -> str:
155
+ """What is owed and when, from the business's own accepted payment term."""
156
+ term = getattr(session, "accepted_term", None)
157
+ schedules = term.get("schedules") if isinstance(term, dict) else None
158
+ for schedule in schedules or []:
159
+ description = schedule.get("description") if isinstance(schedule, dict) else None
160
+ plain = description.get("plain") if isinstance(description, dict) else None
161
+ if isinstance(plain, str) and plain.strip():
162
+ return f"Nothing was charged. {plain.strip()}"
163
+ return "Nothing was charged. Payment is due at your appointment."
164
+
165
+
154
166
  def booked_message(session: Any) -> str:
155
167
  """Plain-language "you're booked" text built from saved state.
156
168
 
@@ -171,6 +183,8 @@ def booked_message(session: Any) -> str:
171
183
  link = getattr(session, "calendar_event_html_link", None)
172
184
  if link:
173
185
  text += f"\nAdded to your Google Calendar: {link}"
186
+ if getattr(session, "payment_route", None) == "pay_at_service":
187
+ text += "\n" + pay_at_service_line(session)
174
188
  booking_url = getattr(getattr(session, "booking", None), "booking_url", None)
175
189
  if booking_url:
176
190
  text += f"\nTo view, change, or cancel it later (worth saving): {booking_url}"
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 0.55.0
2
+ version: 0.56.0
3
3
  name: booking-agent
4
4
  description: |
5
5
  Books time-based services via USP from a Link wallet. Use when the user wants to book, schedule, reserve, or pay for a service from a business (haircut, lesson, repair, class, appointment, consult). The `booking-agent` CLI owns the booking state machine and refuses to proceed when human gates are skipped. Sign the user in to Link on your own, right after install and at the start of every booking (`booking-agent flow link-auth connect`, then start `booking-agent flow link-auth poll` in the background and post the sign-in URL in the same turn, exactly like `pay poll`); never wait for the user to ask to sign in or to say they approved. After `booking-agent flow business use`, follow `next_command` through `use-ucp-checkout-mode`; businesses without native checkout fail closed. Payment uses `booking-agent flow pay request` (default returns immediately with the approval URL), then start `booking-agent flow pay poll` in the background and post the approval URL to the user in the same turn; the poll finishes checkout once they approve. Confirm through `booking-agent flow book poll --wait`; never start an inbound webhook. Surface the approval URL exactly once. Personal identification (name, phone, email, address) must always come from the user's Link account via `link-cli`, never from chat or placeholders. Before any location-based search, the CLI proposes the Link shipping address and the user confirms it or gives another; online services skip this. Talk to the user in plain everyday language only; never expose CLI, UCP, auth, session, or other implementation details unless they explicitly ask.
@@ -19,7 +19,7 @@ metadata:
19
19
  - link-cli
20
20
  install:
21
21
  - kind: uv
22
- package: "booking-agent-cli==0.7.0"
22
+ package: "booking-agent-cli==0.8.0"
23
23
  bins: [booking-agent]
24
24
  user-invocable: true
25
25
  ---
@@ -220,7 +220,7 @@ To take a fixed, published version instead of the latest, fetch it at the releas
220
220
  workflow signs it under, and check the signature before you follow it:
221
221
 
222
222
  ```bash
223
- curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.55.0/booking-agent/skills/booking-agent/SKILL.md
223
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.56.0/booking-agent/skills/booking-agent/SKILL.md
224
224
  gh attestation verify SKILL.md \
225
225
  --repo wix/booking-agent \
226
226
  --signer-workflow wix/booking-agent/.github/workflows/release.yml
@@ -245,17 +245,17 @@ before installing:
245
245
 
246
246
  ```bash
247
247
  dir="$HOME/.booking-agent/verified-wheels"
248
- uvx pip download "booking-agent-cli==0.7.0" --no-deps --only-binary :all: -d "$dir"
248
+ uvx pip download "booking-agent-cli==0.8.0" --no-deps --only-binary :all: -d "$dir"
249
249
  uvx pypi-attestations==0.0.30 verify pypi --repository https://github.com/wix/booking-agent \
250
- "$dir/booking_agent_cli-0.7.0-py3-none-any.whl"
251
- uv tool install --force "$dir/booking_agent_cli-0.7.0-py3-none-any.whl"
250
+ "$dir/booking_agent_cli-0.8.0-py3-none-any.whl"
251
+ uv tool install --force "$dir/booking_agent_cli-0.8.0-py3-none-any.whl"
252
252
 
253
253
  booking-agent --help
254
254
  ```
255
255
 
256
256
  Install only the wheel whose verify step passed, never one that failed it. If `uvx` is not
257
- available, install the same pinned release with `uv tool install booking-agent-cli==0.7.0` or
258
- `pipx install booking-agent-cli==0.7.0`; the CLI checks its own files against the signed release
257
+ available, install the same pinned release with `uv tool install booking-agent-cli==0.8.0` or
258
+ `pipx install booking-agent-cli==0.8.0`; the CLI checks its own files against the signed release
259
259
  on the first `booking-agent flow init` and refuses to book or pay if they do not match.
260
260
 
261
261
  Harnesses that read the `install` block in this skill's metadata do this for you. Do not go on
@@ -308,7 +308,7 @@ prints each effective setting and where it came from; `booking-agent config init
308
308
 
309
309
  The CLI decides whether a newer version is worth mentioning; you never decide from version
310
310
  numbers or release pages. Pass this skill's version on every `flow init` (`--skill-version
311
- 0.55.0`, from the frontmatter above), so the CLI can tell when these instructions and the
311
+ 0.56.0`, from the frontmatter above), so the CLI can tell when these instructions and the
312
312
  installed CLI no longer match.
313
313
 
314
314
  - When a command returns `awaiting_user: update_consent`, ask the question in `prompt` in plain
@@ -470,7 +470,7 @@ confirmation email or on the business's page (`booking_url`). Do not invent a lo
470
470
  **Step 1b — Create a new session:**
471
471
 
472
472
  ```bash
473
- booking-agent flow init --user-request "<user's exact request>" --skill-version 0.55.0 \
473
+ booking-agent flow init --user-request "<user's exact request>" --skill-version 0.56.0 \
474
474
  --skill-path "<absolute path of the SKILL.md you loaded>"
475
475
  ```
476
476
 
@@ -786,8 +786,7 @@ If `flow book` fails with `mandatory_acceptance_pending`, show the user any poli
786
786
  `flow book`. **Never** accept policies or tick boxes on their behalf.
787
787
 
788
788
  If `data.checkout_links` or `data.checkout_policies` are present after a successful book, show
789
- those links before payment. If the merchant sent none, say nothing about policies; do not tell
790
- the user policies are "missing."
789
+ those links before payment. If none were sent, say nothing about policies.
791
790
 
792
791
  **Free and pay-at-appointment bookings (`free`, `at_service`).** `flow book` places the booking
793
792
  directly and `next_command` is `booking-agent flow book poll --wait`. Before running it, tell the user
@@ -796,6 +795,9 @@ charged (or the price is paid at the appointment), and you are now waiting for t
796
795
  confirm. Then run `book poll --wait` and report the result. Never create a payment and skip
797
796
  Steps 6b to 6d.
798
797
 
798
+ **Pay-at-appointment with a checkout (`pay_at_service`).** Nothing is charged; skip `pay request`. Show every `data.payment_disclosures` item (`content_display`) and the amount
799
+ due at the appointment, then run `pay ucp-complete --disclosures-confirmed`. If `merchant_quote.total_changed`, get approval and add `--total-confirmed`.
800
+
799
801
  **Deposit bookings (`deposit_required`).** Before any charge, show `data.due_now_display` (the
800
802
  deposit charged now), the amount due at the appointment, and every item in
801
803
  `data.payment_schedules` and `data.payment_disclosures`, and get the user's agreement. Then run
@@ -927,8 +929,9 @@ UCP `complete_checkout` (CLI builds this internally): `handler_id: "stripe_payme
927
929
 
928
930
  **Payment notes (error recovery only):**
929
931
 
930
- - A live catalog with `payment_timing: at_service` is booked directly. Tell the user the quoted
931
- price is paid at the appointment. Do not run a payment command.
932
+ - A live catalog with `payment_timing: at_service` is booked directly or, when `data.payment_route`
933
+ is `pay_at_service`, through a checkout. Tell the user the quoted price is paid at the
934
+ appointment. Never run `pay request` for it.
932
935
  - A live catalog with `payment_timing: deposit_required` goes through checkout. Charge only the
933
936
  due-now (immediate) amount from the payment term. Show every disclosure and the amount due at
934
937
  the appointment, then pass `--disclosures-confirmed` on `pay request` after the user agrees.
@@ -963,8 +966,7 @@ data, credentials, or shared payment tokens.
963
966
 
964
967
  ### Step 8 and after the booking
965
968
 
966
- Nothing to run: `pay poll` (and `book poll --wait` for free or pay-at-appointment bookings) adds the
967
- calendar event and returns the final answer. See Step 7.
969
+ Nothing to run; Step 7's command already added the calendar event.
968
970
 
969
971
  ## Important
970
972
 
@@ -670,15 +670,28 @@ def payment_price_for_session(
670
670
 
671
671
 
672
672
  DIRECT_BOOKING_ROUTES = frozenset({"free", "at_service"})
673
- CHECKOUT_BOOKING_ROUTES = frozenset({"at_booking", "deposit_required"})
673
+ CHECKOUT_BOOKING_ROUTES = frozenset({"at_booking", "deposit_required", "pay_at_service"})
674
+ # Routes where nothing is collected through the booking flow: no Link spend
675
+ # request, no payment instrument. ``pay_at_service`` still goes through a UCP
676
+ # checkout (so an order exists); ``free`` and ``at_service`` do not.
677
+ NO_PAYMENT_ROUTES = frozenset({"free", "at_service", "pay_at_service"})
674
678
 
675
679
 
676
- def classify_payment_route(live_service: Dict[str, Any]) -> Dict[str, Any]:
680
+ def classify_payment_route(
681
+ live_service: Dict[str, Any],
682
+ *,
683
+ pay_at_service_declared: bool = False,
684
+ ) -> Dict[str, Any]:
677
685
  """Classify a live service into a per-service payment route.
678
686
 
679
687
  Timing is a service property, not a business property. Mixed catalogs
680
688
  (at_booking, at_service, deposit_required, free on one profile) must route
681
689
  each service independently.
690
+
691
+ An ``at_service`` service takes the ``pay_at_service`` checkout route only
692
+ when the business declares the pay-at-service extension; otherwise it keeps
693
+ the direct route (USP 7.6.1: a platform MUST NOT route ``at_service``
694
+ through a checkout the business has not declared the extension for).
682
695
  """
683
696
  policies = live_service.get("policies") if isinstance(live_service, dict) else None
684
697
  pricing = live_service.get("pricing") if isinstance(live_service, dict) else None
@@ -734,7 +747,8 @@ def classify_payment_route(live_service: Dict[str, Any]) -> Dict[str, Any]:
734
747
  if timing == "at_booking":
735
748
  return {"route": "at_booking", "timing": timing, "refusal": None}
736
749
  if timing == "at_service":
737
- return {"route": "at_service", "timing": timing, "refusal": None}
750
+ route = "pay_at_service" if pay_at_service_declared else "at_service"
751
+ return {"route": route, "timing": timing, "refusal": None}
738
752
  if timing == "deposit_required":
739
753
  return {
740
754
  "route": "deposit_required",
@@ -758,9 +772,12 @@ def payment_timing_refusal(
758
772
  live_service: Dict[str, Any],
759
773
  *,
760
774
  allow_deposits: bool = True,
775
+ pay_at_service_declared: bool = False,
761
776
  ) -> Optional[Dict[str, str]]:
762
777
  """Return a fail-closed error when this service cannot be booked."""
763
- classified = classify_payment_route(live_service)
778
+ classified = classify_payment_route(
779
+ live_service, pay_at_service_declared=pay_at_service_declared
780
+ )
764
781
  if classified["refusal"] is not None:
765
782
  return classified["refusal"]
766
783
  if classified["route"] == "deposit_required" and not allow_deposits:
@@ -779,9 +796,13 @@ def payment_timing_changed_refusal(
779
796
  live_service: Dict[str, Any],
780
797
  ) -> Optional[Dict[str, str]]:
781
798
  """Fail closed when a later live read disagrees with the discovered timing."""
782
- classified = classify_payment_route(live_service)
783
799
  previous = getattr(session, "payment_timing", None)
784
800
  previous_route = getattr(session, "payment_route", None)
801
+ # The business's declaration was read when the route was chosen; the live
802
+ # service read only re-checks the service's own timing.
803
+ classified = classify_payment_route(
804
+ live_service, pay_at_service_declared=previous_route == "pay_at_service"
805
+ )
785
806
  new_timing = classified["timing"]
786
807
  new_route = classified["route"]
787
808
  if previous is None and previous_route is None: