hyperprobe-agent 1.2.26b3__tar.gz → 1.2.27__tar.gz

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.
Files changed (37) hide show
  1. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/PKG-INFO +18 -3
  2. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/README.md +17 -2
  3. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/agent.py +41 -5
  4. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/evaluator.py +55 -17
  5. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/monitoring_engine.py +31 -9
  6. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/safety.py +68 -19
  7. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe_agent.egg-info/PKG-INFO +18 -3
  8. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe_agent.egg-info/SOURCES.txt +1 -0
  9. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/setup.py +1 -1
  10. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_agent.py +39 -0
  11. hyperprobe_agent-1.2.27/tests/test_evaluator.py +134 -0
  12. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_fork.py +5 -0
  13. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_metric_probes.py +29 -0
  14. hyperprobe_agent-1.2.27/tests/test_safety.py +149 -0
  15. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_startup.py +91 -2
  16. hyperprobe_agent-1.2.26b3/tests/test_evaluator.py +0 -65
  17. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/LICENSE +0 -0
  18. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/__init__.py +0 -0
  19. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/__init__.py +0 -0
  20. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/broker.py +0 -0
  21. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/logger.py +0 -0
  22. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/probe_output.py +0 -0
  23. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/quota.py +0 -0
  24. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/serializer.py +0 -0
  25. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/core/trace_extractor.py +0 -0
  26. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/protos/__init__.py +0 -0
  27. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/protos/agent_pb2.py +0 -0
  28. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe/protos/agent_pb2_grpc.py +0 -0
  29. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe_agent.egg-info/dependency_links.txt +0 -0
  30. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe_agent.egg-info/requires.txt +0 -0
  31. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/hyperprobe_agent.egg-info/top_level.txt +0 -0
  32. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/setup.cfg +0 -0
  33. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_logger.py +0 -0
  34. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_multithreading_integration.py +0 -0
  35. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_p1_regressions.py +0 -0
  36. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_probe_output.py +0 -0
  37. {hyperprobe_agent-1.2.26b3 → hyperprobe_agent-1.2.27}/tests/test_serializer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperprobe-agent
3
- Version: 1.2.26b3
3
+ Version: 1.2.27
4
4
  Summary: Production-grade, non-breaking live debugger and telemetry agent for Python.
5
5
  Home-page: https://www.hyperprobe.co
6
6
  Author: Arif, Saksham, Karan
@@ -98,7 +98,8 @@ Configure the agent using the following environment variables:
98
98
  | Variable | Description | Default |
99
99
  | --- | --- | --- |
100
100
  | `HYPERPROBE_BROKER_URL` | The URL of the HyperProbe Telemetry Broker. | *Required* |
101
- | `HYPERPROBE_SERVICE_ID` | A unique name identifying this service. | *Required* |
101
+ | `HYPERPROBE_SERVICE_ID` | UUIDv4 service identifier provided by the HyperProbe dashboard. | *Required* |
102
+ | `HYPERPROBE_ALLOW_NON_UUID` | Set to `yes` only to allow a legacy non-UUID service identifier. | *(unset)* |
102
103
  | `HYPERPROBE_ENVIRONMENT` | Deployment environment name (e.g., `production`, `staging`). | *Required* |
103
104
  | `HYPERPROBE_COMMIT_SHA` or `GIT_COMMIT` | Git commit SHA of the running application version. | *Required* |
104
105
  | `HYPERPROBE_DISABLED` | Set to `YES` to explicitly disable the agent. | *(unset)* |
@@ -109,7 +110,7 @@ Configure the agent using the following environment variables:
109
110
  | `HYPERPROBE_BANDWIDTH_KB_PER_SEC` | Max bandwidth limit for telemetry transmissions (quota). | `1024` (1 MB) |
110
111
  | `HYPERPROBE_RPC_TIMEOUT_SEC` | Deadline for broker RPCs. | `10` |
111
112
  | `HYPERPROBE_COOLDOWN_SEC` | Tracing suspension duration after a RED safety state. | `10` |
112
- | `HYPERPROBE_MAX_LAG_MS` | Execution-thread lag threshold for the safety monitor. | `50` |
113
+ | `HYPERPROBE_MAX_LAG_MS` | Per-reading scheduler-lag threshold. YELLOW requires 4/10 breaches; RED requires 7/10 breaches or 3/5 readings above 4x the threshold. | `50` |
113
114
  | `HYPERPROBE_PAUSE_BUDGET_MS` | Per-second capture pause budget for the safety monitor. | `15` |
114
115
  | `HYPERPROBE_REDACT_KEYS` | Comma-separated key patterns to redact. | `password,secret,token,authorization,cookie,key,signature` |
115
116
  | `HYPERPROBE_REDACT_VALUES` | Comma-separated value patterns to redact. | *(empty)* |
@@ -118,8 +119,22 @@ Configure the agent using the following environment variables:
118
119
  | `HYPERPROBE_STACK_FRAME_DEPTH` | Maximum captured stack-frame depth. | `3` |
119
120
  | `HYPERPROBE_MAX_OBJECT_PROPERTIES` | Maximum serialized properties per object. | `50` |
120
121
  | `HYPERPROBE_MAX_STRING_LENGTH` | Maximum serialized string length. | `1024` |
