clawdi 0.13.0 → 0.13.2
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.
- package/dist/index.js +401 -406
- package/dist/skills/hosted/clawdi/SKILL.md +29 -0
- package/egress-addon/clawdi_egress_addon.py +31 -2
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clawdi
|
|
3
|
+
description: "Find and read the user's past agent sessions across Claude Code, Codex, OpenClaw, and Hermes; use connected-service tools such as Gmail, GitHub, Notion, Drive, and Calendar; and read Clawdi share URLs (https://cloud.clawdi.ai/s/...) the user pastes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Clawdi Cloud
|
|
7
|
+
|
|
8
|
+
Use Clawdi Cloud tools through the `clawdi` MCP server.
|
|
9
|
+
|
|
10
|
+
## Sessions
|
|
11
|
+
|
|
12
|
+
- Use `session_search` to find past agent conversations by keyword and obtain session UUIDs.
|
|
13
|
+
- Use `session_read` to read a session by UUID or Clawdi share URL.
|
|
14
|
+
|
|
15
|
+
Call `session_read` when the user provides a Clawdi share URL. When the user refers to a
|
|
16
|
+
past conversation without a UUID, call `session_search` first and then read the matching
|
|
17
|
+
session. Do not use a generic web fetcher for Clawdi share URLs.
|
|
18
|
+
|
|
19
|
+
## Connectors
|
|
20
|
+
|
|
21
|
+
Connected-service tools are registered dynamically from the user's Clawdi Cloud account.
|
|
22
|
+
They can include Gmail, GitHub, Notion, Drive, Calendar, and other services.
|
|
23
|
+
|
|
24
|
+
- Treat the tools as already authenticated for this runtime.
|
|
25
|
+
- If a call reports that no account is connected, ask the user to connect that service in
|
|
26
|
+
the Clawdi Cloud dashboard.
|
|
27
|
+
- Download files returned as signed URLs before processing them.
|
|
28
|
+
- Confirm with the user before side-effecting actions such as sending mail or creating an
|
|
29
|
+
issue.
|
|
@@ -12,7 +12,7 @@ import os
|
|
|
12
12
|
import re
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any
|
|
15
|
-
from urllib.parse import parse_qs,
|
|
15
|
+
from urllib.parse import parse_qs, urlsplit
|
|
16
16
|
|
|
17
17
|
try: # pragma: no cover - exercised only under mitmdump.
|
|
18
18
|
from mitmproxy import ctx, http
|
|
@@ -114,6 +114,7 @@ class ClawdiEgressAddon:
|
|
|
114
114
|
self.secret_file_path = Path(secret_file) if secret_file else None
|
|
115
115
|
self.profiles = load_profiles(self.profile_bundle_path)
|
|
116
116
|
self.secrets = load_secrets(self.secret_file_path)
|
|
117
|
+
validate_profile_secrets(self.profiles, self.secrets)
|
|
117
118
|
self.profile_hosts = profile_host_set(self.profiles)
|
|
118
119
|
|
|
119
120
|
def should_intercept_sni(self, server_name: str) -> bool:
|
|
@@ -205,6 +206,30 @@ def load_secrets(path: Path | None) -> dict[str, str]:
|
|
|
205
206
|
return {str(key): str(value) for key, value in raw.items() if isinstance(value, str)}
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
def validate_profile_secrets(
|
|
210
|
+
profiles: list[dict[str, Any]], secrets: dict[str, str]
|
|
211
|
+
) -> None:
|
|
212
|
+
required: set[str] = set()
|
|
213
|
+
for profile in profiles:
|
|
214
|
+
collect_secret_refs(profile, required)
|
|
215
|
+
missing = sorted(ref for ref in required if not secrets.get(ref))
|
|
216
|
+
if missing:
|
|
217
|
+
raise ConfigError(f"egress profile secrets are missing: {', '.join(missing)}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def collect_secret_refs(value: Any, refs: set[str]) -> None:
|
|
221
|
+
if isinstance(value, list):
|
|
222
|
+
for item in value:
|
|
223
|
+
collect_secret_refs(item, refs)
|
|
224
|
+
return
|
|
225
|
+
if not isinstance(value, dict):
|
|
226
|
+
return
|
|
227
|
+
for key, entry in value.items():
|
|
228
|
+
if isinstance(entry, str) and (key == "secretRef" or key.endswith("SecretRef")):
|
|
229
|
+
refs.add(entry)
|
|
230
|
+
collect_secret_refs(entry, refs)
|
|
231
|
+
|
|
232
|
+
|
|
208
233
|
def profile_host_set(profiles: list[dict[str, Any]]) -> set[str]:
|
|
209
234
|
hosts: set[str] = set()
|
|
210
235
|
for profile in profiles:
|
|
@@ -297,9 +322,13 @@ def host_matches(flow: Any, profile: dict[str, Any]) -> bool:
|
|
|
297
322
|
match = profile.get("match")
|
|
298
323
|
if not isinstance(match, dict):
|
|
299
324
|
return False
|
|
300
|
-
|
|
325
|
+
expected_authority = str(match.get("host", "")).strip().lower()
|
|
326
|
+
expected_host, expected_port = split_authority(expected_authority)
|
|
327
|
+
expected = normalize_host(expected_host)
|
|
301
328
|
if not expected:
|
|
302
329
|
return False
|
|
330
|
+
if expected_port is not None and getattr(flow.request, "port", None) != expected_port:
|
|
331
|
+
return False
|
|
303
332
|
return expected in request_host_candidates(flow)
|
|
304
333
|
|
|
305
334
|
|