bloom-openclaw-skill 1.0.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.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: bloom-openclaw-skill
3
+ Version: 1.0.0
4
+ Summary: Secure proxy skill for OpenClaw agents
5
+ Home-page: https://github.com/bloomtechnologies/bloom-openclaw-skill
6
+ Author: Bloom Technologies
7
+ Author-email: Bloom Technologies <support@bloomtechnologies.app>
8
+ License: MIT
9
+ Project-URL: Homepage, https://bloomtechnologies.app
10
+ Project-URL: Documentation, https://docs.bloomtechnologies.app
11
+ Project-URL: Repository, https://github.com/bloomtechnologies/bloom-openclaw-skill
12
+ Project-URL: Issues, https://github.com/bloomtechnologies/bloom-openclaw-skill/issues
13
+ Keywords: openclaw,security,proxy,iam,authentication,ai-agent
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Security
25
+ Classifier: Topic :: Internet :: Proxy Servers
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: black>=23.0.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
32
+ Dynamic: author
33
+ Dynamic: home-page
34
+ Dynamic: requires-python
35
+
36
+ # Bloom Secure Proxy for OpenClaw
37
+
38
+ Secure your OpenClaw agent in 3 minutes with Bloom's IAM layer.
39
+
40
+ ## Quick Start
41
+
42
+ ### 1. Get Your Bloom Token
43
+
44
+ 1. Sign up at [platform.bloomtechnologies.app](https://platform.bloomtechnologies.app)
45
+ 2. Create an agent in the dashboard
46
+ 3. Get your API key from Profile > API Keys
47
+ 4. Combine as: `bloom_<api_key>_agent_<agent_id>`
48
+
49
+ ### 2. Install the Skill
50
+
51
+ ```bash
52
+ # Clone to your OpenClaw skills directory
53
+ cd ~/.openclaw/skills
54
+ git clone https://github.com/bloomtechnologies/bloom-openclaw-skill bloom-secure-proxy
55
+
56
+ # Or install via pip
57
+ pip install bloom-openclaw-skill
58
+ ```
59
+
60
+ ### 3. Configure
61
+
62
+ Add to your `.env`:
63
+
64
+ ```bash
65
+ BLOOM_AGENT_TOKEN=bloom_xxx_agent_yyy
66
+ ```
67
+
68
+ ### 4. Use It
69
+
70
+ ```python
71
+ from bloom_proxy import bloom_get, bloom_post
72
+
73
+ # All requests now go through Bloom's secure proxy
74
+ response = bloom_get("https://api.github.com/user")
75
+ ```
76
+
77
+ ## Features
78
+
79
+ - **Zero-config security** - Just set one environment variable
80
+ - **Granular permissions** - Control access at method + path level
81
+ - **Full audit trail** - Every request logged in Bloom dashboard
82
+ - **Instant kill switch** - Halt agent immediately if compromised
83
+ - **Prompt injection protection** - Block malicious payloads
84
+
85
+ ## Documentation
86
+
87
+ See [SKILL.md](./SKILL.md) for full documentation.
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,5 @@
1
+ bloom_proxy.py,sha256=GcwMdnELv6IG8LBGuh8BCl9tUTje_iVBa9zbTPuVJdI,11373
2
+ bloom_openclaw_skill-1.0.0.dist-info/METADATA,sha256=DhKjFE1lU-UAWxCdUqUnSmkP1y6kJvSTE0EMXt82fW0,2773
3
+ bloom_openclaw_skill-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
4
+ bloom_openclaw_skill-1.0.0.dist-info/top_level.txt,sha256=a8W8uQhzW91wbftxOZjSKsbeFHkiWezQkURSMh0FrJs,12
5
+ bloom_openclaw_skill-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ bloom_proxy
bloom_proxy.py ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bloom Secure Proxy Skill for OpenClaw
4
+
5
+ Routes all outbound API requests through Bloom's IAM proxy for
6
+ authentication, authorization, audit logging, and security scanning.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import hashlib
12
+ import time
13
+ from typing import Optional, Dict, Any, List
14
+ from urllib.request import Request, urlopen
15
+ from urllib.error import HTTPError, URLError
16
+
17
+ # Configuration
18
+ BLOOM_PROXY_URL = os.environ.get(
19
+ "BLOOM_PROXY_URL",
20
+ "https://iam.bloomtechnologies.app"
21
+ )
22
+ BLOOM_AGENT_TOKEN = os.environ.get("BLOOM_AGENT_TOKEN")
23
+ BLOOM_ORG_ID = os.environ.get("BLOOM_ORG_ID")
24
+
25
+ # Timeouts
26
+ REQUEST_TIMEOUT = int(os.environ.get("BLOOM_TIMEOUT", "30"))
27
+ MAX_RETRIES = int(os.environ.get("BLOOM_RETRIES", "3"))
28
+
29
+ # Cache (optional, in-memory)
30
+ _response_cache: Dict[str, tuple] = {}
31
+ CACHE_ENABLED = os.environ.get("BLOOM_CACHE_ENABLED", "false").lower() == "true"
32
+ CACHE_TTL = int(os.environ.get("BLOOM_CACHE_TTL", "300"))
33
+
34
+
35
+ class BloomProxyError(Exception):
36
+ """Raised when Bloom proxy returns an error."""
37
+ def __init__(self, status_code: int, message: str, details: Optional[dict] = None):
38
+ self.status_code = status_code
39
+ self.message = message
40
+ self.details = details or {}
41
+ super().__init__(f"Bloom Proxy Error {status_code}: {message}")
42
+
43
+
44
+ class BloomSecurityBlock(Exception):
45
+ """Raised when Bloom blocks a request for security reasons."""
46
+ def __init__(self, reason: str, threat_type: str):
47
+ self.reason = reason
48
+ self.threat_type = threat_type
49
+ super().__init__(f"Request blocked: {reason} (threat: {threat_type})")
50
+
51
+
52
+ class BloomKillSwitchActive(Exception):
53
+ """Raised when the agent has been killed."""
54
+ def __init__(self, agent_id: str, killed_at: str):
55
+ self.agent_id = agent_id
56
+ self.killed_at = killed_at
57
+ super().__init__(f"Agent {agent_id} has been killed at {killed_at}")
58
+
59
+
60
+ def _get_cache_key(method: str, url: str, body: Optional[str]) -> str:
61
+ """Generate cache key for a request."""
62
+ content = f"{method}:{url}:{body or ''}"
63
+ return hashlib.sha256(content.encode()).hexdigest()
64
+
65
+
66
+ def _check_cache(cache_key: str) -> Optional[dict]:
67
+ """Check if response is cached and not expired."""
68
+ if not CACHE_ENABLED:
69
+ return None
70
+
71
+ if cache_key in _response_cache:
72
+ response, timestamp = _response_cache[cache_key]
73
+ if time.time() - timestamp < CACHE_TTL:
74
+ return response
75
+ else:
76
+ del _response_cache[cache_key]
77
+ return None
78
+
79
+
80
+ def _store_cache(cache_key: str, response: dict):
81
+ """Store response in cache."""
82
+ if CACHE_ENABLED:
83
+ _response_cache[cache_key] = (response, time.time())
84
+
85
+
86
+ def bloom_request(
87
+ method: str,
88
+ url: str,
89
+ headers: Optional[Dict[str, str]] = None,
90
+ body: Optional[Any] = None,
91
+ timeout: Optional[int] = None,
92
+ skip_cache: bool = False
93
+ ) -> Dict[str, Any]:
94
+ """
95
+ Route an HTTP request through Bloom's secure proxy.
96
+
97
+ Args:
98
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
99
+ url: Target URL
100
+ headers: Optional headers to include
101
+ body: Optional request body (will be JSON-encoded if dict)
102
+ timeout: Optional timeout override
103
+ skip_cache: Skip cache lookup/storage
104
+
105
+ Returns:
106
+ dict with 'status_code', 'headers', 'body' keys
107
+
108
+ Raises:
109
+ BloomProxyError: If proxy returns an error
110
+ BloomSecurityBlock: If request is blocked for security
111
+ BloomKillSwitchActive: If agent has been killed
112
+ """
113
+ if not BLOOM_AGENT_TOKEN:
114
+ raise BloomProxyError(
115
+ 401,
116
+ "BLOOM_AGENT_TOKEN not set. Please configure your Bloom credentials."
117
+ )
118
+
119
+ # Serialize body
120
+ body_str = None
121
+ if body is not None:
122
+ if isinstance(body, (dict, list)):
123
+ body_str = json.dumps(body)
124
+ else:
125
+ body_str = str(body)
126
+
127
+ # Check cache
128
+ cache_key = _get_cache_key(method, url, body_str)
129
+ if not skip_cache:
130
+ cached = _check_cache(cache_key)
131
+ if cached:
132
+ return cached
133
+
134
+ # Build proxy URL - use the direct pass-through format
135
+ # e.g., https://iam.bloomtechnologies.app/https://api.openai.com/v1/chat/completions
136
+ proxy_url = f"{BLOOM_PROXY_URL}/{url}"
137
+
138
+ proxy_headers = {
139
+ "Content-Type": headers.get("Content-Type", "application/json") if headers else "application/json",
140
+ "Authorization": f"Bearer {BLOOM_AGENT_TOKEN}",
141
+ "X-Bloom-Agent-Version": "1.0.0",
142
+ "X-Bloom-Skill": "bloom-openclaw-skill"
143
+ }
144
+
145
+ # Forward original headers (except Authorization which Bloom will inject)
146
+ if headers:
147
+ for key, value in headers.items():
148
+ if key.lower() not in ["authorization", "content-type"]:
149
+ proxy_headers[key] = value
150
+
151
+ if BLOOM_ORG_ID:
152
+ proxy_headers["X-Bloom-Org-ID"] = BLOOM_ORG_ID
153
+
154
+ # Make request with retries
155
+ last_error = None
156
+ for attempt in range(MAX_RETRIES):
157
+ try:
158
+ req = Request(
159
+ proxy_url,
160
+ data=body_str.encode("utf-8") if body_str else None,
161
+ headers=proxy_headers,
162
+ method=method.upper()
163
+ )
164
+
165
+ with urlopen(req, timeout=timeout or REQUEST_TIMEOUT) as resp:
166
+ response_body = resp.read().decode("utf-8")
167
+
168
+ result = {
169
+ "status_code": resp.status,
170
+ "headers": dict(resp.headers),
171
+ "body": response_body
172
+ }
173
+
174
+ # Try to parse JSON response
175
+ try:
176
+ result["json"] = json.loads(response_body)
177
+ except json.JSONDecodeError:
178
+ result["json"] = None
179
+
180
+ # Cache successful GET responses
181
+ if method.upper() == "GET" and result["status_code"] == 200:
182
+ _store_cache(cache_key, result)
183
+
184
+ return result
185
+
186
+ except HTTPError as e:
187
+ error_body = e.read().decode("utf-8")
188
+ try:
189
+ error_data = json.loads(error_body)
190
+ except json.JSONDecodeError:
191
+ error_data = {"message": error_body}
192
+
193
+ # Check for kill switch
194
+ if e.code == 403 and error_data.get("error") == "Agent killed":
195
+ raise BloomKillSwitchActive(
196
+ error_data.get("agent_id", "unknown"),
197
+ error_data.get("killed_at", "unknown")
198
+ )
199
+
200
+ # Check for security block
201
+ if e.code == 403 and error_data.get("security_block"):
202
+ raise BloomSecurityBlock(
203
+ error_data.get("reason", "Request blocked"),
204
+ error_data.get("threat_type", "unknown")
205
+ )
206
+
207
+ # Don't retry 4xx errors (except 429)
208
+ if 400 <= e.code < 500 and e.code != 429:
209
+ raise BloomProxyError(e.code, error_data.get("message", str(e)), error_data)
210
+
211
+ last_error = BloomProxyError(e.code, error_data.get("message", str(e)), error_data)
212
+
213
+ except URLError as e:
214
+ last_error = BloomProxyError(0, f"Network error: {e.reason}")
215
+
216
+ # Exponential backoff
217
+ if attempt < MAX_RETRIES - 1:
218
+ time.sleep(2 ** attempt)
219
+
220
+ raise last_error
221
+
222
+
223
+ def bloom_get(url: str, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
224
+ """Convenience wrapper for GET requests."""
225
+ return bloom_request("GET", url, headers=headers, **kwargs)
226
+
227
+
228
+ def bloom_post(url: str, body: Any, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
229
+ """Convenience wrapper for POST requests."""
230
+ return bloom_request("POST", url, headers=headers, body=body, **kwargs)
231
+
232
+
233
+ def bloom_put(url: str, body: Any, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
234
+ """Convenience wrapper for PUT requests."""
235
+ return bloom_request("PUT", url, headers=headers, body=body, **kwargs)
236
+
237
+
238
+ def bloom_delete(url: str, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
239
+ """Convenience wrapper for DELETE requests."""
240
+ return bloom_request("DELETE", url, headers=headers, **kwargs)
241
+
242
+
243
+ def bloom_patch(url: str, body: Any, headers: Optional[Dict[str, str]] = None, **kwargs) -> Dict[str, Any]:
244
+ """Convenience wrapper for PATCH requests."""
245
+ return bloom_request("PATCH", url, headers=headers, body=body, **kwargs)
246
+
247
+
248
+ # Health check
249
+ def check_bloom_connection() -> Dict[str, Any]:
250
+ """Verify connection to Bloom proxy and get agent status."""
251
+ try:
252
+ req = Request(
253
+ f"{BLOOM_PROXY_URL}/health",
254
+ headers={"Authorization": f"Bearer {BLOOM_AGENT_TOKEN}"} if BLOOM_AGENT_TOKEN else {}
255
+ )
256
+ with urlopen(req, timeout=5) as resp:
257
+ data = json.loads(resp.read().decode("utf-8"))
258
+ return {
259
+ "connected": True,
260
+ "proxy_status": data.get("status", "unknown"),
261
+ "proxy_url": BLOOM_PROXY_URL,
262
+ "agent_token_set": bool(BLOOM_AGENT_TOKEN)
263
+ }
264
+ except Exception as e:
265
+ return {
266
+ "connected": False,
267
+ "error": str(e),
268
+ "proxy_url": BLOOM_PROXY_URL,
269
+ "agent_token_set": bool(BLOOM_AGENT_TOKEN)
270
+ }
271
+
272
+
273
+ def get_agent_status() -> Dict[str, Any]:
274
+ """Get the current agent's status from Bloom."""
275
+ if not BLOOM_AGENT_TOKEN:
276
+ return {"error": "BLOOM_AGENT_TOKEN not set"}
277
+
278
+ try:
279
+ # Extract agent ID from token if it's in the format bloom_xxx_agent_yyy
280
+ agent_id = None
281
+ if "_agent_" in BLOOM_AGENT_TOKEN:
282
+ agent_id = BLOOM_AGENT_TOKEN.split("_agent_")[1]
283
+
284
+ req = Request(
285
+ f"{BLOOM_PROXY_URL}/health",
286
+ headers={"Authorization": f"Bearer {BLOOM_AGENT_TOKEN}"}
287
+ )
288
+ with urlopen(req, timeout=5) as resp:
289
+ return {
290
+ "status": "active",
291
+ "agent_id": agent_id,
292
+ "proxy_reachable": True
293
+ }
294
+ except HTTPError as e:
295
+ if e.code == 403:
296
+ return {
297
+ "status": "killed",
298
+ "agent_id": agent_id if 'agent_id' in dir() else None,
299
+ "proxy_reachable": True
300
+ }
301
+ return {
302
+ "status": "error",
303
+ "error": str(e),
304
+ "proxy_reachable": True
305
+ }
306
+ except Exception as e:
307
+ return {
308
+ "status": "error",
309
+ "error": str(e),
310
+ "proxy_reachable": False
311
+ }
312
+
313
+
314
+ if __name__ == "__main__":
315
+ # Self-test
316
+ print("Bloom Secure Proxy - Connection Test")
317
+ print("=" * 40)
318
+
319
+ status = check_bloom_connection()
320
+
321
+ if status["connected"]:
322
+ print(f" Connected: Yes")
323
+ print(f" Proxy URL: {status['proxy_url']}")
324
+ print(f" Proxy Status: {status['proxy_status']}")
325
+ print(f" Agent Token Set: {status['agent_token_set']}")
326
+ else:
327
+ print(f" Connected: No")
328
+ print(f" Error: {status.get('error', 'Unknown')}")
329
+ print(f" Proxy URL: {status['proxy_url']}")
330
+ print(f" Agent Token Set: {status['agent_token_set']}")
331
+
332
+ print()
333
+
334
+ if not status['agent_token_set']:
335
+ print("Set BLOOM_AGENT_TOKEN environment variable to enable proxy.")
336
+ print("Get your token at: https://platform.bloomtechnologies.app/dashboard")