secenvY 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.
Files changed (60) hide show
  1. secenvY/__init__.py +43 -0
  2. secenvY/agents/__init__.py +17 -0
  3. secenvY/agents/dqn_agent.py +225 -0
  4. secenvY/agents/networks.py +81 -0
  5. secenvY/agents/replay_buffer.py +205 -0
  6. secenvY/core/__init__.py +109 -0
  7. secenvY/core/exceptions.py +41 -0
  8. secenvY/core/schemas.py +489 -0
  9. secenvY/core/vocabularies.py +106 -0
  10. secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/data_level0.bin +0 -0
  11. secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/header.bin +0 -0
  12. secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/length.bin +0 -0
  13. secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/link_lists.bin +0 -0
  14. secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/data_level0.bin +0 -0
  15. secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/header.bin +0 -0
  16. secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/length.bin +0 -0
  17. secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/link_lists.bin +0 -0
  18. secenvY/data/chroma/chroma.sqlite3 +0 -0
  19. secenvY/data/knowledge/components.txt +61 -0
  20. secenvY/data/knowledge/crypto.txt +27 -0
  21. secenvY/data/knowledge/permissions.txt +64 -0
  22. secenvY/data/knowledge/secrets.txt +76 -0
  23. secenvY/data/knowledge/security_basics.txt +19 -0
  24. secenvY/data/knowledge/storage.txt +66 -0
  25. secenvY/data/knowledge/webview.txt +28 -0
  26. secenvY/data/report.json +421 -0
  27. secenvY/data/sample_observation.json +333 -0
  28. secenvY/env/__init__.py +5 -0
  29. secenvY/env/security_env.py +282 -0
  30. secenvY/gateway/__init__.py +11 -0
  31. secenvY/gateway/base.py +59 -0
  32. secenvY/gateway/mock_gateway.py +454 -0
  33. secenvY/gateway/websocket_gateway.py +288 -0
  34. secenvY/services/__init__.py +26 -0
  35. secenvY/services/action_mapper.py +119 -0
  36. secenvY/services/episode_policy.py +74 -0
  37. secenvY/services/reward_calculator.py +125 -0
  38. secenvY/services/security_reasoner/__init__.py +46 -0
  39. secenvY/services/security_reasoner/build_knowledge.py +60 -0
  40. secenvY/services/security_reasoner/chunker.py +23 -0
  41. secenvY/services/security_reasoner/config.py +53 -0
  42. secenvY/services/security_reasoner/embeddings.py +18 -0
  43. secenvY/services/security_reasoner/llm.py +102 -0
  44. secenvY/services/security_reasoner/main.py +133 -0
  45. secenvY/services/security_reasoner/rag.py +79 -0
  46. secenvY/services/security_reasoner/schemas.py +9 -0
  47. secenvY/services/security_reasoner/service.py +288 -0
  48. secenvY/services/security_reasoner/vector_store.py +58 -0
  49. secenvY/services/security_reasoner_main.py +43 -0
  50. secenvY/services/state_builder.py +919 -0
  51. secenvY/services/state_builder_main.py +77 -0
  52. secenvY/services/state_encoder.py +379 -0
  53. secenvY/services/state_encoder_main.py +84 -0
  54. secenvY/services/state_validator.py +267 -0
  55. secenvY/services/state_validator_main.py +59 -0
  56. secenvY/services/tensor_converter.py +99 -0
  57. secenvy-0.1.0.dist-info/METADATA +129 -0
  58. secenvy-0.1.0.dist-info/RECORD +60 -0
  59. secenvy-0.1.0.dist-info/WHEEL +5 -0
  60. secenvy-0.1.0.dist-info/top_level.txt +1 -0