122
+ | `HYPERPROBE_DISABLE_SAFE_EVALUATION` | Set to `true` to explicitly disable safe evaluation restrictions and allow arbitrary method/function calls in probe expressions. | `false` |
121
123
  | `HYPERPROBE_FORK_MODE` | Set to `worker` for preloaded, prefork servers. The parent defers agent initialization and every worker starts fresh process-local state. | `none` |
122
124
 
125
+ Programmatic integrations can set `allow_non_uuid=True` for the same legacy
126
+ bypass. Without an explicit bypass, a non-UUIDv4 service ID prevents agent
127
+ startup while the host application continues normally.
128
+
129
+ ### Safe Evaluation & Security Considerations
130
+
131
+ By default, **safe evaluation is enabled** (`HYPERPROBE_DISABLE_SAFE_EVALUATION=false`). In safe mode:
132
+ - Method and function calls are strictly restricted to a narrow allowlist of side-effect free standard operations.
133
+ - State mutations, assignments, and arbitrary function invocations (such as database calls, network I/O, file operations, or state modifications) are prevented.
134
+
135
+ **Disabling Safe Evaluation (`HYPERPROBE_DISABLE_SAFE_EVALUATION=true`):**
136
+ > ⚠️ **Security Warning**: Setting `HYPERPROBE_DISABLE_SAFE_EVALUATION=true` allows conditions, watch expressions, and log templates to execute arbitrary functions and methods in the host application process. This may lead to unintended side effects, state mutations, performance degradation, or security risks if expressions invoke mutating or blocking operations. Only disable safe evaluation in trusted environments where dynamic method evaluation is strictly required.
137
+
123
138
  ### Debug logging
124
139
 
125
140
  Set `DEBUG` to a comma- or space-separated list of namespaces. Patterns accept
@@ -63,7 +63,8 @@ Configure the agent using the following environment variables:
63
63
  | Variable | Description | Default |
64
64
  | --- | --- | --- |
65
65
  | `HYPERPROBE_BROKER_URL` | The URL of the HyperProbe Telemetry Broker. | *Required* |
66
- | `HYPERPROBE_SERVICE_ID` | A unique name identifying this service. | *Required* |
66
+ | `HYPERPROBE_SERVICE_ID` | UUIDv4 service identifier provided by the HyperProbe dashboard. | *Required* |
67
+ | `HYPERPROBE_ALLOW_NON_UUID` | Set to `yes` only to allow a legacy non-UUID service identifier. | *(unset)* |
67
68
  | `HYPERPROBE_ENVIRONMENT` | Deployment environment name (e.g., `production`, `staging`). | *Required* |
68
69
  | `HYPERPROBE_COMMIT_SHA` or `GIT_COMMIT` | Git commit SHA of the running application version. | *Required* |
69
70
  | `HYPERPROBE_DISABLED` | Set to `YES` to explicitly disable the agent. | *(unset)* |
@@ -74,7 +75,7 @@ Configure the agent using the following environment variables:
74
75
  | `HYPERPROBE_BANDWIDTH_KB_PER_SEC` | Max bandwidth limit for telemetry transmissions (quota). | `1024` (1 MB) |
75
76
  | `HYPERPROBE_RPC_TIMEOUT_SEC` | Deadline for broker RPCs. | `10` |
76
77
  | `HYPERPROBE_COOLDOWN_SEC` | Tracing suspension duration after a RED safety state. | `10` |
77
- | `HYPERPROBE_MAX_LAG_MS` | Execution-thread lag threshold for the safety monitor. | `50` |
78
+ | `HYPERPROBE_MAX_LAG_MS` | Per-reading scheduler-lag threshold. YELLOW requires 4/10 breaches; RED requires 7/10 breaches or 3/5 readings above 4x the threshold. | `50` |
78
79
  | `HYPERPROBE_PAUSE_BUDGET_MS` | Per-second capture pause budget for the safety monitor. | `15` |
79
80
  | `HYPERPROBE_REDACT_KEYS` | Comma-separated key patterns to redact. | `password,secret,token,authorization,cookie,key,signature` |
80
81
  | `HYPERPROBE_REDACT_VALUES` | Comma-separated value patterns to redact. | *(empty)* |
@@ -83,8 +84,22 @@ Configure the agent using the following environment variables:
83
84
  | `HYPERPROBE_STACK_FRAME_DEPTH` | Maximum captured stack-frame depth. | `3` |
84
85
  | `HYPERPROBE_MAX_OBJECT_PROPERTIES` | Maximum serialized properties per object. | `50` |
85
86
  | `HYPERPROBE_MAX_STRING_LENGTH` | Maximum serialized string length. | `1024` |
87
+ | `HYPERPROBE_DISABLE_SAFE_EVALUATION` | Set to `true` to explicitly disable safe evaluation restrictions and allow arbitrary method/function calls in probe expressions. | `false` |
86
88
  | `HYPERPROBE_FORK_MODE` | Set to `worker` for preloaded, prefork servers. The parent defers agent initialization and every worker starts fresh process-local state. | `none` |
87
89
 
