booking-agent-cli 0.6.0__tar.gz → 0.7.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 (223) hide show
  1. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/PKG-INFO +2 -2
  2. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/README.md +1 -1
  3. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/pyproject.toml +1 -1
  4. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/__init__.py +1 -1
  5. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/defaults.env +3 -0
  6. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/__init__.py +1 -1
  7. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/bundled_skill.md +25 -26
  8. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/commands/flow.py +89 -0
  9. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/runtime_prepare.py +200 -0
  10. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/support_report.py +9 -8
  11. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/conftest.py +11 -0
  12. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_regression_index.py +10 -0
  13. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_runtime_prepare.py +244 -1
  14. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_skill_portability.py +1 -1
  15. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/.gitignore +0 -0
  16. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/authorization.py +0 -0
  17. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/backend/__init__.py +0 -0
  18. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/backend/live.py +0 -0
  19. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/calendar/__init__.py +0 -0
  20. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/calendar/filter.py +0 -0
  21. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/calendar/google.py +0 -0
  22. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
  23. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/calendar/scope.py +0 -0
  24. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/circuit_breaker.py +0 -0
  25. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/config.py +0 -0
  26. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/discovery.py +0 -0
  27. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/headless.py +0 -0
  28. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/http_deadlines.py +0 -0
  29. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/http_retry.py +0 -0
  30. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/http_trace.py +0 -0
  31. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/merchant_pacing.py +0 -0
  32. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/models/__init__.py +0 -0
  33. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/models/platform.py +0 -0
  34. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/models/wire.py +0 -0
  35. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/namespace.py +0 -0
  36. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/observability.py +0 -0
  37. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/platform_profile.py +0 -0
  38. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/pop.py +0 -0
  39. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/profile_cache.py +0 -0
  40. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/registry_url.py +0 -0
  41. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/rest_client.py +0 -0
  42. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/secure_file.py +0 -0
  43. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/settings.py +0 -0
  44. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/signals.py +0 -0
  45. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/step_timing.py +0 -0
  46. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/ucp_capabilities.py +0 -0
  47. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/ucp_checkout.py +0 -0
  48. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/ucp_negotiation.py +0 -0
  49. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/ucp_profile.py +0 -0
  50. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent/usp_methods.py +0 -0
  51. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/activity_log.py +0 -0
  52. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/booking_poll.py +0 -0
  53. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/booking_summary.py +0 -0
  54. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/calendar_connection.py +0 -0
  55. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/calendar_event.py +0 -0
  56. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/calendar_preference.py +0 -0
  57. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/catalog.py +0 -0
  58. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
  59. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/commands/__init__.py +0 -0
  60. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/commands/config.py +0 -0
  61. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
  62. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/commands/runtime.py +0 -0
  63. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/commands/update.py +0 -0
  64. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/config_loader.py +0 -0
  65. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/engine.py +0 -0
  66. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/flow_timings.py +0 -0
  67. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/geo_eligibility.py +0 -0
  68. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/geocode.py +0 -0
  69. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/identity.py +0 -0
  70. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/install_integrity.py +0 -0
  71. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/link_cli.py +0 -0
  72. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/main.py +0 -0
  73. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/output.py +0 -0
  74. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/payment_helpers.py +0 -0
  75. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/payment_token.py +0 -0
  76. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/provenance.py +0 -0
  77. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/reschedule.py +0 -0
  78. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/runtime.py +0 -0
  79. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/search_area.py +0 -0
  80. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/session.py +0 -0
  81. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/skill_bundle.py +0 -0
  82. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/skill_integrity.py +0 -0
  83. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/support_bundle_schema.py +0 -0
  84. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/timezone_context.py +0 -0
  85. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/src/booking_agent_cli/updates.py +0 -0
  86. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/.env.tests +0 -0
  87. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/__init__.py +0 -0
  88. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
  89. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/test_latency_soak.py +0 -0
  90. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/test_staging_registry_integration.py +0 -0
  91. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/test_timezone_booking_e2e.py +0 -0
  92. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
  93. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/test_ucp_skill_flow_e2e.py +0 -0
  94. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/e2e/ucp_native.py +0 -0
  95. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/__init__.py +0 -0
  96. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_activity_log.py +0 -0
  97. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_at_rest_hygiene.py +0 -0
  98. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_booking_rejection.py +0 -0
  99. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_bulk_cancel.py +0 -0
  100. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_calendar_connection.py +0 -0
  101. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_calendar_connector.py +0 -0
  102. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_calendar_flow.py +0 -0
  103. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_calendar_preference.py +0 -0
  104. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_checkout_signals.py +0 -0
  105. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_circuit_breaker.py +0 -0
  106. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_config.py +0 -0
  107. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_correlation_flow.py +0 -0
  108. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_declined_spt.py +0 -0
  109. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_discover_registry_rejection.py +0 -0
  110. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_flow_evidence.py +0 -0
  111. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_flow_gates.py +0 -0
  112. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_flow_reschedule.py +0 -0
  113. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_flow_step_timings.py +0 -0
  114. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_google_calendar_oauth.py +0 -0
  115. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_home_visit_address.py +0 -0
  116. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_http_trace.py +0 -0
  117. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_identity_logout.py +0 -0
  118. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_install_integrity.py +0 -0
  119. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_lazy_oauth.py +0 -0
  120. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_link_auth.py +0 -0
  121. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_link_cli_spt.py +0 -0
  122. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_namespace.py +0 -0
  123. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_oauth_listener.py +0 -0
  124. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_oauth_metadata.py +0 -0
  125. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_platform_key_pop.py +0 -0
  126. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_platform_profile.py +0 -0
  127. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_pop_keystore.py +0 -0
  128. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_profile_cache.py +0 -0
  129. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
  130. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_request_retry_semantics.py +0 -0
  131. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_search_area.py +0 -0
  132. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_session_compat.py +0 -0
  133. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_session_retention.py +0 -0
  134. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_skill_integrity.py +0 -0
  135. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_support_report.py +0 -0
  136. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_timezone_context.py +0 -0
  137. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_ucp_cancel_result.py +0 -0
  138. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
  139. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
  140. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_ucp_checkout_validate.py +0 -0
  141. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_ucp_profile.py +0 -0
  142. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_update_command.py +0 -0
  143. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/integration/test_wheel_contents.py +0 -0
  144. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/SOURCE +0 -0
  145. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
  146. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/availability.json +0 -0
  147. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/booking.json +0 -0
  148. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
  149. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/catalog.json +0 -0
  150. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/paid_bookings.json +0 -0
  151. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/profile.json +0 -0
  152. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/registry.json +0 -0
  153. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/rest_common.json +0 -0
  154. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/usp.json +0 -0
  155. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/waitlist.json +0 -0
  156. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/spec_schemas/webhook_event.json +0 -0
  157. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/__init__.py +0 -0
  158. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/fixtures/__init__.py +0 -0
  159. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/fixtures/profiles.py +0 -0
  160. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
  161. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/fixtures/services.py +0 -0
  162. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/__init__.py +0 -0
  163. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/backend.py +0 -0
  164. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/link_cli.py +0 -0
  165. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/mock_calendar.py +0 -0
  166. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/pop_business.py +0 -0
  167. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/pop_transport.py +0 -0
  168. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/ucp_checkout_store.py +0 -0
  169. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/mock/ucp_server.py +0 -0
  170. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/offline_stack/__init__.py +0 -0
  171. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/support/offline_stack/stack.py +0 -0
  172. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/__init__.py +0 -0
  173. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_authorization_policy.py +0 -0
  174. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_booking_poll_credentials.py +0 -0
  175. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_booking_request_error.py +0 -0
  176. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_calendar_filter.py +0 -0
  177. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_catalog.py +0 -0
  178. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_check_version.py +0 -0
  179. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_conditional_writes.py +0 -0
  180. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_delivery_address.py +0 -0
  181. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_discovery.py +0 -0
  182. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_geo_eligibility.py +0 -0
  183. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_geocode.py +0 -0
  184. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_headless.py +0 -0
  185. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_http_deadlines.py +0 -0
  186. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_http_retry.py +0 -0
  187. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_live_search_registry.py +0 -0
  188. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_merchant_pacing.py +0 -0
  189. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_mock_support.py +0 -0
  190. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_money_path_events.py +0 -0
  191. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
  192. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_observability.py +0 -0
  193. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_outbound_member_names.py +0 -0
  194. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_package_layering.py +0 -0
  195. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_provenance.py +0 -0
  196. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_registry_search_filters.py +0 -0
  197. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_registry_search_pricing.py +0 -0
  198. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_registry_search_responses.py +0 -0
  199. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_registry_url.py +0 -0
  200. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_release_notes.py +0 -0
  201. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_rest_client.py +0 -0
  202. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_scoped_method_authorization.py +0 -0
  203. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_session_credentials.py +0 -0
  204. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_spec_conformance.py +0 -0
  205. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_step_timing.py +0 -0
  206. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_ucp_capabilities.py +0 -0
  207. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
  208. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
  209. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_ucp_negotiation.py +0 -0
  210. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_updates.py +0 -0
  211. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/unit/test_wire_model_conformance.py +0 -0
  212. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
  213. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
  214. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
  215. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
  216. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
  217. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
  218. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
  219. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
  220. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
  221. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/010-alg-none.json +0 -0
  222. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.0}/tests/vectors/pop/README.md +0 -0
  223. {booking_agent_cli-0.6.0 → booking_agent_cli-0.7.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.6.0
3
+ Version: 0.7.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.54.0/booking-agent/skills/booking-agent/SKILL.md
81
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.55.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.54.0/booking-agent/skills/booking-agent/SKILL.md
39
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.55.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.6.0"
3
+ version = "0.7.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.6.0"
6
+ __version__ = "0.7.0"
@@ -17,6 +17,9 @@ BOOKING_AGENT_SESSION_DIR=~/.booking-agent
17
17
  # Absolute folder a host keeps between runs. When set, booking-agent links its state and the
18
18
  # Link sign-in into it by itself before every command.
19
19
  # BOOKING_AGENT_PERSISTENT_ROOT=
20
+ # Set to `none` by a host that keeps this runtime's files between conversations itself, so
21
+ # `flow init` does not ask the agent how state is kept.
22
+ # BOOKING_AGENT_PERSISTENCE=
20
23
  BOOKING_AGENT_MAX_SESSIONS=30
21
24
  LINK_CLI_BIN=link-cli
22
25
 
@@ -1,3 +1,3 @@
1
1
  """booking_agent_cli: typer-based stateful CLI for USP booking flows."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 0.54.0
2
+ version: 0.55.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.6.0"
22
+ package: "booking-agent-cli==0.7.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.54.0/booking-agent/skills/booking-agent/SKILL.md
223
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.55.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.6.0" --no-deps --only-binary :all: -d "$dir"
248
+ uvx pip download "booking-agent-cli==0.7.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.6.0-py3-none-any.whl"
251
- uv tool install --force "$dir/booking_agent_cli-0.6.0-py3-none-any.whl"
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"
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.6.0` or
258
- `pipx install booking-agent-cli==0.6.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.7.0` or
258
+ `pipx install booking-agent-cli==0.7.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
@@ -275,22 +275,21 @@ npm install -g @stripe/link-cli
275
275
 
276
276
  ### After installing
277
277
 
278
- **Keep state between conversations.** When the host names a folder that survives between
279
- conversations in `BOOKING_AGENT_PERSISTENT_ROOT`, the CLI links its state and the Link sign-in into
280
- it by itself before every command: do nothing and never mention it. Only when the variable is unset
281
- and this runtime gives you such a folder, run this once per conversation, before `booking-agent flow
282
- sessions list`:
283
-
284
- ```bash
285
- booking-agent runtime prepare --persistent-root <absolute folder that survives>
286
- ```
287
-
288
- Use the same folder every time; it merges what each folder holds (booking files by session, the
289
- newer copy of anything else) and deletes nothing. Add `--link <runtime folder>=<persistent folder>`
290
- for each extra cache folder the runtime asks you to keep. If the user is asked to sign in or
291
- reconnect right after a restart, run `booking-agent runtime doctor` and follow its `next_command`.
292
- When the runtime keeps its files anyway, skip all of this. Do not tell the user about it. If it
293
- fails, continue the booking without it.
278
+ **Keep state between conversations.** `flow init` asks how this runtime keeps its files and refuses
279
+ (`persistence_unresolved`) until you answer. Look at what the runtime offers. If it gives you a
280
+ folder that survives between conversations, re-run the same init with `--persistent-root <absolute
281
+ path of that folder>`: the CLI links saved bookings and the Link sign-in into it, merging what each
282
+ folder holds (booking files by session, the newer copy of anything else) and deleting nothing. If
283
+ the runtime keeps its files anyway, or gives no such folder, re-run it with `--no-persistent-root`.
284
+ If it answers `persistence_folder_not_durable` the folder failed a basic check (a temporary or
285
+ memory-backed place, or not writable): name the folder the runtime really keeps, or use
286
+ `--no-persistent-root`. It sends you back at most twice and then uses the folder as given.
287
+ `persistence_setup_failed` and `persistence_flags_conflict` say what to change. Use the same folder
288
+ every time, never guess a path, never ask the user, and never mention it. A host
289
+ that sets `BOOKING_AGENT_PERSISTENT_ROOT` or `BOOKING_AGENT_PERSISTENCE=none` is never asked. If the
290
+ user is asked to sign in or reconnect right after a restart, run `booking-agent runtime doctor` and
291
+ follow its `next_command`. For extra cache folders the runtime asks you to keep, run `booking-agent
292
+ runtime prepare --persistent-root <folder> --link <runtime folder>=<persistent folder>`.
294
293
 
295
294
  **Sign the user in to Link**, as described in [Link sign-in](#link-sign-in-automatic). It works
296
295
  with no booking in progress. Do not wait for the user to offer to sign in.
@@ -309,7 +308,7 @@ prints each effective setting and where it came from; `booking-agent config init
309
308
 
310
309
  The CLI decides whether a newer version is worth mentioning; you never decide from version
311
310
  numbers or release pages. Pass this skill's version on every `flow init` (`--skill-version
312
- 0.54.0`, from the frontmatter above), so the CLI can tell when these instructions and the
311
+ 0.55.0`, from the frontmatter above), so the CLI can tell when these instructions and the
313
312
  installed CLI no longer match.
314
313
 
315
314
  - When a command returns `awaiting_user: update_consent`, ask the question in `prompt` in plain
@@ -471,7 +470,7 @@ confirmation email or on the business's page (`booking_url`). Do not invent a lo
471
470
  **Step 1b — Create a new session:**
472
471
 
473
472
  ```bash
474
- booking-agent flow init --user-request "<user's exact request>" --skill-version 0.54.0 \
473
+ booking-agent flow init --user-request "<user's exact request>" --skill-version 0.55.0 \
475
474
  --skill-path "<absolute path of the SKILL.md you loaded>"
476
475
  ```
477
476
 
@@ -399,8 +399,17 @@ def init(
399
399
  capability: Optional[List[str]] = typer.Option(
400
400
  None, "--capability", help="A capability the user's request needs; repeatable.",
401
401
  ),
402
+ persistent_root: Optional[str] = typer.Option(
403
+ None, "--persistent-root",
404
+ help="Absolute folder this runtime keeps between conversations; state is linked into it.",
405
+ ),
406
+ no_persistent_root: bool = typer.Option(
407
+ False, "--no-persistent-root",
408
+ help="This runtime keeps its files between conversations anyway, or has no such folder.",
409
+ ),
402
410
  ) -> None:
403
411
  """Initialize a new booking session."""
412
+ _resolve_persistence(persistent_root, no_persistent_root)
404
413
  store = load_store()
405
414
  # Before the session checks: a skill the agent points us at is checked whatever the
406
415
  # flow state, so a changed file is never masked by session_active.
@@ -613,6 +622,86 @@ def _upcoming_duplicate_bookings(store: Any, user_request: str) -> List[Dict[str
613
622
  return matches
614
623
 
615
624
 
625
+ def _resolve_persistence(persistent_root: Optional[str], no_persistent_root: bool) -> None:
626
+ """Make the agent say how this runtime keeps saved state, before any state is written.
627
+
628
+ Skipping the setup used to be silent: nothing failed until the next restart lost the
629
+ booking, the calendar link, and the sign-in. The agent, not the CLI, can see what the runtime
630
+ offers, so it must answer with a folder or an explicit "no folder"; the CLI never guesses one.
631
+ """
632
+ from ..runtime_prepare import (
633
+ MAX_CORRECTIONS,
634
+ PrepareError,
635
+ acknowledge_no_persistence,
636
+ clear_attempts,
637
+ corrections_so_far,
638
+ durability_problem,
639
+ persistence_resolution,
640
+ prepare,
641
+ record_accepted_despite,
642
+ record_correction,
643
+ )
644
+
645
+ if persistent_root and no_persistent_root:
646
+ emit_error(
647
+ "INIT",
648
+ code="persistence_flags_conflict",
649
+ message="Pass either --persistent-root or --no-persistent-root, not both.",
650
+ )
651
+ raise typer.Exit(code=1)
652
+ if persistent_root:
653
+ problem = durability_problem(persistent_root)
654
+ if problem and corrections_so_far() < MAX_CORRECTIONS:
655
+ used = record_correction()
656
+ emit_error(
657
+ "INIT",
658
+ code="persistence_folder_not_durable",
659
+ message=(
660
+ f"That folder cannot be relied on to survive a restart: {problem}. Nothing "
661
+ "was moved. Re-run the same init command with the folder this runtime "
662
+ "really keeps between conversations, or with --no-persistent-root if it keeps "
663
+ f"its files anyway. (Correction {used} of {MAX_CORRECTIONS}; after that the "
664
+ "folder you name is used as given so the booking is never stuck.)"
665
+ ),
666
+ )
667
+ raise typer.Exit(code=1)
668
+ if problem:
669
+ record_accepted_despite(problem)
670
+ else:
671
+ clear_attempts()
672
+ try:
673
+ prepare(persistent_root, [])
674
+ except (PrepareError, OSError) as exc:
675
+ emit_error(
676
+ "INIT",
677
+ code="persistence_setup_failed",
678
+ message=(
679
+ f"Saved state could not be linked into that folder ({exc}). Re-run the same "
680
+ "init command with the folder this runtime really keeps between "
681
+ "conversations, or with --no-persistent-root to go on without it."
682
+ ),
683
+ )
684
+ raise typer.Exit(code=1)
685
+ return
686
+ if no_persistent_root:
687
+ acknowledge_no_persistence()
688
+ return
689
+ if persistence_resolution() is None:
690
+ emit_error(
691
+ "INIT",
692
+ code="persistence_unresolved",
693
+ message=(
694
+ "Saved bookings and the Link sign-in are not in a folder this runtime keeps "
695
+ "between conversations, so a restart would lose them. Look at what this runtime "
696
+ "offers. If it provides a folder that survives restarts, re-run this same init "
697
+ "command with --persistent-root <absolute path of that folder>. If its files are "
698
+ "kept anyway, or it offers no such folder, re-run it with --no-persistent-root. "
699
+ "Decide from the runtime, never ask the user, and never guess a path."
700
+ ),
701
+ )
702
+ raise typer.Exit(code=1)
703
+
704
+
616
705
  def _unfinished_similar_sessions(store: Any, user_request: str) -> List[Dict[str, Any]]:
617
706
  """Unfinished sessions whose request covers most of this one's key terms, newest first."""
618
707
  wanted = _request_terms(user_request)
@@ -14,6 +14,7 @@ import json
14
14
  import os
15
15
  import shutil
16
16
  import sys
17
+ import tempfile
17
18
  import time
18
19
  from pathlib import Path
19
20
  from typing import Dict, List, Optional, Tuple
@@ -25,6 +26,16 @@ STATE_SUBDIR = "booking-agent"
25
26
  LINK_SUBDIR = "link-cli"
26
27
  _LINK_CLI_APP = "link-cli-nodejs"
27
28
  ROOT_ENV = "BOOKING_AGENT_PERSISTENT_ROOT"
29
+ # A host that keeps this runtime's files itself declares so with ``BOOKING_AGENT_PERSISTENCE=none``.
30
+ PERSISTENCE_ENV = "BOOKING_AGENT_PERSISTENCE"
31
+ _ACK_FILE = ".persistence-none"
32
+ _ATTEMPTS_FILE = ".persistence-attempts"
33
+ _BOOT_FILE = ".booking-agent-boot"
34
+ # How many times the CLI sends the agent back after a folder fails the checks. The next one is
35
+ # accepted, so a stubborn agent can never leave a booking stuck on this question.
36
+ MAX_CORRECTIONS = 2
37
+ _TEMP_ROOTS = ("/tmp", "/var/tmp", "/dev/shm", "/run")
38
+ _MEMORY_FILESYSTEMS = frozenset({"tmpfs", "ramfs", "devtmpfs"})
28
39
  _SESSION_FILE = ".usp-session.json"
29
40
  # Order a booking moves through, used to keep the further-along copy of the same session.
30
41
  _PHASE_ORDER = (
@@ -311,6 +322,7 @@ def prepare(persistent_root: str, raw_links: List[str]) -> List[Dict[str, str]]:
311
322
  results.append(link_one(runtime, persistent, private=private))
312
323
  if lifted and results:
313
324
  results[0].update(lifted)
325
+ touch_boot_marker(root)
314
326
  return results
315
327
 
316
328
 
@@ -365,7 +377,21 @@ def diagnose() -> Dict[str, object]:
365
377
  "kind": "configured_root_not_applied", "path": str(state), "severity": "problem",
366
378
  })
