langgraph-api 0.7.102__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/PKG-INFO +1 -1
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/capacity_runner.mjs +4 -4
- langgraph_api-0.8.0/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/cli.py +54 -36
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/config/__init__.py +2 -1
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/config/schemas.py +9 -1
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/http.py +31 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/package.json +1 -1
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/yarn.lock +7 -7
- langgraph_api-0.8.0/langgraph_api/lc_security/__init__.py +29 -0
- langgraph_api-0.8.0/langgraph_api/lc_security/exceptions.py +9 -0
- langgraph_api-0.8.0/langgraph_api/lc_security/policy.py +349 -0
- langgraph_api-0.8.0/langgraph_api/lc_security/transport.py +201 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/webhook.py +101 -62
- langgraph_api-0.7.102/langgraph_api/__init__.py +0 -1
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/.gitignore +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/LICENSE +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/Makefile +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/README.md +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/.gitignore +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/Makefile +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/README.md +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/assistant.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/benchmark-runner.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/benchmark_profiles.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/benchmarks.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/cancel_first_second_completes.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/enqueued_runs_order.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/log-failure.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/meta_workload.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/stream_write.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/thread.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/thread_runs_metadata_search.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/threads_search_metadata.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/types.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/benchmark-runners/wait_write.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/capacity_dd_report.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/capacity_k6.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/capacity_slack_report.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/capacity_urls.mjs +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/clean-cli.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/clean.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/continuous/README.md +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/continuous/pyproject.toml +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/continuous/runner.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/continuous/uv.lock +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/graphs.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/mixed_workload_k6.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/mixed_workload_runner.mjs +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/package.json +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/ramp.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/reporting/dd_reporting.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/reporting/slack_slowest_runs.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/reporting/slack_summary.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/run_local.sh +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/staircase.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/staircase_step_k6.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/tsconfig.json +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/update-revision.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/benchmark/weather.js +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/constraints.txt +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/custom_store.sql +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/forbidden.txt +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/hatch_build.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/healthcheck.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph-cloud-debugging-20260210132856.zip +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/_checkpointer/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/_checkpointer/_adapter.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/_checkpointer/protocol.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/_factory_utils.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/a2a.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/mcp/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/mcp/_constants.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/mcp/_handlers.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/mcp/_models.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/mcp/_routes.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/mcp/_sanitizers.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/meta.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/profile.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/asgi_transport.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/asyncio.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/errors.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/cache.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/command.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/config/_parse.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/encryption/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/encryption/aes_json.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/encryption/context.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/encryption/custom.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/encryption/middleware.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/encryption/shared.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/executor_entrypoint.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/feature_flags.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/graph.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/client.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/generated/core_api_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/ops/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/ops/assistants.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/ops/cache.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/ops/crons.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/ops/runs.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/ops/threads.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/server.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/servicers/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/servicers/checkpointer.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/grpc/servicers/encryption.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/http_metrics.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/http_metrics_utils.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/client.http.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/remote.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/src/preload.mjs +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/src/utils/files.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/traceblock.mts +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/metrics_datadog.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/middleware/ensure_store.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/otel_context.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/queue_entrypoint.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/route.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/self_hosted_logs.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/self_hosted_metrics.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/server.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/state.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/store.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/stream.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/timing/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/timing/profiler.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/timing/timer.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/traceblock.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/cache.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/config.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/errors.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/extract.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/future.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/headers.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/network.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/retriable_client.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/stream_codec.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/utils/uuids.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_api/worker.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/checkpointer.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/_compat.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/channel.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/checkpoint.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/config.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/durability.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/exception.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/graph.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/interrupt.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/messages.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/orchestrator_response.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/runopts.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/stream_mode.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/struct.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/task.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/conversion/value.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/checkpointer_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/checkpointer_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/checkpointer_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/checkpointer_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/core_api_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/core_api_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/core_api_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/core_api_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/encryption_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/encryption_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/encryption_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/encryption_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_api_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_api_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_api_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_api_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_common_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_common_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_common_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/engine_common_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cancel_run_action_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cancel_run_action_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cancel_run_action_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cancel_run_action_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_control_signal_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_control_signal_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_control_signal_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_control_signal_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cron_on_run_completed_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cron_on_run_completed_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cron_on_run_completed_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_cron_on_run_completed_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_durability_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_durability_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_durability_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_durability_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_multitask_strategy_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_multitask_strategy_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_multitask_strategy_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_multitask_strategy_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_run_status_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_run_status_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_run_status_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_run_status_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_store_operation_entry_type_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_store_operation_entry_type_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_store_operation_entry_type_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_store_operation_entry_type_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_stream_mode_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_stream_mode_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_stream_mode_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_stream_mode_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_status_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_status_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_status_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_status_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_stream_mode_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_stream_mode_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_stream_mode_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/enum_thread_stream_mode_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/errors_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/errors_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/errors_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/errors_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/executor_api_pb2.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/executor_api_pb2.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/executor_api_pb2_grpc.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/proto/executor_api_pb2_grpc.pyi +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/sanitize.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_grpc_common/serde.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/checkpoint.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/database.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/lifespan.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/metrics.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/ops.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/queue.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/retry.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/routes.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/langgraph_runtime/store.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/logging.json +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/openapi.json +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/pyproject.toml +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/scripts/build_wheel.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/scripts/create_license.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/scripts/run_a2a_tck.py +0 -0
- {langgraph_api-0.7.102 → langgraph_api-0.8.0}/uv.lock +0 -0
|
@@ -30,11 +30,11 @@ const clusterNameToSettings = {
|
|
|
30
30
|
},
|
|
31
31
|
'py-7-node': {
|
|
32
32
|
url: 'https://cap-bench-py-7-node-5f471cdb8a725e0bbb076cc9fb32b76d.staging.langgraph.app',
|
|
33
|
-
rampEndMultiplier:
|
|
33
|
+
rampEndMultiplier: 5,
|
|
34
34
|
},
|
|
35
35
|
'py-20-node': {
|
|
36
36
|
url: 'https://cap-bench-py-20-node-0970dd3e458059e488db99d48c69ca69.staging.langgraph.app',
|
|
37
|
-
rampEndMultiplier:
|
|
37
|
+
rampEndMultiplier: 10,
|
|
38
38
|
},
|
|
39
39
|
// Distributed runtime multi-node scaling benchmarks
|
|
40
40
|
'dr-1-node': {
|
|
@@ -47,11 +47,11 @@ const clusterNameToSettings = {
|
|
|
47
47
|
},
|
|
48
48
|
'dr-7-node': {
|
|
49
49
|
url: 'https://cap-bench-dr-7-node-fbf64b46fc9b57239764478187abe534.staging.langgraph.app',
|
|
50
|
-
rampEndMultiplier:
|
|
50
|
+
rampEndMultiplier: 5,
|
|
51
51
|
},
|
|
52
52
|
'dr-20-node': {
|
|
53
53
|
url: 'https://cap-bench-dr-20-node-7cea036a01a25a9caec0be0b873f9b0a.staging.langgraph.app',
|
|
54
|
-
rampEndMultiplier:
|
|
54
|
+
rampEndMultiplier: 10,
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
57
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -108,6 +108,47 @@ def _find_open_port(host: str) -> int:
|
|
|
108
108
|
return s.getsockname()[1]
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def _resolve_server_url(
|
|
112
|
+
host: str, port: int, *, mount_prefix: str | None, tunnel: bool
|
|
113
|
+
) -> str:
|
|
114
|
+
"""Return the public-facing base URL for the server.
|
|
115
|
+
|
|
116
|
+
When *tunnel* is True a Cloudflare tunnel is started and the tunnel
|
|
117
|
+
URL is returned; otherwise the local ``http://host:port`` URL is used.
|
|
118
|
+
*mount_prefix* (if any) is appended to the result.
|
|
119
|
+
"""
|
|
120
|
+
from langgraph_api.utils.network import format_hostport # noqa: PLC0415
|
|
121
|
+
|
|
122
|
+
upstream_url = f"http://{format_hostport(host, port)}"
|
|
123
|
+
if mount_prefix:
|
|
124
|
+
upstream_url += mount_prefix
|
|
125
|
+
|
|
126
|
+
if not tunnel:
|
|
127
|
+
return upstream_url
|
|
128
|
+
|
|
129
|
+
logger.info("Starting Cloudflare Tunnel...")
|
|
130
|
+
from langgraph_api.tunneling.cloudflare import start_tunnel # noqa: PLC0415
|
|
131
|
+
|
|
132
|
+
tunnel_obj = start_tunnel(port)
|
|
133
|
+
try:
|
|
134
|
+
public_url = tunnel_obj.url.result(timeout=30)
|
|
135
|
+
except FutureTimeoutError:
|
|
136
|
+
logger.warning(
|
|
137
|
+
"Timed out waiting for Cloudflare Tunnel URL; using local URL %s",
|
|
138
|
+
upstream_url,
|
|
139
|
+
)
|
|
140
|
+
public_url = upstream_url
|
|
141
|
+
except Exception as e:
|
|
142
|
+
tunnel_obj.process.kill()
|
|
143
|
+
raise RuntimeError("Failed to start Cloudflare Tunnel") from e
|
|
144
|
+
|
|
145
|
+
# Only append the prefix if we got a real tunnel URL; on timeout
|
|
146
|
+
# fallback, public_url is already upstream_url which has the prefix.
|
|
147
|
+
if mount_prefix and public_url != upstream_url:
|
|
148
|
+
public_url += mount_prefix
|
|
149
|
+
return public_url
|
|
150
|
+
|
|
151
|
+
|
|
111
152
|
def _resolve_port(host: str, port: int | None) -> int:
|
|
112
153
|
"""Resolve the port to use for the server."""
|
|
113
154
|
if port is not None:
|
|
@@ -209,38 +250,11 @@ def run_server(
|
|
|
209
250
|
debugpy.wait_for_client()
|
|
210
251
|
logger.info("Debugger attached. Starting server...")
|
|
211
252
|
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
):
|
|
218
|
-
from langgraph_api.utils.network import format_hostport # noqa: PLC0415
|
|
219
|
-
|
|
220
|
-
upstream_url = f"http://{format_hostport(host, port)}"
|
|
221
|
-
if mount_prefix:
|
|
222
|
-
upstream_url += mount_prefix
|
|
223
|
-
if tunnel:
|
|
224
|
-
logger.info("Starting Cloudflare Tunnel...")
|
|
225
|
-
from langgraph_api.tunneling.cloudflare import start_tunnel # noqa: PLC0415
|
|
226
|
-
|
|
227
|
-
tunnel_obj = start_tunnel(port)
|
|
228
|
-
try:
|
|
229
|
-
public_url = tunnel_obj.url.result(timeout=30)
|
|
230
|
-
except FutureTimeoutError:
|
|
231
|
-
logger.warning(
|
|
232
|
-
"Timed out waiting for Cloudflare Tunnel URL; using local URL %s",
|
|
233
|
-
upstream_url,
|
|
234
|
-
)
|
|
235
|
-
public_url = upstream_url
|
|
236
|
-
except Exception as e:
|
|
237
|
-
tunnel_obj.process.kill()
|
|
238
|
-
raise RuntimeError("Failed to start Cloudflare Tunnel") from e
|
|
239
|
-
local_url = public_url
|
|
240
|
-
if mount_prefix:
|
|
241
|
-
local_url += mount_prefix
|
|
242
|
-
else:
|
|
243
|
-
local_url = upstream_url
|
|
253
|
+
# Build all env patches up front so that langgraph_api.config (which
|
|
254
|
+
# is read at import time) sees every config value. LANGGRAPH_API_URL
|
|
255
|
+
# is resolved inside the block (it depends on tunnel/mount_prefix) and
|
|
256
|
+
# overwritten then; the empty placeholder ensures patch_environment
|
|
257
|
+
# tracks and restores the original value on exit.
|
|
244
258
|
to_patch = dict(
|
|
245
259
|
MIGRATIONS_PATH=__migrations_path__,
|
|
246
260
|
DATABASE_URI=__database_uri__,
|
|
@@ -258,7 +272,7 @@ def run_server(
|
|
|
258
272
|
LANGGRAPH_UI_CONFIG=json.dumps(ui_config) if ui_config else None,
|
|
259
273
|
LANGGRAPH_CHECKPOINTER=json.dumps(checkpointer) if checkpointer else None,
|
|
260
274
|
LANGGRAPH_UI_BUNDLER="true",
|
|
261
|
-
LANGGRAPH_API_URL=
|
|
275
|
+
LANGGRAPH_API_URL="", # resolved below, inside the patched block
|
|
262
276
|
LANGGRAPH_DISABLE_FILE_PERSISTENCE=str(disable_persistence).lower(),
|
|
263
277
|
LANGGRAPH_RUNTIME_EDITION=runtime_edition,
|
|
264
278
|
# If true, we will not raise on blocking IO calls (via blockbuster)
|
|
@@ -273,9 +287,13 @@ def run_server(
|
|
|
273
287
|
logger.debug(f"Skipping loaded env var {k}={v}")
|
|
274
288
|
continue
|
|
275
289
|
to_patch[k] = v
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
290
|
+
|
|
291
|
+
with patch_environment(**to_patch):
|
|
292
|
+
local_url = _resolve_server_url(
|
|
293
|
+
host, port, mount_prefix=mount_prefix, tunnel=tunnel
|
|
294
|
+
)
|
|
295
|
+
os.environ["LANGGRAPH_API_URL"] = local_url
|
|
296
|
+
|
|
279
297
|
studio_origin = studio_url or _get_ls_origin() or "https://smith.langchain.com"
|
|
280
298
|
full_studio_url = f"{studio_origin}/studio/?baseUrl={local_url}"
|
|
281
299
|
|
|
@@ -351,7 +351,8 @@ FF_CRONS_ENABLED = env("FF_CRONS_ENABLED", cast=bool, default=True)
|
|
|
351
351
|
FF_LOG_DROPPED_EVENTS = env("FF_LOG_DROPPED_EVENTS", cast=bool, default=False)
|
|
352
352
|
FF_LOG_QUERY_AND_PARAMS = env("FF_LOG_QUERY_AND_PARAMS", cast=bool, default=False)
|
|
353
353
|
# Not in public docs: internal feature flag
|
|
354
|
-
FF_USE_REDIS_QUEUE = env("FF_USE_REDIS_QUEUE", cast=bool, default=
|
|
354
|
+
FF_USE_REDIS_QUEUE = env("FF_USE_REDIS_QUEUE", cast=bool, default=True)
|
|
355
|
+
|
|
355
356
|
|
|
356
357
|
# Internal flag intended for testing only
|
|
357
358
|
CRON_SCHEDULER_SLEEP_TIME = env("CRON_SCHEDULER_SLEEP_TIME", cast=int, default=5)
|
|
@@ -386,7 +386,13 @@ class WebhookUrlPolicy(TypedDict, total=False):
|
|
|
386
386
|
max_url_length: int
|
|
387
387
|
"""Maximum permitted URL length in characters; longer inputs are rejected early."""
|
|
388
388
|
disable_loopback: bool
|
|
389
|
-
"""Disallow relative URLs (internal loopback calls) when true."""
|
|
389
|
+
"""Disallow relative URLs (internal loopback calls) and localhost hostnames when true."""
|
|
390
|
+
disable_private_ips: bool
|
|
391
|
+
"""Block RFC 1918 / CGN private IP ranges as webhook targets when true.
|
|
392
|
+
|
|
393
|
+
Defaults to false (private IPs allowed). Set to true to block private
|
|
394
|
+
IP ranges for stricter SSRF protection.
|
|
395
|
+
"""
|
|
390
396
|
|
|
391
397
|
|
|
392
398
|
# Matches things like "${{ env.LG_WEBHOOK_FOO_BAR }}"
|
|
@@ -421,6 +427,8 @@ def _validate_url_policy(
|
|
|
421
427
|
policy["max_url_length"] = 2048
|
|
422
428
|
if "disable_loopback" not in policy:
|
|
423
429
|
policy["disable_loopback"] = False
|
|
430
|
+
if "disable_private_ips" not in policy:
|
|
431
|
+
policy["disable_private_ips"] = False
|
|
424
432
|
return policy
|
|
425
433
|
|
|
426
434
|
|
|
@@ -123,6 +123,37 @@ def get_loopback_client() -> JsonHttpClient:
|
|
|
123
123
|
return _loopback_client
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
_webhook_http_client: JsonHttpClient | None = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def ensure_webhook_http_client() -> JsonHttpClient:
|
|
130
|
+
"""Return (or create) an SSRF-safe HTTP client for webhook dispatch."""
|
|
131
|
+
global _webhook_http_client
|
|
132
|
+
if _webhook_http_client is not None:
|
|
133
|
+
return _webhook_http_client
|
|
134
|
+
|
|
135
|
+
from langgraph_api.lc_security import ssrf_safe_async_client # noqa: PLC0415
|
|
136
|
+
from langgraph_api.webhook import _get_webhook_config # noqa: PLC0415
|
|
137
|
+
|
|
138
|
+
inner = ssrf_safe_async_client(
|
|
139
|
+
policy=_get_webhook_config().base_ssrf_policy,
|
|
140
|
+
retries=2,
|
|
141
|
+
limits=httpx.Limits(max_keepalive_connections=10, keepalive_expiry=60.0),
|
|
142
|
+
follow_redirects=True,
|
|
143
|
+
max_redirects=5,
|
|
144
|
+
)
|
|
145
|
+
_webhook_http_client = JsonHttpClient(client=inner)
|
|
146
|
+
return _webhook_http_client
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def stop_webhook_http_client() -> None:
|
|
150
|
+
global _webhook_http_client
|
|
151
|
+
if _webhook_http_client is None:
|
|
152
|
+
return
|
|
153
|
+
await _webhook_http_client.client.aclose()
|
|
154
|
+
_webhook_http_client = None
|
|
155
|
+
|
|
156
|
+
|
|
126
157
|
def is_retriable_error(exception: BaseException) -> bool:
|
|
127
158
|
# httpx error hierarchy: https://www.python-httpx.org/exceptions/
|
|
128
159
|
# Retry all timeout related errors
|
|
@@ -1316,10 +1316,10 @@ graceful-fs@^4.2.4:
|
|
|
1316
1316
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
|
1317
1317
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
|
1318
1318
|
|
|
1319
|
-
hono@^4.12.
|
|
1320
|
-
version "4.12.
|
|
1321
|
-
resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.
|
|
1322
|
-
integrity sha512-
|
|
1319
|
+
hono@^4.12.14, hono@^4.5.4:
|
|
1320
|
+
version "4.12.14"
|
|
1321
|
+
resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.14.tgz#4777c9512b7c84138e4f09e61e3d2fa305eb1414"
|
|
1322
|
+
integrity sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==
|
|
1323
1323
|
|
|
1324
1324
|
icss-utils@^5.0.0, icss-utils@^5.1.0:
|
|
1325
1325
|
version "5.1.0"
|
|
@@ -1393,9 +1393,9 @@ kuler@^2.0.0:
|
|
|
1393
1393
|
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
|
|
1394
1394
|
|
|
1395
1395
|
"langsmith@>=0.3.33 <1.0.0", langsmith@>=0.4.6, "langsmith@>=0.5.0 <1.0.0":
|
|
1396
|
-
version "0.5.
|
|
1397
|
-
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.
|
|
1398
|
-
integrity sha512-
|
|
1396
|
+
version "0.5.20"
|
|
1397
|
+
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.20.tgz#4021847d2ccd5a86c5eb96060f9bb5f19f80eca5"
|
|
1398
|
+
integrity sha512-ULhLM8RswvQDXufLtNtvclHrWCBx8Cb5UPI6lAZC+8Dq59iHsVPz/3Ac9khWNm1VIvChRsuykixD/WrmzuuA3Q==
|
|
1399
1399
|
dependencies:
|
|
1400
1400
|
p-queue "6.6.2"
|
|
1401
1401
|
uuid "10.0.0"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""lc_security — SSRF protection and security utilities.
|
|
2
|
+
|
|
3
|
+
Vendored from langchainplus/lc_security. When lc-security is published
|
|
4
|
+
to PyPI, delete this package and add the pip dependency instead.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from langgraph_api.lc_security.exceptions import SSRFBlockedError
|
|
8
|
+
from langgraph_api.lc_security.policy import (
|
|
9
|
+
SSRFPolicy,
|
|
10
|
+
validate_hostname,
|
|
11
|
+
validate_resolved_ip,
|
|
12
|
+
validate_url,
|
|
13
|
+
validate_url_sync,
|
|
14
|
+
)
|
|
15
|
+
from langgraph_api.lc_security.transport import (
|
|
16
|
+
SSRFSafeTransport,
|
|
17
|
+
ssrf_safe_async_client,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SSRFBlockedError",
|
|
22
|
+
"SSRFPolicy",
|
|
23
|
+
"SSRFSafeTransport",
|
|
24
|
+
"ssrf_safe_async_client",
|
|
25
|
+
"validate_hostname",
|
|
26
|
+
"validate_resolved_ip",
|
|
27
|
+
"validate_url",
|
|
28
|
+
"validate_url_sync",
|
|
29
|
+
]
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""SSRF protection policy with IP validation and DNS-aware URL checking."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import dataclasses
|
|
5
|
+
import ipaddress
|
|
6
|
+
import socket
|
|
7
|
+
import urllib.parse
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from langgraph_api.lc_security.exceptions import SSRFBlockedError
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Blocklist constants
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
# Ranges that are NEVER valid webhook targets (always blocked regardless of
|
|
20
|
+
# deployment mode).
|
|
21
|
+
_ALWAYS_BLOCKED_IPV4_NETWORKS: tuple[ipaddress.IPv4Network, ...] = tuple(
|
|
22
|
+
ipaddress.IPv4Network(n)
|
|
23
|
+
for n in (
|
|
24
|
+
"169.254.0.0/16", # RFC 3927 - link-local
|
|
25
|
+
"0.0.0.0/8", # RFC 1122 - "this network"
|
|
26
|
+
"192.0.0.0/24", # RFC 6890 - IETF protocol assignments
|
|
27
|
+
"192.0.2.0/24", # RFC 5737 - TEST-NET-1 (documentation)
|
|
28
|
+
"198.18.0.0/15", # RFC 2544 - benchmarking
|
|
29
|
+
"198.51.100.0/24", # RFC 5737 - TEST-NET-2 (documentation)
|
|
30
|
+
"203.0.113.0/24", # RFC 5737 - TEST-NET-3 (documentation)
|
|
31
|
+
"224.0.0.0/4", # RFC 5771 - multicast
|
|
32
|
+
"240.0.0.0/4", # RFC 1112 - reserved for future use
|
|
33
|
+
"255.255.255.255/32", # RFC 919 - limited broadcast
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Loopback ranges — blocked when block_localhost is set, allowed by default.
|
|
38
|
+
_LOOPBACK_IPV4_NETWORKS: tuple[ipaddress.IPv4Network, ...] = tuple(
|
|
39
|
+
ipaddress.IPv4Network(n)
|
|
40
|
+
for n in ("127.0.0.0/8",) # RFC 1122 - loopback
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Private/internal ranges — blocked in SAAS, allowed in self-hosted/BYOC
|
|
44
|
+
# where internal webhook targets are legitimate.
|
|
45
|
+
_PRIVATE_IPV4_NETWORKS: tuple[ipaddress.IPv4Network, ...] = tuple(
|
|
46
|
+
ipaddress.IPv4Network(n)
|
|
47
|
+
for n in (
|
|
48
|
+
"10.0.0.0/8", # RFC 1918 - private class A
|
|
49
|
+
"172.16.0.0/12", # RFC 1918 - private class B
|
|
50
|
+
"192.168.0.0/16", # RFC 1918 - private class C
|
|
51
|
+
"100.64.0.0/10", # RFC 6598 - shared/CGN address space
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_ALWAYS_BLOCKED_IPV6_NETWORKS: tuple[ipaddress.IPv6Network, ...] = tuple(
|
|
56
|
+
ipaddress.IPv6Network(n)
|
|
57
|
+
for n in (
|
|
58
|
+
"fe80::/10", # RFC 4291 - link-local
|
|
59
|
+
"ff00::/8", # RFC 4291 - multicast
|
|
60
|
+
"::ffff:0:0/96", # RFC 4291 - IPv4-mapped IPv6 addresses
|
|
61
|
+
"::0.0.0.0/96", # RFC 4291 - IPv4-compatible IPv6 (deprecated)
|
|
62
|
+
"64:ff9b::/96", # RFC 6052 - NAT64 well-known prefix
|
|
63
|
+
"64:ff9b:1::/48", # RFC 8215 - NAT64 discovery prefix
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
_LOOPBACK_IPV6_NETWORKS: tuple[ipaddress.IPv6Network, ...] = tuple(
|
|
68
|
+
ipaddress.IPv6Network(n)
|
|
69
|
+
for n in ("::1/128",) # RFC 4291 - loopback
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Private/internal IPv6 — blocked in SAAS, allowed in self-hosted/BYOC.
|
|
73
|
+
_PRIVATE_IPV6_NETWORKS: tuple[ipaddress.IPv6Network, ...] = tuple(
|
|
74
|
+
ipaddress.IPv6Network(n)
|
|
75
|
+
for n in (
|
|
76
|
+
"fc00::/7", # RFC 4193 - unique local addresses (ULA)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
_CLOUD_METADATA_IPS: frozenset[str] = frozenset(
|
|
81
|
+
{
|
|
82
|
+
"169.254.169.254",
|
|
83
|
+
"169.254.170.2",
|
|
84
|
+
"100.100.100.200",
|
|
85
|
+
"fd00:ec2::254",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
_CLOUD_METADATA_HOSTNAMES: frozenset[str] = frozenset(
|
|
90
|
+
{
|
|
91
|
+
"metadata.google.internal",
|
|
92
|
+
"metadata.amazonaws.com",
|
|
93
|
+
"metadata",
|
|
94
|
+
"instance-data",
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
_LOCALHOST_NAMES: frozenset[str] = frozenset(
|
|
99
|
+
{
|
|
100
|
+
"localhost",
|
|
101
|
+
"localhost.localdomain",
|
|
102
|
+
"host.docker.internal",
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
_K8S_SUFFIX = ".svc.cluster.local"
|
|
107
|
+
|
|
108
|
+
# NAT64 well-known prefixes
|
|
109
|
+
_NAT64_PREFIX = ipaddress.IPv6Network("64:ff9b::/96")
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# SSRFPolicy
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclasses.dataclass(frozen=True)
|
|
117
|
+
class SSRFPolicy:
|
|
118
|
+
"""Immutable policy controlling which URLs/IPs are considered safe."""
|
|
119
|
+
|
|
120
|
+
allowed_schemes: frozenset[str] = frozenset({"http", "https"})
|
|
121
|
+
block_private_ips: bool = False
|
|
122
|
+
block_localhost: bool = False
|
|
123
|
+
block_cloud_metadata: bool = True
|
|
124
|
+
block_k8s_internal: bool = True
|
|
125
|
+
allowed_hosts: frozenset[str] = frozenset()
|
|
126
|
+
additional_blocked_cidrs: tuple[
|
|
127
|
+
ipaddress.IPv4Network | ipaddress.IPv6Network, ...
|
|
128
|
+
] = ()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Helpers
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _extract_embedded_ipv4(
|
|
137
|
+
addr: ipaddress.IPv6Address,
|
|
138
|
+
) -> ipaddress.IPv4Address | None:
|
|
139
|
+
"""Extract an embedded IPv4 from IPv4-mapped or NAT64 IPv6 addresses."""
|
|
140
|
+
# Check ipv4_mapped first (covers ::ffff:x.x.x.x)
|
|
141
|
+
if addr.ipv4_mapped is not None:
|
|
142
|
+
return addr.ipv4_mapped
|
|
143
|
+
|
|
144
|
+
# Check NAT64 well-known prefix (/96) - embedded IPv4 is in the last 4 bytes.
|
|
145
|
+
# NOTE: We intentionally only extract for the /96 prefix where the
|
|
146
|
+
# last-4-bytes extraction is correct per RFC 6052 §2.2. The /48 discovery
|
|
147
|
+
# prefix (64:ff9b:1::/48, RFC 8215) embeds IPv4 at a different offset
|
|
148
|
+
# and is already blocked outright via _BLOCKED_IPV6_NETWORKS.
|
|
149
|
+
if addr in _NAT64_PREFIX:
|
|
150
|
+
raw = addr.packed
|
|
151
|
+
return ipaddress.IPv4Address(raw[-4:])
|
|
152
|
+
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _ip_in_blocked_networks(
|
|
157
|
+
addr: ipaddress.IPv4Address | ipaddress.IPv6Address,
|
|
158
|
+
policy: SSRFPolicy,
|
|
159
|
+
) -> str | None:
|
|
160
|
+
"""Return a reason string if *addr* falls in a blocked range, else None."""
|
|
161
|
+
if isinstance(addr, ipaddress.IPv4Address):
|
|
162
|
+
# Always-blocked ranges (link-local, reserved, etc.)
|
|
163
|
+
for net in _ALWAYS_BLOCKED_IPV4_NETWORKS:
|
|
164
|
+
if addr in net:
|
|
165
|
+
return "blocked IP range"
|
|
166
|
+
# Loopback — only when block_localhost is set.
|
|
167
|
+
if policy.block_localhost:
|
|
168
|
+
for net in _LOOPBACK_IPV4_NETWORKS:
|
|
169
|
+
if addr in net:
|
|
170
|
+
return "loopback IP range"
|
|
171
|
+
# Private ranges (RFC 1918, CGN) — only when policy says so.
|
|
172
|
+
if policy.block_private_ips:
|
|
173
|
+
for net in _PRIVATE_IPV4_NETWORKS:
|
|
174
|
+
if addr in net:
|
|
175
|
+
return "private IP range"
|
|
176
|
+
for net in policy.additional_blocked_cidrs:
|
|
177
|
+
if isinstance(net, ipaddress.IPv4Network) and addr in net:
|
|
178
|
+
return "blocked CIDR"
|
|
179
|
+
else:
|
|
180
|
+
for net in _ALWAYS_BLOCKED_IPV6_NETWORKS:
|
|
181
|
+
if addr in net:
|
|
182
|
+
return "blocked IP range"
|
|
183
|
+
# Loopback — only when block_localhost is set.
|
|
184
|
+
if policy.block_localhost:
|
|
185
|
+
for net in _LOOPBACK_IPV6_NETWORKS:
|
|
186
|
+
if addr in net:
|
|
187
|
+
return "loopback IP range"
|
|
188
|
+
if policy.block_private_ips:
|
|
189
|
+
for net in _PRIVATE_IPV6_NETWORKS:
|
|
190
|
+
if addr in net:
|
|
191
|
+
return "private IP range"
|
|
192
|
+
for net in policy.additional_blocked_cidrs:
|
|
193
|
+
if isinstance(net, ipaddress.IPv6Network) and addr in net:
|
|
194
|
+
return "blocked CIDR"
|
|
195
|
+
|
|
196
|
+
# Cloud metadata IP check
|
|
197
|
+
if policy.block_cloud_metadata and str(addr) in _CLOUD_METADATA_IPS:
|
|
198
|
+
return "cloud metadata endpoint"
|
|
199
|
+
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Public validation functions
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def validate_resolved_ip(ip_str: str, policy: SSRFPolicy) -> None:
|
|
209
|
+
"""Validate a resolved IP address against the SSRF policy.
|
|
210
|
+
|
|
211
|
+
Raises SSRFBlockedError if the IP is blocked.
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
addr = ipaddress.ip_address(ip_str)
|
|
215
|
+
except ValueError as exc:
|
|
216
|
+
raise SSRFBlockedError("invalid IP address") from exc
|
|
217
|
+
|
|
218
|
+
if isinstance(addr, ipaddress.IPv6Address):
|
|
219
|
+
# Check the original IPv6 address first — this catches addresses
|
|
220
|
+
# in blocked IPv6 networks (e.g. NAT64 discovery prefix 64:ff9b:1::/48)
|
|
221
|
+
# before we attempt IPv4 extraction.
|
|
222
|
+
reason = _ip_in_blocked_networks(addr, policy)
|
|
223
|
+
if reason is not None:
|
|
224
|
+
raise SSRFBlockedError(reason)
|
|
225
|
+
inner = _extract_embedded_ipv4(addr)
|
|
226
|
+
if inner is not None:
|
|
227
|
+
addr = inner
|
|
228
|
+
|
|
229
|
+
reason = _ip_in_blocked_networks(addr, policy)
|
|
230
|
+
if reason is not None:
|
|
231
|
+
raise SSRFBlockedError(reason)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def validate_hostname(hostname: str, policy: SSRFPolicy) -> None:
|
|
235
|
+
"""Validate a hostname against the SSRF policy.
|
|
236
|
+
|
|
237
|
+
Raises SSRFBlockedError if the hostname is blocked.
|
|
238
|
+
"""
|
|
239
|
+
lower = hostname.lower()
|
|
240
|
+
|
|
241
|
+
if policy.block_localhost and lower in _LOCALHOST_NAMES:
|
|
242
|
+
raise SSRFBlockedError("localhost address")
|
|
243
|
+
|
|
244
|
+
if policy.block_cloud_metadata and lower in _CLOUD_METADATA_HOSTNAMES:
|
|
245
|
+
raise SSRFBlockedError("cloud metadata endpoint")
|
|
246
|
+
|
|
247
|
+
if policy.block_k8s_internal and lower.endswith(_K8S_SUFFIX):
|
|
248
|
+
raise SSRFBlockedError("Kubernetes internal DNS")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _effective_allowed_hosts(policy: SSRFPolicy) -> frozenset[str]:
|
|
252
|
+
"""Return the policy's allowed_hosts set."""
|
|
253
|
+
return policy.allowed_hosts
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def validate_url(url: str, policy: SSRFPolicy = SSRFPolicy()) -> None:
|
|
257
|
+
"""Validate a URL against the SSRF policy, including DNS resolution.
|
|
258
|
+
|
|
259
|
+
This is the primary entry-point for async code paths. It delegates
|
|
260
|
+
scheme/hostname/allowed-hosts checks to ``validate_url_sync``, then
|
|
261
|
+
resolves DNS and validates every resolved IP.
|
|
262
|
+
|
|
263
|
+
Raises SSRFBlockedError on any violation.
|
|
264
|
+
"""
|
|
265
|
+
parsed = urllib.parse.urlparse(url)
|
|
266
|
+
hostname = parsed.hostname or ""
|
|
267
|
+
|
|
268
|
+
# Reuse synchronous checks (scheme, hostname, allowed-hosts bypass).
|
|
269
|
+
try:
|
|
270
|
+
validate_url_sync(url, policy)
|
|
271
|
+
except SSRFBlockedError as exc:
|
|
272
|
+
logger.warning(
|
|
273
|
+
"ssrf_blocked",
|
|
274
|
+
hostname=hostname,
|
|
275
|
+
reason=str(exc),
|
|
276
|
+
validation_type="write_time",
|
|
277
|
+
)
|
|
278
|
+
raise
|
|
279
|
+
|
|
280
|
+
# If the host is in the allowed list, validate_url_sync returned
|
|
281
|
+
# successfully and no DNS/IP checks are needed.
|
|
282
|
+
allowed = {h.lower() for h in _effective_allowed_hosts(policy)}
|
|
283
|
+
if hostname.lower() in allowed:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# If localhost is allowed and hostname is a localhost name, skip DNS/IP
|
|
287
|
+
# checks — the resolved IP (e.g. link-local in Docker) is irrelevant
|
|
288
|
+
# since we've explicitly permitted localhost access.
|
|
289
|
+
if not policy.block_localhost and hostname.lower() in _LOCALHOST_NAMES:
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# DNS resolution
|
|
293
|
+
scheme = (parsed.scheme or "").lower()
|
|
294
|
+
port = parsed.port or (443 if scheme == "https" else 80)
|
|
295
|
+
try:
|
|
296
|
+
addrinfo = await asyncio.to_thread(
|
|
297
|
+
socket.getaddrinfo, hostname, port, type=socket.SOCK_STREAM
|
|
298
|
+
)
|
|
299
|
+
except socket.gaierror as exc:
|
|
300
|
+
logger.warning(
|
|
301
|
+
"ssrf_blocked",
|
|
302
|
+
hostname=hostname,
|
|
303
|
+
reason="DNS resolution failed",
|
|
304
|
+
validation_type="write_time",
|
|
305
|
+
)
|
|
306
|
+
raise SSRFBlockedError("DNS resolution failed") from exc
|
|
307
|
+
|
|
308
|
+
# Validate every resolved IP
|
|
309
|
+
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
|
310
|
+
ip_str = sockaddr[0]
|
|
311
|
+
try:
|
|
312
|
+
validate_resolved_ip(ip_str, policy)
|
|
313
|
+
except SSRFBlockedError as exc:
|
|
314
|
+
logger.warning(
|
|
315
|
+
"ssrf_blocked",
|
|
316
|
+
hostname=hostname,
|
|
317
|
+
reason=str(exc),
|
|
318
|
+
validation_type="write_time",
|
|
319
|
+
)
|
|
320
|
+
raise
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def validate_url_sync(url: str, policy: SSRFPolicy = SSRFPolicy()) -> None:
|
|
324
|
+
"""Synchronous URL validation (no DNS resolution).
|
|
325
|
+
|
|
326
|
+
Suitable for Pydantic validators and other sync contexts. Checks scheme
|
|
327
|
+
and hostname patterns only — use validate_url for full DNS-aware checking.
|
|
328
|
+
|
|
329
|
+
Raises SSRFBlockedError on any violation.
|
|
330
|
+
"""
|
|
331
|
+
parsed = urllib.parse.urlparse(url)
|
|
332
|
+
|
|
333
|
+
# Scheme check
|
|
334
|
+
scheme = (parsed.scheme or "").lower()
|
|
335
|
+
if scheme not in policy.allowed_schemes:
|
|
336
|
+
raise SSRFBlockedError(f"scheme '{scheme}' not allowed")
|
|
337
|
+
|
|
338
|
+
# Hostname check
|
|
339
|
+
hostname = parsed.hostname
|
|
340
|
+
if not hostname:
|
|
341
|
+
raise SSRFBlockedError("missing hostname")
|
|
342
|
+
|
|
343
|
+
# Allowed-hosts bypass
|
|
344
|
+
allowed = _effective_allowed_hosts(policy)
|
|
345
|
+
if hostname.lower() in {h.lower() for h in allowed}:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Hostname pattern checks
|
|
349
|
+
validate_hostname(hostname, policy)
|