90
+ Programmatic integrations can set `allow_non_uuid=True` for the same legacy
91
+ bypass. Without an explicit bypass, a non-UUIDv4 service ID prevents agent
92
+ startup while the host application continues normally.
93
+
94
+ ### Safe Evaluation & Security Considerations
95
+
96
+ By default, **safe evaluation is enabled** (`HYPERPROBE_DISABLE_SAFE_EVALUATION=false`). In safe mode:
97
+ - Method and function calls are strictly restricted to a narrow allowlist of side-effect free standard operations.
98
+ - State mutations, assignments, and arbitrary function invocations (such as database calls, network I/O, file operations, or state modifications) are prevented.
99
+
100
+ **Disabling Safe Evaluation (`HYPERPROBE_DISABLE_SAFE_EVALUATION=true`):**
101
+ > ⚠️ **Security Warning**: Setting `HYPERPROBE_DISABLE_SAFE_EVALUATION=true` allows conditions, watch expressions, and log templates to execute arbitrary functions and methods in the host application process. This may lead to unintended side effects, state mutations, performance degradation, or security risks if expressions invoke mutating or blocking operations. Only disable safe evaluation in trusted environments where dynamic method evaluation is strictly required.
102
+
88
103
  ### Debug logging
89
104
 
90
105
  Set `DEBUG` to a comma- or space-separated list of namespaces. Patterns accept
@@ -78,6 +78,22 @@ class HyperProbeAgent:
78
78
  max_lag_ms = options.get("hyperprobe_max_lag_ms") or float(os.getenv("HYPERPROBE_MAX_LAG_MS", 50.0))
79
79
  pause_budget_ms = options.get("hyperprobe_pause_budget_ms") or float(os.getenv("HYPERPROBE_PAUSE_BUDGET_MS", 15.0))
80
80
 
81
+ opt_disable_safe_eval = options.get("disable_safe_evaluation", False)
82
+ disable_safe_eval = (
83
+ os.getenv("HYPERPROBE_DISABLE_SAFE_EVALUATION", "").strip().lower() == "true"
84
+ or opt_disable_safe_eval is True
85
+ or (
86
+ isinstance(opt_disable_safe_eval, str)
87
+ and opt_disable_safe_eval.strip().lower() == "true"
88
+ )
89
+ )
90
+
91
+ if disable_safe_eval:
92
+ logger.forceInfo(
93
+ "\033[1m\033[33m⚠️ [HyperProbe] WARN: Safe evaluation is DISABLED via "
94
+ "HYPERPROBE_DISABLE_SAFE_EVALUATION. Function and method calls are permitted in probe expressions.\033[0m"
95
+ )
96
+
81
97
  self.global_config = {
82
98
  "redact_keys": options.get("redact_keys") or os.getenv("HYPERPROBE_REDACT_KEYS", "password,secret,token,authorization,cookie,key,signature").split(","),
83
99
  "redact_values": options.get("redact_values") or os.getenv("HYPERPROBE_REDACT_VALUES", "").split(","),
@@ -85,7 +101,8 @@ class HyperProbeAgent:
85
101
  "max_array_length": options.get("max_array_length") or int(os.getenv("HYPERPROBE_MAX_ARRAY_LENGTH", 3)),
86
102
  "stack_frame_depth": options.get("stack_frame_depth") or int(os.getenv("HYPERPROBE_STACK_FRAME_DEPTH", 3)),
87
103
  "max_object_properties": options.get("max_object_properties") or int(os.getenv("HYPERPROBE_MAX_OBJECT_PROPERTIES", 50)),
88
- "max_string_length": options.get("max_string_length") or int(os.getenv("HYPERPROBE_MAX_STRING_LENGTH", 1024))
104
+ "max_string_length": options.get("max_string_length") or int(os.getenv("HYPERPROBE_MAX_STRING_LENGTH", 1024)),
105
+ "disable_safe_evaluation": disable_safe_eval,
89
106
  }
90
107
 
91
108
  # Strip empty redaction strings
@@ -108,6 +125,7 @@ class HyperProbeAgent:
108
125
  agent_id=self.agent_id,
109
126
  rpc_timeout_sec=rpc_timeout_sec,
110
127
  )
128
+ self.agent_version = self.broker_client.agent_version
111
129
 
112
130
  # Thread-safe queue for telemetry events
113
131
  self.telemetry_queue = queue.Queue(maxsize=self.max_queue_size)
@@ -168,7 +186,21 @@ class HyperProbeAgent:
168
186
  logger.forceError('\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. HYPERPROBE_SERVICE_ID is required.\033[0m')
169
187
  return None
170
188
  if not UUID_REGEX.match(str(service_id).strip()):
171
- logger.forceInfo('\033[1m\033[33m⚠️ [HyperProbe] WARN: service_id is not a valid UUID, please check again.\033[0m')
189
+ allow_non_uuid = options.get("allow_non_uuid")
190
+ if allow_non_uuid is None:
191
+ allow_non_uuid = (
192
+ os.getenv("HYPERPROBE_ALLOW_NON_UUID", "")
193
+ .strip()
194
+ .lower()
195
+ == "yes"
196
+ )
197
+ elif isinstance(allow_non_uuid, str):
198
+ allow_non_uuid = allow_non_uuid.strip().lower() == "yes"
199
+
200
+ if not allow_non_uuid:
201
+ logger.forceError('\033[1m\033[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. service_id is not a valid UUIDv4. Set HYPERPROBE_ALLOW_NON_UUID=yes to allow non-UUID identifiers.\033[0m')
202
+ return None
203
+ logger.forceInfo('\033[1m\033[33m⚠️ [HyperProbe] WARN: service_id is not a valid UUIDv4, please check again.\033[0m')
172
204
  options["service_id"] = service_id
173
205
 
174
206
  environment = options.get("environment") or os.getenv("HYPERPROBE_ENVIRONMENT")
@@ -215,7 +247,8 @@ class HyperProbeAgent:
215
247
  return None