367
379
  root_hint = root_hint or configured
380
+ persistence: Dict[str, object] = {"folder": root_hint or None, "survived_restart": None}
381
+ if root_hint:
382
+ persistence["survived_restart"] = survived_restart(Path(root_hint))
383
+ if persistence["survived_restart"] is False:
384
+ findings.append({
385
+ "kind": "folder_not_yet_shown_to_survive_a_restart",
386
+ "path": root_hint, "severity": "info",
387
+ })
388
+ despite = _read_attempts().get("accepted_despite")
389
+ if despite:
390
+ findings.append({
391
+ "kind": "folder_accepted_despite_checks", "path": str(despite), "severity": "info",
392
+ })
368
393
  return {
394
+ "persistence": persistence,
369
395
  "configured_root": configured,
370
396
  "state": state_info,
371
397
  "link_sign_in": link_info,
@@ -396,3 +422,177 @@ def auto_prepare() -> Optional[List[Dict[str, str]]]:
396
422
  return prepare(root, [])
397
423
  except (PrepareError, OSError):
398
424
  return None
425
+
426
+
427
+ def persistence_resolution() -> Optional[str]:
428
+ """Why ``flow init`` may go on without asking how this runtime keeps state, or None.
429
+
430
+ ``linked``: saved state already lives in a folder the CLI linked it into.
431
+ ``host_declared``: the host set ``BOOKING_AGENT_PERSISTENCE=none``.
432
+ ``acknowledged``: an agent already said this runtime keeps its files (kept beside the state,
433
+ so a fresh runtime that lost the state is asked again).
434
+ """
435
+ home = settings.booking_agent_home()
436
+ if home.is_symlink():
437
+ return "linked"
438
+ if (settings.get(PERSISTENCE_ENV) or "").strip().lower() == "none":
439
+ return "host_declared"
440
+ if (home / _ACK_FILE).is_file():
441
+ return "acknowledged"
442
+ return None
443
+
444
+
445
+ def acknowledge_no_persistence() -> None:
446
+ home = ensure_private_dir(settings.booking_agent_home())
447
+ write_private_text(home / _ACK_FILE, time.strftime("%Y-%m-%d\n", time.gmtime()))
448
+
449
+
450
+ # ---------------------------------------------------------------------------
451
+ # Checks on the folder an agent names, and the record of how often it was sent back
452
+ # ---------------------------------------------------------------------------
453
+
454
+ def ephemeral_roots() -> List[Path]:
455
+ """Folders whose contents are not kept between runs, resolved."""
456
+ roots = [Path(p) for p in _TEMP_ROOTS] + [Path(tempfile.gettempdir())]
457
+ resolved = []
458
+ for root in roots:
459
+ try:
460
+ resolved.append(root.resolve())
461
+ except OSError:
462
+ continue
463
+ return resolved
464
+
465
+
466
+ def fs_type(path: Path) -> Optional[str]:
467
+ """Filesystem type holding ``path`` (Linux only; None when it cannot be told)."""
468
+ try:
469
+ target = path.resolve()
470
+ best, best_type = "", None
471
+ with open("/proc/self/mountinfo", encoding="utf-8") as f:
472
+ for line in f:
473
+ left, _, right = line.partition(" - ")
474
+ fields = left.split()
475
+ if len(fields) < 5 or not right:
476
+ continue
477
+ mount = fields[4].replace("\\040", " ")
478
+ if (str(target) == mount or str(target).startswith(mount.rstrip("/") + "/")) and len(mount) > len(best):
479
+ best, best_type = mount, right.split()[0]
480
+ return best_type
481
+ except OSError:
482
+ return None
483
+
484
+
485
+ def durability_problem(raw: str) -> Optional[str]:
486
+ """Why the folder an agent named cannot be a place that survives restarts, or None.
487
+
488
+ Only what can be known now: a path that cannot work, or one the OS itself says is not kept.
489
+ A folder on ordinary disk that the host simply never restores cannot be told apart here.
490
+ """
491
+ try:
492
+ path = _absolute(raw, what="The folder")
493
+ except PrepareError as exc:
494
+ return str(exc)
495
+ try:
496
+ resolved = path.resolve()
497
+ except OSError:
498
+ return "the path cannot be resolved"
499
+ if resolved == Path(resolved.anchor):
500
+ return "it is the filesystem root"
501
+ for root in ephemeral_roots():
502
+ if resolved == root or root in resolved.parents:
503
+ return f"it is under {root}, which is cleared between runs"
504
+ kind = fs_type(resolved)
505
+ if kind in _MEMORY_FILESYSTEMS:
506
+ return f"it is on {kind}, which lives in memory"
507
+ ancestor = resolved
508
+ while not ancestor.exists() and ancestor != ancestor.parent:
509
+ ancestor = ancestor.parent
510
+ if not ancestor.is_dir():
511
+ return f"{ancestor} is a file, not a folder"
512
+ if not os.access(ancestor, os.W_OK | os.X_OK):
513
+ return f"{ancestor} is not writable"
514
+ return None
515
+
516
+
517
+ def _attempts_path() -> Path:
518
+ return settings.booking_agent_home() / _ATTEMPTS_FILE
519
+
520
+
521
+ def _read_attempts() -> Dict[str, object]:
522
+ try:
523
+ raw = json.loads(_attempts_path().read_text())
524
+ return raw if isinstance(raw, dict) else {}
525
+ except (OSError, ValueError):
526
+ return {}
527
+
528
+
529
+ def corrections_so_far() -> int:
530
+ value = _read_attempts().get("rejected")
531
+ return value if isinstance(value, int) else 0
532
+
533
+
534
+ def record_correction() -> int:
535
+ """Count one more time the agent was sent back; returns the new count."""
536
+ n = corrections_so_far() + 1
537
+ ensure_private_dir(settings.booking_agent_home())
538
+ write_private_text(_attempts_path(), json.dumps({"rejected": n}))
539
+ return n
540
+
541
+
542
+ def record_accepted_despite(problem: str) -> None:
543
+ ensure_private_dir(settings.booking_agent_home())
544
+ # The count starts over: this folder was used as given, and a later bad folder is a new
545
+ # decision that gets its own corrections instead of being waved through.
546
+ write_private_text(
547
+ _attempts_path(),
548
+ json.dumps({"rejected": 0, "accepted_despite": problem}),
549
+ )
550
+
551
+
552
+ def clear_attempts() -> None:
553
+ try:
554
+ _attempts_path().unlink()
555
+ except OSError:
556
+ pass
557
+
558
+
559
+ def current_boot_id() -> Optional[str]:
560
+ try:
561
+ return Path("/proc/sys/kernel/random/boot_id").read_text().strip() or None
562
+ except OSError:
563
+ return None
564
+
565
+
566
+ def touch_boot_marker(root: Path) -> None:
567
+ """Remember which boot first used this folder, so a later boot proves it survived a restart."""
568
+ boot = current_boot_id()
569
+ if boot is None:
570
+ return
571
+ marker = root / _BOOT_FILE
572
+ try:
573
+ previous = json.loads(marker.read_text())
574
+ except (OSError, ValueError):
575
+ previous = {}
576
+ first = previous.get("first") or boot
577
+ data = {"first": first, "last": boot, "survived": bool(previous.get("survived")) or first != boot}
578
+ try:
579
+ write_private_text(marker, json.dumps(data))
580
+ except OSError:
581
+ pass
582
+
583
+
584
+ def survived_restart(root: Path) -> Optional[bool]:
585
+ """True once this folder has been seen across two boots; None when boots cannot be told.
586
+
587
+ Never a false "yes": the marker lives in the folder, so seeing it again means the folder
588
+ came back. It can stay False after a real restart when sandboxes share one kernel (many
589
+ containers report the host's boot id), so False means "not shown", not "lost".
590
+ """
591
+ try:
592
+ previous = json.loads((root / _BOOT_FILE).read_text())
593
+ except (OSError, ValueError):
594
+ return None
595
+ boot = current_boot_id()
596
+ if boot is None:
597
+ return None
598
+ return bool(previous.get("survived")) or previous.get("first") != boot
@@ -109,14 +109,15 @@ SKILL_PROSE_CODES = frozenset({
109
109
  "discover_radius_required", "link_address_not_found", "missing_network_id_for_spt",
110
110
  "payment_already_captured", "payment_authentication_unsupported",
111
111
  "payment_captured_booking_unconfirmed", "payment_declined", "payment_failed",
112
- "payment_not_processed", "payment_unrecoverable", "reschedule_at_business",
113
- "reschedule_booking_not_active", "reschedule_failed", "reschedule_fee_unsupported",
114
- "reschedule_price_changed", "reschedule_price_unknown", "reschedule_same_slot",
115
- "reschedule_unauthorized", "search_area_input_invalid", "search_area_not_user_confirmed",
116
- "search_area_unconfirmed", "service_address_unconfirmed", "service_details_changed",
117
- "session_active", "session_not_found", "slot_not_selected", "spend_request_not_approved",
118
- "spend_request_replacement_required", "timezone_confirmation_required",
119
- "ucp_complete_failed",
112
+ "payment_not_processed", "payment_unrecoverable", "persistence_flags_conflict",
113
+ "persistence_folder_not_durable", "persistence_setup_failed", "persistence_unresolved",
114
+ "reschedule_at_business", "reschedule_booking_not_active", "reschedule_failed",
115
+ "reschedule_fee_unsupported", "reschedule_price_changed", "reschedule_price_unknown",
116
+ "reschedule_same_slot", "reschedule_unauthorized", "search_area_input_invalid",
117
+ "search_area_not_user_confirmed", "search_area_unconfirmed", "service_address_unconfirmed",
118
+ "service_details_changed", "session_active", "session_not_found", "slot_not_selected",
119
+ "spend_request_not_approved", "spend_request_replacement_required",
120
+ "timezone_confirmation_required", "ucp_complete_failed",
120
121
  })
121
122
  EXPECTED_CODES = SKILL_TABLE_CODES | SKILL_PROSE_CODES
122
123
  # The business is still working; seeing these again is progress, not a failed step.
@@ -13,6 +13,17 @@ def _reset_paid_bookings():
13
13
  reset_paid()
14
14
 
15
15
 
16
+ @pytest.fixture(autouse=True)
17
+ def _host_keeps_files(monkeypatch):
18
+ """Tests are a host that keeps its own files; the init gate has its own tests."""
19
+ monkeypatch.setenv("BOOKING_AGENT_PERSISTENCE", "none")
20
+ from booking_agent_cli import runtime_prepare
21
+
22
+ # Test folders live under the system temp dir; the durability checks have their own tests.
23
+ monkeypatch.setattr(runtime_prepare, "ephemeral_roots", lambda: [])
24
+ monkeypatch.setattr(runtime_prepare, "fs_type", lambda _path: None)
25
+
26
+
16
27
  @pytest.fixture
17
28
  def runner() -> CliRunner:
18
29
  return CliRunner()
@@ -22,6 +22,16 @@ REGRESSIONS = {
22
22
  ("test_runtime_prepare.py", "test_the_recorded_hosted_layout_is_repaired_without_losing_the_real_booking"),
23
23
  ("test_runtime_prepare.py", "test_the_further_along_copy_of_a_session_wins_and_no_booking_is_lost"),
24
24
  ],
25
+ "setting up persistence could be skipped without any error": [
26
+ ("test_runtime_prepare.py", "test_init_refuses_until_the_agent_says_how_state_is_kept"),
27
+ ("test_runtime_prepare.py", "test_a_folder_the_agent_names_is_set_up_and_then_not_asked_again"),
28
+ ("test_runtime_prepare.py", "test_saying_there_is_no_folder_is_remembered_beside_the_state"),
29
+ ],
30
+ "an agent named a folder that cannot survive, or insisted on one and got the flow stuck": [
31
+ ("test_runtime_prepare.py", "test_a_temporary_folder_is_turned_back_and_nothing_moves"),
32
+ ("test_runtime_prepare.py", "test_the_agent_is_sent_back_twice_and_the_third_time_is_accepted"),
33
+ ("test_runtime_prepare.py", "test_a_later_boot_shows_the_folder_survived_a_restart"),
34
+ ],
25
35
  "assistant had to remember to set up persistence": [
26
36
  ("test_runtime_prepare.py", "test_the_named_persistent_folder_is_prepared_before_any_command"),
27
37
  ],