Skip to content

Commit 4f622aa

Browse files
committed
Add webhook verification, markdown memory, skills scanning, metrics — bump to v0.6.0
Brings SDK to feature parity with Symbiont Runtime v1.4.0: - MarkdownMemoryStore for file-based agent context persistence - HMAC/JWT webhook signature verification with provider presets - ClawHavoc skill scanner (10 built-in rules) and skill loader - Metrics collection/export with file, OTLP stub, and composite backends - MetricsClient sub-client for runtime metrics API - 45 new tests (120 total), all passing
1 parent fa5718d commit 4f622aa

14 files changed

Lines changed: 1995 additions & 1 deletion

CHANGELOG.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.0] - 2026-02-15
9+
10+
### Added
11+
12+
#### Markdown Memory Persistence
13+
- **MarkdownMemoryStore** — File-based agent context persistence using markdown format
14+
- `save_context()` / `load_context()` — Atomic save with daily log files
15+
- `delete_context()` / `list_agent_contexts()` — Context lifecycle management
16+
- `compact()` — Remove log files older than retention period
17+
- `get_storage_stats()` — Storage statistics across all agents
18+
19+
#### Webhook Verification
20+
- **HmacVerifier** — HMAC-SHA256 webhook signature verification with prefix stripping
21+
- **JwtVerifier** — JWT-based webhook verification with optional issuer validation
22+
- **WebhookProvider** — Pre-configured providers (GitHub, Stripe, Slack, Custom) with factory method
23+
- **SignatureVerifier** ABC for custom verifier implementations
24+
25+
#### Agent Skills (ClawHavoc Scanning + Loading)
26+
- **SkillScanner** — Security scanning with 10 built-in ClawHavoc rules and custom rule support
27+
- Detects pipe-to-shell, wget-pipe-to-shell, env file references, SOUL.md/memory.md tampering, eval+fetch, base64-decode-exec, rm-rf, chmod-777
28+
- **SkillLoader** — Skill discovery and loading from configured paths
29+
- YAML frontmatter parsing for skill metadata
30+
- Optional SchemaPin signature verification (soft dependency)
31+
- Configurable scan-on-load behavior
32+
33+
#### Metrics Collection & Export
34+
- **MetricsClient** — Sub-client for runtime metrics API (`GET /metrics/snapshot`, etc.)
35+
- **FileMetricsExporter** — Atomic JSON file export with compact mode
36+
- **OtlpExporter** — OTLP export stub (requires `opentelemetry-api`)
37+
- **CompositeExporter** — Fan-out to multiple export backends
38+
- **MetricsCollector** — Background thread for periodic metrics export
39+
- **MetricsSnapshot** — Serializable snapshot with scheduler, task manager, load balancer, and system metrics
40+
41+
#### New Exceptions
42+
- `WebhookVerificationError`, `SkillLoadError`, `SkillScanError`, `MetricsExportError`, `MetricsConfigError`
43+
44+
#### New Pydantic Models
45+
- Webhook: `WebhookProviderType`, `WebhookVerificationConfig`
46+
- Skills: `SignatureStatusType`, `ScanSeverityType`, `ScanFindingModel`, `ScanResultModel`, `SkillMetadataModel`, `LoadedSkillModel`, `SkillsConfig`
47+
- Metrics: `OtlpProtocol`, `OtlpConfig`, `FileMetricsConfig`, `MetricsConfig`, `SchedulerMetricsSnapshot`, `TaskManagerMetricsSnapshot`, `LoadBalancerMetricsSnapshot`, `SystemResourceMetricsSnapshot`, `MetricsSnapshot`
48+
49+
#### Optional Dependencies
50+
- `skills` extra: `schemapin>=0.2.0`
51+
- `metrics` extra: `opentelemetry-api`, `opentelemetry-sdk`, `opentelemetry-exporter-otlp`
52+
53+
### Changed
54+
- Aligned with Symbiont Runtime v1.4.0
55+
- `Client.metrics_client` property — Lazy-loaded `MetricsClient` sub-client
56+
- All new types exported from `symbiont` package
57+
58+
---
59+
860
## [0.5.0] - 2026-02-11
961

1062
### Added

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ dev = [
4848
"ruff>=0.1.0",
4949
"bandit>=1.7.0",
5050
]
51+
skills = [
52+
"schemapin>=0.2.0",
53+
]
54+
metrics = [
55+
"opentelemetry-api>=1.20.0",
56+
"opentelemetry-sdk>=1.20.0",
57+
"opentelemetry-exporter-otlp>=1.20.0",
58+
]
5159

5260
[project.urls]
5361
Homepage = "https://github.com/thirdkeyai/symbiont-sdk-python"

symbiont/__init__.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,22 @@
77
from .exceptions import (
88
APIError,
99
AuthenticationError,
10+
MetricsConfigError,
11+
MetricsExportError,
1012
NotFoundError,
1113
RateLimitError,
14+
SkillLoadError,
15+
SkillScanError,
1216
SymbiontError,
17+
WebhookVerificationError,
18+
)
19+
from .markdown_memory import AgentMemoryContext, MarkdownMemoryStore, StorageStats
20+
from .metrics import (
21+
CompositeExporter,
22+
FileMetricsExporter,
23+
MetricsClient,
24+
MetricsCollector,
25+
MetricsSnapshot,
1326
)
1427
from .models import (
1528
# Core Agent Models
@@ -97,11 +110,23 @@
97110
ScheduleSummary,
98111
UpdateScheduleRequest,
99112
)
113+
from .skills import (
114+
LoadedSkill,
115+
ScanFinding,
116+
ScanResult,
117+
ScanSeverity,
118+
SignatureStatus,
119+
SkillLoader,
120+
SkillLoaderConfig,
121+
SkillMetadata,
122+
SkillScanner,
123+
)
124+
from .webhooks import HmacVerifier, JwtVerifier, SignatureVerifier, WebhookProvider
100125