216
248
  logger.forceInfo(
217
249
  f"[HyperProbe] Python agent successfully started "
218
- f"(ID: {cls._instance.agent_id}, PID: {cls._instance.owner_pid})."
250
+ f"(Version: {cls._instance.agent_version}, "
251
+ f"ID: {cls._instance.agent_id}, PID: {cls._instance.owner_pid})."
219
252
  )
220
253
  return cls._instance
221
254
 
@@ -308,7 +341,8 @@ class HyperProbeAgent:
308
341
 
309
342
  logger.forceInfo(
310
343
  f"[HyperProbe] Python agent started after fork "
311
- f"(ID: {instance.agent_id}, PID: {instance.owner_pid})."
344
+ f"(Version: {instance.agent_version}, "
345
+ f"ID: {instance.agent_id}, PID: {instance.owner_pid})."
312
346
  )
313
347
 
314
348
  @classmethod
@@ -716,7 +750,7 @@ class HyperProbeAgent:
716
750
  return
717
751
 
718
752
  if health == AgentHealth.RED:
719
- logger.error(f"[HyperProbe] Safety Shield Triggered (RED): {reason or ''}. Suspending tracing.")
753
+ logger.error(f"[HyperProbe] Safety Shield Triggered (RED): {reason or ''}. Suspending instrumentation.")
720
754
  with self._agent_lock:
721
755
  self.engine.suspend()
722
756
 
@@ -739,6 +773,7 @@ class HyperProbeAgent:
739
773
  with self._agent_lock:
740
774
  cooldown_expired = self.cooldown_timer is None
741
775
  if cooldown_expired:
776
+ logger.info(f"[HyperProbe] Status back to GREEN. {reason or ''}.")
742
777
  self._resume_instrumentation()
743
778
 
744
779
  def _cooldown_expired(self, cooldown_generation):
@@ -752,6 +787,7 @@ class HyperProbeAgent:
752
787
  with self._agent_lock:
753
788
  if (
754
789
  self.is_shutdown
790
+ or not self.engine.is_suspended
755
791
  or self.safety_monitor.get_health() != AgentHealth.GREEN
756
792
  or self.cooldown_timer is not None
757
793
  ):
@@ -1,6 +1,8 @@
1
+ import os
1
2
  import re
2
3
  import ast
3
4
  from functools import lru_cache
5
+ from typing import Optional
4
6
 
5
7
  from hyperprobe.core.logger import get_logger
6
8
 
@@ -23,6 +25,9 @@ SAFE_METHODS = {
23
25
  "isdigit", "isalpha", "isnumeric", "isalnum",
24
26
  }
25
27
 
28
+ def is_safe_evaluation_enabled() -> bool:
29
+ return os.getenv("HYPERPROBE_DISABLE_SAFE_EVALUATION", "").strip().lower() != "true"
30
+
26
31
  class SafeASTVisitor(ast.NodeVisitor):
27
32
  def visit_Call(self, node):
28
33
  # 1. Is it a direct function call? e.g. len(items)
@@ -46,11 +51,20 @@ class ProbeEvaluator:
46
51
  _placeholder_regex = re.compile(r'\$?\{([^}]+)\}')
47
52
 
48
53
  @classmethod
49
- def _get_or_compile_safe(cls, expr: str):
54
+ def is_safe_evaluation_enabled(cls) -> bool:
55
+ return is_safe_evaluation_enabled()
56
+
57
+ @classmethod
58
+ def _get_or_compile(cls, expr: str, safe: bool = True):
50
59
  # Normalize the string to prevent duplicate cache entries
51
60
  clean_expr = expr.strip()
61
+ if safe:
62
+ return cls._compile_safe(clean_expr)
63
+ return cls._compile_unsafe(clean_expr)
52
64
 
53
- return cls._compile_safe(clean_expr)
65
+ @classmethod
66
+ def _get_or_compile_safe(cls, expr: str):
67
+ return cls._get_or_compile(expr, safe=True)
54
68
 
55
69
  @staticmethod
56
70
  @lru_cache(maxsize=COMPILED_CACHE_SIZE)
@@ -69,24 +83,46 @@ class ProbeEvaluator:
69
83
 
70
84
  return compiled_code
71
85
 
86
+ @staticmethod
87
+ @lru_cache(maxsize=COMPILED_CACHE_SIZE)
88
+ def _compile_unsafe(clean_expr: str):
89
+ # Unsafe mode allows function/method calls without AST validation
90
+ return compile(clean_expr, filename="<string>", mode="eval")
91
+
72
92
  @classmethod
73
- def safe_eval(cls, expr: str, globals_dict: dict, locals_dict: dict):
74
- # Create a shadow copy of globals, forcefully removing dangerous builtins
75
- safe_globals = globals_dict.copy()
76
- safe_globals["__builtins__"] = SAFE_BUILTINS
93
+ def evaluate(cls, expr: str, globals_dict: dict, locals_dict: dict, safe: Optional[bool] = None):
94
+ if safe is None:
95
+ safe = cls.is_safe_evaluation_enabled()
77
96
 
