reskpoints 0.1.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.
reskpoints/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ from .agent_logger import AgentLogger
2
+ from .config import AgentLoggerConfig
3
+ from .decorator import log_action
4
+ from .masking import FieldMasker
5
+ from .models import ActionLog, LogResult
6
+ from .platforms import (
7
+ BasePlatform,
8
+ ConsolePlatform,
9
+ FilePlatform,
10
+ MockPlatform,
11
+ WebhookPlatform,
12
+ )
13
+
14
+ __version__ = "0.1.0"
15
+ __author__ = "RESK Security"
16
+
17
+ __all__ = [
18
+ "AgentLogger",
19
+ "AgentLoggerConfig",
20
+ "log_action",
21
+ "FieldMasker",
22
+ "ActionLog",
23
+ "LogResult",
24
+ "BasePlatform",
25
+ "ConsolePlatform",
26
+ "FilePlatform",
27
+ "MockPlatform",
28
+ "WebhookPlatform",
29
+ ]
@@ -0,0 +1,179 @@
1
+ import os
2
+ import platform
3
+ import random
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .config import AgentLoggerConfig
9
+ from .masking import FieldMasker
10
+ from .models import ActionLog, LogResult, PlatformHealth
11
+ from .platforms import (
12
+ BasePlatform,
13
+ ConsolePlatform,
14
+ FilePlatform,
15
+ MockPlatform,
16
+ WebhookPlatform,
17
+ )
18
+
19
+
20
+ class AgentLogger:
21
+ def __init__(self, config: AgentLoggerConfig | str | Path | None = None):
22
+ if isinstance(config, (str, Path)):
23
+ self.config = AgentLoggerConfig.from_file(str(config))
24
+ elif isinstance(config, AgentLoggerConfig):
25
+ self.config = config
26
+ else:
27
+ self.config = AgentLoggerConfig.from_file("reskpoints.yaml")
28
+
29
+ self.environment = self.config.environment
30
+ self.host = platform.node()
31
+
32
+ self.masker = FieldMasker(
33
+ sensitive_fields=self.config.sensitive_fields,
34
+ enabled=self.config.masking_enabled,
35
+ )
36
+
37
+ self.platforms: dict[str, BasePlatform] = {}
38
+ self._init_platforms()
39
+
40
+ def _init_platforms(self):
41
+ platform_configs = self.config.platforms_config
42
+ registry: dict[str, type[BasePlatform]] = {
43
+ "console": ConsolePlatform,
44
+ "file": FilePlatform,
45
+ "webhook": WebhookPlatform,
46
+ }
47
+
48
+ try:
49
+ from .platforms.prometheus import PrometheusPlatform
50
+ registry["prometheus"] = PrometheusPlatform
51
+ except ImportError:
52
+ pass
53
+
54
+ try:
55
+ from .platforms.datadog import DatadogPlatform
56
+ registry["datadog"] = DatadogPlatform
57
+ except ImportError:
58
+ pass
59
+
60
+ for name, platform_cls in registry.items():
61
+ cfg = platform_configs.get(name, {})
62
+ if cfg.get("enabled", False):
63
+ instance = platform_cls(cfg)
64
+ self.platforms[name] = instance
65
+
66
+ if not self.platforms:
67
+ self.platforms["mock"] = MockPlatform({"enabled": True})
68
+
69
+ def log(
70
+ self,
71
+ agent_id: str,
72
+ action: str,
73
+ probability: float = 1.0,
74
+ params: dict[str, Any] | None = None,
75
+ result: Any = None,
76
+ success: bool = True,
77
+ duration_ms: float | None = None,
78
+ session_id: str | None = None,
79
+ correlation_id: str | None = None,
80
+ sensitive_fields: list[str] | None = None,
81
+ **metadata: Any,
82
+ ) -> list[LogResult]:
83
+ entry = ActionLog(
84
+ agent_id=agent_id,
85
+ session_id=session_id,
86
+ correlation_id=correlation_id,
87
+ action=action,
88
+ probability=probability,
89
+ parameters=params or {},
90
+ result=result,
91
+ success=success,
92
+ duration_ms=duration_ms,
93
+ environment=self.environment,
94
+ host=self.host,
95
+ metadata=metadata,
96
+ sensitive_fields=sensitive_fields or [],
97
+ )
98
+ return self.log_action(entry)
99
+
100
+ def log_action(self, entry: ActionLog) -> list[LogResult]:
101
+ rate = self.config.get_sampling_rate(entry.action)
102
+ if rate < 1.0 and random.random() > rate:
103
+ return [LogResult(success=True, platform="sampling_skip", action_id=entry.id)]
104
+
105
+ if self.config.masking_enabled:
106
+ entry.parameters = self.masker.mask(
107
+ entry.parameters,
108
+ extra_fields=entry.sensitive_fields,
109
+ )
110
+
111
+ results: list[LogResult] = []
112
+ for platform in self.platforms.values():
113
+ result = platform.emit(entry)
114
+ results.append(result)
115
+ return results
116
+
117
+ async def alog(
118
+ self,
119
+ agent_id: str,
120
+ action: str,
121
+ probability: float = 1.0,
122
+ params: dict[str, Any] | None = None,
123
+ result: Any = None,
124
+ success: bool = True,
125
+ duration_ms: float | None = None,
126
+ session_id: str | None = None,
127
+ correlation_id: str | None = None,
128
+ sensitive_fields: list[str] | None = None,
129
+ **metadata: Any,
130
+ ) -> list[LogResult]:
131
+ entry = ActionLog(
132
+ agent_id=agent_id,
133
+ session_id=session_id,
134
+ correlation_id=correlation_id,
135
+ action=action,
136
+ probability=probability,
137
+ parameters=params or {},
138
+ result=result,
139
+ success=success,
140
+ duration_ms=duration_ms,
141
+ environment=self.environment,
142
+ host=self.host,
143
+ metadata=metadata,
144
+ sensitive_fields=sensitive_fields or [],
145
+ )
146
+ return await self.alog_action(entry)
147
+
148
+ async def alog_action(self, entry: ActionLog) -> list[LogResult]:
149
+ rate = self.config.get_sampling_rate(entry.action)
150
+ if rate < 1.0 and random.random() > rate:
151
+ return [LogResult(success=True, platform="sampling_skip", action_id=entry.id)]
152
+
153
+ if self.config.masking_enabled:
154
+ entry.parameters = self.masker.mask(
155
+ entry.parameters,
156
+ extra_fields=entry.sensitive_fields,
157
+ )
158
+
159
+ import asyncio
160
+
161
+ results: list[LogResult] = []
162
+ tasks = [platform.aemit(entry) for platform in self.platforms.values()]
163
+ for coro in asyncio.as_completed(tasks):
164
+ result = await coro
165
+ results.append(result)
166
+ return results
167
+
168
+ def flush(self):
169
+ for platform in self.platforms.values():
170
+ platform.flush()
171
+
172
+ def health(self) -> dict[str, Any]:
173
+ return {
174
+ platform_name: platform.health().to_dict()
175
+ for platform_name, platform in self.platforms.items()
176
+ }
177
+
178
+ def get_platform(self, name: str) -> BasePlatform | None:
179
+ return self.platforms.get(name)
reskpoints/cli.py ADDED
@@ -0,0 +1,163 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from .agent_logger import AgentLogger
7
+ from .config import AgentLoggerConfig
8
+
9
+
10
+ def cmd_log(args):
11
+ logger = AgentLogger(args.config)
12
+ params = {}
13
+ if args.params:
14
+ params = json.loads(args.params)
15
+ result = None
16
+ if args.result:
17
+ result = json.loads(args.result)
18
+ results = logger.log(
19
+ agent_id=args.agent_id,
20
+ action=args.action,
21
+ probability=args.probability,
22
+ params=params,
23
+ result=result,
24
+ success=args.success,
25
+ )
26
+ print(json.dumps([r.to_dict() for r in results], indent=2))
27
+ any_fail = any(not r.success for r in results)
28
+ return 1 if any_fail else 0
29
+
30
+
31
+ def cmd_test(args):
32
+ logger = AgentLogger(args.config)
33
+ print(f"Testing agent logging on {len(logger.platforms)} platform(s)...")
34
+ print()
35
+ for name, platform in logger.platforms.items():
36
+ entry = _test_entry()
37
+ result = platform.emit(entry)
38
+ status = "✓" if result.success else "✗"
39
+ dur = f"{result.duration_ms:.1f}ms" if result.duration_ms else "?"
40
+ print(f" [{status}] {name:15s} {dur:>10s} {result.error or ''}")
41
+ print()
42
+ print("Health:")
43
+ for pname, h in logger.health().items():
44
+ print(f" {pname:15s} status={h['status']} queue={h['queue_size']}")
45
+ return 0
46
+
47
+
48
+ def cmd_status(args):
49
+ logger = AgentLogger(args.config)
50
+ print(f"ReskPoints Agent Logger — {logger.environment}")
51
+ print(f"Host: {logger.host}")
52
+ print(f"Platforms: {len(logger.platforms)}")
53
+ print()
54
+ print(f"{'Platform':<20s} {'Status':<12s} {'Queue':<8s} {'Error':<30s}")
55
+ print("-" * 70)
56
+ for pname, h in logger.health().items():
57
+ print(f"{pname:<20s} {h['status']:<12s} {h['queue_size']:<8d} {h.get('error', '') or '':<30s}")
58
+ return 0
59
+
60
+
61
+ def cmd_tail(args):
62
+ logger = AgentLogger(args.config)
63
+ console = logger.get_platform("console")
64
+ if console is None:
65
+ print("error: console platform not enabled", file=sys.stderr)
66
+ return 1
67
+ print(f"Live tailing logs (Ctrl+C to stop)...")
68
+ try:
69
+ import time
70
+ while True:
71
+ time.sleep(1)
72
+ except KeyboardInterrupt:
73
+ print("\nStopped.")
74
+ return 0
75
+
76
+
77
+ def cmd_replay(args):
78
+ path = Path(args.file)
79
+ if not path.exists():
80
+ print(f"error: file not found: {args.file}", file=sys.stderr)
81
+ return 1
82
+ logger = AgentLogger(args.config)
83
+ count = 0
84
+ with open(path, encoding="utf-8") as f:
85
+ for line in f:
86
+ line = line.strip()
87
+ if not line:
88
+ continue
89
+ data = json.loads(line)
90
+ from .models import ActionLog
91
+ entry = ActionLog.from_dict(data)
92
+ logger.log_action(entry)
93
+ count += 1
94
+ print(f"Replayed {count} log entries")
95
+ return 0
96
+
97
+
98
+ def _test_entry():
99
+ from .models import ActionLog
100
+ return ActionLog(
101
+ agent_id="test-agent",
102
+ action="test_action",
103
+ probability=0.95,
104
+ parameters={"test": True, "value": 42},
105
+ result="ok",
106
+ success=True,
107
+ )
108
+
109
+
110
+ def main():
111
+ parser = argparse.ArgumentParser(
112
+ description="ReskPoints Agent Action Logger CLI",
113
+ formatter_class=argparse.RawDescriptionHelpFormatter,
114
+ )
115
+ parser.add_argument(
116
+ "--config", "-c",
117
+ default="reskpoints.yaml",
118
+ help="Path to config YAML (default: reskpoints.yaml)",
119
+ )
120
+
121
+ subparsers = parser.add_subparsers(dest="command", help="Command")
122
+
123
+ # log
124
+ log_p = subparsers.add_parser("log", help="Log an agent action")
125
+ log_p.add_argument("--agent-id", required=True, help="Agent identifier")
126
+ log_p.add_argument("--action", required=True, help="Action name")
127
+ log_p.add_argument("--probability", type=float, default=1.0, help="Action probability")
128
+ log_p.add_argument("--params", help="JSON string of parameters")
129
+ log_p.add_argument("--result", help="JSON string of result")
130
+ log_p.add_argument("--success", action="store_true", default=True, help="Success flag")
131
+
132
+ # test
133
+ test_p = subparsers.add_parser("test", help="Test all platforms")
134
+
135
+ # status
136
+ subparsers.add_parser("status", help="Show platform health")
137
+
138
+ # tail
139
+ subparsers.add_parser("tail", help="Live tail logs (console)")
140
+
141
+ # replay
142
+ replay_p = subparsers.add_parser("replay", help="Replay logs from a file")
143
+ replay_p.add_argument("file", help="Path to JSONL file")
144
+
145
+ args = parser.parse_args()
146
+
147
+ if not args.command:
148
+ parser.print_help()
149
+ return 1
150
+
151
+ commands = {
152
+ "log": cmd_log,
153
+ "test": cmd_test,
154
+ "status": cmd_status,
155
+ "tail": cmd_tail,
156
+ "replay": cmd_replay,
157
+ }
158
+
159
+ return commands[args.command](args)
160
+
161
+
162
+ if __name__ == "__main__":
163
+ sys.exit(main())
reskpoints/config.py ADDED
@@ -0,0 +1,121 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+
9
+ _ENV_VAR_RE = re.compile(r"\$\{([^}:]+)(?::([^}]*))?\}")
10
+
11
+
12
+ def _resolve_env(value: str) -> str:
13
+ def _replace(m: re.Match) -> str:
14
+ var = m.group(1)
15
+ default = m.group(2)
16
+ return os.environ.get(var, default) if default else os.environ.get(var, "")
17
+
18
+ return _ENV_VAR_RE.sub(_replace, value)
19
+
20
+
21
+ def _resolve_env_recursive(obj: Any) -> Any:
22
+ if isinstance(obj, str):
23
+ return _resolve_env(obj) if "${" in obj else obj
24
+ if isinstance(obj, dict):
25
+ return {k: _resolve_env_recursive(v) for k, v in obj.items()}
26
+ if isinstance(obj, list):
27
+ return [_resolve_env_recursive(v) for v in obj]
28
+ return obj
29
+
30
+
31
+ DEFAULT_CONFIG = {
32
+ "agent_logger": {
33
+ "environment": "${ENV:development}",
34
+ "masking": {
35
+ "enabled": True,
36
+ "sensitive_fields": [
37
+ "api_key",
38
+ "password",
39
+ "token",
40
+ "secret",
41
+ "authorization",
42
+ ],
43
+ },
44
+ "sampling": {
45
+ "default_rate": 1.0,
46
+ "rules": [],
47
+ },
48
+ "buffering": {
49
+ "max_size": 1000,
50
+ "flush_interval": 5.0,
51
+ },
52
+ "retry": {
53
+ "max_attempts": 3,
54
+ "backoff": [0.5, 1.5, 4.5],
55
+ "circuit_breaker": {
56
+ "threshold": 5,
57
+ "recovery_time": 30,
58
+ },
59
+ },
60
+ "platforms": {
61
+ "console": {
62
+ "enabled": True,
63
+ "format": "human",
64
+ },
65
+ },
66
+ }
67
+ }
68
+
69
+
70
+ class AgentLoggerConfig:
71
+ def __init__(self, config: dict[str, Any] | None = None):
72
+ resolved = _resolve_env_recursive(config or {})
73
+ self._raw = resolved
74
+
75
+ section = resolved.get("agent_logger", {})
76
+ self.environment = section.get("environment", "development")
77
+ self.masking_config = section.get("masking", {})
78
+ self.sampling_config = section.get("sampling", {})
79
+ self.buffering_config = section.get("buffering", {})
80
+ self.retry_config = section.get("retry", {})
81
+ self.platforms_config = section.get("platforms", {})
82
+
83
+ @property
84
+ def masking_enabled(self) -> bool:
85
+ return self.masking_config.get("enabled", True)
86
+
87
+ @property
88
+ def sensitive_fields(self) -> list[str]:
89
+ return self.masking_config.get("sensitive_fields", [])
90
+
91
+ @property
92
+ def default_rate(self) -> float:
93
+ return float(self.sampling_config.get("default_rate", 1.0))
94
+
95
+ @property
96
+ def sampling_rules(self) -> list[dict[str, Any]]:
97
+ return self.sampling_config.get("rules", [])
98
+
99
+ def get_sampling_rate(self, action: str) -> float:
100
+ for rule in self.sampling_rules:
101
+ pattern = rule.get("action", "")
102
+ if pattern == action or (pattern.endswith("*") and action.startswith(pattern[:-1])):
103
+ return float(rule.get("rate", self.default_rate))
104
+ return self.default_rate
105
+
106
+ def get_platform_config(self, name: str) -> dict[str, Any]:
107
+ return self.platforms_config.get(name, {})
108
+
109
+ @classmethod
110
+ def from_file(cls, path: str | Path) -> "AgentLoggerConfig":
111
+ path = Path(path)
112
+ if path.exists():
113
+ with open(path, encoding="utf-8") as f:
114
+ data = yaml.safe_load(f) or {}
115
+ else:
116
+ data = {}
117
+ merged = {**DEFAULT_CONFIG, **(data or {})}
118
+ return cls(merged)
119
+
120
+ def to_dict(self) -> dict[str, Any]:
121
+ return self._raw
@@ -0,0 +1,83 @@
1
+ import functools
2
+ import inspect
3
+ import time
4
+ from typing import Any, Callable, TypeVar
5
+
6
+ from .agent_logger import AgentLogger
7
+ from .models import ActionLog
8
+
9
+ F = TypeVar("F", bound=Callable[..., Any])
10
+
11
+
12
+ def log_action(
13
+ agent_id: str | None = None,
14
+ action_name: str | None = None,
15
+ logger: AgentLogger | None = None,
16
+ capture_params: bool = True,
17
+ capture_result: bool = True,
18
+ **extra_metadata: Any,
19
+ ) -> Callable[[F], F]:
20
+ def decorator(func: F) -> F:
21
+ _action_name = action_name or func.__name__
22
+ module = inspect.getmodule(func)
23
+ module_name = module.__name__ if module else ""
24
+
25
+ @functools.wraps(func)
26
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
27
+ _logger = logger or _get_default_logger()
28
+ _agent_id = agent_id or _resolve_agent_id(func)
29
+ t0 = time.time()
30
+ parameters: dict[str, Any] = {}
31
+ if capture_params:
32
+ sig = inspect.signature(func)
33
+ bound = sig.bind(*args, **kwargs)
34
+ bound.apply_defaults()
35
+ parameters = dict(bound.arguments)
36
+ for k, v in parameters.items():
37
+ if hasattr(v, "__dict__"):
38
+ parameters[k] = f"<{type(v).__name__}>"
39
+ try:
40
+ result = func(*args, **kwargs)
41
+ duration = (time.time() - t0) * 1000
42
+ _logger.log(
43
+ agent_id=_agent_id,
44
+ action=f"{module_name}.{_action_name}",
45
+ probability=1.0,
46
+ params=parameters,
47
+ result=result if capture_result else None,
48
+ success=True,
49
+ duration_ms=duration,
50
+ **extra_metadata,
51
+ )
52
+ return result
53
+ except Exception as e:
54
+ duration = (time.time() - t0) * 1000
55
+ _logger.log(
56
+ agent_id=_agent_id,
57
+ action=f"{module_name}.{_action_name}",
58
+ probability=1.0,
59
+ params=parameters,
60
+ result=str(e),
61
+ success=False,
62
+ duration_ms=duration,
63
+ **extra_metadata,
64
+ )
65
+ raise
66
+
67
+ return wrapper # type: ignore
68
+
69
+ return decorator
70
+
71
+
72
+ _default_logger: AgentLogger | None = None
73
+
74
+
75
+ def _get_default_logger() -> AgentLogger:
76
+ global _default_logger
77
+ if _default_logger is None:
78
+ _default_logger = AgentLogger()
79
+ return _default_logger
80
+
81
+
82
+ def _resolve_agent_id(func: Callable[..., Any]) -> str:
83
+ return f"{func.__module__}.{func.__qualname__}"
reskpoints/masking.py ADDED
@@ -0,0 +1,82 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ DEFAULT_SENSITIVE_FIELDS = [
5
+ "api_key",
6
+ "api_key",
7
+ "password",
8
+ "token",
9
+ "secret",
10
+ "authorization",
11
+ "auth_token",
12
+ "access_token",
13
+ "refresh_token",
14
+ "private_key",
15
+ "session_id",
16
+ ]
17
+
18
+ DEFAULT_PATTERNS: list[tuple[str, str]] = [
19
+ (r"\b[A-Za-z0-9_\-]{20,}\b", "***"),
20
+ (r"\b(?:sk-[a-zA-Z0-9]{20,}|pk-[a-zA-Z0-9]{20,})\b", "sk-***"),
21
+ (r"\b[A-Za-z0-9+/]{40,}(?:=){0,2}\b", "***"),
22
+ ]
23
+
24
+
25
+ class FieldMasker:
26
+ def __init__(
27
+ self,
28
+ sensitive_fields: list[str] | None = None,
29
+ patterns: list[tuple[str, str]] | None = None,
30
+ enabled: bool = True,
31
+ ):
32
+ self.sensitive_fields = set(sensitive_fields or DEFAULT_SENSITIVE_FIELDS)
33
+ self.patterns = [(re.compile(p), m) for p, m in (patterns or DEFAULT_PATTERNS)]
34
+ self.enabled = enabled
35
+
36
+ def mask(self, data: dict[str, Any], extra_fields: list[str] | None = None) -> dict[str, Any]:
37
+ if not self.enabled:
38
+ return data
39
+
40
+ fields_to_mask = self.sensitive_fields | set(extra_fields or [])
41
+ result: dict[str, Any] = {}
42
+
43
+ for key, value in data.items():
44
+ if key in fields_to_mask:
45
+ result[key] = self._mask_value(value)
46
+ elif isinstance(value, dict):
47
+ result[key] = self._mask_nested(value, fields_to_mask, depth=0)
48
+ elif isinstance(value, str):
49
+ result[key] = self._apply_regex(value)
50
+ else:
51
+ result[key] = value
52
+
53
+ return result
54
+
55
+ def _mask_value(self, value: Any) -> str:
56
+ s = str(value)
57
+ if len(s) <= 4:
58
+ return "****"
59
+ return s[:2] + "****" + s[-2:] if len(s) > 6 else "****"
60
+
61
+ def _mask_nested(
62
+ self, data: dict[str, Any], fields_to_mask: set[str], depth: int = 0
63
+ ) -> dict[str, Any]:
64
+ if depth > 5:
65
+ return {"_masked": "too_deep"}
66
+ result: dict[str, Any] = {}
67
+ for key, value in data.items():
68
+ if key in fields_to_mask:
69
+ result[key] = self._mask_value(value)
70
+ elif isinstance(value, dict):
71
+ result[key] = self._mask_nested(value, fields_to_mask, depth + 1)
72
+ elif isinstance(value, str):
73
+ result[key] = self._apply_regex(value)
74
+ else:
75
+ result[key] = value
76
+ return result
77
+
78
+ def _apply_regex(self, value: str) -> str:
79
+ for pattern, replacement in self.patterns:
80
+ if pattern.search(value):
81
+ return pattern.sub(replacement, value)
82
+ return value