booking-agent-cli 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/PKG-INFO +2 -2
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/README.md +1 -1
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/pyproject.toml +1 -1
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/__init__.py +1 -1
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/defaults.env +3 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/__init__.py +1 -1
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/bundled_skill.md +21 -21
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/main.py +9 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/runtime_prepare.py +180 -16
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_regression_index.py +8 -1
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_runtime_prepare.py +103 -9
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/.gitignore +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/authorization.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/backend/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/backend/live.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/filter.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/google.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/scope.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/circuit_breaker.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/config.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/discovery.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/headless.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/http_deadlines.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/http_retry.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/http_trace.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/merchant_pacing.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/models/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/models/platform.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/models/wire.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/namespace.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/observability.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/platform_profile.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/pop.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/profile_cache.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/registry_url.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/rest_client.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/secure_file.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/settings.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/signals.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/step_timing.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_capabilities.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_checkout.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_negotiation.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_profile.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/usp_methods.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/activity_log.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/booking_poll.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/booking_summary.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/calendar_connection.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/calendar_event.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/calendar_preference.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/catalog.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/config.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/flow.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/runtime.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/update.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/config_loader.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/engine.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/flow_timings.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/geo_eligibility.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/geocode.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/identity.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/install_integrity.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/link_cli.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/output.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/payment_helpers.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/payment_token.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/provenance.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/reschedule.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/runtime.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/search_area.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/session.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/skill_bundle.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/skill_integrity.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/support_bundle_schema.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/support_report.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/timezone_context.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/updates.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/.env.tests +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/conftest.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_latency_soak.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_staging_registry_integration.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_timezone_booking_e2e.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_ucp_skill_flow_e2e.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/ucp_native.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_activity_log.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_at_rest_hygiene.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_booking_rejection.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_bulk_cancel.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_connection.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_connector.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_flow.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_preference.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_checkout_signals.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_circuit_breaker.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_config.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_correlation_flow.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_declined_spt.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_discover_registry_rejection.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_evidence.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_gates.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_reschedule.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_step_timings.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_google_calendar_oauth.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_home_visit_address.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_http_trace.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_identity_logout.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_install_integrity.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_lazy_oauth.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_link_auth.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_link_cli_spt.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_namespace.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_oauth_listener.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_oauth_metadata.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_platform_key_pop.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_platform_profile.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_pop_keystore.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_profile_cache.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_request_retry_semantics.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_search_area.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_session_compat.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_session_retention.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_skill_integrity.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_skill_portability.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_support_report.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_timezone_context.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_cancel_result.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_checkout_validate.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_profile.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_update_command.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_wheel_contents.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/SOURCE +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/availability.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/booking.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/catalog.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/paid_bookings.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/profile.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/registry.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/rest_common.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/usp.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/waitlist.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/webhook_event.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/profiles.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/services.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/backend.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/link_cli.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/mock_calendar.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/pop_business.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/pop_transport.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/ucp_checkout_store.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/ucp_server.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/offline_stack/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/offline_stack/stack.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/__init__.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_authorization_policy.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_booking_poll_credentials.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_booking_request_error.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_calendar_filter.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_catalog.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_check_version.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_conditional_writes.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_delivery_address.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_discovery.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_geo_eligibility.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_geocode.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_headless.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_http_deadlines.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_http_retry.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_live_search_registry.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_merchant_pacing.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_mock_support.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_money_path_events.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_observability.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_outbound_member_names.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_package_layering.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_provenance.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_search_filters.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_search_pricing.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_search_responses.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_url.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_release_notes.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_rest_client.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_scoped_method_authorization.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_session_credentials.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_spec_conformance.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_step_timing.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_capabilities.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_negotiation.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_updates.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_wire_model_conformance.py +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/010-alg-none.json +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/README.md +0 -0
- {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/SOURCE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: booking-agent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/wix/booking-agent
|
|
6
6
|
Project-URL: Documentation, https://github.com/wix/booking-agent/blob/master/booking-agent/README.md
|
|
@@ -78,7 +78,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
78
78
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
81
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.54.0/booking-agent/skills/booking-agent/SKILL.md
|
|
82
82
|
gh attestation verify SKILL.md --repo wix/booking-agent \
|
|
83
83
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
84
84
|
```
|
|
@@ -36,7 +36,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
36
36
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
39
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.54.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
|
```
|
|
@@ -14,6 +14,9 @@ USP_PLATFORM_CONTACT=support@booking-agent.dev
|
|
|
14
14
|
# Sessions, tokens, and traces. Holds the user .env, so override it only in the
|
|
15
15
|
# process environment.
|
|
16
16
|
BOOKING_AGENT_SESSION_DIR=~/.booking-agent
|
|
17
|
+
# Absolute folder a host keeps between runs. When set, booking-agent links its state and the
|
|
18
|
+
# Link sign-in into it by itself before every command.
|
|
19
|
+
# BOOKING_AGENT_PERSISTENT_ROOT=
|
|
17
20
|
BOOKING_AGENT_MAX_SESSIONS=30
|
|
18
21
|
LINK_CLI_BIN=link-cli
|
|
19
22
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 0.
|
|
2
|
+
version: 0.54.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.
|
|
22
|
+
package: "booking-agent-cli==0.6.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.
|
|
223
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.54.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.
|
|
248
|
+
uvx pip download "booking-agent-cli==0.6.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.
|
|
251
|
-
uv tool install --force "$dir/booking_agent_cli-0.
|
|
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"
|
|
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.
|
|
258
|
-
`pipx install booking-agent-cli==0.
|
|
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
|
|
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,22 @@ npm install -g @stripe/link-cli
|
|
|
275
275
|
|
|
276
276
|
### After installing
|
|
277
277
|
|
|
278
|
-
**Keep state between conversations
|
|
279
|
-
|
|
280
|
-
|
|
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`:
|
|
281
283
|
|
|
282
284
|
```bash
|
|
283
285
|
booking-agent runtime prepare --persistent-root <absolute folder that survives>
|
|
284
286
|
```
|
|
285
287
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
runtime keeps its files anyway, or offers no surviving folder, skip the command. Do not tell the
|
|
293
|
-
user about it. If it fails, continue the booking without it.
|
|
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.
|
|
294
294
|
|
|
295
295
|
**Sign the user in to Link**, as described in [Link sign-in](#link-sign-in-automatic). It works
|
|
296
296
|
with no booking in progress. Do not wait for the user to offer to sign in.
|
|
@@ -309,7 +309,7 @@ prints each effective setting and where it came from; `booking-agent config init
|
|
|
309
309
|
|
|
310
310
|
The CLI decides whether a newer version is worth mentioning; you never decide from version
|
|
311
311
|
numbers or release pages. Pass this skill's version on every `flow init` (`--skill-version
|
|
312
|
-
0.
|
|
312
|
+
0.54.0`, from the frontmatter above), so the CLI can tell when these instructions and the
|
|
313
313
|
installed CLI no longer match.
|
|
314
314
|
|
|
315
315
|
- When a command returns `awaiting_user: update_consent`, ask the question in `prompt` in plain
|
|
@@ -471,7 +471,7 @@ confirmation email or on the business's page (`booking_url`). Do not invent a lo
|
|
|
471
471
|
**Step 1b — Create a new session:**
|
|
472
472
|
|
|
473
473
|
```bash
|
|
474
|
-
booking-agent flow init --user-request "<user's exact request>" --skill-version 0.
|
|
474
|
+
booking-agent flow init --user-request "<user's exact request>" --skill-version 0.54.0 \
|
|
475
475
|
--skill-path "<absolute path of the SKILL.md you loaded>"
|
|
476
476
|
```
|
|
477
477
|
|
|
@@ -29,6 +29,15 @@ app.add_typer(runtime_app, name="runtime")
|
|
|
29
29
|
app.add_typer(skill_app, name="skill")
|
|
30
30
|
app.add_typer(update_app, name="update")
|
|
31
31
|
|
|
32
|
+
@app.callback()
|
|
33
|
+
def _main(ctx: typer.Context) -> None:
|
|
34
|
+
"""Keep state in the host's persistent folder (when it names one) before any command runs."""
|
|
35
|
+
if ctx.invoked_subcommand != "runtime":
|
|
36
|
+
from .runtime_prepare import auto_prepare
|
|
37
|
+
|
|
38
|
+
auto_prepare()
|
|
39
|
+
|
|
40
|
+
|
|
32
41
|
INTERNAL_ERROR_MESSAGE = (
|
|
33
42
|
"The booking assistant hit an unexpected internal error. Tell the user in plain "
|
|
34
43
|
"words that something went wrong on our side, then follow next_command."
|
{booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/runtime_prepare.py
RENAMED
|
@@ -10,6 +10,7 @@ deleted, so file modes on credentials and keys stay as they were.
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import json
|
|
13
14
|
import os
|
|
14
15
|
import shutil
|
|
15
16
|
import sys
|
|
@@ -18,11 +19,19 @@ from pathlib import Path
|
|
|
18
19
|
from typing import Dict, List, Optional, Tuple
|
|
19
20
|
|
|
20
21
|
from booking_agent import settings
|
|
21
|
-
from booking_agent.secure_file import ensure_private_dir
|
|
22
|
+
from booking_agent.secure_file import ensure_private_dir, write_private_text
|
|
22
23
|
|
|
23
24
|
STATE_SUBDIR = "booking-agent"
|
|
24
25
|
LINK_SUBDIR = "link-cli"
|
|
25
26
|
_LINK_CLI_APP = "link-cli-nodejs"
|
|
27
|
+
ROOT_ENV = "BOOKING_AGENT_PERSISTENT_ROOT"
|
|
28
|
+
_SESSION_FILE = ".usp-session.json"
|
|
29
|
+
# Order a booking moves through, used to keep the further-along copy of the same session.
|
|
30
|
+
_PHASE_ORDER = (
|
|
31
|
+
"INIT", "PREREQ_OK", "BUSINESS_SELECTED", "CALENDAR_GATE", "AVAILABILITY_QUERIED",
|
|
32
|
+
"SLOTS_PRESENTED", "SLOT_SELECTED", "BUYER_RESOLVED", "BOOKED", "PAYMENT_PENDING",
|
|
33
|
+
"CONFIRMED", "FAILED",
|
|
34
|
+
)
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
class PrepareError(ValueError):
|
|
@@ -75,6 +84,117 @@ def _is_empty_dir(path: Path) -> bool:
|
|
|
75
84
|
return path.is_dir() and not any(path.iterdir())
|
|
76
85
|
|
|
77
86
|
|
|
87
|
+
def _phase_rank(session: Dict[str, object]) -> int:
|
|
88
|
+
phase = session.get("flow_phase")
|
|
89
|
+
return _PHASE_ORDER.index(phase) if phase in _PHASE_ORDER else -1
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def merge_session_stores(newer: Dict[str, object], older: Dict[str, object]) -> Dict[str, object]:
|
|
93
|
+
"""Union of two saved booking files, by session id, without dropping any booking.
|
|
94
|
+
|
|
95
|
+
A session present in both keeps the copy that got further (a stale ``INIT`` snapshot never
|
|
96
|
+
hides the finished booking). Everything else at the top level comes from ``newer`` and falls
|
|
97
|
+
back to ``older``. Works on the raw JSON so fields this version does not know survive.
|
|
98
|
+
"""
|
|
99
|
+
merged: Dict[str, object] = {**older, **{k: v for k, v in newer.items() if v is not None}}
|
|
100
|
+
by_id: Dict[object, Dict[str, object]] = {}
|
|
101
|
+
order: List[object] = []
|
|
102
|
+
for source in (older, newer):
|
|
103
|
+
for session in source.get("sessions") or []:
|
|
104
|
+
if not isinstance(session, dict):
|
|
105
|
+
continue
|
|
106
|
+
sid = session.get("id")
|
|
107
|
+
if sid not in by_id:
|
|
108
|
+
by_id[sid] = session
|
|
109
|
+
order.append(sid)
|
|
110
|
+
elif _phase_rank(session) >= _phase_rank(by_id[sid]):
|
|
111
|
+
by_id[sid] = session
|
|
112
|
+
merged["sessions"] = [by_id[sid] for sid in order]
|
|
113
|
+
for key in ("sessions_created", "schema_version"):
|
|
114
|
+
values = [v for v in (newer.get(key), older.get(key)) if isinstance(v, int)]
|
|
115
|
+
if values:
|
|
116
|
+
merged[key] = max(values)
|
|
117
|
+
answered = []
|
|
118
|
+
for source in (older, newer):
|
|
119
|
+
for item in source.get("update_prompts_answered") or []:
|
|
120
|
+
if isinstance(item, str) and item not in answered:
|
|
121
|
+
answered.append(item)
|
|
122
|
+
if answered:
|
|
123
|
+
merged["update_prompts_answered"] = answered
|
|
124
|
+
active = newer.get("active_session_id") or older.get("active_session_id")
|
|
125
|
+
merged["active_session_id"] = active if active in by_id else (order[-1] if order else None)
|
|
126
|
+
return merged
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _merge_session_files(src: Path, dst: Path) -> None:
|
|
130
|
+
def _load(path: Path) -> Dict[str, object]:
|
|
131
|
+
try:
|
|
132
|
+
raw = json.loads(path.read_text())
|
|
133
|
+
return raw if isinstance(raw, dict) else {}
|
|
134
|
+
except (OSError, ValueError):
|
|
135
|
+
return {}
|
|
136
|
+
|
|
137
|
+
src_newer = src.stat().st_mtime >= dst.stat().st_mtime
|
|
138
|
+
a, b = _load(src), _load(dst)
|
|
139
|
+
merged = merge_session_stores(a, b) if src_newer else merge_session_stores(b, a)
|
|
140
|
+
write_private_text(dst, json.dumps(merged, indent=2, sort_keys=False))
|
|
141
|
+
src.unlink()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _merge_tree(src: Path, dst: Path, aside: Path, kept: List[str], rel: str = "") -> None:
|
|
145
|
+
"""Move everything from ``src`` into ``dst``; nothing is deleted.
|
|
146
|
+
|
|
147
|
+
A name only in ``src`` moves over. Two folders merge. Two booking files merge by session.
|
|
148
|
+
Any other pair keeps the newer file at ``dst`` and moves the older one under ``aside``.
|
|
149
|
+
"""
|
|
150
|
+
for child in sorted(src.iterdir()):
|
|
151
|
+
target = dst / child.name
|
|
152
|
+
here = f"{rel}/{child.name}" if rel else child.name
|
|
153
|
+
if not (target.exists() or target.is_symlink()):
|
|
154
|
+
shutil.move(str(child), str(target))
|
|
155
|
+
elif child.is_dir() and target.is_dir():
|
|
156
|
+
_merge_tree(child, target, aside, kept, here)
|
|
157
|
+
if _is_empty_dir(child):
|
|
158
|
+
child.rmdir()
|
|
159
|
+
elif child.name == _SESSION_FILE and child.is_file() and target.is_file():
|
|
160
|
+
_merge_session_files(child, target)
|
|
161
|
+
elif (
|
|
162
|
+
child.is_file() and target.is_file()
|
|
163
|
+
and child.stat().st_mtime > target.stat().st_mtime
|
|
164
|
+
):
|
|
165
|
+
# The incoming file is newer: the older one at the destination is kept aside.
|
|
166
|
+
shutil.move(str(target), str(_aside_path(aside, here, kept)))
|
|
167
|
+
shutil.move(str(child), str(target))
|
|
168
|
+
else:
|
|
169
|
+
shutil.move(str(child), str(_aside_path(aside, here, kept)))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _aside_path(aside: Path, rel: str, kept: List[str]) -> Path:
|
|
173
|
+
path = aside / rel
|
|
174
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
kept.append(rel)
|
|
176
|
+
return path
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def merge_dir_into(src: Path, dst: Path, *, aside_name: str) -> Dict[str, str]:
|
|
180
|
+
"""Merge folder ``src`` into ``dst``, then remove the (now empty) ``src``."""
|
|
181
|
+
aside = src.with_name(aside_name)
|
|
182
|
+
aside.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
kept: List[str] = []
|
|
184
|
+
_merge_tree(src, dst, aside, kept)
|
|
185
|
+
if _is_empty_dir(src):
|
|
186
|
+
src.rmdir()
|
|
187
|
+
out = {"merged_existing_contents": "true"}
|
|
188
|
+
if kept:
|
|
189
|
+
out["older_copies_kept_at"] = str(aside)
|
|
190
|
+
else:
|
|
191
|
+
try:
|
|
192
|
+
aside.rmdir()
|
|
193
|
+
except OSError:
|
|
194
|
+
pass
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
78
198
|
def _move_children(src: Path, dst: Path) -> None:
|
|
79
199
|
dst.mkdir(parents=True, exist_ok=True)
|
|
80
200
|
for child in src.iterdir():
|
|
@@ -97,12 +217,14 @@ def link_one(runtime: Path, persistent: Path, *, private: bool) -> Dict[str, str
|
|
|
97
217
|
# Linked into another root (a different conversation chose a different folder).
|
|
98
218
|
# Bring that root's saved state along when this one has none; otherwise leave it alone.
|
|
99
219
|
previous = runtime.resolve()
|
|
100
|
-
if previous.is_dir() and not _is_empty_dir(previous):
|
|
220
|
+
if previous.is_dir() and not _is_empty_dir(previous) and previous != persistent.resolve():
|
|
101
221
|
if _is_empty_dir(persistent):
|
|
102
222
|
_move_children(previous, persistent)
|
|
103
223
|
out["moved_existing_contents"] = "true"
|
|
104
224
|
else:
|
|
105
|
-
out
|
|
225
|
+
out.update(merge_dir_into(
|
|
226
|
+
previous, persistent, aside_name=f"{previous.name}.before-prepare-{int(time.time())}",
|
|
227
|
+
))
|
|
106
228
|
runtime.unlink()
|
|
107
229
|
relinked = True
|
|
108
230
|
elif runtime.exists():
|
|
@@ -116,10 +238,11 @@ def link_one(runtime: Path, persistent: Path, *, private: bool) -> Dict[str, str
|
|
|
116
238
|
runtime.rmdir()
|
|
117
239
|
out["moved_existing_contents"] = "true"
|
|
118
240
|
else:
|
|
119
|
-
# Both hold data:
|
|
120
|
-
aside
|
|
121
|
-
|
|
122
|
-
|
|
241
|
+
# Both hold data: merge them. Booking files merge by session id; for any other
|
|
242
|
+
# file the newer copy stays and the older one is kept aside, never deleted.
|
|
243
|
+
out.update(merge_dir_into(
|
|
244
|
+
runtime, persistent, aside_name=f"{runtime.name}.before-prepare-{int(time.time())}",
|
|
245
|
+
))
|
|
123
246
|
|
|
124
247
|
runtime.parent.mkdir(parents=True, exist_ok=True)
|
|
125
248
|
os.symlink(persistent, runtime, target_is_directory=True)
|
|
@@ -142,25 +265,28 @@ def normalized_root(persistent_root: str) -> Path:
|
|
|
142
265
|
|
|
143
266
|
|
|
144
267
|
def _lift_nested_layout(root: Path) -> Dict[str, str]:
|
|
145
|
-
"""Undo the old nested layout (``<root>/booking-agent/{booking-agent,link-cli}``).
|
|
268
|
+
"""Undo the old nested layout (``<root>/booking-agent/{booking-agent,link-cli}``).
|
|
269
|
+
|
|
270
|
+
Both folders are merged into their proper place, so a stale top-level booking file never
|
|
271
|
+
hides the real one from the nested folder.
|
|
272
|
+
"""
|
|
146
273
|
state = root / STATE_SUBDIR
|
|
147
274
|
lifted: Dict[str, str] = {}
|
|
275
|
+
stamp = int(time.time())
|
|
148
276
|
nested_state = state / STATE_SUBDIR
|
|
149
277
|
if nested_state.is_dir():
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
os.replace(child, state / child.name)
|
|
153
|
-
lifted["lifted_from_nested_layout"] = "true"
|
|
154
|
-
if _is_empty_dir(nested_state):
|
|
155
|
-
nested_state.rmdir()
|
|
278
|
+
merge_dir_into(nested_state, state, aside_name=f"{STATE_SUBDIR}.before-prepare-{stamp}")
|
|
279
|
+
lifted["lifted_from_nested_layout"] = "true"
|
|
156
280
|
nested_link = state / LINK_SUBDIR
|
|
157
281
|
if nested_link.is_dir():
|
|
158
282
|
target = root / LINK_SUBDIR
|
|
159
|
-
if
|
|
283
|
+
if target.exists() and not _is_empty_dir(target):
|
|
284
|
+
merge_dir_into(nested_link, target, aside_name=f"{LINK_SUBDIR}.before-prepare-{stamp}")
|
|
285
|
+
else:
|
|
160
286
|
if target.exists():
|
|
161
287
|
target.rmdir()
|
|
162
288
|
os.replace(nested_link, target)
|
|
163
|
-
|
|
289
|
+
lifted["lifted_from_nested_layout"] = "true"
|
|
164
290
|
return lifted
|
|
165
291
|
|
|
166
292
|
|
|
@@ -223,7 +349,24 @@ def diagnose() -> Dict[str, object]:
|
|
|
223
349
|
for aside in sorted(runtime.parent.glob(f"{runtime.name}.before-prepare-*")) if runtime.parent.is_dir() else []:
|
|
224
350
|
findings.append({"kind": "runtime_copy_kept_aside", "path": str(aside), "severity": "info"})
|
|
225
351
|
|
|
352
|
+
configured = configured_root()
|
|
353
|
+
if configured is not None:
|
|
354
|
+
try:
|
|
355
|
+
expected = normalized_root(configured) / STATE_SUBDIR
|
|
356
|
+
except PrepareError:
|
|
357
|
+
expected = None
|
|
358
|
+
applied = bool(
|
|
359
|
+
expected is not None and state_info["linked_to"]
|
|
360
|
+
and Path(str(state_info["linked_to"])) == expected.resolve()
|
|
361
|
+
)
|
|
362
|
+
if not applied:
|
|
363
|
+
problems = True
|
|
364
|
+
findings.append({
|
|
365
|
+
"kind": "configured_root_not_applied", "path": str(state), "severity": "problem",
|
|
366
|
+
})
|
|
367
|
+
root_hint = root_hint or configured
|
|
226
368
|
return {
|
|
369
|
+
"configured_root": configured,
|
|
227
370
|
"state": state_info,
|
|
228
371
|
"link_sign_in": link_info,
|
|
229
372
|
"findings": findings,
|
|
@@ -232,3 +375,24 @@ def diagnose() -> Dict[str, object]:
|
|
|
232
375
|
f"booking-agent runtime prepare --persistent-root {root_hint}" if problems and root_hint else None
|
|
233
376
|
),
|
|
234
377
|
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def configured_root() -> Optional[str]:
|
|
381
|
+
"""The persistent folder a host names with ``BOOKING_AGENT_PERSISTENT_ROOT``, if any."""
|
|
382
|
+
value = (settings.get(ROOT_ENV) or "").strip()
|
|
383
|
+
return value or None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def auto_prepare() -> Optional[List[Dict[str, str]]]:
|
|
387
|
+
"""Link state into the host-named persistent folder, quietly and idempotently.
|
|
388
|
+
|
|
389
|
+
Nothing happens unless the host set ``BOOKING_AGENT_PERSISTENT_ROOT``. A failure never
|
|
390
|
+
blocks a booking; ``runtime doctor`` reports it.
|
|
391
|
+
"""
|
|
392
|
+
root = configured_root()
|
|
393
|
+
if root is None:
|
|
394
|
+
return None
|
|
395
|
+
try:
|
|
396
|
+
return prepare(root, [])
|
|
397
|
+
except (PrepareError, OSError):
|
|
398
|
+
return None
|
{booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_regression_index.py
RENAMED
|
@@ -14,10 +14,17 @@ REGRESSIONS = {
|
|
|
14
14
|
"state split across two persistent roots": [
|
|
15
15
|
("test_runtime_prepare.py", "test_a_root_already_named_booking_agent_is_not_nested_again"),
|
|
16
16
|
("test_runtime_prepare.py", "test_moving_to_a_different_root_brings_the_saved_state_along"),
|
|
17
|
-
("test_runtime_prepare.py", "
|
|
17
|
+
("test_runtime_prepare.py", "test_moving_to_a_root_that_has_its_own_state_merges_both"),
|
|
18
18
|
("test_runtime_prepare.py", "test_a_state_folder_nested_by_the_old_layout_is_lifted_back_up"),
|
|
19
19
|
("test_runtime_prepare.py", "test_doctor_reports_where_state_lives_and_what_is_orphaned"),
|
|
20
20
|
],
|
|
21
|
+
"restored booking files hidden behind a stale top-level copy (hosted layout)": [
|
|
22
|
+
("test_runtime_prepare.py", "test_the_recorded_hosted_layout_is_repaired_without_losing_the_real_booking"),
|
|
23
|
+
("test_runtime_prepare.py", "test_the_further_along_copy_of_a_session_wins_and_no_booking_is_lost"),
|
|
24
|
+
],
|
|
25
|
+
"assistant had to remember to set up persistence": [
|
|
26
|
+
("test_runtime_prepare.py", "test_the_named_persistent_folder_is_prepared_before_any_command"),
|
|
27
|
+
],
|
|
21
28
|
"sign-in approved but the poll never reported, so the user is asked again": [
|
|
22
29
|
("test_link_auth.py", "test_buyer_resolve_trusts_link_when_the_sign_in_poll_never_reported"),
|
|
23
30
|
],
|
{booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_runtime_prepare.py
RENAMED
|
@@ -66,19 +66,23 @@ def test_moves_existing_contents_and_keeps_their_modes(runner, dirs):
|
|
|
66
66
|
assert (link_dir / "config.json").read_text() == "{}"
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def
|
|
69
|
+
def test_merges_when_both_hold_data_and_keeps_the_older_file_aside(runner, dirs):
|
|
70
70
|
_home, root, link_dir = dirs
|
|
71
71
|
(root / "link-cli").mkdir(parents=True)
|
|
72
|
-
(root / "link-cli" / "config.json").write_text("
|
|
72
|
+
(root / "link-cli" / "config.json").write_text("older")
|
|
73
73
|
link_dir.mkdir(parents=True)
|
|
74
|
-
(link_dir / "config.json").write_text("
|
|
74
|
+
(link_dir / "config.json").write_text("newer")
|
|
75
|
+
(link_dir / "extra.json").write_text("only-here")
|
|
76
|
+
os.utime(root / "link-cli" / "config.json", (1_000, 1_000))
|
|
77
|
+
os.utime(link_dir / "config.json", (2_000, 2_000))
|
|
75
78
|
|
|
76
79
|
result, body = _run(runner, "--persistent-root", str(root))
|
|
77
80
|
|
|
78
81
|
assert result.exit_code == 0, result.stdout
|
|
79
|
-
assert (link_dir / "config.json").read_text() == "
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
assert (link_dir / "config.json").read_text() == "newer"
|
|
83
|
+
assert (link_dir / "extra.json").read_text() == "only-here"
|
|
84
|
+
aside = body["data"]["links"][1]["older_copies_kept_at"]
|
|
85
|
+
assert open(os.path.join(aside, "config.json")).read() == "older"
|
|
82
86
|
|
|
83
87
|
|
|
84
88
|
def test_extra_links_resolve_relative_paths_under_the_root(runner, dirs, tmp_path):
|
|
@@ -187,19 +191,23 @@ def test_moving_to_a_different_root_brings_the_saved_state_along(runner, dirs, t
|
|
|
187
191
|
assert [link["result"] for link in body["data"]["links"]][:2] == ["relinked", "relinked"]
|
|
188
192
|
|
|
189
193
|
|
|
190
|
-
def
|
|
194
|
+
def test_moving_to_a_root_that_has_its_own_state_merges_both(runner, dirs, tmp_path):
|
|
191
195
|
home, root, link_dir = dirs
|
|
192
196
|
_run(runner, "--persistent-root", str(root))
|
|
193
197
|
(root / "link-cli" / "config.json").write_text("old-sign-in")
|
|
198
|
+
(root / "link-cli" / "only-old.json").write_text("kept")
|
|
199
|
+
os.utime(root / "link-cli" / "config.json", (1_000, 1_000))
|
|
194
200
|
other = tmp_path / "other-keep"
|
|
195
201
|
_seed(other / "booking-agent", other / "link-cli", session="newer", link="new-sign-in")
|
|
202
|
+
os.utime(other / "link-cli" / "config.json", (2_000, 2_000))
|
|
196
203
|
|
|
197
204
|
result, body = _run(runner, "--persistent-root", str(other))
|
|
198
205
|
|
|
199
206
|
assert result.exit_code == 0, result.stdout
|
|
200
207
|
assert (link_dir / "config.json").read_text() == "new-sign-in"
|
|
201
|
-
assert (
|
|
202
|
-
assert body["data"]["links"][1]["
|
|
208
|
+
assert (link_dir / "only-old.json").read_text() == "kept"
|
|
209
|
+
assert body["data"]["links"][1]["merged_existing_contents"] == "true"
|
|
210
|
+
assert not (root / "link-cli").exists()
|
|
203
211
|
|
|
204
212
|
|
|
205
213
|
def test_doctor_reports_where_state_lives_and_what_is_orphaned(runner, dirs, tmp_path):
|
|
@@ -228,3 +236,89 @@ def test_doctor_is_quiet_when_everything_is_in_one_place(runner, dirs):
|
|
|
228
236
|
assert result.exit_code == 0, result.stdout
|
|
229
237
|
assert body["data"]["healthy"] is True
|
|
230
238
|
assert body["data"]["findings"] == []
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --- Two booking files from different times are merged, never one hiding the other ---------------
|
|
242
|
+
|
|
243
|
+
def _store(*sessions, active=None, created=None):
|
|
244
|
+
return {
|
|
245
|
+
"schema_version": 2, "sessions_created": created or len(sessions),
|
|
246
|
+
"active_session_id": active, "sessions": list(sessions),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_the_further_along_copy_of_a_session_wins_and_no_booking_is_lost():
|
|
251
|
+
stale = {"id": "s1", "flow_phase": "INIT"}
|
|
252
|
+
real = {"id": "s1", "flow_phase": "CONFIRMED", "booking": {"id": "b1"}}
|
|
253
|
+
other = {"id": "s2", "flow_phase": "PREREQ_OK"}
|
|
254
|
+
merged = runtime_prepare.merge_session_stores(
|
|
255
|
+
_store(stale, active="s1"), _store(real, other, active="s2"),
|
|
256
|
+
)
|
|
257
|
+
by_id = {s["id"]: s for s in merged["sessions"]}
|
|
258
|
+
assert by_id["s1"]["flow_phase"] == "CONFIRMED" and by_id["s1"]["booking"] == {"id": "b1"}
|
|
259
|
+
assert set(by_id) == {"s1", "s2"}
|
|
260
|
+
assert merged["active_session_id"] == "s1"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_the_recorded_hosted_layout_is_repaired_without_losing_the_real_booking(runner, dirs):
|
|
264
|
+
"""A stale INIT store at the top level, the real one nested, a fresh store from today in /root."""
|
|
265
|
+
home, root, link_dir = dirs
|
|
266
|
+
state = root / "booking-agent"
|
|
267
|
+
state.mkdir(parents=True)
|
|
268
|
+
(state / ".usp-session.json").write_text(json.dumps(_store({"id": "run1", "flow_phase": "INIT"})))
|
|
269
|
+
nested = state / "booking-agent"
|
|
270
|
+
nested.mkdir()
|
|
271
|
+
(nested / ".usp-session.json").write_text(json.dumps(_store(
|
|
272
|
+
{"id": "run1", "flow_phase": "PAYMENT_PENDING"}, {"id": "run1b", "flow_phase": "CONFIRMED"},
|
|
273
|
+
)))
|
|
274
|
+
(state / "link-cli").mkdir()
|
|
275
|
+
(state / "link-cli" / "config.json").write_text("nested-link")
|
|
276
|
+
live = home / ".booking-agent"
|
|
277
|
+
live.mkdir()
|
|
278
|
+
(live / ".usp-session.json").write_text(json.dumps(_store({"id": "today", "flow_phase": "CONFIRMED"})))
|
|
279
|
+
|
|
280
|
+
result, _body = _run(runner, "--persistent-root", str(root))
|
|
281
|
+
|
|
282
|
+
assert result.exit_code == 0, result.stdout
|
|
283
|
+
merged = json.loads((home / ".booking-agent" / ".usp-session.json").read_text())
|
|
284
|
+
phases = {s["id"]: s["flow_phase"] for s in merged["sessions"]}
|
|
285
|
+
assert phases == {"run1": "PAYMENT_PENDING", "run1b": "CONFIRMED", "today": "CONFIRMED"}
|
|
286
|
+
assert (link_dir / "config.json").read_text() == "nested-link"
|
|
287
|
+
assert not (state / "booking-agent").exists() and not (state / "link-cli").exists()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# --- A host that names its persistent folder needs no instruction from the assistant ------------
|
|
291
|
+
|
|
292
|
+
def test_the_named_persistent_folder_is_prepared_before_any_command(runner, dirs, monkeypatch):
|
|
293
|
+
home, root, link_dir = dirs
|
|
294
|
+
monkeypatch.setenv("BOOKING_AGENT_PERSISTENT_ROOT", str(root))
|
|
295
|
+
|
|
296
|
+
result = runner.invoke(app, ["flow", "sessions", "list"])
|
|
297
|
+
|
|
298
|
+
assert result.exit_code == 0, result.stdout
|
|
299
|
+
assert (home / ".booking-agent").is_symlink()
|
|
300
|
+
assert (home / ".booking-agent").resolve() == (root / "booking-agent").resolve()
|
|
301
|
+
assert link_dir.resolve() == (root / "link-cli").resolve()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_without_the_variable_nothing_is_linked(runner, dirs):
|
|
305
|
+
home, _root, _link_dir = dirs
|
|
306
|
+
runner.invoke(app, ["flow", "sessions", "list"])
|
|
307
|
+
assert not (home / ".booking-agent").is_symlink()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_a_bad_persistent_folder_never_blocks_a_command(runner, dirs, monkeypatch):
|
|
311
|
+
monkeypatch.setenv("BOOKING_AGENT_PERSISTENT_ROOT", "relative/path")
|
|
312
|
+
result = runner.invoke(app, ["flow", "sessions", "list"])
|
|
313
|
+
assert result.exit_code == 0, result.stdout
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_doctor_flags_a_named_folder_the_state_is_not_in(runner, dirs, monkeypatch):
|
|
317
|
+
_home, root, _link_dir = dirs
|
|
318
|
+
monkeypatch.setenv("BOOKING_AGENT_PERSISTENT_ROOT", str(root))
|
|
319
|
+
|
|
320
|
+
body = json.loads(runner.invoke(app, ["runtime", "doctor"]).stdout)
|
|
321
|
+
|
|
322
|
+
assert body["data"]["healthy"] is False
|
|
323
|
+
assert {f["kind"] for f in body["data"]["findings"]} >= {"configured_root_not_applied"}
|
|
324
|
+
assert body["next_command"] == f"booking-agent runtime prepare --persistent-root {root}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/oauth_listener.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|