78
- # Grab the pre-compiled bytecode instantly
79
- compiled = cls._get_or_compile_safe(expr)
80
-
81
- # Execute the bytecode
82
- return eval(compiled, safe_globals, locals_dict)
97
+ if safe:
98
+ # Create a shadow copy of globals, forcefully removing dangerous builtins
99
+ safe_globals = globals_dict.copy()
100
+ safe_globals["__builtins__"] = SAFE_BUILTINS
101
+
102
+ # Grab the pre-compiled safe bytecode
103
+ compiled = cls._get_or_compile(expr, safe=True)
104
+
105
+ # Execute the bytecode
106
+ return eval(compiled, safe_globals, locals_dict)
107
+ else:
108
+ # Grab the pre-compiled unsafe bytecode allowing full execution
109
+ compiled = cls._get_or_compile(expr, safe=False)
110
+ return eval(compiled, globals_dict, locals_dict)
111
+
112
+ @classmethod
113
+ def safe_eval(cls, expr: str, globals_dict: dict, locals_dict: dict, safe: Optional[bool] = None):
114
+ """Evaluates an expression, delegating to ProbeEvaluator.evaluate."""
115
+ return cls.evaluate(expr, globals_dict, locals_dict, safe=safe)
83
116
 
84
117
  @classmethod
85
- def evaluate_log_template(cls, template: str, globals_dict: dict, locals_dict: dict) -> str:
86
- """Parses and safely formats a log template string."""
118
+ def evaluate_log_template(cls, template: str, globals_dict: dict, locals_dict: dict, safe: Optional[bool] = None) -> str:
119
+ """Parses and formats a log template string."""
87
120
  if not template:
88
121
  return ""
89
122
 
123
+ if safe is None:
124
+ safe = cls.is_safe_evaluation_enabled()
125
+
90
126
  result = []
91
127
  last_idx = 0
92
128
  for match in cls._placeholder_regex.finditer(template):
@@ -95,8 +131,8 @@ class ProbeEvaluator:
95
131
  expr = match.group(1).strip()
96
132
 
97
133
  try:
98
- # Safely execute expression
99
- val = cls.safe_eval(expr, globals_dict, locals_dict)
134
+ # Execute expression
135
+ val = cls.evaluate(expr, globals_dict, locals_dict, safe=safe)
100
136
  result.append(str(val))
101
137
  except Exception as e:
102
138
  result.append(f"<Error: {type(e).__name__}: {str(e)}>")
@@ -106,14 +142,16 @@ class ProbeEvaluator:
106
142
  return "".join(result)
107
143
 
108
144
  @classmethod
109
- def evaluate_watches(cls, watch_expressions: list, globals_dict: dict, locals_dict: dict) -> dict:
145
+ def evaluate_watches(cls, watch_expressions: list, globals_dict: dict, locals_dict: dict, safe: Optional[bool] = None) -> dict:
110
146
  """Evaluates a set of independent watch expressions."""
111
147
  results = {}
112
148
  if not watch_expressions:
113
149
  return results
150
+ if safe is None:
151
+ safe = cls.is_safe_evaluation_enabled()
114
152
  for expr in watch_expressions:
115
153
  try:
116
- results[expr] = cls.safe_eval(expr, globals_dict, locals_dict)
154
+ results[expr] = cls.evaluate(expr, globals_dict, locals_dict, safe=safe)
117
155
  except Exception as e:
118
156
  results[expr] = f"Error: {type(e).__name__}: {str(e)}"
119
157
  return results
@@ -49,6 +49,7 @@ class MonitoringEngine:
49
49
  self.instrumented_files = set()
50
50
  self.is_active = False
51
51
  self.is_suspended = False
52
+ self.safe_evaluation = ProbeEvaluator.is_safe_evaluation_enabled()
52
53
  self.global_config = {}
53
54
  self._redact_keys_re = None
54
55
  self._redact_values_re = None
@@ -112,8 +113,25 @@ class MonitoringEngine:
112
113
  self.global_config = candidate
113
114
  self._redact_keys_re = redact_keys_re
114
115
  self._redact_values_re = redact_values_re
116
+ self.safe_evaluation = self._is_safe_evaluation_enabled(candidate)
115
117
  return True
116
118
 
119
+ def _is_safe_evaluation_enabled(self, config_snapshot=None) -> bool:
120
+ """Resolves whether safe evaluation is active from a config snapshot or internal state."""
121
+ config = config_snapshot if config_snapshot is not None else self.global_config
122
+ if config:
123
+ if "disable_safe_evaluation" in config:
124
+ v = config["disable_safe_evaluation"]
125
+ if isinstance(v, str):
126
+ v = v.strip().lower() == "true"
127
+ return not bool(v)
128
+ if "safe_evaluation" in config:
129
+ v = config["safe_evaluation"]
130
+ if isinstance(v, str):
131
+ v = v.strip().lower() == "true"
132
+ return bool(v)
133
+ return self.safe_evaluation
134
+
117
135
  @staticmethod
118
136
  def _compile_redaction_patterns(patterns):
119
137
  if patterns is None:
@@ -396,10 +414,11 @@ class MonitoringEngine:
396
414
  config_snapshot = self.global_config.copy() if self.global_config else {}
397
415
  redact_keys_re = self._redact_keys_re
398
416
  redact_values_re = self._redact_values_re
417
+ safe_evaluation = self._is_safe_evaluation_enabled(config_snapshot)
399
418
 
400
419
  if probe.condition:
401
420
  try:
402
- cond_val = ProbeEvaluator.safe_eval(probe.condition, globals_dict, locals_dict)
421
+ cond_val = ProbeEvaluator.evaluate(probe.condition, globals_dict, locals_dict, safe=safe_evaluation)
403
422
  if not cond_val:
