cortexhub 0.1.5__tar.gz → 0.1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {cortexhub-0.1.5 → cortexhub-0.1.8}/PKG-INFO +1 -1
  2. {cortexhub-0.1.5 → cortexhub-0.1.8}/pyproject.toml +1 -1
  3. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/adapters/claude_agents.py +9 -7
  4. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/adapters/crewai.py +0 -7
  5. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/adapters/langgraph.py +2 -6
  6. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/adapters/openai_agents.py +2 -7
  7. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/backend/client.py +58 -39
  8. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/client.py +80 -20
  9. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/pipeline.py +8 -0
  10. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/version.py +1 -1
  11. {cortexhub-0.1.5 → cortexhub-0.1.8}/.gitignore +0 -0
  12. {cortexhub-0.1.5 → cortexhub-0.1.8}/LICENSE +0 -0
  13. {cortexhub-0.1.5 → cortexhub-0.1.8}/README.md +0 -0
  14. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/__init__.py +0 -0
  15. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/adapters/__init__.py +0 -0
  16. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/adapters/base.py +0 -0
  17. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/audit/__init__.py +0 -0
  18. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/audit/events.py +0 -0
  19. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/auto_protect.py +0 -0
  20. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/backend/__init__.py +0 -0
  21. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/config.py +0 -0
  22. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/context/__init__.py +0 -0
  23. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/context/enricher.py +0 -0
  24. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/errors.py +0 -0
  25. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/frameworks.py +0 -0
  26. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/guardrails/__init__.py +0 -0
  27. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/guardrails/injection.py +0 -0
  28. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/guardrails/pii.py +0 -0
  29. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/guardrails/secrets.py +0 -0
  30. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/interceptors/__init__.py +0 -0
  31. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/interceptors/llm.py +0 -0
  32. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/interceptors/mcp.py +0 -0
  33. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/policy/__init__.py +0 -0
  34. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/policy/effects.py +0 -0
  35. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/policy/evaluator.py +0 -0
  36. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/policy/loader.py +0 -0
  37. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/policy/models.py +0 -0
  38. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/policy/sync.py +0 -0
  39. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/telemetry/__init__.py +0 -0
  40. {cortexhub-0.1.5 → cortexhub-0.1.8}/src/cortexhub/telemetry/otel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortexhub
3
- Version: 0.1.5
3
+ Version: 0.1.8
4
4
  Summary: CortexHub Python SDK - Policy-as-Code for AI Agents
5
5
  Project-URL: Homepage, https://cortexhub.ai
6
6
  Project-URL: Documentation, https://docs.cortexhub.ai
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cortexhub"
3
- version = "0.1.5"
3
+ version = "0.1.8"
4
4
  description = "CortexHub Python SDK - Policy-as-Code for AI Agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.14"
@@ -94,13 +94,6 @@ class ClaudeAgentsAdapter(ToolAdapter):
94
94
  return
95
95
 
96
96
  cortex_hub = self.cortex_hub
97
- tools = self._discover_tools()
98
- if tools:
99
- cortex_hub.backend.register_tool_inventory(
100
- agent_id=cortex_hub.agent_id,
101
- framework=self.framework_name,
102
- tools=tools,
103
- )
104
97
 
105
98
  # Store original decorator
106
99
  if not hasattr(claude_agent_sdk, _ORIGINAL_TOOL_ATTR):
@@ -123,6 +116,14 @@ class ClaudeAgentsAdapter(ToolAdapter):
123
116
  original_handler = original_decorated.handler
124
117
  tool_name = original_decorated.name
125
118
  tool_description = original_decorated.description
119
+ parameters_schema = None
120
+ if isinstance(input_schema, dict):
121
+ parameters_schema = input_schema
122
+ elif hasattr(input_schema, "model_json_schema"):
123
+ try:
124
+ parameters_schema = input_schema.model_json_schema()
125
+ except Exception:
126
+ parameters_schema = None
126
127
 
127
128
  @wraps(original_handler)
128
129
  async def governed_handler(args: dict[str, Any]) -> dict[str, Any]:
@@ -131,6 +132,7 @@ class ClaudeAgentsAdapter(ToolAdapter):
131
132
  "name": tool_name,
132
133
  "description": tool_description,
133
134
  "framework": "claude_agents",
135
+ "parameters_schema": parameters_schema,
134
136
  }
135
137
 
