intutic-clawde 1.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: intutic-clawde
3
+ Version: 1.8.0
4
+ Summary: Programmatic proxy connection wrappers and warm-path budget gates for Intutic
5
+ Author: Intutic
6
+ Author-email: engineering@intutic.ai
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: requests (>=2.33.0,<3.0.0)
15
+ Requires-Dist: urllib3 (>=2.7.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # intutic-clawde
19
+
20
+ Python SDK for Intutic Agentic AI Governance proxy wrappers.
21
+
@@ -0,0 +1,3 @@
1
+ # intutic-clawde
2
+
3
+ Python SDK for Intutic Agentic AI Governance proxy wrappers.
@@ -0,0 +1,4 @@
1
+ from .client import ClawdeClient
2
+ from .errors import ClawdeError, ClawdeConnectionError, ClawdeVerdictError
3
+
4
+ __all__ = ["ClawdeClient", "ClawdeError", "ClawdeConnectionError", "ClawdeVerdictError"]
@@ -0,0 +1,42 @@
1
+ import time
2
+ import requests
3
+ from typing import Dict, Any, Optional
4
+ from .errors import ClawdeConnectionError
5
+
6
+ class BudgetChecker:
7
+ def __init__(self, base_url: str, api_key: str):
8
+ self.base_url = base_url
9
+ self.api_key = api_key
10
+ self.cache: Dict[str, Dict[str, Any]] = {}
11
+ self.cache_ttl = 30.0 # 30s TTL
12
+
13
+ def check_budget(self, model: str, estimated_tokens: int) -> Dict[str, Any]:
14
+ cache_key = f"{model}:{estimated_tokens}"
15
+ now = time.time()
16
+ cached = self.cache.get(cache_key)
17
+
18
+ if cached and (now - cached["timestamp"]) < self.cache_ttl:
19
+ return cached["result"]
20
+
21
+ try:
22
+ url = f"{self.base_url}/v1/budget/check"
23
+ params = {"model": model, "estimated_tokens": str(estimated_tokens)}
24
+ headers = {
25
+ "Authorization": f"Bearer {self.api_key}",
26
+ "Accept": "application/json",
27
+ }
28
+ res = requests.get(url, params=params, headers=headers, timeout=5.0)
29
+ if res.status_code != 200:
30
+ raise Exception(f"Budget check returned status {res.status_code}")
31
+ result = res.json()
32
+ self.cache[cache_key] = {"result": result, "timestamp": now}
33
+ return result
34
+ except Exception as e:
35
+ raise ClawdeConnectionError(f"Could not reach budget check endpoint: {str(e)}")
36
+
37
+ def update_cached_budget(self, model: str, estimated_tokens: int, remaining_usd: float, allowed: bool) -> None:
38
+ cache_key = f"{model}:{estimated_tokens}"
39
+ self.cache[cache_key] = {
40
+ "result": {"allowed": allowed, "remaining_usd": remaining_usd},
41
+ "timestamp": time.time(),
42
+ }
@@ -0,0 +1,45 @@
1
+ import os
2
+ from typing import Callable, Any, Optional, TypeVar
3
+ from .errors import ClawdeVerdictError
4
+
5
+ T = TypeVar('T')
6
+
7
+ class CircuitBreaker:
8
+ def __init__(self, client: Any):
9
+ self.client = client
10
+
11
+ def wrap(self, tool_name: str, max_cost_usd: Optional[float] = None, fail_open: bool = False) -> Callable[[Callable[[], T]], T]:
12
+ def decorator(fn: Callable[[], T]) -> T:
13
+ # 1. Pre-Check: Context & pre-flight budget validation
14
+ try:
15
+ if max_cost_usd is not None:
16
+ budget = self.client.check_budget("default", 1)
17
+ if not budget.get("allowed", True):
18
+ raise ClawdeVerdictError(
19
+ "kill",
20
+ f"Circuit breaker tripped for tool '{tool_name}': budget exceeded. Remaining: ${budget.get('remaining_usd')}"
21
+ )
22
+ except Exception as e:
23
+ if not fail_open:
24
+ raise e
25
+ if os.environ.get("INTUTIC_DEBUG") == "true":
26
+ print(f"[Clawde SDK] Circuit breaker pre-check failed (failing open): {str(e)}")
27
+
28
+ # 2. Execute the function
29
+ try:
30
+ result = fn()
31
+
32
+ # 3. Post-Check: If the result is a dict with verdict key, inspect it
33
+ if isinstance(result, dict) and result.get("verdict") == "kill":
34
+ raise ClawdeVerdictError("kill", "Execution blocked by governance policy (Verdict: KILL)")
35
+
36
+ return result
37
+ except Exception as e:
38
+ if isinstance(e, ClawdeVerdictError) and not fail_open:
39
+ raise e
40
+ if not fail_open:
41
+ raise e
42
+ if os.environ.get("INTUTIC_DEBUG") == "true":
43
+ print(f"[Clawde SDK] Circuit breaker execution failed (failing open): {str(e)}")
44
+ return None # type: ignore
45
+ return decorator # type: ignore
@@ -0,0 +1,138 @@
1
+ import os
2
+ import time
3
+ import requests
4
+ import json
5
+ from typing import List, Dict, Any, Callable, Optional, Union
6
+ from .errors import ClawdeConnectionError, ClawdeVerdictError
7
+ from .context_resolver import resolve_context
8
+ from .budget_checker import BudgetChecker
9
+ from .circuit_breaker import CircuitBreaker
10
+
11
+ class ClawdeClient:
12
+ def __init__(
13
+ self,
14
+ api_key: str,
15
+ base_url: Optional[str] = None,
16
+ provider: Optional[str] = None,
17
+ auto_context: bool = True,
18
+ timeout: float = 30.0,
19
+ retries: int = 2
20
+ ):
21
+ if not api_key:
22
+ raise ValueError("API key is required to initialize ClawdeClient.")
23
+ self.api_key = api_key
24
+ self.base_url = base_url or os.environ.get("INTUTIC_BASE_URL") or "http://localhost:4000"
25
+ self.provider = provider
26
+ self.auto_context = auto_context
27
+ self.timeout = timeout
28
+ self.retries = retries
29
+
30
+ self.budget_checker = BudgetChecker(self.base_url, self.api_key)
31
+ self.circuit_breaker_wrapper = CircuitBreaker(self)
32
+ self.listeners: Dict[str, List[Callable[[Dict[str, Any]], None]]] = {
33
+ "hijack": [], "enhance": [], "kill": [], "bypass": []
34
+ }
35
+
36
+ def on(self, event: str, callback: Callable[[Dict[str, Any]], None]) -> None:
37
+ if event in self.listeners:
38
+ self.listeners[event].append(callback)
39
+
40
+ def off(self, event: str, callback: Callable[[Dict[str, Any]], None]) -> None:
41
+ if event in self.listeners and callback in self.listeners[event]:
42
+ self.listeners[event].remove(callback)
43
+
44
+ def emit(self, event: str, payload: Dict[str, Any]) -> None:
45
+ if event in self.listeners:
46
+ for cb in self.listeners[event]:
47
+ try:
48
+ cb(payload)
49
+ except Exception:
50
+ pass
51
+
52
+ def check_budget(self, model: str, estimated_tokens: int) -> Dict[str, Any]:
53
+ return self.budget_checker.check_budget(model, estimated_tokens)
54
+
55
+ def resolve_context(self) -> Dict[str, Any]:
56
+ if not self.auto_context:
57
+ return {}
58
+ return resolve_context()
59
+
60
+ def circuit_breaker(self, tool_name: str, max_cost_usd: Optional[float] = None, fail_open: bool = False) -> Callable[[Callable[[], Any]], Any]:
61
+ return self.circuit_breaker_wrapper.wrap(tool_name, max_cost_usd, fail_open)
62
+
63
+ def chat(self, model: str, messages: List[Dict[str, str]], **kwargs: Any) -> Dict[str, Any]:
64
+ # 1. Resolve context
65
+ context = self.resolve_context()
66
+
67
+ # 2. Prepare payload
68
+ request_payload = {
69
+ "model": model,
70
+ "messages": messages,
71
+ **kwargs
72
+ }
73
+
74
+ # 3. Request logic with retries
75
+ max_attempts = self.retries + 1
76
+ last_error = None
77
+
78
+ for attempt in range(1, max_attempts + 1):
79
+ try:
80
+ headers = {
81
+ "Content-Type": "application/json",
82
+ "Authorization": f"Bearer {self.api_key}",
83
+ "X-Intutic-Context": json.dumps(context),
84
+ }
85
+
86
+ if "max_cost_usd" in kwargs:
87
+ headers["X-Intutic-Cost-Limit"] = str(kwargs["max_cost_usd"])
88
+ if "sensitivity_tier" in kwargs:
89
+ headers["X-Intutic-Sensitivity"] = str(kwargs["sensitivity_tier"])
90
+
91
+ url = f"{self.base_url}/v1/chat/completions"
92
+ res = requests.post(url, json=request_payload, headers=headers, timeout=self.timeout)
93
+
94
+ if res.status_code != 200:
95
+ raise Exception(f"HTTP error {res.status_code}: {res.text}")
96
+
97
+ result = res.json()
98
+
99
+ # Extract headers
100
+ verdict = res.headers.get("x-intutic-verdict", "allow")
101
+ remaining = res.headers.get("x-intutic-budget-remaining")
102
+ pct = res.headers.get("x-intutic-budget-pct")
103
+
104
+ result["verdict"] = verdict
105
+ if remaining:
106
+ result["budget_remaining_usd"] = float(remaining)
107
+ if pct:
108
+ result["budget_pct_used"] = float(pct)
109
+
110
+ # Update budget cache
111
+ if "budget_remaining_usd" in result:
112
+ self.budget_checker.update_cached_budget(
113
+ model,
114
+ len(messages),
115
+ result["budget_remaining_usd"],
116
+ verdict != "kill"
117
+ )
118
+
119
+ # Emit events
120
+ if verdict and verdict != "allow":
121
+ self.emit(verdict, result)
122
+
123
+ # Enforcement check
124
+ if verdict == "kill":
125
+ raise ClawdeVerdictError("kill", "Request blocked by policy (Verdict: KILL)")
126
+
127
+ return result
128
+ except Exception as e:
129
+ last_error = e
130
+ if isinstance(e, ClawdeVerdictError):
131
+ raise e
132
+ if attempt < max_attempts:
133
+ if os.environ.get("INTUTIC_DEBUG") == "true":
134
+ print(f"[Clawde SDK] Attempt {attempt} failed, retrying... Error: {str(e)}")
135
+ time.sleep(attempt * 0.1)
136
+ continue
137
+
138
+ raise ClawdeConnectionError(f"Request failed after {max_attempts} attempts. Last error: {str(last_error)}")
@@ -0,0 +1,36 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Dict, Any
5
+
6
+ def resolve_context() -> Dict[str, Any]:
7
+ config_path = Path.home() / ".intutic" / "config.json"
8
+
9
+ # 1. Primary: Read from sync-daemon config
10
+ if config_path.exists():
11
+ try:
12
+ with open(config_path, "r", encoding="utf-8") as f:
13
+ config = json.load(f)
14
+ return {
15
+ "gitBranch": config.get("gitBranch"),
16
+ "jiraTicket": config.get("jiraTicket"),
17
+ "pagerdutyIncident": config.get("pagerdutyIncident"),
18
+ "ciPipeline": config.get("ciPipeline"),
19
+ "workingDirectory": config.get("workingDirectory", os.getcwd()),
20
+ "workspaceId": config.get("workspaceId"),
21
+ "sessionId": config.get("sessionId"),
22
+ }
23
+ except Exception:
24
+ pass
25
+
26
+ # 2. Fallback: Environment variables
27
+ return {
28
+ "workspaceId": os.environ.get("INTUTIC_WORKSPACE_ID"),
29
+ "sessionId": os.environ.get("INTUTIC_SESSION_ID"),
30
+ "gitBranch": os.environ.get("GIT_BRANCH"),
31
+ "ciPipeline": os.environ.get("GITHUB_RUN_ID")
32
+ or os.environ.get("BUILDKITE_BUILD_ID")
33
+ or os.environ.get("CIRCLE_BUILD_NUM"),
34
+ "pagerdutyIncident": os.environ.get("PD_INCIDENT_ID"),
35
+ "workingDirectory": os.getcwd(),
36
+ }
@@ -0,0 +1,13 @@
1
+ class ClawdeError(Exception):
2
+ """Base exception for intutic-clawde SDK."""
3
+ pass
4
+
5
+ class ClawdeConnectionError(ClawdeError):
6
+ """Raised when the SDK cannot reach the Intutic proxy."""
7
+ pass
8
+
9
+ class ClawdeVerdictError(ClawdeError):
10
+ """Raised when the Intutic proxy returns a blocked verdict (KILL)."""
11
+ def __init__(self, verdict: str, message: str):
12
+ super().__init__(message)
13
+ self.verdict = verdict
@@ -0,0 +1,38 @@
1
+ from typing import TypedDict, List, Dict, Any, Optional
2
+
3
+ class ClawdeClientOptions(TypedDict, total=False):
4
+ api_key: str
5
+ base_url: Optional[str]
6
+ provider: Optional[str]
7
+ auto_context: Optional[bool]
8
+ timeout: Optional[float]
9
+ retries: Optional[int]
10
+
11
+ class ChatMessage(TypedDict):
12
+ role: str
13
+ content: str
14
+
15
+ class ChatParams(TypedDict, total=False):
16
+ model: str
17
+ messages: List[ChatMessage]
18
+ temperature: Optional[float]
19
+ max_cost_usd: Optional[float]
20
+ sensitivity_tier: Optional[str]
21
+
22
+ class ChatResponse(Dict[str, Any]):
23
+ verdict: Optional[str]
24
+ budget_remaining_usd: Optional[float]
25
+ budget_pct_used: Optional[float]
26
+
27
+ class ResolvedContext(TypedDict, total=False):
28
+ gitBranch: Optional[str]
29
+ jiraTicket: Optional[str]
30
+ pagerdutyIncident: Optional[str]
31
+ ciPipeline: Optional[str]
32
+ workingDirectory: Optional[str]
33
+ workspaceId: Optional[str]
34
+ sessionId: Optional[str]
35
+
36
+ class BudgetCheckResult(TypedDict):
37
+ allowed: bool
38
+ remaining_usd: float
@@ -0,0 +1,21 @@
1
+ [tool.poetry]
2
+ name = "intutic-clawde"
3
+ version = "1.8.0"
4
+ description = "Programmatic proxy connection wrappers and warm-path budget gates for Intutic"
5
+ authors = ["Intutic <engineering@intutic.ai>"]
6
+ readme = "README.md"
7
+ packages = [{include = "intutic_clawde"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ # 3.9 reached end of life in October 2025. The patched releases of requests and
11
+ # urllib3 below both require >=3.10, so supporting 3.9 means shipping known CVEs.
12
+ python = "^3.10"
13
+ requests = "^2.33.0"
14
+ urllib3 = ">=2.7.0"
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ pytest = "^9.0.3"
18
+
19
+ [build-system]
20
+ requires = ["poetry-core"]
21
+ build-backend = "poetry.core.masonry.api"