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.
Files changed (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,36 @@
1
+ """SSRF protection and security utilities.
2
+
3
+ This is an **internal** module (note the `_security` prefix). It is NOT part of
4
+ the public `trustcoco-core` API and may change or be removed at any time without
5
+ notice. External code should not import from or depend on anything in this
6
+ module. Any vulnerability reports should target the public APIs that use these
7
+ utilities, not this internal module directly.
8
+ """
9
+
10
+ from coco_core._security._exceptions import SSRFBlockedError
11
+ from coco_core._security._policy import (
12
+ SSRFPolicy,
13
+ validate_hostname,
14
+ validate_resolved_ip,
15
+ validate_url,
16
+ validate_url_sync,
17
+ )
18
+ from coco_core._security._transport import (
19
+ SSRFSafeSyncTransport,
20
+ SSRFSafeTransport,
21
+ ssrf_safe_async_client,
22
+ ssrf_safe_client,
23
+ )
24
+
25
+ __all__ = [
26
+ "SSRFBlockedError",
27
+ "SSRFPolicy",
28
+ "SSRFSafeSyncTransport",
29
+ "SSRFSafeTransport",
30
+ "ssrf_safe_async_client",
31
+ "ssrf_safe_client",
32
+ "validate_hostname",
33
+ "validate_resolved_ip",
34
+ "validate_url",
35
+ "validate_url_sync",
36
+ ]
@@ -0,0 +1,9 @@
1
+ """SSRF protection exceptions."""
2
+
3
+
4
+ class SSRFBlockedError(Exception):
5
+ """Raised when a request is blocked by SSRF protection policy."""
6
+
7
+ def __init__(self, reason: str) -> None:
8
+ self.reason = reason
9
+ super().__init__(f"SSRF blocked: {reason}")
@@ -0,0 +1,313 @@
1
+ """SSRF protection policy with IP validation and DNS-aware URL checking."""
2
+
3
+ import asyncio
4
+ import dataclasses
5
+ import ipaddress
6
+ import os
7
+ import socket
8
+ import urllib.parse
9
+
10
+ from coco_core._security._exceptions import SSRFBlockedError
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Blocklist constants
14
+ # ---------------------------------------------------------------------------
15
+
16
+ _BLOCKED_IPV4_NETWORKS: tuple[ipaddress.IPv4Network, ...] = tuple(
17
+ ipaddress.IPv4Network(n)
18
+ for n in (
19
+ "10.0.0.0/8", # RFC 1918 - private class A
20
+ "172.16.0.0/12", # RFC 1918 - private class B
21
+ "192.168.0.0/16", # RFC 1918 - private class C
22
+ "127.0.0.0/8", # RFC 1122 - loopback
23
+ "169.254.0.0/16", # RFC 3927 - link-local
24
+ "0.0.0.0/8", # RFC 1122 - "this network"
25
+ "100.64.0.0/10", # RFC 6598 - shared/CGN address space
26
+ "192.0.0.0/24", # RFC 6890 - IETF protocol assignments
27
+ "192.0.2.0/24", # RFC 5737 - TEST-NET-1 (documentation)
28
+ "198.18.0.0/15", # RFC 2544 - benchmarking
29
+ "198.51.100.0/24", # RFC 5737 - TEST-NET-2 (documentation)
30
+ "203.0.113.0/24", # RFC 5737 - TEST-NET-3 (documentation)
31
+ "224.0.0.0/4", # RFC 5771 - multicast
32
+ "240.0.0.0/4", # RFC 1112 - reserved for future use
33
+ "255.255.255.255/32", # RFC 919 - limited broadcast
34
+ )
35
+ )
36
+
37
+ _BLOCKED_IPV6_NETWORKS: tuple[ipaddress.IPv6Network, ...] = tuple(
38
+ ipaddress.IPv6Network(n)
39
+ for n in (
40
+ "::1/128", # RFC 4291 - loopback
41
+ "fc00::/7", # RFC 4193 - unique local addresses (ULA)
42
+ "fe80::/10", # RFC 4291 - link-local
43
+ "ff00::/8", # RFC 4291 - multicast
44
+ "::ffff:0:0/96", # RFC 4291 - IPv4-mapped IPv6 addresses
45
+ "::0.0.0.0/96", # RFC 4291 - IPv4-compatible IPv6 (deprecated)
46
+ "64:ff9b::/96", # RFC 6052 - NAT64 well-known prefix
47
+ "64:ff9b:1::/48", # RFC 8215 - NAT64 discovery prefix
48
+ )
49
+ )
50
+
51
+ _CLOUD_METADATA_IPS: frozenset[str] = frozenset(
52
+ {
53
+ "169.254.169.254", # AWS, GCP, Azure, DigitalOcean, Oracle Cloud
54
+ "169.254.170.2", # AWS ECS task metadata
55
+ "169.254.170.23", # AWS EKS Pod Identity Agent
56
+ "100.100.100.200", # Alibaba Cloud metadata
57
+ "fd00:ec2::254", # AWS EC2 IMDSv2 over IPv6 (Nitro instances)
58
+ "fd00:ec2::23", # AWS EKS Pod Identity Agent (IPv6)
59
+ "fe80::a9fe:a9fe", # OpenStack Nova metadata (IPv6 link-local)
60
+ }
61
+ )
62
+
63
+ # Network ranges that are always blocked when block_cloud_metadata=True,
64
+ # independent of block_private_ips. The entire link-local range is used by
65
+ # cloud metadata services across providers.
66
+ _CLOUD_METADATA_NETWORKS: tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...] = (
67
+ ipaddress.IPv4Network("169.254.0.0/16"),
68
+ )
69
+
70
+ _CLOUD_METADATA_HOSTNAMES: frozenset[str] = frozenset(
71
+ {
72
+ "metadata.google.internal",
73
+ "metadata.amazonaws.com",
74
+ "metadata",
75
+ "instance-data",
76
+ }
77
+ )
78
+
79
+ _LOCALHOST_NAMES: frozenset[str] = frozenset(
80
+ {
81
+ "localhost",
82
+ "localhost.localdomain",
83
+ "host.docker.internal",
84
+ }
85
+ )
86
+
87
+ _K8S_SUFFIX = ".svc.cluster.local"
88
+
89
+ _LOOPBACK_IPV4 = ipaddress.IPv4Network("127.0.0.0/8")
90
+ _LOOPBACK_IPV6 = ipaddress.IPv6Address("::1")
91
+
92
+ # NAT64 well-known prefixes
93
+ _NAT64_PREFIX = ipaddress.IPv6Network("64:ff9b::/96")
94
+ _NAT64_DISCOVERY_PREFIX = ipaddress.IPv6Network("64:ff9b:1::/48")
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # SSRFPolicy
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ @dataclasses.dataclass(frozen=True)
103
+ class SSRFPolicy:
104
+ """Immutable policy controlling which URLs/IPs are considered safe."""
105
+
106
+ allowed_schemes: frozenset[str] = frozenset({"http", "https"})
107
+ block_private_ips: bool = True
108
+ block_localhost: bool = True
109
+ block_cloud_metadata: bool = True
110
+ block_k8s_internal: bool = True
111
+ allowed_hosts: frozenset[str] = frozenset()
112
+ additional_blocked_cidrs: tuple[
113
+ ipaddress.IPv4Network | ipaddress.IPv6Network, ...
114
+ ] = ()
115
+
116
+
117
+ DEFAULT_SSRF_POLICY = SSRFPolicy()
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Helpers
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ def _extract_embedded_ipv4(
126
+ addr: ipaddress.IPv6Address,
127
+ ) -> ipaddress.IPv4Address | None:
128
+ """Extract an embedded IPv4 from IPv4-mapped or NAT64 IPv6 addresses."""
129
+ # Check ipv4_mapped first (covers ::ffff:x.x.x.x)
130
+ if addr.ipv4_mapped is not None:
131
+ return addr.ipv4_mapped
132
+
133
+ # Check NAT64 prefixes — embedded IPv4 is in the last 4 bytes
134
+ if addr in _NAT64_PREFIX or addr in _NAT64_DISCOVERY_PREFIX:
135
+ raw = addr.packed
136
+ return ipaddress.IPv4Address(raw[-4:])
137
+
138
+ return None
139
+
140
+
141
+ def _ip_in_blocked_networks(
142
+ addr: ipaddress.IPv4Address | ipaddress.IPv6Address,
143
+ policy: SSRFPolicy,
144
+ ) -> str | None:
145
+ """Return a reason string if *addr* falls in a blocked range, else None."""
146
+ # NOTE: if profiling shows this is a hot path, consider memoising with
147
+ # @functools.lru_cache (key on (addr, id(policy))).
148
+ if isinstance(addr, ipaddress.IPv4Address):
149
+ if policy.block_private_ips:
150
+ for blocked_ipv4_net in _BLOCKED_IPV4_NETWORKS:
151
+ if addr in blocked_ipv4_net:
152
+ return "private IP range"
153
+ for blocked_cidr in policy.additional_blocked_cidrs:
154
+ if isinstance(blocked_cidr, ipaddress.IPv4Network) and addr in blocked_cidr:
155
+ return "blocked CIDR"
156
+ else:
157
+ if policy.block_private_ips:
158
+ for blocked_ipv6_net in _BLOCKED_IPV6_NETWORKS:
159
+ if addr in blocked_ipv6_net:
160
+ return "private IP range"
161
+ for blocked_cidr in policy.additional_blocked_cidrs:
162
+ if isinstance(blocked_cidr, ipaddress.IPv6Network) and addr in blocked_cidr:
163
+ return "blocked CIDR"
164
+
165
+ # Loopback check — independent of block_private_ips so that
166
+ # block_localhost=True still catches 127.x.x.x / ::1 even when
167
+ # private IPs are allowed.
168
+ if policy.block_localhost:
169
+ if isinstance(addr, ipaddress.IPv4Address) and (
170
+ addr in _LOOPBACK_IPV4 or addr in ipaddress.IPv4Network("0.0.0.0/8")
171
+ ):
172
+ return "localhost address"
173
+ if isinstance(addr, ipaddress.IPv6Address) and addr == _LOOPBACK_IPV6:
174
+ return "localhost address"
175
+
176
+ # Cloud metadata check — IP set *and* network ranges (e.g. 169.254.0.0/16).
177
+ # Independent of block_private_ips so that allow_private=True still blocks
178
+ # cloud metadata endpoints.
179
+ if policy.block_cloud_metadata:
180
+ if str(addr) in _CLOUD_METADATA_IPS:
181
+ return "cloud metadata endpoint"
182
+ for net in _CLOUD_METADATA_NETWORKS:
183
+ if addr in net:
184
+ return "cloud metadata endpoint"
185
+
186
+ return None
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Public validation functions
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ def validate_resolved_ip(ip_str: str, policy: SSRFPolicy) -> None:
195
+ """Validate a resolved IP address against the SSRF policy.
196
+
197
+ Raises SSRFBlockedError if the IP is blocked.
198
+ """
199
+ try:
200
+ addr = ipaddress.ip_address(ip_str)
201
+ except ValueError as exc:
202
+ msg = "invalid IP address"
203
+ raise SSRFBlockedError(msg) from exc
204
+
205
+ if isinstance(addr, ipaddress.IPv6Address):
206
+ inner = _extract_embedded_ipv4(addr)
207
+ if inner is not None:
208
+ addr = inner
209
+
210
+ reason = _ip_in_blocked_networks(addr, policy)
211
+ if reason is not None:
212
+ raise SSRFBlockedError(reason)
213
+
214
+
215
+ def validate_hostname(hostname: str, policy: SSRFPolicy) -> None:
216
+ """Validate a hostname against the SSRF policy.
217
+
218
+ Raises SSRFBlockedError if the hostname is blocked.
219
+ """
220
+ lower = hostname.lower()
221
+
222
+ if policy.block_localhost and lower in _LOCALHOST_NAMES:
223
+ msg = "localhost address"
224
+ raise SSRFBlockedError(msg)
225
+
226
+ if policy.block_cloud_metadata and lower in _CLOUD_METADATA_HOSTNAMES:
227
+ msg = "cloud metadata endpoint"
228
+ raise SSRFBlockedError(msg)
229
+
230
+ if policy.block_k8s_internal and lower.endswith(_K8S_SUFFIX):
231
+ msg = "Kubernetes internal DNS"
232
+ raise SSRFBlockedError(msg)
233
+
234
+
235
+ def _effective_allowed_hosts(policy: SSRFPolicy) -> frozenset[str]:
236
+ """Return allowed_hosts, augmented for local environments."""
237
+ extra: set[str] = set()
238
+ if os.environ.get("COCO_ENV", "").startswith("local"):
239
+ extra.update({"localhost", "testserver"})
240
+ if extra:
241
+ return policy.allowed_hosts | frozenset(extra)
242
+ return policy.allowed_hosts
243
+
244
+
245
+ async def validate_url(url: str, policy: SSRFPolicy = DEFAULT_SSRF_POLICY) -> None:
246
+ """Validate a URL against the SSRF policy, including DNS resolution.
247
+
248
+ This is the primary entry-point for async code paths. It delegates
249
+ scheme/hostname/allowed-hosts checks to `validate_url_sync`, then
250
+ resolves DNS and validates every resolved IP.
251
+
252
+ Raises:
253
+ SSRFBlockedError: If the URL violates the policy.
254
+ """
255
+ parsed = urllib.parse.urlparse(url)
256
+ hostname = parsed.hostname or ""
257
+
258
+ validate_url_sync(url, policy)
259
+
260
+ allowed = {h.lower() for h in _effective_allowed_hosts(policy)}
261
+ if hostname.lower() in allowed:
262
+ return
263
+
264
+ scheme = (parsed.scheme or "").lower()
265
+ port = parsed.port or (443 if scheme == "https" else 80)
266
+ try:
267
+ addrinfo = await asyncio.to_thread(
268
+ socket.getaddrinfo, hostname, port, type=socket.SOCK_STREAM
269
+ )
270
+ except socket.gaierror as exc:
271
+ msg = "DNS resolution failed"
272
+ raise SSRFBlockedError(msg) from exc
273
+
274
+ for _family, _type, _proto, _canonname, sockaddr in addrinfo:
275
+ validate_resolved_ip(str(sockaddr[0]), policy)
276
+
277
+
278
+ def validate_url_sync(url: str, policy: SSRFPolicy = DEFAULT_SSRF_POLICY) -> None:
279
+ """Synchronous URL validation (no DNS resolution).
280
+
281
+ Suitable for Pydantic validators and other sync contexts. Checks scheme
282
+ and hostname patterns only - use `validate_url` for full DNS-aware checking.
283
+
284
+ Raises:
285
+ SSRFBlockedError: If the URL violates the policy.
286
+ """
287
+ parsed = urllib.parse.urlparse(url)
288
+
289
+ scheme = (parsed.scheme or "").lower()
290
+ if scheme not in policy.allowed_schemes:
291
+ msg = f"scheme '{scheme}' not allowed"
292
+ raise SSRFBlockedError(msg)
293
+
294
+ hostname = parsed.hostname
295
+ if not hostname:
296
+ msg = "missing hostname"
297
+ raise SSRFBlockedError(msg)
298
+
299
+ allowed = _effective_allowed_hosts(policy)
300
+ if hostname.lower() in {h.lower() for h in allowed}:
301
+ return
302
+
303
+ try:
304
+ ipaddress.ip_address(hostname)
305
+ validate_resolved_ip(hostname, policy)
306
+ except SSRFBlockedError:
307
+ raise
308
+ except ValueError:
309
+ pass
310
+ else:
311
+ return
312
+
313
+ validate_hostname(hostname, policy)
@@ -0,0 +1,155 @@
1
+ """SSRF Protection - thin wrapper raising ValueError for internal callers.
2
+
3
+ Delegates all validation to `coco_core._security._policy`.
4
+ """
5
+
6
+ import os
7
+ import socket
8
+ from typing import Annotated, Any
9
+ from urllib.parse import urlparse
10
+
11
+ from pydantic import (
12
+ AnyHttpUrl,
13
+ BeforeValidator,
14
+ HttpUrl,
15
+ )
16
+
17
+ from coco_core._security._exceptions import SSRFBlockedError
18
+ from coco_core._security._policy import (
19
+ SSRFPolicy,
20
+ )
21
+ from coco_core._security._policy import (
22
+ validate_resolved_ip as _validate_resolved_ip,
23
+ )
24
+ from coco_core._security._policy import (
25
+ validate_url_sync as _validate_url_sync,
26
+ )
27
+
28
+
29
+ def _policy_for(*, allow_private: bool, allow_http: bool) -> SSRFPolicy:
30
+ """Build an `SSRFPolicy` from the legacy flag interface."""
31
+ schemes = frozenset({"http", "https"}) if allow_http else frozenset({"https"})
32
+ return SSRFPolicy(
33
+ allowed_schemes=schemes,
34
+ block_private_ips=not allow_private,
35
+ block_localhost=not allow_private,
36
+ block_cloud_metadata=True,
37
+ block_k8s_internal=True,
38
+ )
39
+
40
+
41
+ def validate_safe_url(
42
+ url: str | AnyHttpUrl,
43
+ *,
44
+ allow_private: bool = False,
45
+ allow_http: bool = True,
46
+ ) -> str:
47
+ """Validate a URL for SSRF protection.
48
+
49
+ This function validates URLs to prevent Server-Side Request Forgery (SSRF) attacks
50
+ by blocking requests to private networks and cloud metadata endpoints.
51
+
52
+ Args:
53
+ url: The URL to validate (string or Pydantic HttpUrl).
54
+ allow_private: If `True`, allows private IPs and localhost (for development).
55
+ Cloud metadata endpoints are ALWAYS blocked.
56
+ allow_http: If `True`, allows both HTTP and HTTPS. If `False`, only HTTPS.
57
+
58
+ Returns:
59
+ The validated URL as a string.
60
+
61
+ Raises:
62
+ ValueError: If URL is invalid or potentially dangerous.
63
+ """
64
+ url_str = str(url)
65
+ parsed = urlparse(url_str)
66
+ hostname = parsed.hostname or ""
67
+
68
+ # Test-environment bypass (preserved from original implementation)
69
+ if (
70
+ os.environ.get("COCO_ENV") == "local_test"
71
+ and hostname.startswith("test")
72
+ and "server" in hostname
73
+ ):
74
+ return url_str
75
+
76
+ policy = _policy_for(allow_private=allow_private, allow_http=allow_http)
77
+
78
+ # Synchronous scheme + hostname checks
79
+ try:
80
+ _validate_url_sync(url_str, policy)
81
+ except SSRFBlockedError as exc:
82
+ raise ValueError(str(exc)) from exc
83
+
84
+ # DNS resolution and IP validation
85
+ try:
86
+ addr_info = socket.getaddrinfo(
87
+ hostname,
88
+ parsed.port or (443 if parsed.scheme == "https" else 80),
89
+ socket.AF_UNSPEC,
90
+ socket.SOCK_STREAM,
91
+ )
92
+
93
+ for result in addr_info:
94
+ ip_str: str = result[4][0] # type: ignore[assignment]
95
+ try:
96
+ _validate_resolved_ip(ip_str, policy)
97
+ except SSRFBlockedError as exc:
98
+ raise ValueError(str(exc)) from exc
99
+
100
+ except socket.gaierror as e:
101
+ msg = f"Failed to resolve hostname '{hostname}': {e}"
102
+ raise ValueError(msg) from e
103
+ except OSError as e:
104
+ msg = f"Network error while validating URL: {e}"
105
+ raise ValueError(msg) from e
106
+
107
+ return url_str
108
+
109
+
110
+ def is_safe_url(
111
+ url: str | AnyHttpUrl,
112
+ *,
113
+ allow_private: bool = False,
114
+ allow_http: bool = True,
115
+ ) -> bool:
116
+ """Non-throwing version of `validate_safe_url`."""
117
+ try:
118
+ validate_safe_url(url, allow_private=allow_private, allow_http=allow_http)
119
+ except ValueError:
120
+ return False
121
+ else:
122
+ return True
123
+
124
+
125
+ def _validate_url_ssrf_strict(v: Any) -> Any:
126
+ """Validate URL for SSRF protection (strict mode)."""
127
+ if isinstance(v, str):
128
+ validate_safe_url(v, allow_private=False, allow_http=True)
129
+ return v
130
+
131
+
132
+ def _validate_url_ssrf_https_only(v: Any) -> Any:
133
+ if isinstance(v, str):
134
+ validate_safe_url(v, allow_private=False, allow_http=False)
135
+ return v
136
+
137
+
138
+ def _validate_url_ssrf_relaxed(v: Any) -> Any:
139
+ """Validate URL for SSRF protection (relaxed mode - allows private IPs)."""
140
+ if isinstance(v, str):
141
+ validate_safe_url(v, allow_private=True, allow_http=True)
142
+ return v
143
+
144
+
145
+ # Annotated types with SSRF protection
146
+ SSRFProtectedUrl = Annotated[HttpUrl, BeforeValidator(_validate_url_ssrf_strict)]
147
+ SSRFProtectedUrlRelaxed = Annotated[
148
+ HttpUrl, BeforeValidator(_validate_url_ssrf_relaxed)
149
+ ]
150
+ SSRFProtectedHttpsUrl = Annotated[
151
+ HttpUrl, BeforeValidator(_validate_url_ssrf_https_only)
152
+ ]
153
+ SSRFProtectedHttpsUrlStr = Annotated[
154
+ str, BeforeValidator(_validate_url_ssrf_https_only)
155
+ ]