secenvY/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ """Android Security Analyzer Gymnasium Environment & Agent (SecEnv)."""
2
+
3
+ import gymnasium as gym
4
+
5
+ # Register standard AndroidSecurityEnv-v0 environment
6
+ gym.register(
7
+ id="AndroidSecurityEnv-v0",
8
+ entry_point="secenvY.env.security_env:SecurityEnv",
9
+ max_episode_steps=100,
10
+ )
11
+
12
+ from secenvY.agents.dqn_agent import DuelingDQNAgent
13
+ from secenvY.agents.networks import DuelingQNetwork
14
+ from secenvY.agents.replay_buffer import PrioritizedReplayBuffer, UniformReplayBuffer
15
+ from secenvY.env.security_env import SecurityEnv
16
+ from secenvY.gateway.base import SystemGateway
17
+ from secenvY.gateway.mock_gateway import MockSystemGateway
18
+ from secenvY.gateway.websocket_gateway import WebSocketSystemGateway
19
+ from secenvY.services.action_mapper import ActionMapper
20
+ from secenvY.services.episode_policy import EpisodePolicy
21
+ from secenvY.services.reward_calculator import RewardCalculator
22
+ from secenvY.services.state_builder import StateBuilder
23
+ from secenvY.services.state_encoder import StateEncoder
24
+ from secenvY.services.state_validator import StateValidator
25
+ from secenvY.services.tensor_converter import TensorConverter
26
+
27
+ __all__ = [
28
+ "SecurityEnv",
29
+ "DuelingDQNAgent",
30
+ "DuelingQNetwork",
31
+ "PrioritizedReplayBuffer",
32
+ "UniformReplayBuffer",
33
+ "SystemGateway",
34
+ "MockSystemGateway",
35
+ "WebSocketSystemGateway",
36
+ "ActionMapper",
37
+ "StateBuilder",
38
+ "StateEncoder",
39
+ "StateValidator",
40
+ "TensorConverter",
41
+ "RewardCalculator",
42
+ "EpisodePolicy",
43
+ ]
@@ -0,0 +1,17 @@
1
+ """Dueling Double Deep Q-Network agent, neural network architectures, and replay buffers."""
2
+
3
+ from secenvY.agents.dqn_agent import DuelingDQNAgent
4
+ from secenvY.agents.networks import DuelingQNetwork
5
+ from secenvY.agents.replay_buffer import (
6
+ PrioritizedReplayBuffer,
7
+ SumTree,
8
+ UniformReplayBuffer,
9
+ )
10
+
11
+ __all__ = [
12
+ "DuelingDQNAgent",
13
+ "DuelingQNetwork",
14
+ "PrioritizedReplayBuffer",
15
+ "UniformReplayBuffer",
16
+ "SumTree",
17
+ ]
@@ -0,0 +1,225 @@
1
+ """Dueling Double Deep Q-Network (Dueling Double DQN) Agent with Prioritized Experience Replay."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import random
7
+ from typing import Any, Dict, List, Optional, Tuple, Union
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.optim as optim
12
+
13
+ from secenvY.agents.networks import DuelingQNetwork
14
+ from secenvY.agents.replay_buffer import PrioritizedReplayBuffer, UniformReplayBuffer
15
+
16
+
17
+ class DuelingDQNAgent:
18
+ """Reinforcement Learning Agent implementing Dueling Double DQN with Prioritized Experience Replay (PER).
19
+
20
+ Args:
21
+ state_dim: Dimension of observation tensor (default: 117).
22
+ action_dim: Number of discrete actions (default: 8).
23
+ hidden_dim: Hidden dimension of backbone network (default: 256).
24
+ lr: Learning rate for Adam optimizer (default: 1e-4).
25
+ gamma: Discount factor for Bellman target (default: 0.99).
26
+ tau: Soft target network update coefficient (default: 0.005).
27
+ target_update_interval: Hard update interval if tau is 1.0 (default: 500).
28
+ buffer_capacity: Max transitions stored in experience replay (default: 100,000).
29
+ use_per: Whether to use Prioritized Experience Replay (default: True).
30
+ per_alpha: PER prioritization exponent (default: 0.6).
31
+ per_beta_start: Initial importance sampling exponent (default: 0.4).
32
+ max_grad_norm: Maximum gradient norm for clipping (default: 10.0).
33
+ device: Torch computing device (default: cpu / cuda if available).
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ state_dim: int = 117,
39
+ action_dim: int = 8,
40
+ hidden_dim: int = 256,
41
+ lr: float = 1e-4,
42
+ gamma: float = 0.99,
43
+ tau: float = 0.005,
44
+ target_update_interval: int = 500,
45
+ buffer_capacity: int = 100_000,
46
+ use_per: bool = True,
47
+ per_alpha: float = 0.6,
48
+ per_beta_start: float = 0.4,
49
+ max_grad_norm: float = 10.0,
50
+ device: Optional[Union[str, torch.device]] = None,
51
+ ) -> None:
52
+ if device is None:
53
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
54
+ else:
55
+ self.device = torch.device(device)
56
+
57
+ self.state_dim = state_dim
58
+ self.action_dim = action_dim
59
+ self.gamma = gamma
60
+ self.tau = tau
61
+ self.target_update_interval = target_update_interval
62
+ self.max_grad_norm = max_grad_norm
63
+ self.use_per = use_per
64
+
65
+ # Neural Networks: Online and Target Q-Networks
66
+ self.online_net = DuelingQNetwork(state_dim, action_dim, hidden_dim).to(self.device)
67
+ self.target_net = DuelingQNetwork(state_dim, action_dim, hidden_dim).to(self.device)
68
+ self.target_net.load_state_dict(self.online_net.state_dict())
69
+ self.target_net.eval()
70
+ for param in self.target_net.parameters():
71
+ param.requires_grad = False
72
+
73
+ # Optimizer and Huber Loss Criterion
74
+ self.optimizer = optim.Adam(self.online_net.parameters(), lr=lr, eps=1e-8)
75
+ self.criterion = nn.SmoothL1Loss(reduction="none")
76
+
77
+ # Replay Memory (PER or Uniform)
78
+ self.replay_buffer: Union[PrioritizedReplayBuffer, UniformReplayBuffer]
79
+ if use_per:
80
+ self.replay_buffer = PrioritizedReplayBuffer(
81
+ capacity=buffer_capacity,
82
+ alpha=per_alpha,
83
+ beta_start=per_beta_start,
84
+ device=self.device,
85
+ )
86
+ else:
87
+ self.replay_buffer = UniformReplayBuffer(
88
+ capacity=buffer_capacity,
89
+ device=self.device,
90
+ )
91
+
92
+ self.total_steps = 0
93
+ self.train_steps = 0
94
+
95
+ def select_action(
96
+ self,
97
+ state: np.ndarray | List[float],
98
+ epsilon: float = 0.0,
99
+ action_mask: Optional[Union[np.ndarray, List[bool]]] = None,
100
+ ) -> int:
101
+ """Select action using epsilon-greedy exploration policy with optional action masking.
102
+
103
+ Args:
104
+ state: State vector of shape (117,) or list of floats.
105
+ epsilon: Exploration probability in [0.0, 1.0].
106
+ action_mask: Optional boolean mask of shape (action_dim,) where True indicates valid action.
107
+
108
+ Returns:
109
+ Discrete action ID in range [0, action_dim - 1].
110
+ """
111
+ self.total_steps += 1
112
+
113
+ if action_mask is not None:
114
+ mask_arr = np.array(action_mask, dtype=bool)
115
+ valid_indices = np.where(mask_arr)[0]
116
+ if len(valid_indices) == 0:
117
+ valid_indices = np.arange(self.action_dim)
118
+ else:
119
+ valid_indices = np.arange(self.action_dim)
120
+
121
+ if random.random() < epsilon:
122
+ return int(random.choice(valid_indices))
123
+
124
+ state_tensor = torch.tensor(
125
+ np.array(state, dtype=np.float32),
126
+ device=self.device,
127
+ dtype=torch.float32,
128
+ ).unsqueeze(0)
129
+
130
+ self.online_net.eval()
131
+ with torch.no_grad():
132
+ q_values = self.online_net(state_tensor).squeeze(0)
133
+ if action_mask is not None:
134
+ mask_tensor = torch.tensor(mask_arr, device=self.device, dtype=torch.bool)
135
+ q_values[~mask_tensor] = -1e9
136
+ action = int(q_values.argmax(dim=-1).item())
137
+ self.online_net.train()
138
+
139
+ return action
140
+
141
+ def train_step(self, batch_size: int = 32) -> Optional[float]:
142
+ """Perform a single Double DQN training step over a batch from replay buffer.
143
+
144
+ Args:
145
+ batch_size: Number of transitions to sample.
146
+
147
+ Returns:
148
+ Computed scalar loss value, or None if buffer has insufficient transitions.
149
+ """
150
+ if len(self.replay_buffer) < batch_size:
151
+ return None
152
+
153
+ self.train_steps += 1
154
+
155
+ # 1. Sample batch
156
+ if self.use_per:
157
+ states, actions, rewards, next_states, dones, indices, weights = self.replay_buffer.sample(batch_size) # type: ignore
158
+ else:
159
+ states, actions, rewards, next_states, dones = self.replay_buffer.sample(batch_size) # type: ignore
160
+ weights = torch.ones_like(rewards)
161
+ indices = []
162
+
163
+ # 2. Compute current Q-values Q(s, a; θ)
164
+ current_q = self.online_net(states).gather(1, actions)
165
+
166
+ # 3. Double DQN Target: a* = argmax_{a'} Q(s', a'; θ), y = r + γ Q(s', a*; θ^-)
167
+ with torch.no_grad():
168
+ next_actions = self.online_net(next_states).argmax(dim=-1, keepdim=True)
169
+ target_q_next = self.target_net(next_states).gather(1, next_actions)
170
+ target_q = rewards + (1.0 - dones) * self.gamma * target_q_next
171
+
172
+ # 4. Compute TD Errors and Huber Loss
173
+ td_errors = (current_q - target_q).detach()
174
+ loss_elements = self.criterion(current_q, target_q)
175
+ loss = (loss_elements * weights).mean()
176
+
177
+ # 5. Backpropagation and Gradient Clipping
178
+ self.optimizer.zero_grad()
179
+ loss.backward()
180
+ if self.max_grad_norm > 0:
181
+ nn.utils.clip_grad_norm_(self.online_net.parameters(), max_norm=self.max_grad_norm)
182
+ self.optimizer.step()
183
+
184
+ # 6. Update PER priorities
185
+ if self.use_per and indices:
186
+ self.replay_buffer.update_priorities(indices, td_errors.abs()) # type: ignore
187
+
188
+ # 7. Update Target Network
189
+ self._update_target_network()
190
+
191
+ return float(loss.item())
192
+
193
+ def _update_target_network(self) -> None:
194
+ """Update target network parameters via Polyak soft update or periodic hard copy."""
195
+ if self.tau < 1.0:
196
+ # Soft Polyak update: θ^- ← τ θ + (1 - τ) θ^-
197
+ for target_param, online_param in zip(self.target_net.parameters(), self.online_net.parameters()):
198
+ target_param.data.copy_(self.tau * online_param.data + (1.0 - self.tau) * target_param.data)
199
+ else:
200
+ # Hard periodic update
201
+ if self.train_steps % self.target_update_interval == 0:
202
+ self.target_net.load_state_dict(self.online_net.state_dict())
203
+
204
+ def save_checkpoint(self, filepath: str) -> None:
205
+ """Save model and optimizer state to disk."""
206
+ os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
207
+ torch.save(
208
+ {
209
+ "online_net_state": self.online_net.state_dict(),
210
+ "target_net_state": self.target_net.state_dict(),
211
+ "optimizer_state": self.optimizer.state_dict(),
212
+ "total_steps": self.total_steps,
213
+ "train_steps": self.train_steps,
214
+ },
215
+ filepath,
216
+ )
217
+
218
+ def load_checkpoint(self, filepath: str) -> None:
219
+ """Load model and optimizer state from disk."""
220
+ checkpoint = torch.load(filepath, map_location=self.device)
221
+ self.online_net.load_state_dict(checkpoint["online_net_state"])
222
+ self.target_net.load_state_dict(checkpoint["target_net_state"])
223
+ self.optimizer.load_state_dict(checkpoint["optimizer_state"])
224
+ self.total_steps = checkpoint.get("total_steps", 0)
225
+ self.train_steps = checkpoint.get("train_steps", 0)
@@ -0,0 +1,81 @@
1
+ """PyTorch neural network architectures for Dueling Deep Q-Networks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+
8
+
9
+ class DuelingQNetwork(nn.Module):
10
+ """Dueling Deep Q-Network decomposing Q(s, a) into Value V(s) and Advantage A(s, a) streams.
11
+
12
+ Q(s, a) = V(s) + (A(s, a) - 1/|A| * sum_{a'} A(s, a'))
13
+
14
+ Args:
15
+ state_dim: Input observation space dimension (default: 117).
16
+ action_dim: Discrete action space size (default: 8).
17
+ hidden_dim: Number of hidden units in backbone layers (default: 256).
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ state_dim: int = 117,
23
+ action_dim: int = 8,
24
+ hidden_dim: int = 256,
25
+ ) -> None:
26
+ super().__init__()
27
+ self.state_dim = state_dim
28
+ self.action_dim = action_dim
29
+
30
+ # Shared Feature Extractor Backbone
31
+ self.backbone = nn.Sequential(
32
+ nn.Linear(state_dim, hidden_dim),
33
+ nn.LayerNorm(hidden_dim),
34
+ nn.ReLU(),
35
+ nn.Linear(hidden_dim, hidden_dim),
36
+ nn.ReLU(),
37
+ )
38
+
39
+ # Value Stream: V(s) -> [B, 1]
40
+ self.value_stream = nn.Sequential(
41
+ nn.Linear(hidden_dim, hidden_dim // 2),
42
+ nn.ReLU(),
43
+ nn.Linear(hidden_dim // 2, 1),
44
+ )
45
+
46
+ # Advantage Stream: A(s, a) -> [B, action_dim]
47
+ self.advantage_stream = nn.Sequential(
48
+ nn.Linear(hidden_dim, hidden_dim // 2),
49
+ nn.ReLU(),
50
+ nn.Linear(hidden_dim // 2, action_dim),
51
+ )
52
+
53
+ def forward(self, state: torch.Tensor) -> torch.Tensor:
54
+ """Forward pass computing Q-values for all discrete actions.
55
+
56
+ Args:
57
+ state: Tensor of shape (batch_size, state_dim) or (state_dim,).
58
+
59
+ Returns:
60
+ Tensor of shape (batch_size, action_dim) containing Q-values.
61
+ """
62
+ if state.dim() == 1:
63
+ state = state.unsqueeze(0)
64
+
65
+ features = self.backbone(state)
66
+ value = self.value_stream(features) # [B, 1]
67
+ advantage = self.advantage_stream(features) # [B, action_dim]
68
+
69
+ # Aggregation: Q(s, a) = V(s) + (A(s, a) - mean(A(s, :)))
70
+ q_values = value + (advantage - advantage.mean(dim=-1, keepdim=True))
71
+ return q_values
72
+
73
+ def compute_value_and_advantage(self, state: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
74
+ """Expose raw value and advantage streams for inspection and explainability."""
75
+ if state.dim() == 1:
76
+ state = state.unsqueeze(0)
77
+
78
+ features = self.backbone(state)
79
+ value = self.value_stream(features)
80
+ advantage = self.advantage_stream(features)
81
+ return value, advantage
@@ -0,0 +1,205 @@
1
+ """Uniform and Prioritized Experience Replay buffers for RL agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ import random
7
+ from typing import List, Optional, Tuple
8
+ import numpy as np
9
+ import torch
10
+
11
+
12
+ class UniformReplayBuffer:
13
+ """Standard circular Experience Replay buffer sampling uniformly at random."""
14
+
15
+ def __init__(self, capacity: int = 100_000, device: Optional[torch.device] = None) -> None:
16
+ self.capacity = capacity
17
+ self.device = device or torch.device("cpu")
18
+ self.buffer: deque = deque(maxlen=capacity)
19
+
20
+ def push(
21
+ self,
22
+ state: np.ndarray | List[float],
23
+ action: int,
24
+ reward: float,
25
+ next_state: np.ndarray | List[float],
26
+ done: bool,
27
+ ) -> None:
28
+ """Add a transition tuple to the buffer."""
29
+ self.buffer.append((
30
+ np.array(state, dtype=np.float32),
31
+ int(action),
32
+ float(reward),
33
+ np.array(next_state, dtype=np.float32),
34
+ bool(done),
35
+ ))
36
+
37
+ def sample(self, batch_size: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
38
+ """Sample a batch of transitions uniformly at random."""
39
+ transitions = random.sample(self.buffer, batch_size)
40
+ states, actions, rewards, next_states, dones = zip(*transitions)
41
+
42
+ return (
43
+ torch.tensor(np.array(states), dtype=torch.float32, device=self.device),
44
+ torch.tensor(actions, dtype=torch.long, device=self.device).unsqueeze(1),
45
+ torch.tensor(rewards, dtype=torch.float32, device=self.device).unsqueeze(1),
46
+ torch.tensor(np.array(next_states), dtype=torch.float32, device=self.device),
47
+ torch.tensor(dones, dtype=torch.float32, device=self.device).unsqueeze(1),
48
+ )
49
+
50
+ def __len__(self) -> int:
51
+ return len(self.buffer)
52
+
53
+
54
+ class SumTree:
55
+ """Binary SumTree data structure for O(log N) prioritized sampling and priority updates."""
56
+
57
+ def __init__(self, capacity: int) -> None:
58
+ self.capacity = capacity
59
+ self.tree = np.zeros(2 * capacity - 1, dtype=np.float64)
60
+ self.data: List[Optional[Tuple[np.ndarray, int, float, np.ndarray, bool]]] = [None] * capacity
61
+ self.write_ptr = 0
62
+ self.n_entries = 0
63
+
64
+ def _propagate(self, idx: int, change: float) -> None:
65
+ parent = (idx - 1) // 2
66
+ self.tree[parent] += change
67
+ if parent != 0:
68
+ self._propagate(parent, change)
69
+
70
+ def _retrieve(self, idx: int, s: float) -> int:
71
+ left = 2 * idx + 1
72
+ right = left + 1
73
+
74
+ if left >= len(self.tree):
75
+ return idx
76
+
77
+ if s <= self.tree[left]:
78
+ return self._retrieve(left, s)
79
+ else:
80
+ return self._retrieve(right, s - self.tree[left])
81
+
82
+ @property
83
+ def total_priority(self) -> float:
84
+ return float(self.tree[0])
85
+
86
+ def add(self, priority: float, data: Tuple[np.ndarray, int, float, np.ndarray, bool]) -> None:
87
+ tree_idx = self.write_ptr + self.capacity - 1
88
+ self.data[self.write_ptr] = data
89
+ self.update(tree_idx, priority)
90
+
91
+ self.write_ptr = (self.write_ptr + 1) % self.capacity
92
+ if self.n_entries < self.capacity:
93
+ self.n_entries += 1
94
+
95
+ def update(self, tree_idx: int, priority: float) -> None:
96
+ change = priority - self.tree[tree_idx]
97
+ self.tree[tree_idx] = priority
98
+ if tree_idx != 0:
99
+ self._propagate(tree_idx, change)
100
+
101
+ def get_leaf(self, val: float) -> Tuple[int, float, Tuple[np.ndarray, int, float, np.ndarray, bool]]:
102
+ tree_idx = self._retrieve(0, val)
103
+ data_idx = tree_idx - self.capacity + 1
104
+ return tree_idx, float(self.tree[tree_idx]), self.data[data_idx] # type: ignore
105
+
106
+
107
+ class PrioritizedReplayBuffer:
108
+ """Prioritized Experience Replay (PER) buffer using SumTree for efficient sampling."""
109
+
110
+ def __init__(
111
+ self,
112
+ capacity: int = 100_000,
113
+ alpha: float = 0.6,
114
+ beta_start: float = 0.4,
115
+ beta_frames: int = 100_000,
116
+ eps: float = 1e-5,
117
+ device: Optional[torch.device] = None,
118
+ ) -> None:
119
+ self.tree = SumTree(capacity)
120
+ self.capacity = capacity
121
+ self.alpha = alpha
122
+ self.beta_start = beta_start
123
+ self.beta_frames = beta_frames
124
+ self.eps = eps
125
+ self.device = device or torch.device("cpu")
126
+ self.max_priority = 1.0
127
+ self.frame = 0
128
+
129
+ def _get_beta(self) -> float:
130
+ return min(1.0, self.beta_start + self.frame * (1.0 - self.beta_start) / max(1, self.beta_frames))
131
+
132
+ def push(
133
+ self,
134
+ state: np.ndarray | List[float],
135
+ action: int,
136
+ reward: float,
137
+ next_state: np.ndarray | List[float],
138
+ done: bool,
139
+ ) -> None:
140
+ """Add transition with maximal priority to ensure initial exploration."""
141
+ data = (
142
+ np.array(state, dtype=np.float32),
143
+ int(action),
144
+ float(reward),
145
+ np.array(next_state, dtype=np.float32),
146
+ bool(done),
147
+ )
148
+ priority = (self.max_priority + self.eps) ** self.alpha
149
+ self.tree.add(priority, data)
150
+
151
+ def sample(
152
+ self, batch_size: int
153
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, List[int], torch.Tensor]:
154
+ """Sample a batch proportionally to priorities with Importance Sampling weights."""
155
+ self.frame += 1
156
+ beta = self._get_beta()
157
+
158
+ segment = self.tree.total_priority / batch_size
159
+ indices: List[int] = []
160
+ priorities: List[float] = []
161
+ transitions: List[Tuple[np.ndarray, int, float, np.ndarray, bool]] = []
162
+
163
+ for i in range(batch_size):
164
+ a = segment * i
165
+ b = segment * (i + 1)
166
+ s = random.uniform(a, b)
167
+ tree_idx, p, data = self.tree.get_leaf(s)
168
+
169
+ if data is None:
170
+ # Fallback for floating point boundary
171
+ tree_idx, p, data = self.tree.get_leaf(random.uniform(0, max(1e-5, self.tree.total_priority)))
172
+
173
+ indices.append(tree_idx)
174
+ priorities.append(max(self.eps, p))
175
+ transitions.append(data)
176
+
177
+ # Importance sampling weights: w_i = (N * P(i))^(-beta) / max(w)
178
+ probs = np.array(priorities) / max(1e-8, self.tree.total_priority)
179
+ weights = (len(self) * probs) ** (-beta)
180
+ weights = weights / weights.max()
181
+
182
+ states, actions, rewards, next_states, dones = zip(*transitions)
183
+
184
+ return (
185
+ torch.tensor(np.array(states), dtype=torch.float32, device=self.device),
186
+ torch.tensor(actions, dtype=torch.long, device=self.device).unsqueeze(1),
187
+ torch.tensor(rewards, dtype=torch.float32, device=self.device).unsqueeze(1),
188
+ torch.tensor(np.array(next_states), dtype=torch.float32, device=self.device),
189
+ torch.tensor(dones, dtype=torch.float32, device=self.device).unsqueeze(1),
190
+ indices,
191
+ torch.tensor(weights, dtype=torch.float32, device=self.device).unsqueeze(1),
192
+ )
193
+
194
+ def update_priorities(self, indices: List[int], td_errors: np.ndarray | torch.Tensor) -> None:
195
+ """Update transition priorities with new TD errors."""
196
+ if isinstance(td_errors, torch.Tensor):
197
+ td_errors = td_errors.detach().cpu().numpy().flatten()
198
+
199
+ for idx, error in zip(indices, td_errors):
200
+ p = float((abs(error) + self.eps) ** self.alpha)
201
+ self.tree.update(idx, p)
202
+ self.max_priority = max(self.max_priority, abs(error))
203
+
204
+ def __len__(self) -> int:
205
+ return self.tree.n_entries
@@ -0,0 +1,109 @@
1
+ """Core schemas, vocabularies, and exception definitions for secenvY."""
2
+
3
+ from secenvY.core.exceptions import (
4
+ EpisodeInactiveError,
5
+ GatewayError,
6
+ InvalidActionError,
7
+ SecurityEnvError,
8
+ SessionError,
9
+ StateValidationError,
10
+ ValidationError,
11
+ )
12
+ from secenvY.core.schemas import (
13
+ ActionCommand,
14
+ AndroidState,
15
+ AppMetadataState,
16
+ DynamicCryptoRuntimeState,
17
+ DynamicExportedIPCState,
18
+ DynamicLoggingState,
19
+ DynamicNetworkState,
20
+ DynamicStorageState,
21
+ DynamicUIState,
22
+ DynamicWebViewRuntimeState,
23
+ ExecutionResult,
24
+ ExplorationCoverageState,
25
+ MetaAction,
26
+ PermissionState,
27
+ RAGSecurityEvaluation,
28
+ RuntimeLifecycleState,
29
+ SecurityAnalysis,
30
+ Severity,
31
+ StaticAttackSurfaceState,
32
+ StaticCallGraphState,
33
+ StaticCryptoState,
34
+ StaticStorageState,
35
+ StaticWebViewState,
36
+ TransitionRecord,
37
+ VulnerabilityFlags,
38
+ )
39
+ from secenvY.core.vocabularies import (
40
+ ACTION_CATEGORIES,
41
+ ACTION_CATEGORIES_LIST,
42
+ ACTION_CATEGORIES_MAP,
43
+ CRITICAL_PERMISSIONS,
44
+ CRITICAL_PERMISSIONS_LIST,
45
+ CRITICAL_PERMISSIONS_MAP,
46
+ CRYPTO_ALGORITHMS_LIST,
47
+ CRYPTO_ALGORITHMS_MAP,
48
+ STATIC_CRYPTO_ALGORITHMS,
49
+ ActionCategory,
50
+ CriticalPermission,
51
+ CryptoAlgorithm,
52
+ )
53
+
54
+ __all__ = [
55
+ # Exceptions
56
+ "SecurityEnvError",
57
+ "StateValidationError",
58
+ "ValidationError",
59
+ "GatewayError",
60
+ "InvalidActionError",
61
+ "SessionError",
62
+ "EpisodeInactiveError",
63
+ # Vocabularies & Enums
64
+ "ActionCategory",
65
+ "MetaAction",
66
+ "CriticalPermission",
67
+ "CryptoAlgorithm",
68
+ "CRITICAL_PERMISSIONS",
69
+ "CRITICAL_PERMISSIONS_LIST",
70
+ "CRITICAL_PERMISSIONS_MAP",
71
+ "STATIC_CRYPTO_ALGORITHMS",
72
+ "CRYPTO_ALGORITHMS_LIST",
73
+ "CRYPTO_ALGORITHMS_MAP",
74
+ "ACTION_CATEGORIES",
75
+ "ACTION_CATEGORIES_LIST",
76
+ "ACTION_CATEGORIES_MAP",
77
+ # Canonical State Models
78
+ "AndroidState",
79
+ "AppMetadataState",
80
+ "PermissionState",
81
+ "StaticAttackSurfaceState",
82
+ "StaticStorageState",
83
+ "StaticCryptoState",
84
+ "StaticWebViewState",
85
+ "StaticCallGraphState",
86
+ "RuntimeLifecycleState",
87
+ "DynamicUIState",
88
+ "DynamicLoggingState",
89
+ "DynamicStorageState",
90
+ "DynamicCryptoRuntimeState",
91
+ "DynamicWebViewRuntimeState",
92
+ "DynamicNetworkState",
93
+ "DynamicExportedIPCState",
94
+ "ExplorationCoverageState",
95
+ "VulnerabilityFlags",
96
+ # Commands & Results
97
+ "ActionCommand",
98
+ "ExecutionResult",
99
+ "TransitionRecord",
100
+ # RAG Reasoner Schemas
101
+ "Severity",
102
+ "RAGSecurityEvaluation",
103
+ "SecurityAnalysis",
104
+ # Transitional Aliases
105
+ "ValidatedObservation",
106
+ "StaticAnalysisContext",
107
+ "UIContext",
108
+ "FridaRuntimeSinks",
109
+ ]