404
423
  return
405
424
  except Exception as e:
@@ -440,7 +459,7 @@ class MonitoringEngine:
440
459
  serialization_context = {}
441
460
 
442
461
  if getattr(probe, 'watch_expressions', None):
443
- watches_result = ProbeEvaluator.evaluate_watches(probe.watch_expressions, globals_dict, locals_dict)
462
+ watches_result = ProbeEvaluator.evaluate_watches(probe.watch_expressions, globals_dict, locals_dict, safe=safe_evaluation)
444
463
  serialized_watches = {}
445
464
  for watch_name, watch_value in watches_result.items():
446
465
  watch_path = f"watch[{json.dumps(str(watch_name))}]"
@@ -498,7 +517,7 @@ class MonitoringEngine:
498
517
 
499
518
  # LOG TEMPLATE CAPTURE
500
519
  elif probe.type == 2:
501
- evaluated = ProbeEvaluator.evaluate_log_template(probe.template, globals_dict, locals_dict)
520
+ evaluated = ProbeEvaluator.evaluate_log_template(probe.template, globals_dict, locals_dict, safe=safe_evaluation)
502
521
  if redact_values_re and redact_values_re.search(evaluated):
503
522
  evaluated = redact_values_re.sub("[REDACTED Value]", evaluated)
504
523
  if len(evaluated) > max_string_length:
@@ -529,11 +548,14 @@ class MonitoringEngine:
529
548
  globals_dict = frame.f_globals
530
549
  locals_dict = frame.f_locals
531
550
 
551
+ with self.lock:
552
+ safe_evaluation = self._is_safe_evaluation_enabled()
553
+
532
554
  try:
533
555
  if probe.condition:
534
556
  try:
535
- if not ProbeEvaluator.safe_eval(
536
- probe.condition, globals_dict, locals_dict
557
+ if not ProbeEvaluator.evaluate(
558
+ probe.condition, globals_dict, locals_dict, safe=safe_evaluation
537
559
  ):
538
560
  return
539
561
  except Exception as error:
@@ -553,8 +575,8 @@ class MonitoringEngine:
553
575
  # an empty expression reaches the SDK.
554
576
  return
555
577
  try:
556
- raw_value = ProbeEvaluator.safe_eval(
557
- metric_expression, globals_dict, locals_dict
578
+ raw_value = ProbeEvaluator.evaluate(
579
+ metric_expression, globals_dict, locals_dict, safe=safe_evaluation
558
580
  )
559
581
  except Exception as error:
560
582
  self._report_metric_error(probe, str(error))
@@ -578,8 +600,8 @@ class MonitoringEngine:
578
600
  )
579
601
  if correlation_expression:
580
602
  try:
581
- correlation_value = ProbeEvaluator.safe_eval(
582
- correlation_expression, globals_dict, locals_dict
603
+ correlation_value = ProbeEvaluator.evaluate(
604
+ correlation_expression, globals_dict, locals_dict, safe=safe_evaluation
583
605
  )
584
606
  except Exception as error:
585
607
  self._report_metric_error(probe, f"Error: {error}")
@@ -1,5 +1,6 @@
1
1
  import time
2
2
  import threading
3
+ from collections import deque
3
4
 
4
5
  class AgentHealth:
5
6
  GREEN = 'GREEN' # All good
@@ -8,6 +9,14 @@ class AgentHealth:
8
9
 
9
10
 
10
11
  class SafetyMonitor:
12
+ _SAMPLE_INTERVAL_SECONDS = 0.1
13
+ _LAG_WINDOW_SIZE = 10
14
+ _LAG_YELLOW_BREACHES = 4
15
+ _LAG_RED_BREACHES = 7
16
+ _SEVERE_LAG_WINDOW_SIZE = 5
17
+ _SEVERE_LAG_RED_BREACHES = 3
18
+ _SEVERE_LAG_MULTIPLIER = 4.0
19
+
11
20
  def __init__(self, on_state_change, max_lag_ms=50.0, pause_budget_ms=15.0):
12
21
  self.on_state_change = on_state_change
13
22
  self.max_lag_ms = max_lag_ms
@@ -17,6 +26,7 @@ class SafetyMonitor:
17
26
  self.cumulative_pause_time = 0.0
18
27
  self.last_window_reset = time.monotonic()
19
28
  self.last_thread_lag_ms = 0.0
29
+ self.thread_lag_samples = deque(maxlen=self._LAG_WINDOW_SIZE)
20
30
 
21
31
  self.lock = threading.RLock()
22
32
  self._notification_lock = threading.RLock()
@@ -42,47 +52,79 @@ class SafetyMonitor:
42
52
  def report_pause_duration(self, ms: float):
43
53
  with self.lock:
44
54
  self.cumulative_pause_time += ms
45
- transition = self._evaluate_health_locked(self.last_thread_lag_ms)
55
+ transition = self._evaluate_health_locked()
46
56
 
47
57
  self._notify_state_change(transition)
48
58
 
49
59
  def _run_loop(self):
50
- last_heartbeat = time.monotonic()
60
+ while self.is_running:
61
+ wait_started_at = time.monotonic()
62
+ if self.stop_event.wait(self._SAMPLE_INTERVAL_SECONDS):
63
+ break
51
64
 
52
- while self.is_running and not self.stop_event.wait(0.1):
53
65
  now = time.monotonic()
54
- thread_lag_ms = (now - last_heartbeat - 0.1) * 1000.0
55
- last_heartbeat = now
66
+ thread_lag_ms = max(
67
+ 0.0,
68
+ (
69
+ now
70
+ - wait_started_at
71
+ - self._SAMPLE_INTERVAL_SECONDS
72
+ ) * 1000.0,
73
+ )
56
74
 
57
75
  with self.lock:
58
- self.last_thread_lag_ms = thread_lag_ms
76
+ self._record_thread_lag_locked(thread_lag_ms)
59
77
  if now - self.last_window_reset > 1.0:
60
78
  self.cumulative_pause_time = 0.0
61
79
  self.last_window_reset = now
62
80
 
63
- transition = self._evaluate_health_locked(thread_lag_ms)
81
+ transition = self._evaluate_health_locked()
64
82
 
65
83
  self._notify_state_change(transition)
66
84
 
67
- def _check_health(self, thread_lag_ms: float = 0.0):
85
+ def _check_health(self, thread_lag_ms=None):
68
86
  """
69
- Preserves the existing method signature while ensuring that
70
- callbacks execute outside the state lock.
87
+ Records an optional lag sample and executes callbacks outside the
88
+ state lock.
71
89
  """
72
90
  with self.lock:
73
- transition = self._evaluate_health_locked(thread_lag_ms)
91
+ if thread_lag_ms is not None:
92
+ self._record_thread_lag_locked(thread_lag_ms)
93
+ transition = self._evaluate_health_locked()
74
94
 
75
95
  self._notify_state_change(transition)
76
96
 
77
- def _evaluate_health_locked(self, thread_lag_ms: float = 0.0):
97
+ def _record_thread_lag_locked(self, thread_lag_ms: float):
98
+ thread_lag_ms = max(0.0, thread_lag_ms)
99
+ self.last_thread_lag_ms = thread_lag_ms
100
+ self.thread_lag_samples.append(thread_lag_ms)
101
+
102
+ def _evaluate_health_locked(self):
78
103
  previous_health = self.health
79
104
  reason = None
80
105
 
81
- if thread_lag_ms > self.max_lag_ms:
106
+ recent_lags = list(self.thread_lag_samples)
107
+ lag_breaches = sum(
108
+ lag > self.max_lag_ms for lag in recent_lags
109
+ )
110
+ severe_lag_ms = self.max_lag_ms * self._SEVERE_LAG_MULTIPLIER
111
+ severe_lag_breaches = sum(
112
+ lag > severe_lag_ms
113
+ for lag in recent_lags[-self._SEVERE_LAG_WINDOW_SIZE:]
114
+ )
115
+
116
+ if severe_lag_breaches >= self._SEVERE_LAG_RED_BREACHES:
82
117
  self.health = AgentHealth.RED
83
118
  reason = (
84
- f"Execution Thread Lag ({thread_lag_ms:.1f}ms) exceeded "
85
- f"limit ({self.max_lag_ms}ms)"
119
+ f"Severe Execution Thread Lag exceeded {severe_lag_ms}ms in "
120
+ f"{severe_lag_breaches}/{self._SEVERE_LAG_WINDOW_SIZE} "
121
+ f"recent readings"
122
+ )
123
+ elif lag_breaches >= self._LAG_RED_BREACHES:
124
+ self.health = AgentHealth.RED
125
+ reason = (
126
+ f"Execution Thread Lag exceeded {self.max_lag_ms}ms in "
127
+ f"{lag_breaches}/{self._LAG_WINDOW_SIZE} recent readings"
86
128
  )
87
129
  elif self.cumulative_pause_time > self.pause_budget_ms:
88
130
  self.health = AgentHealth.RED
@@ -90,12 +132,19 @@ class SafetyMonitor:
90
132
  f"Cumulative Pause Budget ({self.cumulative_pause_time:.1f}ms) "
91
133
  f"exceeded limit ({self.pause_budget_ms}ms)"
92
134
  )
93
- elif thread_lag_ms > self.max_lag_ms / 2.0:
135
+ elif severe_lag_breaches > 0:
136
+ self.health = AgentHealth.YELLOW
137
+ reason = (
138
+ f"Moderate impact: Severe Execution Thread Lag exceeded "
139
+ f"{severe_lag_ms}ms in {severe_lag_breaches}/"
140
+ f"{self._SEVERE_LAG_WINDOW_SIZE} recent readings"
141
+ )
142
+ elif lag_breaches >= self._LAG_YELLOW_BREACHES:
94
143
  self.health = AgentHealth.YELLOW
95
144
  reason = (
96
- f"Moderate impact: Execution Thread Lag "
97
- f"({thread_lag_ms:.1f}ms) reached 50% of limit "
98
- f"({self.max_lag_ms}ms)"
145
+ f"Moderate impact: Execution Thread Lag exceeded "
146
+ f"{self.max_lag_ms}ms in {lag_breaches}/"
147
+ f"{self._LAG_WINDOW_SIZE} recent readings"
99
148
  )
100
149
  elif self.cumulative_pause_time > self.pause_budget_ms / 2.0:
101
150
  self.health = AgentHealth.YELLOW
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperprobe-agent
3
- Version: 1.2.26b3
3
+ Version: 1.2.27
4
4
  Summary: Production-grade, non-breaking live debugger and telemetry agent for Python.
5
5
  Home-page: https://www.hyperprobe.co
6
6
  Author: Arif, Saksham, Karan
@@ -98,7 +98,8 @@ Configure the agent using the following environment variables:
98
98
  | Variable | Description | Default |
99
99
  | --- | --- | --- |
100
100
  | `HYPERPROBE_BROKER_URL` | The URL of the HyperProbe Telemetry Broker. | *Required* |
101
- | `HYPERPROBE_SERVICE_ID` | A unique name identifying this service. | *Required* |
101
+ | `HYPERPROBE_SERVICE_ID` | UUIDv4 service identifier provided by the HyperProbe dashboard. | *Required* |
102
+ | `HYPERPROBE_ALLOW_NON_UUID` | Set to `yes` only to allow a legacy non-UUID service identifier. | *(unset)* |
102
103
  | `HYPERPROBE_ENVIRONMENT` | Deployment environment name (e.g., `production`, `staging`). | *Required* |
103
104
  | `HYPERPROBE_COMMIT_SHA` or `GIT_COMMIT` | Git commit SHA of the running application version. | *Required* |
104
105
  | `HYPERPROBE_DISABLED` | Set to `YES` to explicitly disable the agent. | *(unset)* |
@@ -109,7 +110,7 @@ Configure the agent using the following environment variables:
109
110
  | `HYPERPROBE_BANDWIDTH_KB_PER_SEC` | Max bandwidth limit for telemetry transmissions (quota). | `1024` (1 MB) |
110
111
  | `HYPERPROBE_RPC_TIMEOUT_SEC` | Deadline for broker RPCs. | `10` |
111
112
  | `HYPERPROBE_COOLDOWN_SEC` | Tracing suspension duration after a RED safety state. | `10` |
112
- | `HYPERPROBE_MAX_LAG_MS` | Execution-thread lag threshold for the safety monitor. | `50` |
113
+ | `HYPERPROBE_MAX_LAG_MS` | Per-reading scheduler-lag threshold. YELLOW requires 4/10 breaches; RED requires 7/10 breaches or 3/5 readings above 4x the threshold. | `50` |
113
114
  | `HYPERPROBE_PAUSE_BUDGET_MS` | Per-second capture pause budget for the safety monitor. | `15` |
114
115
  | `HYPERPROBE_REDACT_KEYS` | Comma-separated key patterns to redact. | `password,secret,token,authorization,cookie,key,signature` |
115
116
  | `HYPERPROBE_REDACT_VALUES` | Comma-separated value patterns to redact. | *(empty)* |
@@ -118,8 +119,22 @@ Configure the agent using the following environment variables:
118
119
  | `HYPERPROBE_STACK_FRAME_DEPTH` | Maximum captured stack-frame depth. | `3` |
119
120
  | `HYPERPROBE_MAX_OBJECT_PROPERTIES` | Maximum serialized properties per object. | `50` |
120
121
  | `HYPERPROBE_MAX_STRING_LENGTH` | Maximum serialized string length. | `1024` |
122
+ | `HYPERPROBE_DISABLE_SAFE_EVALUATION` | Set to `true` to explicitly disable safe evaluation restrictions and allow arbitrary method/function calls in probe expressions. | `false` |
121
123
  | `HYPERPROBE_FORK_MODE` | Set to `worker` for preloaded, prefork servers. The parent defers agent initialization and every worker starts fresh process-local state. | `none` |
122
124
 
125
+ Programmatic integrations can set `allow_non_uuid=True` for the same legacy
126
+ bypass. Without an explicit bypass, a non-UUIDv4 service ID prevents agent
127
+ startup while the host application continues normally.
128
+
129
+ ### Safe Evaluation & Security Considerations
130
+
131
+ By default, **safe evaluation is enabled** (`HYPERPROBE_DISABLE_SAFE_EVALUATION=false`). In safe mode:
132
+ - Method and function calls are strictly restricted to a narrow allowlist of side-effect free standard operations.
133
+ - State mutations, assignments, and arbitrary function invocations (such as database calls, network I/O, file operations, or state modifications) are prevented.
134
+
135
+ **Disabling Safe Evaluation (`HYPERPROBE_DISABLE_SAFE_EVALUATION=true`):**
136
+ > ⚠️ **Security Warning**: Setting `HYPERPROBE_DISABLE_SAFE_EVALUATION=true` allows conditions, watch expressions, and log templates to execute arbitrary functions and methods in the host application process. This may lead to unintended side effects, state mutations, performance degradation, or security risks if expressions invoke mutating or blocking operations. Only disable safe evaluation in trusted environments where dynamic method evaluation is strictly required.
137
+
123
138
  ### Debug logging
124
139
 
125
140
  Set `DEBUG` to a comma- or space-separated list of namespaces. Patterns accept
@@ -29,5 +29,6 @@ tests/test_metric_probes.py
29
29
  tests/test_multithreading_integration.py
30
30
  tests/test_p1_regressions.py
31
31
  tests/test_probe_output.py
32
+ tests/test_safety.py
32
33
  tests/test_serializer.py
33
34
  tests/test_startup.py
@@ -7,7 +7,7 @@ long_description = (this_directory / "README.md").read_text() if (this_directory
7
7
 
8
8
  setup(
9
9
  name="hyperprobe-agent",
10
- version="1.2.26beta3",
10
+ version="1.2.27",
11
11
  author="Arif, Saksham, Karan",
12
12
  author_email="arif@hypertest.co, saksham@hypertest.co, karan@hypertest.co",
13
13
  description="Production-grade, non-breaking live debugger and telemetry agent for Python.",