136
138
  governed_fn = govern_execution(
@@ -70,13 +70,6 @@ class CrewAIAdapter(ToolAdapter):
70
70
  from crewai.tools.structured_tool import CrewStructuredTool
71
71
 
72
72
  cortex_hub = self.cortex_hub
73
- tools = self._discover_tools()
74
- if tools:
75
- cortex_hub.backend.register_tool_inventory(
76
- agent_id=cortex_hub.agent_id,
77
- framework=self.framework_name,
78
- tools=tools,
79
- )
80
73
 
81
74
  # Patch CrewStructuredTool.invoke (primary execution path)
82
75
  if not getattr(CrewStructuredTool, _PATCHED_ATTR, False):
@@ -93,6 +93,7 @@ class LangGraphAdapter(ToolAdapter):
93
93
  """Governed tool invocation."""
94
94
  tool_name = getattr(self, "name", "unknown_tool")
95
95
  tool_description = getattr(self, "description", None)
96
+ parameters_schema = adapter._extract_parameters_schema(self)
96
97
 
97
98
  # Extract args - preserve structure without rewriting
98
99
  if isinstance(input, dict):
@@ -109,6 +110,7 @@ class LangGraphAdapter(ToolAdapter):
109
110
  "name": tool_name,
110
111
  "description": tool_description,
111
112
  "framework": "langgraph",
113
+ "parameters_schema": parameters_schema,
112
114
  }
113
115
  governed_fn = govern_execution(
114
116
  tool_fn=lambda *a, **kw: tool_original_invoke(
@@ -411,12 +413,6 @@ class LangGraphAdapter(ToolAdapter):
411
413
  name = tool.get("name") or "unknown_tool"
412
414
  self._discovered_tools[name] = tool
413
415
 
414
- self.cortex_hub.backend.register_tool_inventory(
415
- agent_id=self.cortex_hub.agent_id,
416
- framework=self.framework_name,
417
- tools=list(self._discovered_tools.values()),
418
- )
419
-
420
416
  def _normalize_tools(self, tools: Any) -> list[dict[str, Any]]:
421
417
  """Convert tool objects to inventory payloads."""
422
418
  normalized: list[dict[str, Any]] = []
@@ -67,13 +67,6 @@ class OpenAIAgentsAdapter(ToolAdapter):
67
67
  return
68
68
 
69
69
  cortex_hub = self.cortex_hub
70
- tools = self._discover_tools()
71
- if tools:
72
- cortex_hub.backend.register_tool_inventory(
73
- agent_id=cortex_hub.agent_id,
74
- framework=self.framework_name,
75
- tools=tools,
76
- )
77
70
 
78
71
  # Store original function_tool decorator
79
72
  if not hasattr(tool_module, _ORIGINAL_FUNCTION_TOOL_ATTR):
@@ -109,6 +102,7 @@ class OpenAIAgentsAdapter(ToolAdapter):
109
102
  original_invoke = tool.on_invoke_tool
110
103
  tool_name = tool.name
111
104
  tool_description = tool.description
105
+ parameters_schema = tool.params_json_schema or tool.strict_json_schema
112
106
 
113
107
  async def governed_invoke(ctx, input_json: str) -> Any:
114
108
  """Governed tool invocation."""
@@ -121,6 +115,7 @@ class OpenAIAgentsAdapter(ToolAdapter):
121
115
  "name": tool_name,
122
116
  "description": tool_description,
123
117
  "framework": "openai_agents",
118
+ "parameters_schema": parameters_schema,
124
119
  }
125
120
 
126
121
  # Create governed function
@@ -7,7 +7,6 @@ import httpx
7
7
  from typing import Any
8
8
  from dataclasses import dataclass
9
9
 
10
- from cortexhub.version import __version__
11
10
 
12
11
  logger = structlog.get_logger(__name__)
13
12
 
@@ -92,7 +91,7 @@ class BackendClient:
92
91
  timeout=10.0,
93
92
  )
94
93
 
