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 +29 -0
- reskpoints/agent_logger.py +179 -0
- reskpoints/cli.py +163 -0
- reskpoints/config.py +121 -0
- reskpoints/decorator.py +83 -0
- reskpoints/masking.py +82 -0
- reskpoints/models.py +105 -0
- reskpoints/platforms/__init__.py +13 -0
- reskpoints/platforms/base.py +165 -0
- reskpoints/platforms/console.py +36 -0
- reskpoints/platforms/datadog.py +52 -0
- reskpoints/platforms/file_log.py +22 -0
- reskpoints/platforms/mock.py +23 -0
- reskpoints/platforms/opentelemetry.py +56 -0
- reskpoints/platforms/prometheus.py +39 -0
- reskpoints/platforms/webhook.py +55 -0
- reskpoints/py.typed +0 -0
- reskpoints-0.1.0.dist-info/METADATA +259 -0
- reskpoints-0.1.0.dist-info/RECORD +22 -0
- reskpoints-0.1.0.dist-info/WHEEL +4 -0
- reskpoints-0.1.0.dist-info/entry_points.txt +2 -0
- reskpoints-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
reskpoints/decorator.py
ADDED
|
@@ -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
|