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
coco_core/load/load.py
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
"""Load Coco objects from JSON strings or objects.
|
|
2
|
+
|
|
3
|
+
## How it works
|
|
4
|
+
|
|
5
|
+
Each `Serializable` Coco object has a unique identifier (its "class path"), which
|
|
6
|
+
is a list of strings representing the module path and class name. For example:
|
|
7
|
+
|
|
8
|
+
- `AIMessage` -> `["coco_core", "messages", "ai", "AIMessage"]`
|
|
9
|
+
- `ChatPromptTemplate` -> `["coco_core", "prompts", "chat", "ChatPromptTemplate"]`
|
|
10
|
+
|
|
11
|
+
When deserializing, the class path from the JSON `'id'` field is checked against an
|
|
12
|
+
allowlist. If the class is not in the allowlist, deserialization raises a `ValueError`.
|
|
13
|
+
|
|
14
|
+
## Threat model
|
|
15
|
+
|
|
16
|
+
A serialized Coco payload crosses a trust boundary because the manifest
|
|
17
|
+
may contain serialized objects and configuration that affect runtime behavior.
|
|
18
|
+
For example, a payload can configure a chat model with a custom `base_url`,
|
|
19
|
+
custom headers, a different model name, or other constructor arguments. These
|
|
20
|
+
are supported features, but they also mean the payload contents should be
|
|
21
|
+
treated as executable configuration rather than plain text.
|
|
22
|
+
|
|
23
|
+
Concretely, deserialization instantiates Python objects, so any constructor
|
|
24
|
+
(`__init__`) or validator on an allowed class can run during `load()`. A
|
|
25
|
+
crafted payload that is allowed to reach an unintended class — or an intended
|
|
26
|
+
class with attacker-controlled kwargs — could cause network calls, file
|
|
27
|
+
operations, or environment-variable access while the object is being built.
|
|
28
|
+
|
|
29
|
+
!!! warning "Do not use with untrusted input"
|
|
30
|
+
|
|
31
|
+
If the source is untrusted, avoid calling `load()` / `loads()` on it. If
|
|
32
|
+
you must, restrict `allowed_objects` to types that do not execute logic
|
|
33
|
+
during init — `allowed_objects='messages'` (or an explicit list of
|
|
34
|
+
message classes) is the safe choice. Keep `secrets_from_env=False`.
|
|
35
|
+
|
|
36
|
+
The `allowed_objects` parameter controls which classes can be deserialized:
|
|
37
|
+
|
|
38
|
+
- **Explicit list of classes** (recommended for untrusted input): only those
|
|
39
|
+
specific classes are allowed.
|
|
40
|
+
- **`'messages'`**: chat-message classes only (e.g. `AIMessage`,
|
|
41
|
+
`HumanMessage`). Safe for untrusted input.
|
|
42
|
+
- **`'core'` (current default)** — *unsafe with untrusted manifests.*
|
|
43
|
+
Classes defined in the serialization mappings under `coco_core`
|
|
44
|
+
(messages, documents, prompts, etc.).
|
|
45
|
+
- **`'all'`** — *unsafe with untrusted manifests.* Every class in the
|
|
46
|
+
serialization mappings, including partner chat models and LLMs and their
|
|
47
|
+
constructor kwargs (endpoint URLs, headers, model names, etc.).
|
|
48
|
+
|
|
49
|
+
!!! note "Side effects in allowed classes"
|
|
50
|
+
|
|
51
|
+
Deserialization calls `__init__` on allowed classes. If those classes perform
|
|
52
|
+
side effects during initialization (network calls, file operations, etc.),
|
|
53
|
+
those side effects will occur. The allowlist prevents instantiation of
|
|
54
|
+
classes outside the allowlist, but does not sandbox the allowed classes
|
|
55
|
+
themselves or constrain their constructor kwargs.
|
|
56
|
+
|
|
57
|
+
For example, an untrusted manifest could deserialize a chat model whose
|
|
58
|
+
`base_url` (or `endpoint_url`) points at an attacker-controlled host. Any
|
|
59
|
+
request that model makes is then directed there — a Server-Side Request
|
|
60
|
+
Forgery (SSRF) vector. This is *expected behavior*: deserialization
|
|
61
|
+
faithfully reconstructs the configuration carried by the manifest, custom
|
|
62
|
+
endpoints included, and Coco does not special-case or strip such
|
|
63
|
+
kwargs. The mitigation is to **only deserialize manifests you trust**,
|
|
64
|
+
and for untrusted input to restrict `allowed_objects` to `'messages'`
|
|
65
|
+
or an explicit list of classes that take no endpoint configuration.
|
|
66
|
+
|
|
67
|
+
Import paths are also validated against trusted namespaces before any module is
|
|
68
|
+
imported.
|
|
69
|
+
|
|
70
|
+
### Best practices
|
|
71
|
+
|
|
72
|
+
- Use the most restrictive `allowed_objects` possible. For untrusted input,
|
|
73
|
+
pass an explicit list of classes or `'messages'`. `'core'` and `'all'`
|
|
74
|
+
are unsafe with untrusted manifests — only use them when the source
|
|
75
|
+
serves the entire payload, including its configuration.
|
|
76
|
+
- Keep `secrets_from_env` set to `False` (the default). If you must use it,
|
|
77
|
+
ensure the serialized data comes from a fully trusted source, as a crafted
|
|
78
|
+
payload can read arbitrary environment variables.
|
|
79
|
+
- When using `secrets_map`, include only the specific secrets that the
|
|
80
|
+
serialized object requires.
|
|
81
|
+
|
|
82
|
+
### Injection protection (escape-based)
|
|
83
|
+
|
|
84
|
+
During serialization, plain dicts that contain an `'lc'` key are escaped by wrapping
|
|
85
|
+
them: `{"__lc_escaped__": {...}}`. During deserialization, escaped dicts are unwrapped
|
|
86
|
+
and returned as plain dicts, NOT instantiated as LC objects.
|
|
87
|
+
|
|
88
|
+
This is an allowlist approach: only dicts explicitly produced by
|
|
89
|
+
`Serializable.to_json()` (which are NOT escaped) are treated as LC objects;
|
|
90
|
+
everything else is user data.
|
|
91
|
+
|
|
92
|
+
Even if an attacker's payload includes `__lc_escaped__` wrappers, it will be unwrapped
|
|
93
|
+
to plain dicts and NOT instantiated as malicious objects.
|
|
94
|
+
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from coco_core.load import load
|
|
99
|
+
from coco_core.prompts import ChatPromptTemplate
|
|
100
|
+
from coco_core.messages import AIMessage, HumanMessage
|
|
101
|
+
|
|
102
|
+
# Use default allowlist (classes from mappings) - recommended
|
|
103
|
+
obj = load(data)
|
|
104
|
+
|
|
105
|
+
# Allow only specific classes (most restrictive)
|
|
106
|
+
obj = load(
|
|
107
|
+
data,
|
|
108
|
+
allowed_objects=[
|
|
109
|
+
ChatPromptTemplate,
|
|
110
|
+
AIMessage,
|
|
111
|
+
HumanMessage,
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
import importlib
|
|
118
|
+
import json
|
|
119
|
+
import os
|
|
120
|
+
from collections.abc import Callable, Iterable
|
|
121
|
+
from typing import Any, Literal, cast
|
|
122
|
+
|
|
123
|
+
from coco_core._api import beta
|
|
124
|
+
from coco_core._api.deprecation import warn_deprecated
|
|
125
|
+
from coco_core.load._validation import _is_escaped_dict, _unescape_value
|
|
126
|
+
from coco_core.load.mapping import (
|
|
127
|
+
_JS_SERIALIZABLE_MAPPING,
|
|
128
|
+
_OG_SERIALIZABLE_MAPPING,
|
|
129
|
+
OLD_CORE_NAMESPACES_MAPPING,
|
|
130
|
+
SERIALIZABLE_MAPPING,
|
|
131
|
+
)
|
|
132
|
+
from coco_core.load.serializable import Serializable
|
|
133
|
+
|
|
134
|
+
DEFAULT_NAMESPACES = [
|
|
135
|
+
"coco",
|
|
136
|
+
"coco_core",
|
|
137
|
+
"coco_community",
|
|
138
|
+
"coco_anthropic",
|
|
139
|
+
"coco_groq",
|
|
140
|
+
"coco_google_genai",
|
|
141
|
+
"coco_aws",
|
|
142
|
+
"coco_openai",
|
|
143
|
+
"coco_google_vertexai",
|
|
144
|
+
"coco_mistralai",
|
|
145
|
+
"coco_fireworks",
|
|
146
|
+
"coco_xai",
|
|
147
|
+
"coco_sambanova",
|
|
148
|
+
"coco_perplexity",
|
|
149
|
+
]
|
|
150
|
+
# Namespaces for which only deserializing via the SERIALIZABLE_MAPPING is allowed.
|
|
151
|
+
# Load by path is not allowed.
|
|
152
|
+
DISALLOW_LOAD_FROM_PATH = [
|
|
153
|
+
"coco_community",
|
|
154
|
+
"coco",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
ALL_SERIALIZABLE_MAPPINGS = {
|
|
158
|
+
**SERIALIZABLE_MAPPING,
|
|
159
|
+
**OLD_CORE_NAMESPACES_MAPPING,
|
|
160
|
+
**_OG_SERIALIZABLE_MAPPING,
|
|
161
|
+
**_JS_SERIALIZABLE_MAPPING,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Modern message classes admitted by `allowed_objects='messages'`. Legacy types
|
|
165
|
+
# (BaseMessage / BaseMessageChunk, ChatMessage / ChatMessageChunk, FunctionMessage /
|
|
166
|
+
# FunctionMessageChunk) are intentionally excluded — `BaseMessage` is abstract and
|
|
167
|
+
# the chat/function variants are superseded by `ToolMessage` and tool calling.
|
|
168
|
+
_MESSAGES_ALLOWED_CLASS_NAMES = frozenset(
|
|
169
|
+
{
|
|
170
|
+
"AIMessage",
|
|
171
|
+
"AIMessageChunk",
|
|
172
|
+
"HumanMessage",
|
|
173
|
+
"HumanMessageChunk",
|
|
174
|
+
"SystemMessage",
|
|
175
|
+
"SystemMessageChunk",
|
|
176
|
+
"ToolMessage",
|
|
177
|
+
"ToolMessageChunk",
|
|
178
|
+
"RemoveMessage",
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Cache for the default allowed class paths computed from mappings
|
|
183
|
+
# Maps mode ("all", "core", or "messages") to the cached set of paths
|
|
184
|
+
_default_class_paths_cache: dict[str, set[tuple[str, ...]]] = {}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _get_default_allowed_class_paths(
|
|
188
|
+
allowed_object_mode: Literal["all", "core", "messages"],
|
|
189
|
+
) -> set[tuple[str, ...]]:
|
|
190
|
+
"""Get the default allowed class paths from the serialization mappings.
|
|
191
|
+
|
|
192
|
+
This uses the mappings as the source of truth for what classes are allowed
|
|
193
|
+
by default. Both the legacy paths (keys) and current paths (values) are included.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
allowed_object_mode: either `'all'`, `'core'`, or `'messages'`.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Set of class path tuples that are allowed by default.
|
|
200
|
+
"""
|
|
201
|
+
if allowed_object_mode in _default_class_paths_cache:
|
|
202
|
+
return _default_class_paths_cache[allowed_object_mode]
|
|
203
|
+
|
|
204
|
+
allowed_paths: set[tuple[str, ...]] = set()
|
|
205
|
+
for key, value in ALL_SERIALIZABLE_MAPPINGS.items():
|
|
206
|
+
if allowed_object_mode == "core" and value[0] != "coco_core":
|
|
207
|
+
continue
|
|
208
|
+
if allowed_object_mode == "messages" and (
|
|
209
|
+
value[0] != "coco_core"
|
|
210
|
+
or value[-1] not in _MESSAGES_ALLOWED_CLASS_NAMES
|
|
211
|
+
):
|
|
212
|
+
continue
|
|
213
|
+
allowed_paths.add(key)
|
|
214
|
+
allowed_paths.add(value)
|
|
215
|
+
|
|
216
|
+
_default_class_paths_cache[allowed_object_mode] = allowed_paths
|
|
217
|
+
return _default_class_paths_cache[allowed_object_mode]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _block_jinja2_templates(
|
|
221
|
+
class_path: tuple[str, ...],
|
|
222
|
+
kwargs: dict[str, Any],
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Block jinja2 templates during deserialization for security.
|
|
225
|
+
|
|
226
|
+
Jinja2 templates can execute arbitrary code, so they are blocked by default when
|
|
227
|
+
deserializing objects with `template_format='jinja2'`.
|
|
228
|
+
|
|
229
|
+
Note:
|
|
230
|
+
We intentionally do NOT check the `class_path` here to keep this simple and
|
|
231
|
+
future-proof. If any new class is added that accepts `template_format='jinja2'`,
|
|
232
|
+
it will be automatically blocked without needing to update this function.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
class_path: The class path tuple being deserialized (unused).
|
|
236
|
+
kwargs: The kwargs dict for the class constructor.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If `template_format` is `'jinja2'`.
|
|
240
|
+
"""
|
|
241
|
+
_ = class_path # Unused - see docstring for rationale. Kept to satisfy signature.
|
|
242
|
+
if kwargs.get("template_format") == "jinja2":
|
|
243
|
+
msg = (
|
|
244
|
+
"Jinja2 templates are not allowed during deserialization for security "
|
|
245
|
+
"reasons. Use 'f-string' template format instead, or explicitly allow "
|
|
246
|
+
"jinja2 by providing a custom init_validator."
|
|
247
|
+
)
|
|
248
|
+
raise ValueError(msg)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def default_init_validator(
|
|
252
|
+
class_path: tuple[str, ...],
|
|
253
|
+
kwargs: dict[str, Any],
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Default init validator that blocks jinja2 templates.
|
|
256
|
+
|
|
257
|
+
This is the default validator used by `load()` and `loads()` when no custom
|
|
258
|
+
validator is provided.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
class_path: The class path tuple being deserialized.
|
|
262
|
+
kwargs: The kwargs dict for the class constructor.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
ValueError: If template_format is `'jinja2'`.
|
|
266
|
+
"""
|
|
267
|
+
_block_jinja2_templates(class_path, kwargs)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
AllowedObject = type[Serializable]
|
|
271
|
+
"""Type alias for classes that can be included in the `allowed_objects` parameter.
|
|
272
|
+
|
|
273
|
+
Must be a `Serializable` subclass (the class itself, not an instance).
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
InitValidator = Callable[[tuple[str, ...], dict[str, Any]], None]
|
|
277
|
+
"""Type alias for a callable that validates kwargs during deserialization.
|
|
278
|
+
|
|
279
|
+
The callable receives:
|
|
280
|
+
|
|
281
|
+
- `class_path`: A tuple of strings identifying the class being instantiated
|
|
282
|
+
(e.g., `('coco', 'schema', 'messages', 'AIMessage')`).
|
|
283
|
+
- `kwargs`: The kwargs dict that will be passed to the constructor.
|
|
284
|
+
|
|
285
|
+
The validator should raise an exception if the object should not be deserialized.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _compute_allowed_class_paths(
|
|
290
|
+
allowed_objects: Iterable[AllowedObject],
|
|
291
|
+
import_mappings: dict[tuple[str, ...], tuple[str, ...]],
|
|
292
|
+
) -> set[tuple[str, ...]]:
|
|
293
|
+
"""Return allowed class paths from an explicit list of classes.
|
|
294
|
+
|
|
295
|
+
A class path is a tuple of strings identifying a serializable class, derived from
|
|
296
|
+
`Serializable.lc_id()`. For example: `('coco_core', 'messages', 'AIMessage')`.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
allowed_objects: Iterable of `Serializable` subclasses to allow.
|
|
300
|
+
import_mappings: Mapping of legacy class paths to current class paths.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Set of allowed class paths.
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
```python
|
|
307
|
+
# Allow a specific class
|
|
308
|
+
_compute_allowed_class_paths([MyPrompt], {}) ->
|
|
309
|
+
{("coco_core", "prompts", "MyPrompt")}
|
|
310
|
+
|
|
311
|
+
# Include legacy paths that map to the same class
|
|
312
|
+
import_mappings = {("old", "Prompt"): ("coco_core", "prompts", "MyPrompt")}
|
|
313
|
+
_compute_allowed_class_paths([MyPrompt], import_mappings) ->
|
|
314
|
+
{("coco_core", "prompts", "MyPrompt"), ("old", "Prompt")}
|
|
315
|
+
```
|
|
316
|
+
"""
|
|
317
|
+
allowed_objects_list = list(allowed_objects)
|
|
318
|
+
|
|
319
|
+
allowed_class_paths: set[tuple[str, ...]] = set()
|
|
320
|
+
for allowed_obj in allowed_objects_list:
|
|
321
|
+
if not isinstance(allowed_obj, type) or not issubclass(
|
|
322
|
+
allowed_obj, Serializable
|
|
323
|
+
):
|
|
324
|
+
msg = "allowed_objects must contain Serializable subclasses." # type: ignore[unreachable]
|
|
325
|
+
raise TypeError(msg)
|
|
326
|
+
|
|
327
|
+
class_path = tuple(allowed_obj.lc_id())
|
|
328
|
+
allowed_class_paths.add(class_path)
|
|
329
|
+
# Add legacy paths that map to the same class.
|
|
330
|
+
for mapping_key, mapping_value in import_mappings.items():
|
|
331
|
+
if tuple(mapping_value) == class_path:
|
|
332
|
+
allowed_class_paths.add(mapping_key)
|
|
333
|
+
return allowed_class_paths
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class Reviver:
|
|
337
|
+
"""Reviver for JSON objects.
|
|
338
|
+
|
|
339
|
+
Used as the `object_hook` for `json.loads` to reconstruct Coco objects from
|
|
340
|
+
their serialized JSON representation.
|
|
341
|
+
|
|
342
|
+
Only classes in the allowlist can be instantiated.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
def __init__(
|
|
346
|
+
self,
|
|
347
|
+
allowed_objects: Iterable[AllowedObject]
|
|
348
|
+
| Literal["all", "core", "messages"]
|
|
349
|
+
| None = None,
|
|
350
|
+
secrets_map: dict[str, str] | None = None,
|
|
351
|
+
valid_namespaces: list[str] | None = None,
|
|
352
|
+
secrets_from_env: bool = False, # noqa: FBT001,FBT002
|
|
353
|
+
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]]
|
|
354
|
+
| None = None,
|
|
355
|
+
*,
|
|
356
|
+
ignore_unserializable_fields: bool = False,
|
|
357
|
+
init_validator: InitValidator | None = default_init_validator,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Initialize the reviver.
|
|
360
|
+
|
|
361
|
+
See the module docstring for the threat model around `load()`/`loads()`:
|
|
362
|
+
a serialized payload may carry constructor configuration that affects
|
|
363
|
+
runtime behavior (custom `base_url`, headers, model name, etc.). Do not
|
|
364
|
+
use `'core'` or `'all'` with untrusted manifests.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
allowed_objects: Allowlist of classes that can be deserialized.
|
|
368
|
+
- Explicit list of classes (recommended for untrusted input):
|
|
369
|
+
only those specific classes are allowed.
|
|
370
|
+
- `'messages'`: chat-message classes only (e.g. `AIMessage`,
|
|
371
|
+
`HumanMessage`). Safe for untrusted input.
|
|
372
|
+
- `'core'` (current default): unsafe with untrusted manifests.
|
|
373
|
+
Classes defined in the serialization mappings under
|
|
374
|
+
`coco_core`.
|
|
375
|
+
- `'all'`: unsafe with untrusted manifests. Every class in the
|
|
376
|
+
serialization mappings, including partner chat models and
|
|
377
|
+
LLMs and their constructor kwargs. See
|
|
378
|
+
`coco_core.load.mapping` for the full list.
|
|
379
|
+
secrets_map: A map of secrets to load.
|
|
380
|
+
|
|
381
|
+
Only include the specific secrets the serialized object
|
|
382
|
+
requires. If a secret is not found in the map, it will be loaded
|
|
383
|
+
from the environment if `secrets_from_env` is `True`.
|
|
384
|
+
valid_namespaces: Additional namespaces (modules) to allow during
|
|
385
|
+
deserialization, beyond the default trusted namespaces.
|
|
386
|
+
secrets_from_env: Whether to load secrets from the environment.
|
|
387
|
+
|
|
388
|
+
A crafted payload can name arbitrary environment variables in
|
|
389
|
+
its `secret` fields, so enabling this on untrusted data can leak
|
|
390
|
+
sensitive values. Keep this `False` (the default) unless the
|
|
391
|
+
serialized data is fully trusted.
|
|
392
|
+
additional_import_mappings: A dictionary of additional namespace mappings.
|
|
393
|
+
|
|
394
|
+
You can use this to override default mappings or add new mappings.
|
|
395
|
+
|
|
396
|
+
When `allowed_objects` is `None` (using defaults), paths from these
|
|
397
|
+
mappings are also added to the allowed class paths.
|
|
398
|
+
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
|
399
|
+
init_validator: Optional callable to validate kwargs before instantiation.
|
|
400
|
+
|
|
401
|
+
If provided, this function is called with `(class_path, kwargs)` where
|
|
402
|
+
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
|
403
|
+
The validator should raise an exception if the object should not be
|
|
404
|
+
deserialized, otherwise return `None`.
|
|
405
|
+
|
|
406
|
+
Defaults to `default_init_validator` which blocks jinja2 templates.
|
|
407
|
+
"""
|
|
408
|
+
if allowed_objects is None:
|
|
409
|
+
warn_deprecated(
|
|
410
|
+
since="1.3.3",
|
|
411
|
+
message=(
|
|
412
|
+
"The default value of `allowed_objects` will change in a future "
|
|
413
|
+
"version. Pass an explicit value (e.g., "
|
|
414
|
+
"allowed_objects='messages' or allowed_objects='core') to suppress "
|
|
415
|
+
"this warning."
|
|
416
|
+
),
|
|
417
|
+
pending=True,
|
|
418
|
+
)
|
|
419
|
+
allowed_objects = "core"
|
|
420
|
+
|
|
421
|
+
self.secrets_from_env = secrets_from_env
|
|
422
|
+
self.secrets_map = secrets_map or {}
|
|
423
|
+
# By default, only support coco, but user can pass in additional namespaces
|
|
424
|
+
self.valid_namespaces = (
|
|
425
|
+
[*DEFAULT_NAMESPACES, *valid_namespaces]
|
|
426
|
+
if valid_namespaces
|
|
427
|
+
else DEFAULT_NAMESPACES
|
|
428
|
+
)
|
|
429
|
+
self.additional_import_mappings = additional_import_mappings or {}
|
|
430
|
+
self.import_mappings = (
|
|
431
|
+
{
|
|
432
|
+
**ALL_SERIALIZABLE_MAPPINGS,
|
|
433
|
+
**self.additional_import_mappings,
|
|
434
|
+
}
|
|
435
|
+
if self.additional_import_mappings
|
|
436
|
+
else ALL_SERIALIZABLE_MAPPINGS
|
|
437
|
+
)
|
|
438
|
+
# Compute allowed class paths:
|
|
439
|
+
# - "all" -> use default paths from mappings (+ additional_import_mappings)
|
|
440
|
+
# - Explicit list -> compute from those classes
|
|
441
|
+
if allowed_objects in ("all", "core", "messages"):
|
|
442
|
+
self.allowed_class_paths: set[tuple[str, ...]] | None = (
|
|
443
|
+
_get_default_allowed_class_paths(allowed_objects).copy()
|
|
444
|
+
)
|
|
445
|
+
# Add paths from additional_import_mappings to the defaults
|
|
446
|
+
if self.additional_import_mappings:
|
|
447
|
+
for key, value in self.additional_import_mappings.items():
|
|
448
|
+
self.allowed_class_paths.add(key)
|
|
449
|
+
self.allowed_class_paths.add(value)
|
|
450
|
+
else:
|
|
451
|
+
self.allowed_class_paths = _compute_allowed_class_paths(
|
|
452
|
+
cast("Iterable[AllowedObject]", allowed_objects), self.import_mappings
|
|
453
|
+
)
|
|
454
|
+
self.ignore_unserializable_fields = ignore_unserializable_fields
|
|
455
|
+
self.init_validator = init_validator
|
|
456
|
+
|
|
457
|
+
def __call__(self, value: dict[str, Any]) -> Any:
|
|
458
|
+
"""Revive the value.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
value: The value to revive.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
The revived value.
|
|
465
|
+
|
|
466
|
+
Raises:
|
|
467
|
+
ValueError: If the namespace is invalid.
|
|
468
|
+
ValueError: If trying to deserialize something that cannot
|
|
469
|
+
be deserialized in the current version of trustcoco-core.
|
|
470
|
+
NotImplementedError: If the object is not implemented and
|
|
471
|
+
`ignore_unserializable_fields` is False.
|
|
472
|
+
"""
|
|
473
|
+
if (
|
|
474
|
+
value.get("lc") == 1
|
|
475
|
+
and value.get("type") == "secret"
|
|
476
|
+
and value.get("id") is not None
|
|
477
|
+
):
|
|
478
|
+
[key] = value["id"]
|
|
479
|
+
if key in self.secrets_map:
|
|
480
|
+
return self.secrets_map[key]
|
|
481
|
+
if self.secrets_from_env and key in os.environ and os.environ[key]:
|
|
482
|
+
return os.environ[key]
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
value.get("lc") == 1
|
|
487
|
+
and value.get("type") == "not_implemented"
|
|
488
|
+
and value.get("id") is not None
|
|
489
|
+
):
|
|
490
|
+
if self.ignore_unserializable_fields:
|
|
491
|
+
return None
|
|
492
|
+
msg = (
|
|
493
|
+
"Trying to load an object that doesn't implement "
|
|
494
|
+
f"serialization: {value}"
|
|
495
|
+
)
|
|
496
|
+
raise NotImplementedError(msg)
|
|
497
|
+
|
|
498
|
+
if (
|
|
499
|
+
value.get("lc") == 1
|
|
500
|
+
and value.get("type") == "constructor"
|
|
501
|
+
and value.get("id") is not None
|
|
502
|
+
):
|
|
503
|
+
[*namespace, name] = value["id"]
|
|
504
|
+
mapping_key = tuple(value["id"])
|
|
505
|
+
|
|
506
|
+
if (
|
|
507
|
+
self.allowed_class_paths is not None
|
|
508
|
+
and mapping_key not in self.allowed_class_paths
|
|
509
|
+
):
|
|
510
|
+
msg = (
|
|
511
|
+
f"Deserialization of {mapping_key!r} is not allowed. "
|
|
512
|
+
"The default (allowed_objects='core') only permits core "
|
|
513
|
+
"trustcoco-core classes. To allow trusted partner integrations, "
|
|
514
|
+
"use allowed_objects='all'. Alternatively, pass an explicit list "
|
|
515
|
+
"of allowed classes via allowed_objects=[...]. "
|
|
516
|
+
"See coco_core.load.mapping for the full allowlist."
|
|
517
|
+
)
|
|
518
|
+
raise ValueError(msg)
|
|
519
|
+
|
|
520
|
+
if (
|
|
521
|
+
namespace[0] not in self.valid_namespaces
|
|
522
|
+
# The root namespace ["coco"] is not a valid identifier.
|
|
523
|
+
or namespace == ["coco"]
|
|
524
|
+
):
|
|
525
|
+
msg = f"Invalid namespace: {value}"
|
|
526
|
+
raise ValueError(msg)
|
|
527
|
+
# Determine explicit import path
|
|
528
|
+
if mapping_key in self.import_mappings:
|
|
529
|
+
import_path = self.import_mappings[mapping_key]
|
|
530
|
+
# Split into module and name
|
|
531
|
+
import_dir, name = import_path[:-1], import_path[-1]
|
|
532
|
+
elif namespace[0] in DISALLOW_LOAD_FROM_PATH:
|
|
533
|
+
msg = (
|
|
534
|
+
"Trying to deserialize something that cannot "
|
|
535
|
+
"be deserialized in current version of trustcoco-core: "
|
|
536
|
+
f"{mapping_key}."
|
|
537
|
+
)
|
|
538
|
+
raise ValueError(msg)
|
|
539
|
+
else:
|
|
540
|
+
# Otherwise, treat namespace as path.
|
|
541
|
+
import_dir = namespace
|
|
542
|
+
|
|
543
|
+
# Validate import path is in trusted namespaces before importing
|
|
544
|
+
if import_dir[0] not in self.valid_namespaces:
|
|
545
|
+
msg = f"Invalid namespace: {value}"
|
|
546
|
+
raise ValueError(msg)
|
|
547
|
+
|
|
548
|
+
# We don't need to recurse on kwargs
|
|
549
|
+
# as json.loads will do that for us.
|
|
550
|
+
kwargs = value.get("kwargs", {})
|
|
551
|
+
|
|
552
|
+
# Run the init_validator (e.g., jinja2 blocking) before importing
|
|
553
|
+
# to fail fast on security violations.
|
|
554
|
+
if self.init_validator is not None:
|
|
555
|
+
self.init_validator(mapping_key, kwargs)
|
|
556
|
+
|
|
557
|
+
mod = importlib.import_module(".".join(import_dir))
|
|
558
|
+
|
|
559
|
+
cls = getattr(mod, name)
|
|
560
|
+
|
|
561
|
+
# The class must be a subclass of Serializable.
|
|
562
|
+
if not issubclass(cls, Serializable):
|
|
563
|
+
msg = f"Invalid namespace: {value}"
|
|
564
|
+
raise ValueError(msg)
|
|
565
|
+
|
|
566
|
+
return cls(**kwargs)
|
|
567
|
+
|
|
568
|
+
return value
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@beta()
|
|
572
|
+
def loads(
|
|
573
|
+
text: str,
|
|
574
|
+
*,
|
|
575
|
+
allowed_objects: Iterable[AllowedObject]
|
|
576
|
+
| Literal["all", "core", "messages"]
|
|
577
|
+
| None = None,
|
|
578
|
+
secrets_map: dict[str, str] | None = None,
|
|
579
|
+
valid_namespaces: list[str] | None = None,
|
|
580
|
+
secrets_from_env: bool = False,
|
|
581
|
+
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
|
|
582
|
+
ignore_unserializable_fields: bool = False,
|
|
583
|
+
init_validator: InitValidator | None = default_init_validator,
|
|
584
|
+
) -> Any:
|
|
585
|
+
"""Revive a Coco class from a JSON string.
|
|
586
|
+
|
|
587
|
+
Equivalent to `load(json.loads(text))`.
|
|
588
|
+
|
|
589
|
+
Only classes in the allowlist can be instantiated. The default allowlist
|
|
590
|
+
includes core Coco types (messages, prompts, documents, etc.). See
|
|
591
|
+
`coco_core.load.mapping` for the full list.
|
|
592
|
+
|
|
593
|
+
!!! warning "Do not use with untrusted input"
|
|
594
|
+
|
|
595
|
+
A serialized payload may carry constructor kwargs that affect runtime
|
|
596
|
+
behavior (custom `base_url`, headers, model name, etc.), so it should be
|
|
597
|
+
treated as executable configuration rather than plain text. For example,
|
|
598
|
+
deserializing a model whose `base_url` points at an attacker-controlled
|
|
599
|
+
host can result in Server-Side Request Forgery (SSRF); this is expected
|
|
600
|
+
behavior, since `loads()` faithfully reconstructs the configuration in
|
|
601
|
+
the manifest. If the source is untrusted, avoid calling `loads()` on it;
|
|
602
|
+
if you must, pass `allowed_objects='messages'` or an explicit list of
|
|
603
|
+
message classes. See the module-level threat model for details.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
text: The string to load.
|
|
607
|
+
allowed_objects: Allowlist of classes that can be deserialized.
|
|
608
|
+
|
|
609
|
+
- Explicit list of classes (recommended for untrusted input): only
|
|
610
|
+
those specific classes are allowed.
|
|
611
|
+
- `'messages'`: chat-message classes only. Safe for untrusted input.
|
|
612
|
+
- `'core'` (current default): unsafe with untrusted manifests.
|
|
613
|
+
Classes defined in the serialization mappings under
|
|
614
|
+
`coco_core`.
|
|
615
|
+
- `'all'`: unsafe with untrusted manifests. Every class in the
|
|
616
|
+
serialization mappings, including partner chat models and LLMs
|
|
617
|
+
and their constructor kwargs. See `coco_core.load.mapping`
|
|
618
|
+
for the full list.
|
|
619
|
+
- `[]`: Disallow all deserialization (will raise on any object).
|
|
620
|
+
secrets_map: A map of secrets to load.
|
|
621
|
+
|
|
622
|
+
Only include the specific secrets the serialized object requires. If
|
|
623
|
+
a secret is not found in the map, it will be loaded from the
|
|
624
|
+
environment if `secrets_from_env` is `True`.
|
|
625
|
+
valid_namespaces: Additional namespaces (modules) to allow during
|
|
626
|
+
deserialization, beyond the default trusted namespaces.
|
|
627
|
+
secrets_from_env: Whether to load secrets from the environment.
|
|
628
|
+
|
|
629
|
+
A crafted payload can name arbitrary environment variables in its
|
|
630
|
+
`secret` fields, so enabling this on untrusted data can leak
|
|
631
|
+
sensitive values. Keep this `False` (the default) unless the
|
|
632
|
+
serialized data is fully trusted.
|
|
633
|
+
additional_import_mappings: A dictionary of additional namespace mappings.
|
|
634
|
+
|
|
635
|
+
You can use this to override default mappings or add new mappings.
|
|
636
|
+
|
|
637
|
+
When `allowed_objects` is `None` (using defaults), paths from these
|
|
638
|
+
mappings are also added to the allowed class paths.
|
|
639
|
+
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
|
640
|
+
init_validator: Optional callable to validate kwargs before instantiation.
|
|
641
|
+
|
|
642
|
+
If provided, this function is called with `(class_path, kwargs)` where
|
|
643
|
+
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
|
644
|
+
The validator should raise an exception if the object should not be
|
|
645
|
+
deserialized, otherwise return `None`.
|
|
646
|
+
|
|
647
|
+
Defaults to `default_init_validator` which blocks jinja2 templates.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Revived Coco objects.
|
|
651
|
+
|
|
652
|
+
Raises:
|
|
653
|
+
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
|
|
654
|
+
"""
|
|
655
|
+
if allowed_objects is None:
|
|
656
|
+
warn_deprecated(
|
|
657
|
+
since="1.3.3",
|
|
658
|
+
message=(
|
|
659
|
+
"The default value of `allowed_objects` will change in a future "
|
|
660
|
+
"version. Pass an explicit list of allowed classes (or "
|
|
661
|
+
"'messages' for untrusted input that contains only chat "
|
|
662
|
+
"messages) to suppress this warning."
|
|
663
|
+
),
|
|
664
|
+
pending=True,
|
|
665
|
+
)
|
|
666
|
+
allowed_objects = "core"
|
|
667
|
+
|
|
668
|
+
# Parse JSON and delegate to load() for proper escape handling
|
|
669
|
+
raw_obj = json.loads(text)
|
|
670
|
+
return load(
|
|
671
|
+
raw_obj,
|
|
672
|
+
allowed_objects=allowed_objects,
|
|
673
|
+
secrets_map=secrets_map,
|
|
674
|
+
valid_namespaces=valid_namespaces,
|
|
675
|
+
secrets_from_env=secrets_from_env,
|
|
676
|
+
additional_import_mappings=additional_import_mappings,
|
|
677
|
+
ignore_unserializable_fields=ignore_unserializable_fields,
|
|
678
|
+
init_validator=init_validator,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@beta()
|
|
683
|
+
def load(
|
|
684
|
+
obj: Any,
|
|
685
|
+
*,
|
|
686
|
+
allowed_objects: Iterable[AllowedObject]
|
|
687
|
+
| Literal["all", "core", "messages"]
|
|
688
|
+
| None = None,
|
|
689
|
+
secrets_map: dict[str, str] | None = None,
|
|
690
|
+
valid_namespaces: list[str] | None = None,
|
|
691
|
+
secrets_from_env: bool = False,
|
|
692
|
+
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
|
|
693
|
+
ignore_unserializable_fields: bool = False,
|
|
694
|
+
init_validator: InitValidator | None = default_init_validator,
|
|
695
|
+
) -> Any:
|
|
696
|
+
"""Revive a Coco class from a JSON object.
|
|
697
|
+
|
|
698
|
+
Use this if you already have a parsed JSON object, eg. from `json.load` or
|
|
699
|
+
`orjson.loads`.
|
|
700
|
+
|
|
701
|
+
Only classes in the allowlist can be instantiated. The default allowlist
|
|
702
|
+
includes core Coco types (messages, prompts, documents, etc.). See
|
|
703
|
+
`coco_core.load.mapping` for the full list.
|
|
704
|
+
|
|
705
|
+
!!! warning "Do not use with untrusted input"
|
|
706
|
+
|
|
707
|
+
A serialized payload may carry constructor kwargs that affect runtime
|
|
708
|
+
behavior (custom `base_url`, headers, model name, etc.), so it should be
|
|
709
|
+
treated as executable configuration rather than plain text. For example,
|
|
710
|
+
deserializing a model whose `base_url` points at an attacker-controlled
|
|
711
|
+
host can result in Server-Side Request Forgery (SSRF); this is expected
|
|
712
|
+
behavior, since `load()` faithfully reconstructs the configuration in
|
|
713
|
+
the manifest. If the source is untrusted, avoid calling `load()` on it;
|
|
714
|
+
if you must, pass `allowed_objects='messages'` or an explicit list of
|
|
715
|
+
message classes. See the module-level threat model for details.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
obj: The object to load.
|
|
719
|
+
allowed_objects: Allowlist of classes that can be deserialized.
|
|
720
|
+
|
|
721
|
+
- Explicit list of classes (recommended for untrusted input): only
|
|
722
|
+
those specific classes are allowed.
|
|
723
|
+
- `'messages'`: chat-message classes only. Safe for untrusted input.
|
|
724
|
+
- `'core'` (current default): unsafe with untrusted manifests.
|
|
725
|
+
Classes defined in the serialization mappings under
|
|
726
|
+
`coco_core`.
|
|
727
|
+
- `'all'`: unsafe with untrusted manifests. Every class in the
|
|
728
|
+
serialization mappings, including partner chat models and LLMs
|
|
729
|
+
and their constructor kwargs. See `coco_core.load.mapping`
|
|
730
|
+
for the full list.
|
|
731
|
+
- `[]`: Disallow all deserialization (will raise on any object).
|
|
732
|
+
secrets_map: A map of secrets to load.
|
|
733
|
+
|
|
734
|
+
Only include the specific secrets the serialized object requires.
|
|
735
|
+
|
|
736
|
+
If a secret is not found in the map, it will be loaded from the environment
|
|
737
|
+
if `secrets_from_env` is `True`.
|
|
738
|
+
valid_namespaces: Additional namespaces (modules) to allow during
|
|
739
|
+
deserialization, beyond the default trusted namespaces.
|
|
740
|
+
secrets_from_env: Whether to load secrets from the environment.
|
|
741
|
+
|
|
742
|
+
A crafted payload can name arbitrary environment variables in its
|
|
743
|
+
`secret` fields, so enabling this on untrusted data can leak
|
|
744
|
+
sensitive values. Keep this `False` (the default) unless the
|
|
745
|
+
serialized data is fully trusted.
|
|
746
|
+
additional_import_mappings: A dictionary of additional namespace mappings.
|
|
747
|
+
|
|
748
|
+
You can use this to override default mappings or add new mappings.
|
|
749
|
+
|
|
750
|
+
When `allowed_objects` is `None` (using defaults), paths from these
|
|
751
|
+
mappings are also added to the allowed class paths.
|
|
752
|
+
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
|
753
|
+
init_validator: Optional callable to validate kwargs before instantiation.
|
|
754
|
+
|
|
755
|
+
If provided, this function is called with `(class_path, kwargs)` where
|
|
756
|
+
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
|
757
|
+
The validator should raise an exception if the object should not be
|
|
758
|
+
deserialized, otherwise return `None`.
|
|
759
|
+
|
|
760
|
+
Defaults to `default_init_validator` which blocks jinja2 templates.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Revived Coco objects.
|
|
764
|
+
|
|
765
|
+
Raises:
|
|
766
|
+
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
|
|
767
|
+
|
|
768
|
+
Example:
|
|
769
|
+
```python
|
|
770
|
+
from coco_core.load import load, dumpd
|
|
771
|
+
from coco_core.messages import AIMessage
|
|
772
|
+
|
|
773
|
+
msg = AIMessage(content="Hello")
|
|
774
|
+
data = dumpd(msg)
|
|
775
|
+
|
|
776
|
+
# Deserialize using default allowlist
|
|
777
|
+
loaded = load(data)
|
|
778
|
+
|
|
779
|
+
# Or with explicit allowlist
|
|
780
|
+
loaded = load(data, allowed_objects=[AIMessage])
|
|
781
|
+
|
|
782
|
+
# Or extend defaults with additional mappings
|
|
783
|
+
loaded = load(
|
|
784
|
+
data,
|
|
785
|
+
additional_import_mappings={
|
|
786
|
+
("my_pkg", "MyClass"): ("my_pkg", "module", "MyClass"),
|
|
787
|
+
},
|
|
788
|
+
)
|
|
789
|
+
```
|
|
790
|
+
"""
|
|
791
|
+
if allowed_objects is None:
|
|
792
|
+
warn_deprecated(
|
|
793
|
+
since="1.3.3",
|
|
794
|
+
message=(
|
|
795
|
+
"The default value of `allowed_objects` will change in a future "
|
|
796
|
+
"version. Pass an explicit list of allowed classes (or "
|
|
797
|
+
"'messages' for untrusted input that contains only chat "
|
|
798
|
+
"messages) to suppress this warning."
|
|
799
|
+
),
|
|
800
|
+
pending=True,
|
|
801
|
+
)
|
|
802
|
+
allowed_objects = "core"
|
|
803
|
+
|
|
804
|
+
reviver = Reviver(
|
|
805
|
+
allowed_objects,
|
|
806
|
+
secrets_map,
|
|
807
|
+
valid_namespaces,
|
|
808
|
+
secrets_from_env,
|
|
809
|
+
additional_import_mappings,
|
|
810
|
+
ignore_unserializable_fields=ignore_unserializable_fields,
|
|
811
|
+
init_validator=init_validator,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
def _load(obj: Any) -> Any:
|
|
815
|
+
if isinstance(obj, dict):
|
|
816
|
+
# Check for escaped dict FIRST (before recursing).
|
|
817
|
+
# Escaped dicts are user data that should NOT be processed as LC objects.
|
|
818
|
+
if _is_escaped_dict(obj):
|
|
819
|
+
return _unescape_value(obj)
|
|
820
|
+
|
|
821
|
+
# Not escaped - recurse into children then apply reviver
|
|
822
|
+
loaded_obj = {k: _load(v) for k, v in obj.items()}
|
|
823
|
+
return reviver(loaded_obj)
|
|
824
|
+
if isinstance(obj, list):
|
|
825
|
+
return [_load(o) for o in obj]
|
|
826
|
+
return obj
|
|
827
|
+
|
|
828
|
+
return _load(obj)
|