95
- def validate_api_key(self) -> tuple[bool, SDKConfig | None]:
94
+ def validate_api_key(self, *, agent_id: str | None = None) -> tuple[bool, SDKConfig | None]:
96
95
  """Validate API key with backend and get SDK configuration.
97
96
 
98
97
  Returns:
@@ -127,7 +126,46 @@ class BackendClient:
127
126
 
128
127
  # Convert policy objects to Cedar string if needed
129
128
  raw_policies = policies_data.get("policies", [])
129
+ if agent_id:
130
+ raw_policies = [
131
+ policy for policy in raw_policies if policy.get("agent_id") == agent_id
132
+ ]
130
133
  guardrail_configs: dict[str, GuardrailConfig] = {}
134
+
135
+ def _merge_guardrail_config(
136
+ existing: GuardrailConfig | None,
137
+ incoming: GuardrailConfig,
138
+ ) -> GuardrailConfig:
139
+ if not existing:
140
+ return incoming
141
+ priority = {"block": 3, "redact": 2, "allow": 1}
142
+ action = (
143
+ incoming.action
144
+ if priority.get(incoming.action, 0) > priority.get(existing.action, 0)
145
+ else existing.action
146
+ )
147
+ if existing.pii_types is None or incoming.pii_types is None:
148
+ pii_types = None
149
+ else:
150
+ pii_types = sorted(set(existing.pii_types + incoming.pii_types))
151
+ if existing.secret_types is None or incoming.secret_types is None:
152
+ secret_types = None
153
+ else:
154
+ secret_types = sorted(set(existing.secret_types + incoming.secret_types))
155
+ custom_patterns = (existing.custom_patterns or []) + (incoming.custom_patterns or [])
156
+ if existing.redaction_scope == "both" or incoming.redaction_scope == "both":
157
+ redaction_scope = "both"
158
+ elif existing.redaction_scope != incoming.redaction_scope:
159
+ redaction_scope = "both"
160
+ else:
161
+ redaction_scope = existing.redaction_scope
162
+ return GuardrailConfig(
163
+ action=action,
164
+ pii_types=pii_types,
165
+ secret_types=secret_types,
166
+ custom_patterns=custom_patterns,
167
+ redaction_scope=redaction_scope,
168
+ )
131
169
 
132
170
  if isinstance(raw_policies, list):
133
171
  # Backend returns list of policy objects - extract Cedar code
@@ -171,12 +209,16 @@ class BackendClient:
171
209
  description=cp.get("description"),
172
210
  enabled=cp.get("enabled", True),
173
211
  ))
174
- guardrail_configs["pii"] = GuardrailConfig(
175
- action=gc.get("action", "redact"),
176
- pii_types=gc.get("pii_types"),
177
- custom_patterns=custom_patterns if custom_patterns else None,
178
- redaction_scope=gc.get("redaction_scope", "both"),
212
+ merged = _merge_guardrail_config(
213
+ guardrail_configs.get("pii"),
214
+ GuardrailConfig(
215
+ action=gc.get("action", "redact"),
216
+ pii_types=gc.get("pii_types"),
217
+ custom_patterns=custom_patterns if custom_patterns else None,
218
+ redaction_scope=gc.get("redaction_scope", "both"),
219
+ ),
179
220
  )
221
+ guardrail_configs["pii"] = merged
180
222
  elif "secrets" in policy_key:
181
223
  custom_patterns = []
182
224
  raw_patterns = gc.get("custom_patterns") or []
@@ -187,12 +229,16 @@ class BackendClient:
187
229
  description=cp.get("description"),
188
230
  enabled=cp.get("enabled", True),
189
231
  ))
190
- guardrail_configs["secrets"] = GuardrailConfig(
191
- action=gc.get("action", "redact"),
192
- secret_types=gc.get("secret_types"),
193
- custom_patterns=custom_patterns if custom_patterns else None,
194
- redaction_scope=gc.get("redaction_scope", "both"),
232
+ merged = _merge_guardrail_config(
233
+ guardrail_configs.get("secrets"),
234
+ GuardrailConfig(
235
+ action=gc.get("action", "redact"),
236
+ secret_types=gc.get("secret_types"),
237
+ custom_patterns=custom_patterns if custom_patterns else None,
238
+ redaction_scope=gc.get("redaction_scope", "both"),
239
+ ),
195
240
  )
241
+ guardrail_configs["secrets"] = merged
196
242
  policies_str = "\n".join(cedar_parts)
197
243
  else:
198
244
  policies_str = raw_policies or ""
@@ -254,33 +300,6 @@ class BackendClient:
254
300
  if self._client:
255
301
  self._client.close()
256
302
 
257
- def register_tool_inventory(
258
- self,
259
- *,
260
- agent_id: str,
261
- framework: str,
262
- tools: list[dict],
263
- ) -> dict[str, Any]:
264
- """Register agent tool inventory on init. Overwrites previous."""
265
-
266
- if not self._client:
267
- return {"agent_id": agent_id, "tools_count": 0}
268
-
269
- try:
270
- response = self._client.post(
271
- "/tool-inventory",
272
- json={
273
- "agent_id": agent_id,
274
- "framework": framework,
275
- "sdk_version": __version__,
276
- "tools": tools,
277
- },
278
- )
279
- return response.json()
280
- except Exception as e:
281
- logger.warning("Failed to register tool inventory", error=str(e))
282
- return {"agent_id": agent_id, "tools_count": len(tools)}
283
-
284
303
  def create_approval(
285
304
  self,
286
305
  *,
@@ -159,7 +159,7 @@ class CortexHub:
159
159
 
160
160
  # Validate API key and get configuration (including policies)
161
161
  self.backend = BackendClient(self.api_key, self.api_base_url)
162
- is_valid, sdk_config = self.backend.validate_api_key()
162
+ is_valid, sdk_config = self.backend.validate_api_key(agent_id=self.agent_id)
163
163
 
164
164
  if not is_valid:
165
165
  raise ConfigurationError(
@@ -439,6 +439,7 @@ class CortexHub:
439
439
  framework: str,
440
440
  call_original: Callable[[], Any],
441
441
  tool_description: str | None = None,
442
+ parameters_schema: dict[str, Any] | None = None,
442
443
  principal: Principal | dict[str, str] | None = None,
443
444
  resource_type: str = "Tool",
444
445
  ) -> Any:
@@ -475,6 +476,10 @@ class CortexHub:
475
476
  PolicyViolationError: If policy denies
476
477
  ApprovalDeniedError: If approval denied
477
478
  """