101126
# Load environment variables from .env file
102127
load_dotenv()
103128

104-
__version__ = "0.5.0"
129+
__version__ = "0.6.0"
105130

106131
__all__ = [
107132
# Client
@@ -155,10 +180,29 @@
155180
'ScheduleActionResponse', 'DeleteScheduleResponse',
156181
'SchedulerHealthResponse',
157182

183+
# Webhook Verification
184+
'WebhookProvider', 'HmacVerifier', 'JwtVerifier', 'SignatureVerifier',
185+
186+
# Markdown Memory
187+
'MarkdownMemoryStore', 'AgentMemoryContext', 'StorageStats',
188+
189+
# Skills
190+
'SkillLoader', 'SkillScanner', 'LoadedSkill', 'ScanResult', 'ScanFinding',
191+
'ScanSeverity', 'SignatureStatus', 'SkillLoaderConfig', 'SkillMetadata',
192+
193+
# Metrics
194+
'MetricsClient', 'MetricsCollector', 'MetricsSnapshot',
195+
'FileMetricsExporter', 'CompositeExporter',
196+
158197
# Exceptions
159198
'SymbiontError',
160199
'APIError',
161200
'AuthenticationError',
162201
'NotFoundError',
163202
'RateLimitError',
203+
'WebhookVerificationError',
204+
'SkillLoadError',
205+
'SkillScanError',
206+
'MetricsExportError',
207+
'MetricsConfigError',
164208
]

symbiont/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def __init__(self,
153153
self._schedules: Optional[ScheduleClient] = None
154154
self._channels: Optional[ChannelClient] = None
155155
self._agentpin: Optional[Any] = None
156+
self._metrics_client: Optional[Any] = None
156157

157158
@property
158159
def schedules(self) -> ScheduleClient:
@@ -176,6 +177,14 @@ def agentpin(self):
176177
self._agentpin = AgentPinClient(self)
177178
return self._agentpin
178179

180+
@property
181+
def metrics_client(self):
182+
"""Lazy-loaded metrics client for runtime metrics queries."""
183+
if self._metrics_client is None:
184+
from .metrics import MetricsClient
185+
self._metrics_client = MetricsClient(self)
186+
return self._metrics_client
187+
179188
def _request(self, method: str, endpoint: str, **kwargs):
180189
"""Make an HTTP request to the API.
181190

symbiont/exceptions.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,77 @@ def __init__(self, message: str = "Endpoint rate limit exceeded", endpoint_id: s
283283
"""
284284
super().__init__(message, 429)
285285
self.endpoint_id = endpoint_id
286+
287+
288+
# =============================================================================
289+
# Phase 5 Webhook, Skill & Metrics Exception Classes
290+
# =============================================================================
291+
292+
class WebhookVerificationError(SymbiontError):
293+
"""Raised when webhook signature verification fails."""
294+
295+
def __init__(self, message: str = "Webhook verification failed", header_name: str = None):
296+
"""Initialize the WebhookVerificationError.
297+
298+
Args:
299+
message: Error message describing the verification failure.
300+
header_name: Optional header name that failed verification.
301+
"""
302+
super().__init__(message)
303+
self.header_name = header_name
304+
305+
306+
class SkillLoadError(SymbiontError):
307+
"""Raised when a skill fails to load."""
308+
309+
def __init__(self, message: str = "Skill load error", skill_path: str = None):
310+
"""Initialize the SkillLoadError.
311+
312+
Args:
313+
message: Error message describing the load failure.
314+
skill_path: Optional path to the skill that failed to load.
315+
"""
316+
super().__init__(message)
317+
self.skill_path = skill_path
318+
319+
320+
class SkillScanError(SymbiontError):
321+
"""Raised when skill scanning encounters an error."""
322+
323+
def __init__(self, message: str = "Skill scan error", findings_count: int = None):
324+
"""Initialize the SkillScanError.
325+
326+
Args:
327+
message: Error message describing the scan error.
328+
findings_count: Optional number of findings detected.
329+
"""
330+
super().__init__(message)
331+
self.findings_count = findings_count
332+
333+
334+
class MetricsExportError(SymbiontError):
335+
"""Raised when metrics export operations fail."""
336+
337+
def __init__(self, message: str = "Metrics export failed", backend: str = None):
338+
"""Initialize the MetricsExportError.
339+
340+
Args:
341+
message: Error message describing the export failure.
342+
backend: Optional backend name that failed.
343+
"""
344+
super().__init__(message)
345+
self.backend = backend
346+
347+
348+
class MetricsConfigError(SymbiontError):
349+
"""Raised when metrics configuration is invalid."""
350+
351+
def __init__(self, message: str = "Metrics configuration error", config_field: str = None):
352+
"""Initialize the MetricsConfigError.
353+
354+
Args:
355+
message: Error message describing the configuration issue.
356+
config_field: Optional configuration field that is invalid.
357+
"""
358+
super().__init__(message)
359+
self.config_field = config_field

0 commit comments

Comments
 (0)