coco-framework 0.1.0__py3-none-any.whl
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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""SSRF-safe httpx transport with DNS resolution and IP pinning."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from coco_core._security._exceptions import SSRFBlockedError
|
|
9
|
+
from coco_core._security._policy import (
|
|
10
|
+
DEFAULT_SSRF_POLICY,
|
|
11
|
+
SSRFPolicy,
|
|
12
|
+
_effective_allowed_hosts,
|
|
13
|
+
validate_resolved_ip,
|
|
14
|
+
validate_url_sync,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Keys that AsyncHTTPTransport accepts (forwarded from factory kwargs).
|
|
18
|
+
_TRANSPORT_KWARGS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"verify",
|
|
21
|
+
"cert",
|
|
22
|
+
"trust_env",
|
|
23
|
+
"http1",
|
|
24
|
+
"http2",
|
|
25
|
+
"limits",
|
|
26
|
+
"retries",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SSRFSafeTransport(httpx.AsyncBaseTransport):
|
|
32
|
+
"""httpx async transport that validates DNS results against an SSRF policy.
|
|
33
|
+
|
|
34
|
+
For every outgoing request the transport:
|
|
35
|
+
1. Checks the URL scheme against `policy.allowed_schemes`.
|
|
36
|
+
2. Validates the hostname against blocked patterns.
|
|
37
|
+
3. Resolves DNS and validates **all** returned IPs.
|
|
38
|
+
4. Rewrites the request to connect to the first valid IP while
|
|
39
|
+
preserving the original `Host` header and TLS SNI hostname.
|
|
40
|
+
|
|
41
|
+
Redirects are re-validated on each hop because `follow_redirects`
|
|
42
|
+
is set on the *client*, causing `handle_async_request` to be called
|
|
43
|
+
again for each redirect target.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
|
|
49
|
+
**transport_kwargs: object,
|
|
50
|
+
) -> None:
|
|
51
|
+
self._policy = policy
|
|
52
|
+
self._inner = httpx.AsyncHTTPTransport(**transport_kwargs) # type: ignore[arg-type]
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------------ #
|
|
55
|
+
# Core request handler
|
|
56
|
+
# ------------------------------------------------------------------ #
|
|
57
|
+
|
|
58
|
+
async def handle_async_request(
|
|
59
|
+
self,
|
|
60
|
+
request: httpx.Request,
|
|
61
|
+
) -> httpx.Response:
|
|
62
|
+
hostname = request.url.host or ""
|
|
63
|
+
scheme = request.url.scheme.lower()
|
|
64
|
+
|
|
65
|
+
# 1-3. Scheme, hostname, and pattern checks (reuse sync validator).
|
|
66
|
+
validate_url_sync(str(request.url), self._policy)
|
|
67
|
+
|
|
68
|
+
# Allowed-hosts bypass - skip DNS/IP validation entirely.
|
|
69
|
+
allowed = {h.lower() for h in _effective_allowed_hosts(self._policy)}
|
|
70
|
+
if hostname.lower() in allowed:
|
|
71
|
+
return await self._inner.handle_async_request(request)
|
|
72
|
+
|
|
73
|
+
# 4. DNS resolution
|
|
74
|
+
port = request.url.port or (443 if scheme == "https" else 80)
|
|
75
|
+
try:
|
|
76
|
+
addrinfo = await asyncio.to_thread(
|
|
77
|
+
socket.getaddrinfo,
|
|
78
|
+
hostname,
|
|
79
|
+
port,
|
|
80
|
+
type=socket.SOCK_STREAM,
|
|
81
|
+
)
|
|
82
|
+
except socket.gaierror as exc:
|
|
83
|
+
msg = "DNS resolution failed"
|
|
84
|
+
raise SSRFBlockedError(msg) from exc
|
|
85
|
+
|
|
86
|
+
if not addrinfo:
|
|
87
|
+
msg = "DNS resolution returned no results"
|
|
88
|
+
raise SSRFBlockedError(msg)
|
|
89
|
+
|
|
90
|
+
# 5. Validate ALL resolved IPs - any blocked means reject.
|
|
91
|
+
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
|
92
|
+
ip_str: str = sockaddr[0] # type: ignore[assignment]
|
|
93
|
+
validate_resolved_ip(ip_str, self._policy)
|
|
94
|
+
|
|
95
|
+
# 6. Pin to first resolved IP.
|
|
96
|
+
pinned_ip = addrinfo[0][4][0]
|
|
97
|
+
|
|
98
|
+
# 7. Rewrite URL to use pinned IP, preserving Host header and SNI.
|
|
99
|
+
pinned_url = request.url.copy_with(host=pinned_ip)
|
|
100
|
+
|
|
101
|
+
# Build extensions dict, adding sni_hostname for HTTPS so TLS
|
|
102
|
+
# certificate validation uses the original hostname.
|
|
103
|
+
extensions = dict(request.extensions)
|
|
104
|
+
if scheme == "https":
|
|
105
|
+
extensions["sni_hostname"] = hostname.encode("ascii")
|
|
106
|
+
|
|
107
|
+
pinned_request = httpx.Request(
|
|
108
|
+
method=request.method,
|
|
109
|
+
url=pinned_url,
|
|
110
|
+
headers=request.headers, # Host header already set to original
|
|
111
|
+
content=request.content,
|
|
112
|
+
extensions=extensions,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return await self._inner.handle_async_request(pinned_request)
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------ #
|
|
118
|
+
# Lifecycle
|
|
119
|
+
# ------------------------------------------------------------------ #
|
|
120
|
+
|
|
121
|
+
async def aclose(self) -> None:
|
|
122
|
+
await self._inner.aclose()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------- #
|
|
126
|
+
# Factory
|
|
127
|
+
# ---------------------------------------------------------------------- #
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SSRFSafeSyncTransport(httpx.BaseTransport):
|
|
131
|
+
"""httpx sync transport that validates DNS results against an SSRF policy.
|
|
132
|
+
|
|
133
|
+
Sync mirror of `SSRFSafeTransport`. See that class for full documentation.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
|
|
139
|
+
**transport_kwargs: object,
|
|
140
|
+
) -> None:
|
|
141
|
+
self._policy = policy
|
|
142
|
+
self._inner = httpx.HTTPTransport(**transport_kwargs) # type: ignore[arg-type]
|
|
143
|
+
|
|
144
|
+
def handle_request(
|
|
145
|
+
self,
|
|
146
|
+
request: httpx.Request,
|
|
147
|
+
) -> httpx.Response:
|
|
148
|
+
hostname = request.url.host or ""
|
|
149
|
+
scheme = request.url.scheme.lower()
|
|
150
|
+
|
|
151
|
+
validate_url_sync(str(request.url), self._policy)
|
|
152
|
+
|
|
153
|
+
allowed = {h.lower() for h in _effective_allowed_hosts(self._policy)}
|
|
154
|
+
if hostname.lower() in allowed:
|
|
155
|
+
return self._inner.handle_request(request)
|
|
156
|
+
|
|
157
|
+
port = request.url.port or (443 if scheme == "https" else 80)
|
|
158
|
+
try:
|
|
159
|
+
addrinfo = socket.getaddrinfo(
|
|
160
|
+
hostname,
|
|
161
|
+
port,
|
|
162
|
+
type=socket.SOCK_STREAM,
|
|
163
|
+
)
|
|
164
|
+
except socket.gaierror as exc:
|
|
165
|
+
msg = "DNS resolution failed"
|
|
166
|
+
raise SSRFBlockedError(msg) from exc
|
|
167
|
+
|
|
168
|
+
if not addrinfo:
|
|
169
|
+
msg = "DNS resolution returned no results"
|
|
170
|
+
raise SSRFBlockedError(msg)
|
|
171
|
+
|
|
172
|
+
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
|
173
|
+
ip_str: str = sockaddr[0] # type: ignore[assignment]
|
|
174
|
+
validate_resolved_ip(ip_str, self._policy)
|
|
175
|
+
|
|
176
|
+
pinned_ip = addrinfo[0][4][0]
|
|
177
|
+
pinned_url = request.url.copy_with(host=pinned_ip)
|
|
178
|
+
|
|
179
|
+
extensions = dict(request.extensions)
|
|
180
|
+
if scheme == "https":
|
|
181
|
+
extensions["sni_hostname"] = hostname.encode("ascii")
|
|
182
|
+
|
|
183
|
+
pinned_request = httpx.Request(
|
|
184
|
+
method=request.method,
|
|
185
|
+
url=pinned_url,
|
|
186
|
+
headers=request.headers,
|
|
187
|
+
content=request.content,
|
|
188
|
+
extensions=extensions,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return self._inner.handle_request(pinned_request)
|
|
192
|
+
|
|
193
|
+
def close(self) -> None:
|
|
194
|
+
self._inner.close()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------- #
|
|
198
|
+
# Factories
|
|
199
|
+
# ---------------------------------------------------------------------- #
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def ssrf_safe_client(
|
|
203
|
+
policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
|
|
204
|
+
**kwargs: object,
|
|
205
|
+
) -> httpx.Client:
|
|
206
|
+
"""Create an `httpx.Client` with SSRF protection."""
|
|
207
|
+
transport_kwargs: dict[str, object] = {}
|
|
208
|
+
client_kwargs: dict[str, object] = {}
|
|
209
|
+
for key, value in kwargs.items():
|
|
210
|
+
if key in _TRANSPORT_KWARGS:
|
|
211
|
+
transport_kwargs[key] = value
|
|
212
|
+
else:
|
|
213
|
+
client_kwargs[key] = value
|
|
214
|
+
|
|
215
|
+
transport = SSRFSafeSyncTransport(policy=policy, **transport_kwargs)
|
|
216
|
+
|
|
217
|
+
client_kwargs.setdefault("follow_redirects", True)
|
|
218
|
+
client_kwargs.setdefault("max_redirects", 10)
|
|
219
|
+
|
|
220
|
+
return httpx.Client(
|
|
221
|
+
transport=transport,
|
|
222
|
+
**client_kwargs, # type: ignore[arg-type]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def ssrf_safe_async_client(
|
|
227
|
+
policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
|
|
228
|
+
**kwargs: object,
|
|
229
|
+
) -> httpx.AsyncClient:
|
|
230
|
+
"""Create an `httpx.AsyncClient` with SSRF protection.
|
|
231
|
+
|
|
232
|
+
Drop-in replacement for `httpx.AsyncClient(...)` - callers just swap
|
|
233
|
+
the constructor call. Transport-specific kwargs (`verify`, `cert`,
|
|
234
|
+
`retries`, etc.) are forwarded to the inner `AsyncHTTPTransport`;
|
|
235
|
+
everything else goes to the `AsyncClient`.
|
|
236
|
+
"""
|
|
237
|
+
transport_kwargs: dict[str, object] = {}
|
|
238
|
+
client_kwargs: dict[str, object] = {}
|
|
239
|
+
for key, value in kwargs.items():
|
|
240
|
+
if key in _TRANSPORT_KWARGS:
|
|
241
|
+
transport_kwargs[key] = value
|
|
242
|
+
else:
|
|
243
|
+
client_kwargs[key] = value
|
|
244
|
+
|
|
245
|
+
transport = SSRFSafeTransport(policy=policy, **transport_kwargs)
|
|
246
|
+
|
|
247
|
+
# Apply defaults only if not overridden by caller.
|
|
248
|
+
client_kwargs.setdefault("follow_redirects", True)
|
|
249
|
+
client_kwargs.setdefault("max_redirects", 10)
|
|
250
|
+
|
|
251
|
+
return httpx.AsyncClient(
|
|
252
|
+
transport=transport,
|
|
253
|
+
**client_kwargs, # type: ignore[arg-type]
|
|
254
|
+
)
|
coco_core/agents.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Schema definitions for representing agent actions, observations, and return values.
|
|
2
|
+
|
|
3
|
+
!!! warning
|
|
4
|
+
|
|
5
|
+
The schema definitions are provided for backwards compatibility.
|
|
6
|
+
|
|
7
|
+
!!! warning
|
|
8
|
+
|
|
9
|
+
New agents should be built using the
|
|
10
|
+
[`coco` library](https://pypi.org/project/langchain/), which provides a
|
|
11
|
+
simpler and more flexible way to define agents.
|
|
12
|
+
|
|
13
|
+
See docs on [building agents](https://docs.langchain.com/oss/python/langchain/agents).
|
|
14
|
+
|
|
15
|
+
Agents use language models to choose a sequence of actions to take.
|
|
16
|
+
|
|
17
|
+
A basic agent works in the following manner:
|
|
18
|
+
|
|
19
|
+
1. Given a prompt an agent uses an LLM to request an action to take
|
|
20
|
+
(e.g., a tool to run).
|
|
21
|
+
2. The agent executes the action (e.g., runs the tool), and receives an observation.
|
|
22
|
+
3. The agent returns the observation to the LLM, which can then be used to generate
|
|
23
|
+
the next action.
|
|
24
|
+
4. When the agent reaches a stopping condition, it returns a final return value.
|
|
25
|
+
|
|
26
|
+
The schemas for the agents themselves are defined in `coco.agents.agent`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
from collections.abc import Sequence
|
|
33
|
+
from typing import Any, Literal
|
|
34
|
+
|
|
35
|
+
from coco_core.load.serializable import Serializable
|
|
36
|
+
from coco_core.messages import (
|
|
37
|
+
AIMessage,
|
|
38
|
+
BaseMessage,
|
|
39
|
+
FunctionMessage,
|
|
40
|
+
HumanMessage,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentAction(Serializable):
|
|
45
|
+
"""Represents a request to execute an action by an agent.
|
|
46
|
+
|
|
47
|
+
The action consists of the name of the tool to execute and the input to pass
|
|
48
|
+
to the tool. The log is used to pass along extra information about the action.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
tool: str
|
|
52
|
+
"""The name of the `Tool` to execute."""
|
|
53
|
+
|
|
54
|
+
tool_input: str | dict[Any, Any]
|
|
55
|
+
"""The input to pass in to the `Tool`."""
|
|
56
|
+
|
|
57
|
+
log: str
|
|
58
|
+
"""Additional information to log about the action.
|
|
59
|
+
|
|
60
|
+
This log can be used in a few ways. First, it can be used to audit what exactly the
|
|
61
|
+
LLM predicted to lead to this `(tool, tool_input)`.
|
|
62
|
+
|
|
63
|
+
Second, it can be used in future iterations to show the LLMs prior thoughts. This is
|
|
64
|
+
useful when `(tool, tool_input)` does not contain full information about the LLM
|
|
65
|
+
prediction (for example, any `thought` before the tool/tool_input).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
type: Literal["AgentAction"] = "AgentAction"
|
|
69
|
+
|
|
70
|
+
# Override init to support instantiation by position for backward compat.
|
|
71
|
+
def __init__(
|
|
72
|
+
self, tool: str, tool_input: str | dict[Any, Any], log: str, **kwargs: Any
|
|
73
|
+
):
|
|
74
|
+
"""Create an `AgentAction`.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
tool: The name of the tool to execute.
|
|
78
|
+
tool_input: The input to pass in to the `Tool`.
|
|
79
|
+
log: Additional information to log about the action.
|
|
80
|
+
"""
|
|
81
|
+
super().__init__(tool=tool, tool_input=tool_input, log=log, **kwargs)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def is_lc_serializable(cls) -> bool:
|
|
85
|
+
"""`AgentAction` is serializable.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
`True`
|
|
89
|
+
"""
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def get_lc_namespace(cls) -> list[str]:
|
|
94
|
+
"""Get the namespace of the Coco object.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
`["coco", "schema", "agent"]`
|
|
98
|
+
"""
|
|
99
|
+
return ["coco", "schema", "agent"]
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def messages(self) -> Sequence[BaseMessage]:
|
|
103
|
+
"""Return the messages that correspond to this action."""
|
|
104
|
+
return _convert_agent_action_to_messages(self)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AgentActionMessageLog(AgentAction):
|
|
108
|
+
"""Representation of an action to be executed by an agent.
|
|
109
|
+
|
|
110
|
+
This is similar to `AgentAction`, but includes a message log consisting of
|
|
111
|
+
chat messages.
|
|
112
|
+
|
|
113
|
+
This is useful when working with `ChatModels`, and is used to reconstruct
|
|
114
|
+
conversation history from the agent's perspective.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
message_log: Sequence[BaseMessage]
|
|
118
|
+
"""Similar to log, this can be used to pass along extra information about what exact
|
|
119
|
+
messages were predicted by the LLM before parsing out the `(tool, tool_input)`.
|
|
120
|
+
|
|
121
|
+
This is again useful if `(tool, tool_input)` cannot be used to fully recreate the
|
|
122
|
+
LLM prediction, and you need that LLM prediction (for future agent iteration).
|
|
123
|
+
|
|
124
|
+
Compared to `log`, this is useful when the underlying LLM is a chat model (and
|
|
125
|
+
therefore returns messages rather than a string).
|
|
126
|
+
"""
|
|
127
|
+
# Ignoring type because we're overriding the type from AgentAction.
|
|
128
|
+
# And this is the correct thing to do in this case.
|
|
129
|
+
# The type literal is used for serialization purposes.
|
|
130
|
+
type: Literal["AgentActionMessageLog"] = "AgentActionMessageLog" # type: ignore[assignment]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AgentStep(Serializable):
|
|
134
|
+
"""Result of running an `AgentAction`."""
|
|
135
|
+
|
|
136
|
+
action: AgentAction
|
|
137
|
+
"""The `AgentAction` that was executed."""
|
|
138
|
+
|
|
139
|
+
observation: Any
|
|
140
|
+
"""The result of the `AgentAction`."""
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def messages(self) -> Sequence[BaseMessage]:
|
|
144
|
+
"""Messages that correspond to this observation."""
|
|
145
|
+
return _convert_agent_observation_to_messages(self.action, self.observation)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class AgentFinish(Serializable):
|
|
149
|
+
"""Final return value of an `ActionAgent`.
|
|
150
|
+
|
|
151
|
+
Agents return an `AgentFinish` when they have reached a stopping condition.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
return_values: dict[Any, Any]
|
|
155
|
+
"""Dictionary of return values."""
|
|
156
|
+
|
|
157
|
+
log: str
|
|
158
|
+
"""Additional information to log about the return value.
|
|
159
|
+
|
|
160
|
+
This is used to pass along the full LLM prediction, not just the parsed out
|
|
161
|
+
return value.
|
|
162
|
+
|
|
163
|
+
For example, if the full LLM prediction was `Final Answer: 2` you may want to just
|
|
164
|
+
return `2` as a return value, but pass along the full string as a `log` (for
|
|
165
|
+
debugging or observability purposes).
|
|
166
|
+
"""
|
|
167
|
+
type: Literal["AgentFinish"] = "AgentFinish"
|
|
168
|
+
|
|
169
|
+
def __init__(self, return_values: dict[Any, Any], log: str, **kwargs: Any):
|
|
170
|
+
"""Override init to support instantiation by position for backward compat."""
|
|
171
|
+
super().__init__(return_values=return_values, log=log, **kwargs)
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def is_lc_serializable(cls) -> bool:
|
|
175
|
+
"""Return `True` as this class is serializable."""
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def get_lc_namespace(cls) -> list[str]:
|
|
180
|
+
"""Get the namespace of the Coco object.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
`["coco", "schema", "agent"]`
|
|
184
|
+
"""
|
|
185
|
+
return ["coco", "schema", "agent"]
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def messages(self) -> Sequence[BaseMessage]:
|
|
189
|
+
"""Messages that correspond to this observation."""
|
|
190
|
+
return [AIMessage(content=self.log)]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _convert_agent_action_to_messages(
|
|
194
|
+
agent_action: AgentAction,
|
|
195
|
+
) -> Sequence[BaseMessage]:
|
|
196
|
+
"""Convert an agent action to a message.
|
|
197
|
+
|
|
198
|
+
This code is used to reconstruct the original AI message from the agent action.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
agent_action: Agent action to convert.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
`AIMessage` that corresponds to the original tool invocation.
|
|
205
|
+
"""
|
|
206
|
+
if isinstance(agent_action, AgentActionMessageLog):
|
|
207
|
+
return agent_action.message_log
|
|
208
|
+
return [AIMessage(content=agent_action.log)]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _convert_agent_observation_to_messages(
|
|
212
|
+
agent_action: AgentAction, observation: Any
|
|
213
|
+
) -> Sequence[BaseMessage]:
|
|
214
|
+
"""Convert an agent action to a message.
|
|
215
|
+
|
|
216
|
+
This code is used to reconstruct the original AI message from the agent action.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
agent_action: Agent action to convert.
|
|
220
|
+
observation: Observation to convert to a message.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
`AIMessage` that corresponds to the original tool invocation.
|
|
224
|
+
"""
|
|
225
|
+
if isinstance(agent_action, AgentActionMessageLog):
|
|
226
|
+
return [_create_function_message(agent_action, observation)]
|
|
227
|
+
content = observation
|
|
228
|
+
if not isinstance(observation, str):
|
|
229
|
+
try:
|
|
230
|
+
content = json.dumps(observation, ensure_ascii=False)
|
|
231
|
+
except Exception:
|
|
232
|
+
content = str(observation)
|
|
233
|
+
return [HumanMessage(content=content)]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _create_function_message(
|
|
237
|
+
agent_action: AgentAction, observation: Any
|
|
238
|
+
) -> FunctionMessage:
|
|
239
|
+
"""Convert agent action and observation into a function message.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
agent_action: the tool invocation request from the agent.
|
|
243
|
+
observation: the result of the tool invocation.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
`FunctionMessage` that corresponds to the original tool invocation.
|
|
247
|
+
"""
|
|
248
|
+
if not isinstance(observation, str):
|
|
249
|
+
try:
|
|
250
|
+
content = json.dumps(observation, ensure_ascii=False)
|
|
251
|
+
except Exception:
|
|
252
|
+
content = str(observation)
|
|
253
|
+
else:
|
|
254
|
+
content = observation
|
|
255
|
+
return FunctionMessage(
|
|
256
|
+
name=agent_action.tool,
|
|
257
|
+
content=content,
|
|
258
|
+
)
|