479
+ expected_types = self._extract_expected_arg_types(parameters_schema)
480
+
481
+ expected_types = self._extract_expected_arg_types(parameters_schema)
482
+
478
483
  # Step 1: Build request (structured, NOT flattened)
479
484
  policy_args = self._sanitize_policy_args(args)
480
485
  request = self.build_request(
@@ -515,6 +520,21 @@ class CortexHub:
515
520
  span.set_attribute("cortexhub.raw.args", json.dumps(args, default=str))
516
521
 
517
522
  try:
523
+ if expected_types and self.enforce and self.evaluator:
524
+ mismatches = self._validate_arg_types(args, expected_types)
525
+ if mismatches:
526
+ message = f"Tool argument type mismatch: {'; '.join(mismatches)}"
527
+ span.add_event(
528
+ "policy.decision",
529
+ attributes={
530
+ "decision.effect": "deny",
531
+ "decision.policy_id": "",
532
+ "decision.reasoning": message,
533
+ "decision.policy_name": "type_mismatch",
534
+ },
535
+ )
536
+ span.set_status(Status(StatusCode.ERROR, message))
537
+ raise PolicyViolationError(message, reasoning=message)
518
538
  # Step 4: Policy evaluation (enforcement mode only)
519
539
  if self.enforce and self.evaluator:
520
540
  if os.getenv("CORTEXHUB_DEBUG_POLICY", "").lower() in ("1", "true", "yes"):
@@ -636,6 +656,7 @@ class CortexHub:
636
656
  framework: str,
637
657
  call_original: Callable[[], Any],
638
658
  tool_description: str | None = None,
659
+ parameters_schema: dict[str, Any] | None = None,
639
660
  principal: Principal | dict[str, str] | None = None,
640
661
  resource_type: str = "Tool",
641
662
  ) -> Any:
@@ -684,6 +705,21 @@ class CortexHub:
684
705
  span.set_attribute("cortexhub.raw.args", json.dumps(args, default=str))
685
706
 
686
707
  try:
708
+ if expected_types and self.enforce and self.evaluator:
709
+ mismatches = self._validate_arg_types(args, expected_types)
710
+ if mismatches:
711
+ message = f"Tool argument type mismatch: {'; '.join(mismatches)}"
712
+ span.add_event(
713
+ "policy.decision",
714
+ attributes={
715
+ "decision.effect": "deny",
716
+ "decision.policy_id": "",
717
+ "decision.reasoning": message,
718
+ "decision.policy_name": "type_mismatch",
719
+ },
720
+ )
721
+ span.set_status(Status(StatusCode.ERROR, message))
722
+ raise PolicyViolationError(message, reasoning=message)
687
723
  # Step 4: Policy evaluation (enforcement mode only)
688
724
  if self.enforce and self.evaluator:
689
725
  decision = self.evaluator.evaluate(request)
@@ -1316,6 +1352,49 @@ class CortexHub:
1316
1352
  return "string"
1317
1353
  return "unknown"
1318
1354
 
1355
+ def _extract_expected_arg_types(self, parameters_schema: dict[str, Any] | None) -> dict[str, str]:
1356
+ if not parameters_schema or not isinstance(parameters_schema, dict):
1357
+ return {}
1358
+ properties = parameters_schema.get("properties")
1359
+ if not isinstance(properties, dict):
1360
+ return {}
1361
+ expected: dict[str, str] = {}
1362
+ for name, schema in properties.items():
1363
+ if not isinstance(schema, dict):
1364
+ continue
1365
+ raw_type = schema.get("type")
1366
+ if isinstance(raw_type, list):
1367
+ raw_type = next((t for t in raw_type if t != "null"), None)
1368
+ if not isinstance(raw_type, str):
1369
+ continue
1370
+ normalized = raw_type.lower()
1371
+ if normalized == "integer":
1372
+ normalized = "number"
1373
+ if normalized in ("number", "string", "boolean"):
1374
+ expected[str(name)] = normalized
1375
+ return expected
1376
+
1377
+ def _validate_arg_types(self, args: dict[str, Any], expected_types: dict[str, str]) -> list[str]:
1378
+ if not isinstance(args, dict):
1379
+ return []
1380
+ mismatches: list[str] = []
1381
+ for name, expected in expected_types.items():
1382
+ if name not in args:
1383
+ continue
1384
+ value = args.get(name)
1385
+ if value is None:
1386
+ continue
1387
+ if expected == "number":
1388
+ if not isinstance(value, int) or isinstance(value, bool):
1389
+ mismatches.append(f"{name} expected number but got {type(value).__name__}")
1390
+ elif expected == "string":
1391
+ if not isinstance(value, str):
1392
+ mismatches.append(f"{name} expected string but got {type(value).__name__}")
1393
+ elif expected == "boolean":
1394
+ if not isinstance(value, bool):
1395
+ mismatches.append(f"{name} expected boolean but got {type(value).__name__}")
1396
+ return mismatches
1397
+
1319
1398
  def trace_llm_call(
1320
1399
  self,
1321
1400
  model: str,
@@ -1785,25 +1864,6 @@ class CortexHub:
1785
1864
  if not isinstance(cleaned_args, dict):
1786
1865
  return {}
1787
1866
 
1788
- if "amount" in cleaned_args:
1789
- from decimal import Decimal, InvalidOperation
1790
-
1791
- amount = cleaned_args.get("amount")
1792
- dec_amount: Decimal | None = None
1793
- if isinstance(amount, int):
1794
- dec_amount = Decimal(amount)
1795
- elif isinstance(amount, str):
1796
- try:
1797
- dec_amount = Decimal(amount)
1798
- except InvalidOperation:
1799
- dec_amount = None
1800
- if dec_amount is not None:
1801
- quantized = dec_amount.quantize(Decimal("0.01"))
1802
- if quantized == dec_amount:
1803
- cleaned_args["amount_cents"] = int(
1804
- (quantized * 100).to_integral_value()
1805
- )
1806
-
1807
1867
  return cleaned_args
1808
1868
 
1809
1869
  def _summarize_policy_args(self, args: dict[str, Any]) -> dict[str, Any]:
@@ -46,6 +46,12 @@ def govern_execution(
46
46
  framework = tool_metadata.get("framework", "unknown")
47
47
  model = tool_metadata.get("model", "unknown")
48
48
  prompt = tool_metadata.get("prompt")
49
+ parameters_schema = (
50
+ tool_metadata.get("parameters_schema")
51
+ or tool_metadata.get("params_json_schema")
52
+ or tool_metadata.get("input_schema")
53
+ or tool_metadata.get("strict_json_schema")
54
+ )
49
55
  call_original = tool_metadata.get("call_original")
50
56
  if call_original is None:
51
57
  call_original = tool_fn
@@ -78,6 +84,7 @@ def govern_execution(
78
84
  args=kwargs, # Use kwargs for structured arguments
79
85
  framework=framework,
80
86
  call_original=lambda: tool_fn(*args, **kwargs),
87
+ parameters_schema=parameters_schema,
81
88
  )
82
89
  return async_wrapper
83
90
  else:
@@ -88,5 +95,6 @@ def govern_execution(
88
95
  args=kwargs, # Use kwargs for structured arguments
89
96
  framework=framework,
90
97
  call_original=lambda: tool_fn(*args, **kwargs),
98
+ parameters_schema=parameters_schema,
91
99
  )
92
100
  return sync_wrapper
@@ -1,3 +1,3 @@
1
1
  """Version information for CortexHub SDK."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.5"
File without changes
File without changes
File without changes